본문 바로가기
AI2026년 7월 25일6분 읽기

하이브리드 검색 — BM25와 벡터를 RRF로 합치기

YS
김영삼
조회 4
하이브리드 검색 — BM25와 벡터를 RRF로 합치기

하이브리드 검색은 키워드 기반 검색(BM25 같은 희소 검색)과 임베딩 기반 벡터 검색(밀집 검색)을 함께 돌린 뒤 두 결과를 하나의 순위로 합치는 방식이다. 벡터 검색이 놓치는 "정확한 단어·코드·품번" 같은 신호를 키워드 검색이 잡아주고, 키워드 검색이 놓치는 "의미는 같은데 표현이 다른" 문장을 벡터 검색이 잡아준다. 둘의 약점이 서로 다르다는 게 핵심이다.

솔직히 처음엔 "벡터 검색이 의미까지 이해하는데 굳이 옛날 방식인 BM25를 왜 섞나" 싶었다. 그런데 사내 문서에서 ERR_5021 같은 에러 코드나 정확한 API 이름을 물으면 벡터 검색이 엉뚱한 걸 물어오는 일이 잦았다. 임베딩은 희귀 토큰을 뭉개는 경향이 있어서다. 그때 BM25를 다시 꺼내들었다.

두 검색의 성격 차이

상황BM25(키워드)벡터(임베딩)
정확한 품번·코드·고유명사강함약함
동의어·다른 표현약함강함
오타·약어중간중간
긴 자연어 질문약함강함

표를 보면 왜 섞는지가 분명해진다. 둘의 강점이 거의 반대다. 한쪽만 쓰면 반대편 질문에서 무너진다.

문제: 점수를 그냥 더하면 안 된다

두 검색을 합칠 때 초보들이 흔히 하는 실수가 BM25 점수와 코사인 유사도를 그냥 가중합하는 것이다. 그런데 BM25 점수는 상한이 없는 값(0~수십)이고 코사인은 -1~1이다. 스케일이 완전히 달라서 정규화 없이 더하면 한쪽이 지배한다. min-max 정규화를 해도 쿼리마다 분포가 달라 불안정하다.

그래서 실무에서 널리 쓰는 게 RRF(Reciprocal Rank Fusion, 역순위 융합)다. 점수 값 자체를 버리고 순위만 쓴다. 각 검색에서 문서가 몇 등인지만 보고 합치기 때문에 스케일 문제가 원천적으로 사라진다.

def rrf_fuse(bm25_ranking, vector_ranking, k=60):
    # 각 리스트는 문서 id를 순위 순서대로 담고 있다
    scores = {}
    for ranking in (bm25_ranking, vector_ranking):
        for rank, doc_id in enumerate(ranking):
            # 1등이면 1/(k+1), 2등이면 1/(k+2) ...
            scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1)
    return sorted(scores, key=scores.get, reverse=True)

공식은 score = Σ 1/(k + rank)로 단순하다. 상수 k(보통 60)는 상위 순위의 영향력을 얼마나 완만하게 할지를 조절한다. k가 작으면 1등에 극단적으로 쏠리고, 크면 순위 차이가 완만하게 반영된다. 두 검색 모두에서 상위권에 든 문서가 자연스럽게 위로 올라온다는 게 RRF의 매력이다.

실전 배치

구현 순서는 이렇게 잡으면 무난하다.

  1. 같은 질의로 BM25와 벡터 검색을 각각 top-50 정도 뽑는다.
  2. RRF로 융합해 하나의 순위를 만든다.
  3. (선택) 융합 결과 상위 20~30개를 리랭커에 넘겨 최종 정밀 정렬한다.

하이브리드 검색과 리랭커는 경쟁 관계가 아니라 순서대로 얹는 관계다. 하이브리드가 recall을 넓게 확보하고, 리랭커가 그 안에서 정밀도를 높인다. 요즘 벡터 DB(예: 하이브리드 검색을 내장한 엔진들)는 BM25와 벡터를 한 쿼리로 돌리고 RRF까지 옵션으로 지원하는 경우가 많아, 직접 두 인덱스를 관리하지 않아도 되는 방향으로 가고 있다.

한국어에서 주의할 점

BM25는 토크나이징에 크게 좌우된다. 한국어를 공백 기준으로만 쪼개면 조사가 붙은 "환불을", "환불이"가 서로 다른 토큰이 되어 매칭이 깨진다.

한국어에서 키워드 검색을 제대로 쓰려면 형태소 분석기로 어간을 뽑아 색인하는 게 낫다. 이 전처리를 건너뛰면 "BM25를 넣었는데 왜 안 좋아지지?"라는 결론에 이르기 쉽다. 나도 여기서 한 번 헤맸다. 반대로 벡터 쪽은 다국어 임베딩 모델이 조사 변화를 어느 정도 흡수해주므로, 하이브리드로 묶으면 서로의 빈틈이 메워진다.

자주 묻는 질문

RRF 말고 가중합은 아예 못 쓰나요?

쓸 수는 있다. 다만 BM25 점수와 코사인 유사도의 스케일·분포가 쿼리마다 달라서 안정적인 가중치를 찾기가 까다롭다. 점수 정규화를 정교하게 하고 도메인 평가셋으로 가중치를 튜닝할 여력이 있다면 가중합이 더 세밀할 수 있지만, 손이 덜 가면서 튼튼한 기본값을 원하면 RRF가 낫다.

k 값(60)은 꼭 그대로 써야 하나요?

60은 원 논문에서 제안된 관례적 기본값일 뿐이다. 대체로 무난하지만 절대적이진 않다. 상위 순위를 더 강하게 반영하고 싶으면 낮추고, 두 검색 결과를 더 고르게 섞고 싶으면 높인다. 실제 튜닝 여지는 크지 않은 편이라, 나는 60으로 시작해 평가지표에 눈에 띄는 문제가 없으면 그냥 둔다.

벡터 검색만으로 충분한 경우는 없나요?

있다. 질문이 대부분 자연어 의미 검색이고 정확한 코드·고유명사 조회가 드물다면 벡터 단독으로도 괜찮다. 하이브리드는 공짜가 아니다 — 인덱스가 둘로 늘고 융합 단계가 추가된다. 벡터 검색이 특정 유형(에러코드, 품번, 정확한 함수명) 질문에서 반복적으로 무너질 때 도입을 고려하는 게 순서다.

두 검색의 top-k는 같게 맞춰야 하나요?

반드시 같을 필요는 없다. 다만 RRF는 순위 기반이라 한쪽만 극단적으로 크게 뽑으면 융합이 그쪽으로 치우칠 수 있다. 실무에선 둘 다 비슷하게(예: 각 50) 뽑아 융합한 뒤 리랭커로 좁히는 구성이 관리하기 편했다.

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록