본문으로 이동
메뉴 여닫기
환경 설정 메뉴 여닫기
개인 메뉴 여닫기
로그인하지 않음
지금 편집한다면 당신의 IP 주소가 공개될 수 있습니다.
Hybrid search; 하이브리드 검색
단어 일치 기반 어휘 검색과 임베딩 기반 의미 검색을 함께 돌려 결과를 하나의 순위로 합치는 검색 방식

하이브리드 검색(hybrid search)은 BM25 같은 어휘(lexical, 희소) 검색과 문장 임베딩을 이용한 밀집(dense, 의미) 벡터 검색을 같은 질의로 실행하고, 두 결과 목록을 하나로 합쳐 내놓는 방식이다. 두 방식이 서로 다른 종류의 질의에서 실패하므로 합치면 어느 한쪽만 쓸 때보다 안정적인 결과를 얻는다는 생각에서 출발했다. RAG 시스템의 검색 단계와 사내 문서 검색, 전자상거래 상품 검색에서 쓰인다.

RAG (인공지능) 문서의 희소 검색과 하이브리드 검색 절에 개요가 있다. 이 문서는 두 결과를 합치는 융합 함수의 수식과 특성, 주요 검색 엔진과 벡터 데이터베이스의 지원 방식, 구현과 조정 방법을 다룬다.

질의 유형 어휘 검색(BM25) 밀집 벡터 검색
제품 번호, 오류 코드("E1234"), 법 조항 번호 강함(정확 일치) 약함(드문 문자열의 임베딩이 불안정)
고유명사, 신조어, 사내 약어 강함 학습 데이터에 없으면 약함
의역, 동의어("자동차 고장" → "차량 수리") 약함(겹치는 단어 없음) 강함
자연어 질문("휴가를 쪼개 쓸 수 있나요?") 보통 강함
다른 언어로 된 문서 불가 다국어 모델이면 가능

학습 데이터와 다른 분야에서는 밀집 검색 모델이 BM25보다 못한 경우가 많다는 BEIR 벤치마크 결과도 하이브리드를 택하는 근거다.[1]

  1. 질의를 두 검색기에 동시에 보낸다. 어휘 검색은 역색인에서, 밀집 검색은 HNSW 같은 근사 최근접 이웃 색인에서 각각 상위 N개(후보 창)를 가져온다.
  2. 두 목록을 융합 함수로 합쳐 하나의 순위를 만든다. 한쪽에만 있는 문서도 포함된다.
  3. 필요하면 상위 몇십 개를 리랭커로 다시 정렬한다.

권한, 날짜, 카테고리 같은 필터는 두 검색기 모두에 똑같이 걸어야 한다. 한쪽에만 걸면 걸러진 문서가 다른 쪽을 통해 다시 들어온다.

두 검색기의 점수는 척도가 다르다. BM25 점수는 상한이 없고 질의 길이에 따라 커지며, 코사인 유사도는 -1~1 사이이지만 모델마다 실제 분포가 좁은 구간에 몰려 있다. 그래서 점수를 그대로 더할 수 없고, 순위만 쓰거나 점수를 정규화한 뒤 합친다.

상호 순위 융합(RRF)

편집 원본 편집

상호 순위 융합(Reciprocal Rank Fusion, RRF)은 점수를 버리고 순위만 쓴다. 2009년 코맥(Cormack) 등이 제안했으며, 문서 d의 점수는

RRF(d)=∑r∈Rwrk+rankr(d)

이다. R은 검색기들의 결과 목록, rankr(d)는 목록 r에서 d의 순위(1부터), 목록에 없으면 그 항은 0이다. 원 논문은 k=60, 가중치 wr=1을 썼다.[2] k가 크면 1위와 10위의 점수 차가 줄어 여러 목록에 고르게 등장한 문서가 유리해지고, 작으면 한 목록의 최상위 문서가 강하게 밀린다.

  • 장점: 정규화가 필요 없고, 점수 분포가 다른 어떤 검색기도 바로 합칠 수 있으며, 조정할 것이 거의 없다.
  • 단점: 점수 차이 정보를 버린다. 한쪽에서 압도적으로 맞는 문서(정확히 일치하는 오류 코드)와 근소하게 1위인 문서를 같게 취급한다.

점수 정규화 후 가중 결합

편집 원본 편집

각 검색기의 점수를 같은 범위로 정규화한 뒤 가중합(볼록 결합, convex combination)을 한다.

s(d)=α⋅ϕ(sdense(d))+(1−α)⋅ϕ(slex(d))

정규화 함수 ϕ로는 다음이 쓰인다.

  • 최소-최대(min-max): ϕ(s)=s−minmax⁡−min. 후보 창 안의 최소·최대를 쓰므로 창 크기에 따라 값이 바뀐다.
  • z 점수: 평균을 빼고 표준편차로 나눈다.
  • 이론적 최소-최대: 코사인처럼 이론적 하한이 정해진 점수는 관측값 대신 이론적 최솟값을 쓴다.
  • L2 정규화: 점수 벡터의 L2 노름으로 나눈다.

브루크(Bruch) 등은 2022년 분석에서 RRF가 생각보다 매개변수에 민감하고, 볼록 결합이 학습 분야 안팎 모두에서 RRF보다 나았으며, 가중치 α 하나만 맞추면 되므로 적은 수의 레이블 예제로도 조정할 수 있다고 보고했다. 볼록 결합의 성능은 정규화 방식 선택에 크게 좌우되지 않았다.[3] 정리하면, 평가 데이터가 없으면 RRF로 시작하고, 질의-정답 쌍을 수십~수백 개라도 만들 수 있으면 가중 결합의 α를 맞춰 보는 것이 합리적이다.

분포 기반 점수 융합(DBSF)

편집 원본 편집

Qdrant의 DBSF(Distribution-Based Score Fusion)는 검색기마다 점수 분포를 정규화한 뒤 합친다.[4] 최소-최대처럼 극단값 하나에 흔들리는 문제를 줄이려는 방식이다.

한 모델로 두 표현

편집 원본 편집

SPLADE처럼 언어 모델로 단어 가중치(희소 벡터)를 학습하는 방법이나, BGE-M3처럼 한 모델이 밀집 벡터와 희소 가중치, 토큰별 다중 벡터를 함께 내는 모델을 쓰면 하나의 인코더로 하이브리드 검색을 구성할 수 있다.[5] 이 경우 희소 쪽도 학습된 가중치라 BM25보다 의미를 더 반영하지만, 형태소 분석이나 사용자 사전으로 제어하던 정확 일치 동작은 달라진다.

검색 엔진·벡터 DB의 지원

편집 원본 편집

2026년 10월 기준 각 제품의 공식 문서에서 확인한 내용이다. 버전에 따라 기본값과 API가 바뀌므로 쓰는 버전의 문서를 다시 확인해야 한다.

제품 방식 융합 기본값·비고
엘라스틱서치 리트리버 API로 BM25(standard)와 kNN 리트리버를 묶음 RRF 리트리버, linear 리트리버(정규화: none, minmax, l2_norm) RRF rank_constant 기본 60, rank_window_size 기본 10, 리트리버별 가중치 가능[6][7]
OpenSearch hybrid 쿼리(하위 쿼리 최대 5개) + 검색 파이프라인 정규화 프로세서(min_max, l2, z_score)와 결합(산술·기하·조화 평균), score-ranker 프로세서로 RRF 정규화·결합은 검색 파이프라인에서 지정[8]
Weaviate hybrid 질의 하나로 키워드(BM25) 검색과 벡터 검색 relativeScoreFusion, rankedFusion alpha=1이면 순수 벡터, 0이면 순수 키워드. v1.24부터 relativeScoreFusion이 기본[9]
Qdrant Query API의 prefetch로 희소·밀집 벡터를 각각 검색 후 융합 RRF, DBSF Query API는 v1.10.0, DBSF는 v1.11.0부터. RRF의 k 기본값은 2이며 v1.16.0부터 바꿀 수 있음[4]
Milvus hybrid_search에 필드별 AnnSearchRequest 여러 개 RRFRanker, WeightedRanker RRF k는 보통 60, 0 초과 16384 미만. 텍스트 필드에서 BM25 희소 벡터를 만드는 내장 함수 제공[10]
PostgreSQL + pgvector 내장 융합 없음. PostgreSQL 전문 검색과 벡터 검색을 따로 질의 애플리케이션에서 RRF나 크로스 인코더로 합침 README에 RRF·크로스 인코더 예제 링크[11]

Qdrant의 RRF 기본 k가 원 논문의 60과 다른 것처럼, 같은 "RRF"라도 제품마다 상수가 다를 수 있다.

RRF와 가중 결합

편집 원본 편집
def rrf(rankings, k=60, weights=None):
    """rankings: 검색기별 [문서ID, ...] (1위부터)"""
    weights = weights or [1.0] * len(rankings)
    scores = {}
    for w, ranking in zip(weights, rankings):
        for rank, doc in enumerate(ranking, start=1):
            scores[doc] = scores.get(doc, 0.0) + w / (k + rank)
    return sorted(scores.items(), key=lambda x: x[1], reverse=True)

def minmax(scores):
    lo, hi = min(scores.values()), max(scores.values())
    return {d: (s - lo) / (hi - lo) if hi > lo else 1.0 for d, s in scores.items()}

def convex(lex, dense, alpha=0.5):
    """lex, dense: {문서ID: 원점수}. 한쪽에 없는 문서는 그쪽 점수 0으로 본다."""
    lex, dense = minmax(lex), minmax(dense)
    fused = {d: alpha * dense.get(d, 0.0) + (1 - alpha) * lex.get(d, 0.0)
             for d in set(lex) | set(dense)}
    return sorted(fused.items(), key=lambda x: x[1], reverse=True)

bm25_rank = ["d3", "d1", "d7", "d2"]
dense_rank = ["d1", "d5", "d3", "d9"]
print(rrf([bm25_rank, dense_rank]))
#d1 0.03252, d3 0.03227, d5 0.01613, d7 0.01587, ...
print(convex({"d3": 12.1, "d1": 9.8, "d7": 4.0},
             {"d1": 0.83, "d5": 0.80, "d3": 0.71}))
#d1 0.858, d3 0.5, d5 0.375, d7 0.0

두 목록에서 모두 상위인 d1, d3이 위로 오고, 한쪽에서만 나온 d5, d7이 그 뒤에 온다.

희소 벡터와 밀집 벡터를 각각 prefetch로 검색한 뒤 RRF로 합치는 예다(Qdrant 문서 예제).[4]

from qdrant_client import QdrantClient, models

client = QdrantClient(url="http://localhost:6333")
client.query_points(
    collection_name="docs",
    prefetch=[
        models.Prefetch(query=models.SparseVector(indices=[1, 42], values=[0.22, 0.8]),
                        using="sparse", limit=20),
        models.Prefetch(query=[0.01, 0.45, 0.67], using="dense", limit=20),
    ],
    query=models.RrfQuery(rrf=models.Rrf()),
    limit=10,
)

엘라스틱서치

편집 원본 편집
{
  "retriever": {
    "rrf": {
      "retrievers": [
        { "standard": { "query": { "match": { "body": "휴가 분할 사용" } } } },
        { "knn": { "field": "body_vector", "query_vector": [0.12, -0.03, 0.88],
                   "k": 50, "num_candidates": 200 } }
      ],
      "rank_constant": 60,
      "rank_window_size": 50
    }
  }
}

rank_window_size는 각 리트리버에서 융합에 넣을 후보 수다. 기본값 10은 작으므로, 뒤에 리랭커를 둔다면 넉넉히 키운다.[6]

  • 후보 창: 각 검색기에서 몇 개를 가져올지(보통 수십~수백). 너무 작으면 한쪽에서 11위인 정답이 융합에 들어오지도 못한다.
  • 가중치: 질의 유형에 따라 최적 비율이 다르다. 코드·번호가 많은 질의 로그라면 어휘 쪽 가중치를 높이고, 자연어 질문이 많으면 밀집 쪽을 높인다. 질의를 분류해 가중치를 바꾸는 방식도 쓴다.
  • 평가: 실제 질의와 정답 문서 쌍으로 Recall@k, nDCG@10, MRR을 잰다. 어휘만, 밀집만, 하이브리드, 하이브리드+리랭커를 같은 평가 세트로 비교해야 효과를 알 수 있다.
  • 지연 시간: 두 검색을 병렬로 실행하면 지연은 느린 쪽에 맞춰지고, 비용은 두 색인을 유지하는 만큼 늘어난다.
  • 한국어: 어휘 쪽 품질은 형태소 분석과 사용자 사전에 달려 있다. 형태소 분석이 신조어를 잘못 자르면 그 질의는 밀집 쪽에만 의존하게 된다.
  1. ↑ Nandan Thakur 외, 〈BEIR: A Heterogenous Benchmark for Zero-shot Evaluation of Information Retrieval Models〉, NeurIPS 2021. arXiv:2104.08663
  2. ↑ Gordon V. Cormack, Charles L. A. Clarke, Stefan Büttcher, 〈Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning Methods〉, SIGIR 2009. PDF
  3. ↑ Sebastian Bruch, Siyu Gai, Amir Ingber, 〈An Analysis of Fusion Functions for Hybrid Retrieval〉, 2022. arXiv:2210.11934
  4. ↑ 4.0 4.1 4.2 Qdrant 문서, Hybrid Queries
  5. ↑ Jianlv Chen 외, 〈M3-Embedding: Multi-Linguality, Multi-Functionality, Multi-Granularity Text Embeddings Through Self-Knowledge Distillation〉, 2024. arXiv:2402.03216
  6. ↑ 6.0 6.1 Elasticsearch 문서, RRF retriever
  7. ↑ Elasticsearch 문서, Linear retriever
  8. ↑ OpenSearch 문서, Hybrid search; Hybrid query
  9. ↑ Weaviate 문서, Hybrid search
  10. ↑ Milvus 문서, RRF Ranker; Multi-Vector Hybrid Search
  11. ↑ pgvector README, Hybrid Search 절