Rank Fusion
Rank Fusion
- BM25 등 sparse 검색의 장점
- 별도의 모델 및 훈련 과정이 필요 없음
- 검색 속도가 빠름
- 학습시키지 않은 분야(out-of-domain)에서 성능이 높음
- 벡터 검색 등 dense 검색의 장점
- 동의어 등을 비롯한 단어/문장의 의미 반영
- Rank Fusion: 서로 다른 검색 결과 목록에서 등수를 결합
Rank fusion
- Reciprocal Rank Fusion: 등수의 역수 합계 (보통 𝑘 = 60)
- Inverse Square Rank: log(목록의 개수) × 등수 제곱의 역수 합계
- Rank Biased Centroids: 𝜙^등수 합계 (0 < 𝜙 < 1)
Benham & Culpepper (2017) Risk-reward trade-offs in rank fusion
성능 실험
- 단순 BM25로 검색하는 것과 비교할 때, Rank Fusion을 사용하는 것이 성능이 더 좋을 때(Win)도 있고, 나쁠 때(Loss)도 있음
- ISR, logISR을 제외하면 대체로 비슷하지만 RRF가 성능이 좀 더 좋음
SQL 문
-- (1) 벡터 검색 결과
WITH vector_result AS (
SELECT
id,
title,
title_embedding <=> CAST(:query_emb AS vector) AS score,
ROW_NUMBER() OVER (
ORDER BY title_embedding <=> CAST(:query_emb AS vector)
) AS rank_vec
FROM wikipages
LIMIT 100
),
-- (2) 키워드 검색 결과
text_result AS (
SELECT
id,
title,
title_nouns <@> :query AS score,
ROW_NUMBER() OVER (
ORDER BY title_nouns <@> :query
) AS rank_text
FROM wikipages
LIMIT 100
),
-- (3) 검색 결과 합치기
merged AS (
SELECT
COALESCE(v.id, t.id) AS id,
COALESCE(v.title, t.title) AS title,
v.rank_vec,
t.rank_text
FROM vector_result v
FULL OUTER JOIN text_result t ON v.id = t.id
)
-- (4) RRF 계산
SELECT
id,
title,
rank_vec,
rank_text,
(
COALESCE(1.0 / (60 + rank_vec), 0.0) +
COALESCE(1.0 / (60 + rank_text), 0.0)
) AS rrf_score
FROM merged
ORDER BY rrf_score DESC
LIMIT 10;
Common Table Expression (CTE)
- WITH는 복잡한 SQL을 여러 단계로 나누어 임시 테이블처럼 사용할 수 있게
WITH temp AS (
SELECT ...
)
SELECT * FROM temp;
- 현재 쿼리에서는 벡터 검색 결과, 키워드 검색 결과, 합친 결과를 나타내는데 사용
WITH
vector_result AS (...),
text_result AS (...),
merged AS (...)
ROW_NUMBER() OVER (ORDER BY ...)
- ROW_NUMBER(): 윈도우 함수. 순서대로 행 번호를 붙임
- OVER는 SQL의 윈도우 함수의 적용 방식을 지정
- OVER에서 사용하는 표현:
- PARTITION BY: 집단으로 나누어 적용(GROUP BY와 비슷하지만 행의 수는 원래대로 유지)
- ORDER BY: 정렬 순서대로 적용
COALESCE
- NULL이 아닌 첫 번째 값을 반환
- 검색 결과에서 순위에 없을 경우 id나 title이 누락되어 NULL이 될 수 있음
- 순위에 있는 결괏값을 사용
COALESCE(v.id, t.id)
SQL 문 실행
# 1. 검색할 질문 설정
search_query = "컴퓨터"
# 2. 질문을 임베딩 벡터로 변환 (기존에 로드한 sbert 모델 사용)
query_embedding = sbert.encode(search_query).tolist()
query_embedding_json = json.dumps(query_embedding)
# 3. 쿼리
rrf_search_query = sqlalchemy.text(f"""여기에 SQL 문을 넣으세요""")
# 4. 결과 출력
pd.read_sql(
rrf_search_query, engine,
params={"query_emb": query_embedding_json, "query": search_query})