본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 15. 12:50

BM25 + Dense Fusion: 키워드 검색이 RAG를 구원하는 순간

요약

순수 벡터 검색이 가진 어휘적 격차(lexical gap) 문제를 분석하고, 식별자나 오류 코드와 같은 정확한 일치가 필요한 상황에서 발생하는 한계를 설명합니다. 이를 해결하기 위해 BM25와 밀집 검색을 결합한 하이브리드 방식의 필요성을 강조합니다.

핵심 포인트

  • 순수 벡터 검색은 식별자, 오류 코드, SKU 등 정확한 문구 검색에 취약함
  • 임베딩 모델은 의미적 유사성에 최적화되어 있어 고유 명사 처리에 한계가 있음
  • 어휘적 격차 문제를 해결하기 위해 BM25와 Dense Fusion 결합이 필요함
  • 정확한 매칭이 중요한 RAG 시스템 구축을 위한 전략적 접근 제시

사용자가 지원 봇에 ERR_CONN_RESET_4290을 입력합니다. 해결 방법은 해당 코드가 정확히 세 번 언급된 런북(runbook)에 들어 있습니다. 하지만 당신의 리트리버(retriever)는 연결 시간 초과(connection timeouts), 재시도 정책(retry policies), TLS 핸드셰이크(TLS handshakes)에 관한 다섯 개의 청크(chunks)를 반환합니다. 정답 문서는 상위 50개 안에 없습니다. 모델은 네트워크를 확인하라는 자신감 넘치고 일반적인 답변을 작성하고, 사용자는 채팅을 시작했을 때보다 더 화가 난 채로 창을 닫습니다.

이러한 실패에는 이름이 있습니다. 그것은 어휘적 격차(lexical gap)이며, 순수 벡터 검색(pure vector search)은 매일 이 함정에 빠집니다. 임베딩(Embeddings)은 의미를 포착하도록 훈련되었으며, ERR_CONN_RESET_4290은 문장 트랜스포머(sentence transformer)에게 거의 아무런 의미가 없습니다. 이는 코퍼스(corpus) 내의 다른 모든 에러 문자열 근처에 위치하는 벡터로 평균화되는 토큰 수프(token soup)일 뿐입니다. 인간이 0.5초 만에 발견할 수 있는 정확한 일치(exact match)가 코사인 유사도(cosine similarity)에는 보이지 않는 것입니다.

밀집 검색(Dense retrieval)이 조용히 실패하는 지점

밀집 임베딩(Dense embeddings)은 의역(paraphrase)에서 승리합니다. "구독을 어떻게 취소하나요"라고 물으면, 단어가 하나도 공유되지 않더라도 "구독 종료(terminating your subscription)"라는 제목의 문서를 찾아냅니다. 이것이 바로 벡터 검색이 RAG를 장악한 이유입니다.

그 대가는 정확한 문자에 의존하는 모든 것을 뭉개버린다는 점입니다. 이 문제가 발생하는 카테고리는 다음과 같습니다:

  • 식별자 (Identifiers): 주문 번호, 사용자 ID, 티켓 참조 번호, 커밋 SHA.
  • 제품 코드 및 SKU: MX-4400-BLK, iPhone15,2, 부품 번호.
  • 오류 코드 및 로그 라인 (Error codes and log lines): ERR_CONN_RESET_4290, HTTP 429, 스택 프레임 (stack frames).
  • 희귀 고유 명사 (Rare proper nouns): 모델이 본 적 없는 사람 이름, 설정 플래그 (config flag), 내부 서비스 이름.
  • 정확한 문구가 중요한 법률 또는 정책 언어: 단어 하나만 틀려도 의미가 변하는 경우.

임베딩 모델 (Embedding model)은 이 모든 것을 의미적 이웃 (semantic neighborhoods)에 최적화된 공간으로 매핑합니다. 하지만 SKU는 의미적 이웃이 없습니다. 그것은 이해되는 것이 아니라 매칭되어야 하는 대상입니다.

BM25는 여기서 여전히 승리하는 오래된 도구입니다

BM25는 단어 빈도 (term frequency)와 역문서 빈도 (inverse document frequency)를 기반으로 구축된 1990년대의 랭킹 함수 (ranking function)입니다. 이 함수는 쿼리 용어가 문서에 얼마나 자주 나타나는지를 기준으로 점수를 매기되, 전체 코퍼스 (corpus)에서 해당 용어가 얼마나 흔한지에 따라 가중치를 낮추고 문서 길이에 맞춰 조정합니다. 학습도 필요 없고, GPU도 필요 없으며, 벡터 (vectors)도 필요 없습니다.

정확한 용어 쿼리 (exact-term queries)에 있어서 BM25는 타의 추종을 불허합니다. 만약 ERR_CONN_RESET_4290이 전체 코퍼스에서 단 한 번 등장한다면, 해당 용어의 역문서 빈도 (inverse document frequency)가 엄청나게 높기 때문에 BM25는 그 문서를 첫 번째로 랭킹합니다. 결과적으로 런북 (runbook)이 표면 위로 드러나고, 모델은 올바른 컨텍스트 (context)를 얻게 됩니다.

다음은 토큰화된 코퍼스에 대해 rank_bm25 라이브러리를 사용하는 최소한의 BM25 리트리버 (retriever) 예시입니다.

from rank_bm25 import BM25Okapi
import re

...

이것이 희소 (sparse) 측면의 전부입니다. 이는 어휘적 관련성 (lexical relevance)에 따라 순위가 매겨진 문서 인덱스를 반환합니다. 토크나이저 (tokenizer)가 언더스코어 (_)와 숫자를 유지하므로, 오류 코드가 조각나지 않고 하나의 토큰으로 유지된다는 점에 주목하세요.

왜 하나만 선택하지 않는가

함정은 이것을 선택의 문제로 취급하는 것입니다. 밀집 (Dense)이냐 희소 (Sparse)냐 하는 문제 말이죠. 그렇지 않습니다. 각각은 상대방의 강점 영역에서 실패합니다.

"파손된 품목에 대한 환불 기간이 어떻게 되나요?"와 같은 쿼리는 밀집 리트리버 (dense retriever)를 필요로 합니다. 매칭되는 정책 문서에는 공유되는 단어가 거의 없이 "결함이 있는 제품은 30일 이내에 반품을 받습니다"라고 적혀 있을 수 있기 때문입니다. 반면 MX-4400-BLK 품절과 같은 쿼리는 BM25를 필요로 합니다. SKU 자체가 질문의 전부이기 때문입니다.

실제 운영 트래픽(Production traffic)은 이 두 가지가 혼합되어 있으며, 종종 동일한 쿼리 안에 공존합니다. 예를 들어, "MX-4400-BLK가 파손 정책(damage policy)에 포함되나요?"라는 질문은 SKU가 정확히 일치해야 함과 동시에 정책 내용이 의미론적(semantically)으로 일치해야 합니다. 따라서 두 리트리버(retriever)를 모두 실행하고 그 결과들을 병합해야 합니다.

상호 순위 결합 (Reciprocal Rank Fusion): 점수 튜닝 없는 병합

가장 단순한 병합 방식은 두 점수 리스트를 정규화(normalize)한 뒤 더하는 것입니다. 하지만 이 방식은 끊임없이 실패합니다. BM25 점수는 범위가 정해져 있지 않고 코퍼스(corpus)에 따라 달라집니다. 반면 코사인 유사도(Cosine similarity)는 상위권의 좁은 범위 내에 머뭅니다. 이들을 동일한 척도로 맞추려 하면, 코퍼스가 바뀌는 순간 가중치(weights)가 어긋나게 됩니다.

상호 순위 결합(Reciprocal Rank Fusion, RRF)은 이 문제 전체를 우회합니다. RRF는 원시 점수(raw scores)를 무시하고, 각 리스트에서 각 문서가 차지하는 순위(rank position)만을 고려합니다. 어느 한 리스트에서라도 높은 순위를 차지한 문서는 높은 결합 점수(fused score)를 얻게 됩니다. 한 문서에 대한 공식은 모든 순위 리스트에 대해 1 / (k + rank)를 합산하는 것이며, 여기서 k는 상수(원문 Cormack et al. 논문에서 제시된 일반적인 기본값은 60)이고, rank는 해당 리스트에서의 문서 위치입니다.

상수 k는 상위 순위의 기여도를 완만하게 만들어, 단일 리스트가 전체 결과를 지배하지 못하게 합니다. 이것이 RRF가 매우 다른 점수 분포 사이에서도 안정성을 유지하는 이유입니다. 이 방식에서는 그 어떤 것도 정규화할 필요가 없습니다.

from collections import defaultdict

def reciprocal_rank_fusion(
...

이것이 결합(fusion) 단계의 전부입니다. 이 함수는 개수에 상관없이 어떤 순위 리스트든 입력받아 하나의 병합된 순위를 반환합니다. 리스트가 BM25에서 왔는지, 벡터 스토어(vector store)에서 왔는지, 혹은 나중에 추가한 세 번째 리트리버에서 왔는지는 상관하지 않습니다.

약 50줄로 구현하는 하이브리드 리트리버

이제 두 리트리버를 연결하고 결합해 보겠습니다. 여기의 밀집(dense) 측면은 sentence-transformer와 인메모리 행렬(in-memory matrix)에 대한 코사인 유사도를 사용하므로, 벡터 데이터베이스 없이도 예제가 실행됩니다. DenseIndex를 사용 중인 Qdrant, pgvector 또는 Pinecone 클라이언트로 교체하더라도 결합 코드는 변경되지 않습니다.

import numpy as np
from sentence_transformers import SentenceTransformer

...

각 리트리버(Retriever)는 더 넓은 풀(여기서는 50개)을 가져오므로, 한쪽에서 40위로 랭크된 문서라도 퓨전(Fusion) 과정을 통해 순위가 올라갈 기회를 얻습니다. 두 풀을 합친 후 최종 k개로 자릅니다. 정확한 용어(Exact-term) 쿼리는 BM25 리스트를 통해 해당 문서를 찾아내고, 의역(Paraphrase) 쿼리는 밀집(Dense) 리스트를 통해 문서를 찾아내며, 병합된 쿼리는 두 가지 모두를 찾아냅니다.

프로덕션 환경에 맞게 구축하기

인메모리(In-memory) 버전은 개념을 익히기 위한 것입니다. 이를 실제로 운영할 때는 몇 가지 사항이 변경됩니다.

리트리버를 병렬로 실행하세요. 이들은 상태(State)를 공유하지 않습니다. BM25 쿼리와 벡터(Vector) 쿼리를 동시에 실행하고, 둘 다 반환되면 퓨전하세요. 이렇게 하면 하이브리드 단계의 지연 시간(Latency)은 두 작업의 합이 아니라, 더 느린 쪽의 시간이 됩니다.

스토리지에 BM25 기능이 있다면 이를 활용하세요. Postgres는 ts_rank를 이용한 전문 검색(Full-text search)을 지원합니다. Elasticsearch와 OpenSearch는 BM25를 기본 스코어러(Scorer)로 제공합니다. Qdrant, Weaviate, Milvus는 이제 희소 벡터(Sparse vectors)와 하이브리드 쿼리를 네이티브로 지원합니다. 별도의 rank_bm25 인덱스가 전혀 필요하지 않은 경우가 많으며, 전체 코퍼스(Corpus)를 메모리에 유지해야 하는 부담도 피할 수 있습니다.

트래픽이 편향되어 있다면 리스트에 가중치를 부여하세요. RRF(Reciprocal Rank Fusion)는 각 기여도를 스케일링함으로써 리스트별 가중치를 적용할 수 있습니다. 만약 코퍼스가 코드 중심적이고 정확한 일치(Exact match)가 더 중요하다면, BM25 리스트의 기여도에 곱셈을 적용하세요. 처음에는 동일한 가중치로 시작하고, 평가(Eval) 결과에 따라 조정하십시오.

퓨전된 상위 k개를 재순위화(Rerank)하세요. 하이브리드 퓨전은 올바른 문서를 후보군(Candidate set)에 포함시키는 역할을 합니다. 퓨전된 상위 50개에 대해 크로스 인코더(Cross-encoder) 재순위화 모델을 적용하면 실제 관련도(Relevance)에 따라 순서를 정렬할 수 있습니다. 퓨전은 재현율(Recall)을 개선하고, 재순위화는 정밀도(Precision)를 개선합니다. 이 둘은 상호 보완적으로 작동합니다.

밀집(Dense) 측면을 완전히 건너뛸 수 있는 경우

코퍼스(Corpus)가 작고 쿼리(Query)가 거의 모두 정확한 용어 검색(Exact-term lookup)인 경우(예: 부품 카탈로그, 로그 검색, 내부 ID 식별기), BM25만으로도 정확도와 비용 측면 모두에서 벡터 스토어(Vector store)를 이길 수 있습니다. 임베딩 모델(Embedding model)도, 인덱스 구축(Index build)도, GPU도 필요하지 않습니다. 더 무거운 도구를 찾기 전에 먼저 측정하십시오. 하이브리드(Hybrid) 방식의 핵심은 밀집(Dense) 방식이 항상 옳다는 것이 아닙니다. 두 리트리버(Retriever)가 서로 반대 방향으로 실패하며, 이 둘을 융합(Fusing)함으로써 각각이 남기는 간극을 메울 수 있다는 점에 있습니다.

다음에 당신의 봇이 에러 코드나 SKU를 제대로 찾지 못한다면, 해결책은 아마 더 나은 임베딩 모델이 아닐 것입니다. 그것은 단번에 읽어 내려갈 수 있는 40줄 정도의 BM25 코드와 랭크 퓨전(Rank fusion)일 가능성이 높습니다.

이 내용이 유용했다면

하이브리드 검색(Hybrid retrieval)은 RAG Pocket Guide에서 청킹(Chunking), 리랭킹(Reranking), 그리고 이러한 변화가 코퍼스의 재현율(Recall)을 실제로 높였는지 알려주는 평가 방법론(Eval methodology)과 함께 처음부터 끝까지 다루는 패턴 중 하나입니다. 만약 당신의 검색 계층(Retrieval layer)이 사용자가 입력한 정확한 내용을 계속 놓치고 있다면, 그 부분부터 책을 읽기 시작하십시오.

RAG Pocket Guide

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0