검색 결과의 순위 결합
검색 점수 또는 순위로 결과를 결합하는 일곱 가지 방법을 비교하고, RRF로 키워드 검색과 벡터 검색의 결과를 합친다.
하이브리드 검색과 순위 결합
- 하이브리드 검색은 키워드 검색과 벡터 검색의 결과를 하나의 목록으로 결합
- 키워드 검색(BM25)은 질문과 일치하는 단어가 있는 청크에 높은 점수 부여
- 벡터 검색은 질문과 의미가 가까운 청크에 높은 점수 부여
- 각 검색기가 매긴 점수 또는 각 목록에서의 순위로 청크별 결합 점수를 계산한 뒤 높은 순으로 정렬
검색 점수를 사용하는 방법
- 점수 정규화는 검색기마다 다른 점수 척도를 맞추는 과정
- BM25 점수에는 고정된 상한이 없고, 코사인 유사도의 범위는 −1에서 1
- 원점수를 그대로 더하면 점수 범위가 큰 검색기의 영향이 커지므로 합산 전에 정규화
- 각 청크가 등장한 결과 목록만 계산에 포함
- si는 검색기 i가 해당 청크에 준 정규화 점수
- m은 해당 청크가 등장한 결과 목록의 수
| 방법 | 청크의 결합 점수 | 해석 |
|---|---|---|
| CombSUM | ∑isi | 검색기별 점수의 합계 |
| CombMNZ | m∑isi | 점수 합계에 목록 수를 곱해 여러 검색기가 함께 찾은 청크에 가중치 부여 |
- 정규화한 점수로 청크 A와 B를 비교
- 청크 A는 키워드 검색에서 0.9점. 벡터 검색 결과에는 미포함
- 청크 B는 키워드 검색에서 0.5점, 벡터 검색에서 0.4점
- CombSUM에서는 A와 B 모두 0.9점으로 동점(0.9=0.5+0.4)
- CombMNZ에서는 A가 1×0.9=0.9점, B가 2×(0.5+0.4)=1.8점으로 두 검색 결과에 모두 등장한 B가 상위
순위를 사용하는 방법
- 청크가 등장한 각 목록에서 순위를 점수로 바꾼 뒤 합산
- 원점수 대신 순위만 쓰므로 점수 척도가 달라도 정규화가 필요 없음
- ri는 검색기 i의 결과 목록에서 해당 청크의 순위(1위부터 시작)
- ni는 그 목록에 포함된 청크 수
| 방법 | 청크의 결합 점수 | 해석 |
|---|---|---|
| 보르다(Borda) | ∑inini−ri+1 | 한 등수 내려갈 때마다 같은 폭으로 점수 감소. 결과가 10개면 1위는 1점, 2위는 0.9점, 10위는 0.1점 |
| RRF (Reciprocal Rank Fusion) | ∑ik+ri1 | 순위에 상수 k를 더한 값의 역수를 합산. 흔히 쓰는 k=60에서는 1위와 2위가 각각 1/61, 1/62점으로 차이가 작아 여러 목록에 든 청크에 유리 |
| ISR (Inverse Square Rank) | m∑iri21 | 순위 제곱의 역수를 합산한 뒤 목록 수를 곱해 상위권과 중복 등장에 가중치 부여. 한 목록에서 2위의 기여분은 1위의 1/4 |
| logISR | log(m)∑iri21 | 목록 수가 늘어날 때 가중치는 ISR보다 완만하게 증가. 한 목록에만 등장한 청크는 log(1)=0이므로 1위여도 0점 |
| RBC (Rank Biased Centroids) | ∑i(1−ϕ)ϕri−1 | 한 등수 내려갈 때마다 직전 점수에 ϕ를 곱해 상위권에 점수 집중. 0<ϕ<1에서 ϕ가 작을수록 점수가 빠르게 감소. ϕ=0.5이면 1위부터 3위까지는 각각 0.5점, 0.25점, 0.125점 |
출처 — Benham과 Culpepper(2017), 「Risk-Reward Trade-offs in Rank Fusion」 표 1
같은 검색 결과에서 RRF와 ISR 비교
| 청크 | 키워드 검색 | 벡터 검색 |
|---|---|---|
| A | 1위 | 목록에 없음 |
| B | 3위 | 3위 |
- RRF(k=60)에서는 두 검색기가 함께 찾은 B가 더 높은 순위
- A: 1/(60+1)≈0.0164점
- B: 1/(60+3)+1/(60+3)≈0.0317점
- ISR에서는 키워드 검색 1위인 A가 더 높은 순위
- A: 1×1/12=1점
- B: 2×(1/32+1/32)=4/9점
검색 순위 합치기
- 앞 실습의
search_bm25와search_dense로 같은 질문을 검색 search_hybrid는 두 목록의 순위를 RRF 점수로 바꿔 청크별로 합산
def search_hybrid(query, k=3, rrf_k=60): # 두 검색기의 전체 순위를 RRF로 합침
fused = {}
for results in (search_bm25(query, len(chunks)), search_dense(query, len(chunks))):
for rank, (i, _) in enumerate(results, start=1):
fused[i] = fused.get(i, 0) + 1 / (rrf_k + rank)
top = sorted(fused.items(), key=lambda item: -item[1])[:k]
return [(i, round(score, 4)) for i, score in top]
query = "불량품이면 반품 기한이 더 길어요?"
print("BM25:", search_bm25(query))
print("벡터:", search_dense(query))
print("RRF:", search_hybrid(query))
실행 결과
BM25: [(10, 4.37), (11, 3.04), (13, 2.88)]
벡터: [(13, 0.548), (10, 0.538), (15, 0.52)]
RRF: [(10, 0.0325), (13, 0.0323), (11, 0.0318)]
- 정답은 불량품의 반품 기한을 담은 10번 청크. 벡터 검색은 반품 후 환불 절차를 다룬 13번을 1위로 올림.
- 10번은 BM25 1위, 벡터 2위라 1/61+1/62≈0.0325. 13번은 BM25 3위, 벡터 1위라 1/63+1/61≈0.0323. 두 목록에서 모두 높은 10번이 1위.
- 항상 나아지는 것은 아님. "키캡 뺐는데 환불 돼요?"는 BM25 1위가 정답 12번이지만, 벡터 검색 상위권에 없어 RRF 상위 3개에서 빠짐.
query를 바꿔 직접 비교해 볼 것.
퀴즈
키워드 검색과 벡터 검색의 점수를 정규화하는 이유는?
퀴즈를 풀려면 대화형 기능을 불러와야 합니다.