왜 벡터 검색만으로는 부족한가
RAG의 기본 파이프라인은 임베딩으로 후보 문서를 뽑는 단일 단계 검색에 의존한다. 하지만 임베딩은 문장 전체를 하나의 벡터로 압축하기 때문에, 질의의 핵심 키워드와 문서의 세부 맥락이 어긋나도 코사인 유사도상으로는 가깝게 나오는 경우가 많다. 결과적으로 top-5 안에 정답 문서가 들어와야 하는데 top-20 밖으로 밀리는 리콜 손실이 발생한다. LLM에 전달하는 컨텍스트 창이 제한적이라 상위 몇 개만 넣게 되는데, 여기서 순위가 어긋나면 답변 품질이 그대로 무너진다.
핵심 원인은 "검색 단계와 판단 단계의 해상도 차이"다. 임베딩 검색은 빠르지만 거칠고, 질의-문서 쌍을 함께 보는 정밀 모델은 느리지만 정확하다. 재순위는 이 둘을 분업시킨다.
2단계 검색: retrieve 후 rerank
실전 구조는 넓게 뽑고 정밀하게 자르는 방식이다. 먼저 벡터(또는 BM25) 검색으로 후보 50~100개를 넉넉히 확보하고, cross-encoder 재순위 모델로 질의와 각 문서를 함께 입력해 관련도 점수를 다시 매긴다. bi-encoder(임베딩)는 질의와 문서를 따로 인코딩하지만, cross-encoder는 둘을 한 번에 넣어 토큰 단위 상호작용을 보므로 정밀도가 크게 높아진다.
| 구분 | Bi-encoder(임베딩 검색) | Cross-encoder(재순위) |
|---|---|---|
| 입력 방식 | 질의·문서 개별 인코딩 | 질의+문서 쌍을 동시 입력 |
| 속도 | 빠름(사전 인덱싱 가능) | 느림(쌍마다 추론) |
| 정확도 | 중간 | 높음 |
| 적정 처리량 | 수백만 문서 | 수십~수백 후보 |
파이썬으로 구현하기
오픈소스 cross-encoder로 재순위를 붙이는 최소 예시다. 벡터 검색으로 받은 후보 리스트를 점수순으로 다시 정렬한다.
from sentence_transformers import CrossEncoder
# 다국어 재순위 모델
reranker = CrossEncoder("BAAI/bge-reranker-v2-m3", max_length=512)
def rerank(query: str, candidates: list[dict], top_k: int = 5):
pairs = [(query, c["text"]) for c in candidates]
scores = reranker.predict(pairs) # 쌍마다 관련도 점수
for c, s in zip(candidates, scores):
c["rerank_score"] = float(s)
ranked = sorted(candidates, key=lambda c: c["rerank_score"], reverse=True)
return ranked[:top_k]
# 1차 벡터 검색은 넉넉히(top 50), 재순위 후 top 5만 LLM에 전달
candidates = vector_search(query, top_k=50)
context_docs = rerank(query, candidates, top_k=5)
하이브리드 검색과 결합
재순위의 성능은 1차 후보의 리콜에 종속된다. 정답이 후보 50개 안에 없으면 아무리 좋은 재순위도 못 살린다. 그래서 벡터 검색과 키워드 검색(BM25)을 합친 하이브리드로 후보 풀의 리콜을 먼저 끌어올린 뒤 재순위를 얹는 것이 안전하다. PostgreSQL + pgvector 환경이라면 두 점수를 합쳐 후보를 뽑을 수 있다.
-- 벡터 유사도와 전문검색을 함께 사용해 후보 확보
SELECT id, text,
1 - (embedding <=> :qvec) AS vec_score,
ts_rank(tsv, plainto_tsquery('simple', :q)) AS kw_score
FROM documents
WHERE tsv @@ plainto_tsquery('simple', :q)
OR (embedding <=> :qvec) < 0.6
ORDER BY (embedding <=> :qvec)
LIMIT 50;
여기서 얻은 50개를 앞의 rerank()에 그대로 넘기면 된다. 후보 확보는 리콜, 재순위는 정밀도를 담당하는 분업 구조다.
운영 시 주의점
첫째, 지연시간이다. cross-encoder는 후보 수에 비례해 느려진다. 후보 100개면 100번 추론하므로, GPU 배치 추론을 쓰거나 후보 수를 50 이하로 제한해 p95 응답시간을 관리해야 한다. 둘째, max_length를 넘는 긴 청크는 뒷부분이 잘려 점수가 왜곡되므로 청크 길이를 재순위 모델 한계에 맞춰야 한다. 셋째, 점수 절대값에 임계값을 걸어 "관련 문서 없음"을 판별하면 환각을 줄일 수 있다. 다만 임계값은 모델·도메인마다 다르므로 실제 질의 로그로 보정해야 한다.
효과를 측정하는 법
재순위 도입은 반드시 정량 평가로 검증한다. 골든 질의-정답 세트를 만들고 nDCG@k, Recall@k, MRR을 재순위 전후로 비교한다. 특히 "1차 검색 Recall@50"과 "재순위 후 Recall@5"를 나눠 보면 병목이 후보 확보인지 순위 정렬인지 구분할 수 있다. 전자가 낮으면 하이브리드·청크 전략을, 후자가 낮으면 재순위 모델 교체를 검토한다. 지표 없이 체감으로 판단하면 개선과 회귀를 구별할 수 없다.