
대규모 시맨틱 검색 (Semantic Retrieval): 하이브리드 검색 (Hybrid Search), 재순위화 (Reranking)
요약
RAG 시스템의 성능 저하는 LLM보다 검색(Retrieval) 단계의 문제에서 비롯되는 경우가 많습니다. 벡터 검색의 한계를 극복하기 위해 BM25와 같은 어휘 검색을 결합한 하이브리드 검색의 중요성을 강조합니다.
핵심 포인트
- 벡터 검색은 정확한 용어 일치나 희귀 식별자 검색에 취약함
- 도메인 특화 용어 및 기술 어휘 처리에 한계가 있음
- BM25 기반의 어휘 검색은 정확한 토큰 매칭에 탁월함
- 고성능 RAG를 위해 하이브리드 검색 및 재순위화가 필수적임
내가 RAG 시스템에서 목격한 첫 번째 운영 장애는 LLM 때문이 아니었습니다. 그것은 검색 (retrieval) 때문이었습니다.
모델은 괜찮았습니다. 프롬프트 (prompt)도 괜찮았습니다. 문제가 있었던 것은 다음과 같습니다: 우리의 벡터 검색 (vector search)이 세 번째로 관련 있는 청크 (chunk)를 확신을 가지고 첫 번째 위치에 반환하고 있었고, 실제 정답은 우리가 컨텍스트 윈도우 (context window)에 입력하는 top-k 범위를 훨씬 벗어난 15위권 근처에 파묻혀 있었습니다. LLM은 잘못된 컨텍스트를 받았을 때 예상할 수 있는 그대로의 행동을 했습니다. 답변이 주장하는 내용을 전혀 말하지 않은 문서를 인용하며, 그럴듯하게 들리는 틀린 답을 환각 (hallucinate) 했습니다.
아무도 검색을 먼저 살펴보지 않았습니다. 모두가 모델을 먼저 살펴보았습니다. 이것은 내가 함께 일했던 모든 팀에서 반복적으로 목격한 패턴입니다.
- 정확한 일치 민감도 (Exact match sensitivity).
ETIMEDOUT과 같은 오류 코드나max_tokens와 같은 API 파라미터를 검색할 때, 밀집 임베딩 (Dense embeddings)은 해당 리터럴 토큰을 전혀 언급하지 않으면서 의미론적으로만 "가까운" 문서들을 기꺼이 반환합니다. 코사인 유사도 (Cosine similarity)는 당신이 정확한 문자열 일치를 필요로 한다는 사실을 신경 쓰지 않습니다. - 희귀 용어 및 식별자 (Rare terms and identifiers). 제품 SKU, 함수 이름, 티켓 번호, 설정 키(config keys) 등은 임베딩 모델이 다른 모든 것과 동일한 잠재 공간 (Latent space)으로 압축해 버리며, 희귀 토큰들은 모델이 학습한 데이터 분포에 의해 평활화 (Smoothed out)됩니다.
- 도메인 드리프트 (Domain drift). 기성 임베딩 모델들은 일반적인 웹 텍스트로 학습되었습니다. Kubernetes 매니페스트, Terraform 모듈, 그리고 내부 약어로 가득 찬 귀하의 내부 지식 베이스는 모델이 거의 본 적 없는 분포 속에 존재합니다.
이 중 그 어떤 것도 50개의 문서로 구성된 데모에서는 나타나지 않습니다. 이것은 인덱스가 수십만 개의 청크 (Chunks)를 넘어서고 시맨틱 이웃 (Semantic neighborhoods)이 붐비기 시작할 때 나타납니다. 수많은 문서가 쿼리 벡터 (Query vector) 근처에 클러스터링되며, 과거에 "관련 있는" 것과 "주제적으로 인접한" 것을 구분해주던 랭킹 신호 (Ranking signal)가 노이즈로 가득 차게 됩니다.
여기 아무도 데모 영상에 넣지 않는 부분이 있습니다. 벡터 검색 (Vector search)은 요란하게 실패하지 않습니다. 그것은 그럴듯해 보이지만 틀린 결과를 조용히 반환함으로써 실패하며, 이는 고장 난 것처럼 보이지 않기 때문에 최악의 종류의 실패입니다.
어휘 검색 (Lexical Search)이 여전히 중요한 이유
BM25 (Elasticsearch, Lucene, 그리고 대부분의 "클래식" 검색 엔진 뒤에 있는 랭킹 함수)는 임베딩이 존재하게 된 지금 흔히 레거시 기술로 취급받곤 합니다. 하지만 그렇지 않습니다. BM25는 단어 빈도 / 역문서 빈도 (Term-frequency / inverse-document-frequency) 점수 산정 함수이며, 밀집 검색 (Dense retrieval)이 취약한 바로 그 부분, 즉 정확하거나 거의 정확한 용어 일치, 희귀 식별자, 구조화된 기술 어휘에 매우 탁월합니다.
OAuth documentation 코퍼스(corpus)를 대상으로 "OAuth2 refresh_token grant_type"를 검색하면, BM25는 이를 정확히 찾아낼 것입니다. 왜냐하면 이들은 소수의 매우 관련성 높은 문서에 등장하는 문자 그대로의 토큰(literal tokens)이기 때문입니다. 반면 밀집 리트리버(dense retriever)는 refresh_token이라는 문구를 전혀 사용하지 않은 채, 일반적인 인증 흐름(authentication flows)에 관한 개념적으로 관련된 페이지를 반환할 수도 있습니다.
핵심은 BM25가 밀집 리트리버보다 더 낫다는 것이 아닙니다. 그들이 실패하는 지점이 서로 다르며 상호 보완적이라는 점입니다. 이러한 상호 보완성이야말로 하이브리드 검색(hybrid search)을 사용하는 온전한 근거입니다. 이는 단순히 유행이기 때문이 아니라, 실패 모드(failure modes)가 거의 겹치지 않기 때문입니다.
하이브리드 검색 (Hybrid Search): 아키텍처
기본적인 수준에서 하이브리드 검색은 두 개의 리트리벌(retrieval) 경로를 병렬로 실행하고 이를 병합합니다.
[
실제로 이를 구축하기 전까지는 잘 드러나지 않는 몇 가지 주목할 만한 사항들이 있습니다.
점수 융합(Score fusion)은 간단하지 않습니다. BM25 점수와 코사인 유사도(cosine similarity) 점수는 완전히 다른 스케일(scale)과 분포를 가집니다. 단순히 이들을 평균 낼 수는 없습니다. 두 가지 일반적인 접근 방식은 다음과 같습니다:
- 상호 순위 융합 (Reciprocal Rank Fusion, RRF). 원시 점수(raw scores)를 결합하는 대신, *순위(ranks)*를 결합합니다. 각 문서는 각 리트리버로부터
1 / (k + rank)를 할당받으며, 이를 모든 리트리버에 대해 합산합니다. 이 방식은 스케일 불일치 문제를 완전히 우회하며, 이것이 RRF가 거의 모든 프로덕션 하이브리드 검색 구현체(Elasticsearch, Weaviate, Vespa 모두 기본적으로 지원)에서 나타나는 이유입니다. - 가중 선형 결합 (Weighted linear combination). 두 점수 분포를 정규화(min-max 또는 z-score)한 후, 조정 가능한 알파(alpha) 값과 결합합니다. 더 유연하지만, 가중치가 이제 튜닝하고 드리프트(drift)를 모니터링해야 하는 또 다른 하이퍼파라미터(hyperparameter)가 됩니다.
제가 본 대부분의 팀은 별도의 보정(calibration)이 필요 없고 즉시 사용해도 상당히 잘 작동하는 RRF(Reciprocal Rank Fusion)로 시작합니다. 그러다 추측이 아닌 실제 관련성 신호(relevance signal)에 맞춰 알파(alpha) 값을 실제로 튜닝할 수 있을 만큼 충분한 쿼리 로그(query logs)가 쌓이면 가중치 융합(weighted fusion) 방식으로 넘어갑니다.
중복 제거(Deduplication)는 사람들이 예상하는 것보다 더 중요합니다. 만약 청킹(chunking) 전략에 어떠한 중복(슬라이딩 윈도우 청킹, 겹치는 문단 등)이 있다면, 동일한 근본 콘텐츠가 두 검색 경로 모두에서 거의 중복된 청크로 나타날 수 있습니다. 이는 진정으로 다른 콘텐츠에 할당되어야 할 후보군(candidate set)의 슬롯을 조용히 잡아먹게 됩니다.
메타데이터 필터링(Metadata filtering)은 가능한 한 빨리 수행되어야 합니다. 만약 쿼리가 특정 제품 버전, 테넌트(tenant), 또는 문서 유형으로 범위가 제한되어 있다는 것을 알고 있다면, 비용이 많이 드는 작업을 수행한 후가 아니라 그 전에 필터링하십시오. 사후 필터링(post-hoc filtering)을 한다는 것은 결국 버려지게 될 방대한 양의 후보들에 대해 ANN(Approximate Nearest Neighbor) 비용과 BM25 비용을 이미 지불했다는 것을 의미합니다. 사전 필터링(사후 필터가 아닌 필터링된 ANN 쿼리로서의 방식)은 이 전체 파이프라인에서 가장 레버리지가 높고 노력이 적게 드는 최적화 방법 중 하나이며, v1 구현 단계에서 가장 흔히 누락되는 부분이기도 합니다.
후보군 폭발 (Candidate Explosion)
빠지기 쉬운 실패 모드는 다음과 같습니다. 하이브리드 파이프라인이 성숙해짐에 따라, "안전하게" 하기 위해 각 검색기(retriever)에서 상위 50개 또는 상위 100개를 가져오기 시작하고, 이를 150~200개의 문서로 구성된 후보 풀(candidate pool)로 융합한 다음, 그 전부를 재순위화 모델(reranker)에 입력하는 것입니다.
이것이 바로 후보군 폭발(candidate explosion)이며, 이는 조용히 지연 시간(latency)을 잡아먹는 주범입니다. ANN 검색 자체는 매우 빠릅니다. 잘 튜닝된 HNSW 또는 IVF 인덱스의 경우 높은 k 값에서도 보통 50ms 미만이 소요됩니다. 문제는 다운스트림(downstream)에 있습니다. 유지하는 모든 후보는 재순위화 모델이 점수를 매겨야 하는 대상이며, 재순위화 비용은 후보 수에 따라 대략 선형적으로(때로는 그보다 더 심하게) 증가합니다.
"안전하게 더 많은 후보를 가져오자"는 본능은 200ms짜리 검색 파이프라인을 2초짜리로 만들어버리는 바로 그 본능입니다.
재순위화 (Reranking): Cross-Encoder가 존재하는 이유
Bi-encoders (Sentence-Transformers와 같은 표준 임베딩 모델 설정)는 쿼리(Query)와 문서(Document)를 각각 독립적인 고정 벡터로 인코딩한 다음, 코사인 유사도(Cosine Similarity)를 통해 비교합니다. 이것이 벡터 검색(Vector Search)을 빠르게 만드는 핵심입니다. 문서 임베딩을 한 번만 미리 계산(Precompute)해 두면, 쿼리 시점에는 내적(Dot Product)만 계산하면 되기 때문입니다.
Cross-encoders는 완전히 다르게 작동합니다. 이들은 쿼리와 후보 문서를 하나의 입력으로서 함께 받아들이며, 전체 쌍을 트랜스포머(Transformer)를 통해 공동으로 실행합니다. 이를 통해 모델은 쿼리 토큰(Token)과 문서 토큰을 동시에 주의(Attend)할 수 있으며, 이는 훨씬 더 정확한 관련성 판단을 생성하지만, 아무것도 미리 계산할 수 없음을 의미합니다. 모든 쿼리-문서 쌍은 추론(Inference) 시점에 매번 새로운 순전파(Forward Pass)를 필요로 합니다.
Bi-Encoder (빠르지만 정밀도가 낮음)
Query → Encoder → Vector Q
Doc → Encoder → Vector D
...
이것이 재순위화(Reranking)의 근본적인 트레이드오프(Tradeoff)입니다. Cross-encoders는 쿼리와 문서 사이의 전체적인 상호작용을 파악하기 때문에 실제 관련성을 판단하는 데 훨씬 뛰어나지만, 전체 인덱스(Index)로 확장할 수는 없습니다. 쿼리당 500,000개의 문서를 Cross-encode 할 수는 없기 때문입니다. 따라서 이들은 두 번째 단계로 사용됩니다. 즉, 저렴한 검색(Retrieval)을 통해 상위 N개를 뽑은 다음, 그 결과들에 대해서만 비용이 많이 들지만 정확한 모델로 다시 점수를 매기는(Re-score) 방식입니다.
User Query
│
▼
...
Cohere Rerank, BGE-reranker, 그리고 ColBERT 스타일의 후기 상호작용(Late-interaction) 모델(Bi-encoder와 완전한 Cross-encoder 사이의 중간 단계)은 모두 지연 시간(Latency)과 정확도(Accuracy) 곡선의 서로 다른 지점에서 이 동일한 문제를 해결하기 위해 존재합니다. 특히 ColBERT는 주목할 만한데, 전체 공동 주의(Full Joint Attention) 대신 토큰별 후기 상호작용(Per-token Late Interaction)을 수행하기 때문입니다. 이를 통해 Cross-encoders의 정확도 이점을 대부분 가져가면서도 계산 비용은 훨씬 적게 들게 합니다. 다만, 문서당 하나의 벡터 대신 토큰 수준의 임베딩을 인덱싱해야 하므로 저장 공간(Storage Footprint)은 더 많이 차지합니다.
정밀도-재현율 트레이드오프 (The Precision-Recall Tradeoff)
이 섹션은 여러분의 검색 파이프라인 (retrieval pipeline)이 실제로 작동하는지 여부를 결정하는 부분이며, 대부분의 사람들이 "멋진" 아키텍처 다이어그램을 보기 위해 그냥 지나치는 부분이기도 합니다.
검색 파이프라인의 모든 조절 요소(ANN 단계의 top-k, BM25의 top-k, 퓨전 전략 (fusion strategy), 재순위화 (reranker) 컷오프)는 사실 단 하나의 축을 따라 움직이는 것과 같습니다. 즉, 정답 문서를 놓치지 않는 것 (재현율 (recall))과 틀린 문서를 포함하지 않는 것 (정밀도 (precision)) 사이에서 어느 쪽을 더 우선시할 것인가의 문제입니다.
높은 재현율 (High Recall) 경로
│
더 많은 후보군 검색
...
마지막 줄이 사람들이 흔히 실수하는 부분입니다: 재순위화 (reranking)는 검색 (retrieval) 단계에서 이미 찾아낸 것들의 순서만 바꿀 수 있습니다. 만약 1단계 재현율 (recall)이 낮아서, ANN 검색의 top-100 안에 실제로 질문에 답이 되는 문서가 포함되지 않았다면, 이후 단계에서 아무리 정교한 교차 인코더 (cross-encoder)를 사용하더라도 이를 해결할 수 없습니다. 이것이 바로 팀들이 검색 단계에서는 재현율 (recall)에 집착하고, 재순위화 (reranking) 단계에서는 정밀도 (precision)에 집착하는 이유입니다. 이들은 의도적으로 파이프라인의 서로 다른 부분에 할당된 서로 다른 작업입니다.
실제로는 매우 구체적인 엔지니어링 결정으로 나타납니다: 1단계 후보군 세트 (candidate set)의 크기를 얼마나 크게 잡아야 할까요? 너무 작으면 재순위화 (reranking)가 실행되기도 전에 최대 재현율 (recall)의 한계가 결정되어 버립니다. 너무 크면 관련성이 없을 것이 뻔한 후보군들에 대해 재순위화 지연 시간 (reranking latency)을 지불하게 됩니다. 보편적인 숫자는 없습니다. 이는 코퍼스 밀도 (corpus density), 청크 크기 (chunk size), 그리고 임베딩 공간 (embedding space)이 시맨틱적으로 얼마나 밀집되어 있는지에 따라 달라지며, 바로 이 때문에 추측하는 대신 실제로 이를 측정해야 합니다.
여기서 실제로 중요한 지표들
이 지표들을 연구 논문 수준으로 다룰 필요는 없지만, 엔지니어들이 왜 이들을 추적하는지는 반드시 알아야 합니다:
- Recall@K: 실제로 관련 있는 모든 문서 중, 상위 K개 안에 포함된 비율은 얼마인가? 이것은 당신의 한계 지표 (ceiling metric)입니다. 만약 Recall@50이 낮다면, 그 어떤 재순위화 (reranker)도 당신을 구할 수 없습니다. 이는 랭킹 (ranking)의 문제가 아니라 검색 (retrieval)의 문제입니다.
- Precision@K: 반환된 K개의 문서 중 실제로 관련 있는 문서의 비율은 얼마인가? 이는 작은 K 값에서 가장 중요합니다. 왜냐하면 작은 K 값이 실제로 LLM의 컨텍스트 윈도우 (context window)에 도달하기 때문입니다.
- MRR (Mean Reciprocal Rank): 평균적으로 첫 번째 관련 결과가 얼마나 높은 순위에 있었는가? 여러 개의 유효한 답변보다는 보통 하나의 명확한 정답이 존재하는 경우 (지원 티켓, 문서 조회 등)에 유용합니다.
- NDCG: 정밀도 (precision)와 유사하지만, 이진적인 관련/비관련 (relevant/irrelevant) 방식이 아닌 위치와 관련성 등급 (relevance grade)에 따라 가중치를 부여합니다. 문서가 엄격한 Yes/No 방식이 아닌 등급화된 관련성을 가질 때 더 유용합니다.
- Hit Rate: 가장 단순한 신호입니다. 상위 K개 안에 관련 있는 문서가 단 하나라도 나타났는가? 대략적인 상태 점검 (health check) 용도로는 좋지만, 유일한 지표로 쓰기에는 부적절합니다.
- Latency percentiles (P50/P95/P99): 평균 지연 시간 (average latency)은 거짓말을 하기 때문입니다. P50은 150ms여서 데모 시에는 훌륭하게 느껴질 수 있지만, 일부 쿼리가 콜드 캐시 미스 (cold cache misses)를 유발하거나 재순위화 (reranker) 배치 (batching)의 엣지 케이스 (edge cases)에 걸릴 경우 P99는 2.5초가 될 수 있습니다. P99는 당신의 가장 화가 난 사용자가 경험하게 될 수치입니다.
제가 가장 오랫동안 내재화하는 데 걸렸던 교훈은 이것입니다: 모델의 품질보다 검색 (retrieval) 품질이 더 중요하다. 팀들은 Recall@20이 60%에 머물러 있는 동안 프롬프트 (prompt)를 튜닝하고 LLM을 교체하는 데 몇 주를 소비합니다. 검색을 먼저 해결하십시오. 정답이 컨텍스트 (context)에 포함되지 않았다면, 세상에서 가장 뛰어난 모델이라도 질문에 올바르게 답할 수 없습니다.
청킹 (Chunking)이 이 모든 것에 미치는 영향
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기