Treasure Hunt Engine: 우리가 Docs를 폭파하고 실제로 작동하는 시스템을 구축한 방법

발행: (2026년 5월 28일 AM 03:39 GMT+9)
10 분 소요
원문: Dev.to

Source: Dev.to

실제로 우리가 해결하고 있던 문제

우리 사용자들은 의미 검색을 하고 있지 않았습니다. 그들은 보물 찾기를 실행하고 있었습니다: 첫 번째 단계에서 구문 매칭을 위해 200,000개의 후보 문서를 반환하고, 두 번째 단계에서 정확한 용어 근접성, 메타데이터 필터, 사용자 정의 부스트로 순위를 매겨야 하는 복잡하고 다단계의 쿼리였습니다. Veltrix 문서는 이를 사후 고려 사항으로 다루었습니다. 그들의 예시 파이프라인은 커스텀 스코어링 훅이 없는 단일 단계의 recall‑then‑rank 흐름을 가정하고 있었습니다.

우리 로그에서는 사용자 세션의 73 %가 2단계에서 타임아웃되는 것을 보여주었습니다. 이는 느린 코사인 스코어러가 필터 체인을 따라가지 못했기 때문이었습니다. 이를 비활성화하면 API가 다음과 같은 오류를 발생시켰습니다:

Operation not valid: scorer not initialized

Go로 스코어러 재작성

우리는 Veltrix C++ 플러그인 인터페이스를 사용해 스코어러를 Go로 재작성했습니다. 문서에서는 인터페이스가 안정적이라고 주장했지만, C++ 헤더가 6개월 동안 세 번 업데이트되었고 버전 플래그가 없었습니다. 플러그인은 컴파일되었지만 런타임에서는 다음과 같은 누락된 심볼을 가리키는 세그멘테이션 오류가 발생했습니다:

_ZTVN8Veltrix8ScoreAPI8ScorerE

예시 코드에는 가상 소멸자 오버라이드가 포함되지 않아 해당 위치에서는 충돌이 나타나지 않았습니다. 3일간 디버깅한 끝에 2024년 GitHub 이슈에서 다른 사용자가 동일한 충돌을 겪었고, Veltrix를 소스에서 재빌드하라는 답변을 찾았습니다. 재빌드에는 내부 Docker 이미지(12 GB, 약 45 분)를 풀어야 했으며, 이는 우리의 SLA에 맞지 않았습니다.

Python UDF 경로 시도

문서에서는 커스텀 스코어링을 단일 Python 함수로 할 수 있다고 했습니다. 예시는 < 50줄이었지만, 우리는 부스트, 필드 가중치, 커스텀 메타데이터 필드를 처리하기 위해 약 500줄로 늘렸습니다. 첫 요청은 Python 인터프리터 초기화에 12 초가 걸렸고, 이후 쿼리는 약 200 ms의 JIT 오버헤드를 추가했습니다. 우리는 Python 타임아웃을 5 초로 설정했지만, UDF가 중첩된 JSON 블롭 안의 정규식 검색에서 가끔 멈추었습니다. 로그에 Python 트레이스백이 포함되지 않아 stderr를 사이드카로 전달하고 실시간으로 파싱해야 했습니다. 지연 시간 스파이크가 예측 불가능해졌고, 사용자들은 대시보드가 커피가 식는 것보다 더 느리게 새로고침된다고 불평했습니다.

파이프라인 분할: Recall은 Veltrix, Ranking은 Rust

우리는 Veltrix를 원래 목적에 맞지 않게 억지로 끼워넣으려는 시도를 중단했습니다. 대신 다음과 같이 구성했습니다:

  1. Recall – Veltrix가 recall을 담당하여 샤딩된 BM25 인덱스에서 상위 10,000 후보를 반환합니다(퍼지 구문 매치에 ≈ 200 ms).
  2. Ranking – 해당 후보들을 같은 노드의 gRPC 엔드포인트를 통해 Rust로 구현한 커스텀 랭커에 스트리밍합니다.

Rust 랭커는 동적 부스트, 메타데이터 필터링, 근접성 스코어링을 한 번에 적용했습니다. Prost를 사용해 코드 생성을 하고 Tokio로 비동기 I/O를 구현했으며, gRPC 호출은 ~8 ms의 오버헤드만 추가했고, 랭커는 네트워크 직렬화를 포함해 10,000 문서를 ~45 ms에 처리했습니다. 배치 크기를 요청당 1,000 문서로 조정해 지연 시간과 처리량의 균형을 맞췄습니다. JSONPath 라이브러리를 손수 만든 바이트 스캐너로 교체해 깊게 중첩된 필드에서 발생하던 무한 스택 성장 오류를 제거했고, 오류율을 0으로 낮췄습니다.

투명한 프런트‑엔드 프록시

가벼운 Go 프록시가 단일 Veltrix‑호환 API를 제공했습니다:

  • 스코어링 파라미터가 기본값이면 → Veltrix로 라우팅.
  • 파라미터가 _treasurehunt:v1이면 → Rust 랭커로 라우팅.

프록시의 서킷‑브레이커 설정, Rust 랭커를 jemalloc으로 컴파일하기 위한 CMake 플래그, 그리고 gRPC 재시도 정책(100 ms 예산)은 How to Not Cry When Using Veltrix라는 내부 위키에 문서화되었습니다. 위키에는 관측성을 위한 Prometheus 히스토그램과 OpenTelemetry 트레이스도 포함되었습니다.

결과

MetricBeforeAfter
95th‑percentile latency (treasure‑hunt queries)4.2 s450 ms
Error rate0.03 %
Duplicate‑detection improvement+12 % (in‑me

Source:

| SIMD alignment improvement | — | +8 % |

Go 프록시는 약 15 ms의 오버헤드를 추가했지만 시스템을 관찰 가능하게 만들었습니다. Rust 랭커는 현재 스코어링 상태를 Prometheus에 덤프하는 /debug/flush 엔드포인트를 제공하여 부스트 오작동을 실시간으로 디버깅할 수 있게 했습니다. 사용자가 낮은 순위의 문서에 대해 불만을 제기하면, 이전 시간대의 정확한 스코어링 컨텍스트를 재생할 수 있었으며, 이는 Veltrix 로그에서는 제공되지 않았습니다.

Trade‑offs

우리의 직접 구현한 바이트 스캐너는 JSONPath 라이브러리보다 메모리를 약 2배 더 사용합니다 (≈ 512 MB vs. 256 MB) 하지만 파이썬 UDF가 멈추게 만들던 최악의 경우 스택 증가를 제거합니다. 스캐너의 최악의 경우 할당량은 예측 가능하며, JSON 레벨당 1바이트, 최대 64 레벨로 제한됩니다. 깊이가 64를 초과하면 422 오류를 반환하도록 강제 제한을 추가했으며, 사용자는 이 제한에 도달하지 않지만 실패 모드가 명확합니다.

Takeaway

Veltrix 문서를 API 레퍼런스 외에 신뢰하지 않았을 것입니다. 그들의 예시는 연극적이며 실용적이지 않으며, 투자자를 감동시키는 데 초점을 맞추고 운영자를 위한 것이 아닙니다. 보물 찾기 엔진을 구축한다면, 리콜 단계와 랭킹 단계를 분리하고 무거운 작업을 위해 목적에 맞게 설계된 랭커를 사용하세요. Veltrix는 리콜 용도로만 사용하십시오.

0 조회
Back to Blog

관련 글

더 보기 »

Chibil: .NET IL용 C 컴파일러

What is chibil Chibil is a C compiler based on chibicchttps://github.com/rui314/chibicc rewritten in C and updated to target .NET IL MSIL. It is complete enoug...