하이브리드 쿼리
하이브리드 쿼리 (Hybrid Queries)
포인트마다 여러 개의 named vector를 쓸 수 있게 되면서, 여러 쿼리를 결합하거나 검색을 여러 단계로 수행할 때 가장 좋은 결과가 나오는 경우가 생겼어요. 예를 들어 텍스트 검색에서는 밀집 벡터의 의미 이해와 희소 벡터의 정확한 단어 매칭을 함께 얻기 위해 둘을 결합하는 게 유용하죠. Qdrant는 이를 가능하게 하는 유연한 통합 인터페이스인 Query API를 제공해요.
v1.10.0부터 사용 가능
쿼리 결합의 핵심 구성 요소는 prefetch 파라미터예요. 쿼리에 prefetch가 하나라도 있으면 Qdrant는 먼저 prefetch 쿼리를 수행하고, 그 결과에 대해 메인 쿼리를 적용해요. prefetch 안에 다시 prefetch를 넣어 중첩할 수도 있어요. offset 파라미터는 메인 쿼리에만 영향을 주므로, prefetch의 limit은 메인 쿼리의 limit + offset 이상이어야 해요. 그렇지 않으면 빈 결과를 받을 수 있어요.
하이브리드 검색 (Hybrid Search)
서로 다른 표현으로 같은 데이터를 갖고 있을 때, 각 표현에 대한 쿼리 결과를 단일 결과로 결합하는 문제는 굉장히 흔해요. Qdrant는 다른 쿼리들의 결과를 융합(fusing)하는 몇 가지 방법을 제공해요: rrf와 dbsf.
Reciprocal Rank Fusion (RRF)
RRF는 각 쿼리 내에서 결과의 위치(랭크) 를 고려해, 여러 결과 집합에서 상위에 가깝게 나타나는 문서를 부스팅해요. 문서의 점수는 각 결과 집합에서의 랭크로 계산돼요:
$$ score(d\in D) = \sum_{r_d\in R(d)} \frac{1}{k + \frac{r_d + 1}{w_r} - 1} $$
여기서 $D$는 모든 결과에 걸친 포인트 집합, $R(d)$는 특정 문서의 랭크 집합, $k$는 상수(기본 2), $r_d$는 랭킹 $r$에서 문서 $d$의 랭크, $w_r$은 랭킹 $r$의 가중치(기본 1)예요. Qdrant는 0-기반 랭크 위치를 사용해요 — 최상위 결과는 $r_d = 0$이죠. 가중치를 명시하지 않으면 $w_r$이 1이라 공식이 원래 RRF 함수로 단순화돼요:
$$ score(d\in D) = \sum_{r_d\in R(d)} \frac{1}{k + r_d} $$
하이브리드 검색에서 RRF를 쓰는 전형적인 예는 희소·밀집 벡터를 각각 보유한 서로 다른 named vector에 대해 두 prefetch를 수행하는 경우예요.
RRF 상수 k 설정 — v1.16.0부터 공식의 상수 $k$를 설정할 수 있어요.
가중 RRF (Weighted RRF) — v1.17.0부터 기본적으로 각 쿼리에 동일한 가중치가 부여돼요. 그런데 실제로는 자연어 쿼리에서 밀집 retriever가 우세하고, 식별자 위주 쿼리에서는 BM25가 이기는 등 retriever마다 강점이 달라요. rrf 쿼리는 각 prefetch의 상대 가중치를 구성할 수 있게 해줘요. 예를 들어 두 prefetch에 첫 번째 3.0, 두 번째 1.0의 가중치를 주면, 첫 번째 쿼리에서 3등인 문서가 두 번째 쿼리에서 1등인 문서와 같은 점수를 받아요. 가중치는 배열의 숫자로 제공하며, prefetch가 정의된 순서대로 각각 적용되고 가중치 개수는 prefetch 개수와 일치해야 해요.
가중치는 마음대로 튜닝할 게 아니라 데이터로 테스트해서 설정하는 게 가장 확실해요. 평가셋(알려진 관련 문서가 있는 쿼리)이 있으면 평가 쿼리를 둘로 나눠 첫 절반으로 가중치를 시도하고 둘째 절반으로 측정하세요. 튜닝한 쿼리로 측정하면 결과가 부풀어요. 평가셋이 없으면 기본 (1.0, 1.0)을 유지하세요 — 측정 없는 수동 튜닝이 기본값을 이기기 어려워요. retriever가 바뀌거나(새 임베딩 모델, 새 청킹) 코퍼스가 크게 변하면 재튜닝하세요.
Distribution-Based Score Fusion (DBSF)
v1.11.0부터
DBSF는 각 쿼리의 원시 점수를 유지하되 결합 전에 분포를 정규화해요. 각 retriever의 반환 집합에 대해 평균 $\mu$와 표본 표준편차 $\sigma$를 계산하고, 3-시그마 극값을 끝점으로 사용해 모든 점수를 정규화해요:
$$ \hat{s} = \frac{s - (\mu - 3\sigma)}{6\sigma} $$
정규화된 점수를 retriever 간에 합산해요. 각 retriever가 같은 비교 가능한 범위로 기여하므로 점수 크기 차이가 더 이상 문제가 되지 않아요.
dbsf는 상태 비저장(stateless)이라 정규화 한계를 각 쿼리가 반환한 포인트들로부터 계산해요. 점수는 [0, 1]로 클리핑되지 않아요. 반환된 점수가 모두 동일하거나(또는 포인트 하나만 반환) 0으로 나눌 상황이면 DBSF는 0.5를 내보내요.
DBSF는 retriever의 원시 점수가 크기(magnitude) 정보를 담는다고 신뢰할 때 합리적인 선택이에요. 보정이 잘 된 retriever에서는 DBSF가 튜닝된 가중 RRF를 이길 수 있고, 그 외에는 가중 RRF가 이겨요. 어느 쪽도 일반적으로 우세하지 않으므로 평가셋으로 골라야 해요. 두 가지 주의점: 통계가 prefetch top-k(작은 표본)에서 나오고, 그 안의 단일 지배적 이상치가 해당 쿼리의 정규화를 왜곡할 수 있어요. 순위가 불안정하면 prefetch limit을 늘려보세요.
Fusion 방법 선택
| 상황 | 선택 |
|---|---|
| 튜닝할 평가셋(알려진 관련 문서가 있는 쿼리) 있음 | train/val 분할로 가중치를 튜닝한 Weighted RRF |
| retriever 원시 점수를 신뢰하고 평가셋 없음 | DBSF |
| 평가셋도 강한 점수 사전지식도 없음 | RRF (안전한 기본값) |
흔한 요청 중 하나가 "밀집·희소 점수의 알파-가중 선형 결합"인데, 이는 점수를 먼저 정규화하지 않으면 신뢰할 수 없어요. 밀집(코사인, 경계 있음)과 희소(BM25, 경계 없음) 점수는 서로 다른 스케일을 갖고 쿼리마다도 변하므로, 원시 점수에 고정 알파를 쓰면 어느 한쪽이 지배하게 돼요. RRF는 랭크로, DBSF는 분포 정규화로 이 문제를 우회해요.
Multi-Stage 쿼리 (다단계 쿼리)
일반적으로 더 큰 벡터 표현이 더 정확한 검색 결과를 주지만 계산 비용이 비싸요. 이 효과를 완화하는 알려진 기법이 검색을 두 단계로 나누는 거예요:
- 먼저 더 작고 저렴한 표현으로 큰 후보 목록을 얻는다.
- 그리고 더 크고 정확한 표현으로 후보를 재스코어링한다.
이 아이디어를 중심으로 검색 아키텍처를 만드는 방법은 여러 가지예요:
- 첫 단계에 양자화된 벡터, 두 번째 단계에 전체 정밀도 벡터
- MRL(Matryoshka Representation Learning)로 짧은 벡터로 후보를 생성하고 긴 벡터로 정제
- 일반 밀집 벡터로 후보를 prefetch하고 ColBERT 같은 멀티벡터 모델로 재스코어링
Qdrant는 이런 단계적 쿼리를 위한 편리한 인터페이스를 제공해서, 거친 결과를 먼저 가져오고 나중에 더 큰 벡터로 정제하게 해줘요. rescoring에만 쓰는 벡터는 HNSW 인덱스를 끄면 메모리를 아낄 수 있어요 — rescoring은 HNSW를 쓰지 않으므로 해당 벡터의 HNSW 설정에서 m=0으로 두면 돼요.
예시: 짧은 MRL 바이트 벡터로 1000개 결과를 가져온 뒤 전체 벡터로 재스코어링해 상위 10개를 얻거나, 기본 벡터로 100개를 가져온 뒤 멀티벡터로 재스코어링해 상위 10개를 얻는 식이에요. 이런 기법들을 단일 쿼리에 모두 결합할 수도 있어요.
Formula Query로 커스텀 스코어링
v1.14.0부터
formula 쿼리는 prefetch 점수($score), payload 필드, 지수·가우시안 decay 같은 내장 헬퍼로 최종 점수를 구성하게 해줘요. 전형적인 패턴은 prefetch에서 RRF나 DBSF로 retriever를 융합한 뒤, 그 prefetch를 재랭킹 로직(신선도 decay, 인기 부스팅, 지리 decay, 카테고리 조건부 배수 등)을 얹는 formula 쿼리로 감싸는 거예요.
주의: decay 가중치를 융합된 $score의 스케일에 맞춰 보정하세요. RRF 점수는 작고(1/(k+rank) 항의 합), decay 함수는 [0, 1] 값을 반환하므로, 가중치 없는 decay 항은 더 작은 계수를 곱하지 않으면 융합 점수를 지배하게 돼요. decay를 작업에 맞게 튜닝된 계수를 곱하는 곱셈 표현식으로 감싸세요.
분산 컬렉션에서의 Fusion
앞의 예는 fusion을 prefetch 안에 두는데, 멀티샤드 컬렉션에서 fusion은 메인 쿼리일 때만 모든 샤드에 걸쳐 결과를 병합해요 — top-level query 필드에 retriever를 prefetch로 두는 식이죠. fusion이 prefetch 안에 있으면 각 샤드가 로컬 결과에 대해 fusion을 계산하므로 융합 순위가 전역이 아닌 샤드별이 돼요. 샤드 간에 융합하려면 fusion을 메인 쿼리로 만들어야 해요. 메인 쿼리는 단일 연산이므로 fusion과 formula를 동시에 할 수 없고, 융합 결과 위에 formula 재스코어를 유지하려면 단일 샤드를 써야 해요.
Grouping
v1.11.0부터
여러 포인트가 같은 항목에 속할 때 결과에서 같은 항목의 중복을 피하려면 특정 필드로 결과를 그룹화할 수 있어요. REST API(Schema)로 제공되며, 그룹화·lookup의 자세한 내용은 검색 문서의 grouping과 lookup 레퍼런스를 참고하세요.