RAG 개발 3편 - Retrieval, Hybrid Search, Reranking으로 검색 품질 높이기
RAG에서 검색 품질은 답변 품질을 거의 결정합니다. dense retrieval, BM25, hybrid search, query rewrite, metadata filtering, reranking을 어떻게 조합해야 하는지 단계별로 설명합니다.
검색 품질이 곧 RAG 품질이다
RAG 시스템에서 LLM은 가져온 문서를 바탕으로 답을 조합합니다. 즉, 처음에 잘못 가져오면 뒤에서 아무리 프롬프트를 잘 써도 한계가 있습니다.
그래서 검색 파이프라인은 아래 질문에 답할 수 있어야 합니다.
- 어떤 질문에 어떤 문서를 가져오는가
- 왜 그 문서를 top-k에 올렸는가
- 잘못된 검색은 어디서 발생하는가
Dense Retrieval만으로 충분할까
벡터 검색은 문장 의미를 잘 잡지만, 아래 경우에는 약할 수 있습니다.
- 에러 코드
- API path
- 클래스/함수 이름
- 정확한 제품명
- 약어와 내부 용어
예:
ERR_AUTH_401/v1/tokens/refreshSpring Cloud Gateway
이런 쿼리는 BM25 같은 키워드 기반 검색이 더 강할 때가 많습니다.
그래서 Hybrid Search가 필요하다
Hybrid Search는 보통 두 결과를 결합합니다.
- sparse retrieval: BM25
- dense retrieval: vector similarity
예시 흐름:
User Query
-> Query normalization
-> BM25 top-k
-> Vector top-k
-> Merge
-> Rerank
-> Final context
이 방식의 장점은 정확한 토큰과 의미 유사성을 모두 잡을 수 있다는 점입니다.
Query Rewrite는 생각보다 큰 차이를 만든다
사용자 질문은 종종 너무 짧고 추상적입니다.
예:
- “로그인 안됨”
- “배포 에러”
- “권한 문제”
이 상태로 검색하면 결과가 퍼집니다.
Rewrite 예:
원문: "권한 문제"
변환: "AWS IAM 또는 Kubernetes RBAC 권한 부족으로 요청이 거부되는 경우의 원인과 해결 방법"
단, rewrite는 과하게 확장하면 원문 의도를 왜곡할 수 있으므로 주의해야 합니다.
metadata filtering은 필수다
RAG 검색은 “모든 문서 중 유사한 것”만 찾는 방식으로 끝나면 안 됩니다.
같이 고려할 필터:
- 언어
- 제품
- 카테고리
- 문서 공개 범위
- 최신성
- 사용자 권한
예:
search(
query="토큰 재발급 방법",
filters={
"language": "ko",
"product": "console",
"visibility": "public"
}
)
필터가 없으면 내부 문서가 외부 답변에 섞이거나, 오래된 버전 문서가 상위에 뜰 수 있습니다.
top-k는 얼마나 가져와야 할까
많이 가져온다고 좋은 게 아닙니다.
일반적으로:
- retrieval 단계: top 10~30
- rerank 후 최종 전달: top 3~8
이 정도에서 출발하는 경우가 많습니다.
너무 적으면 놓치고, 너무 많으면 노이즈가 섞입니다.
Reranking은 왜 강력한가
retrieval은 대략 관련된 문서를 찾는 단계고, reranking은 질문에 가장 직접적으로 답하는 순서로 다시 정렬하는 단계입니다.
예를 들어 top-10에 좋은 문서가 들어왔더라도 7위에 있으면 LLM 컨텍스트에 못 들어갈 수 있습니다. reranker는 이 순서를 바로잡아 줍니다.
특히 긴 문서, 중복 chunk, 비슷한 표현이 많은 도메인에서 효과가 큽니다.
검색 파이프라인 예시
def retrieve_context(query: str):
rewritten = rewrite_query(query)
sparse_hits = bm25_search(rewritten, top_k=10)
dense_hits = vector_search(rewritten, top_k=10)
merged = merge_hits(sparse_hits, dense_hits)
reranked = rerank(query, merged)
return reranked[:5]
핵심은 원문 질문을 기준으로 최종 순서를 정하는 것입니다. rewrite 결과만 믿고 끝내면 원질문과 어긋날 수 있습니다.
duplicate suppression도 필요하다
같은 문서의 인접 chunk들이 top 결과에 여러 개 뜨는 경우가 많습니다.
문제:
- 컨텍스트가 한 문서로 과도하게 쏠림
- 다양한 근거를 못 가져옴
- 토큰 낭비
해결 방법:
- 같은
doc_id에서 최대 n개까지만 허용 - 인접 chunk 병합
- MMR(Maximal Marginal Relevance) 적용
검색 실패 유형을 분류해보자
운영 중에는 검색 실패를 유형별로 나눠봐야 합니다.
1. recall failure
정답 문서 자체를 못 가져옴
원인:
- 잘못된 청킹
- 부정확한 query
- 임베딩 한계
- 필터 과도
2. ranking failure
정답 문서는 가져왔지만 순위가 낮음
원인:
- reranking 부재
- merge 로직 문제
- 중복 결과 편향
3. grounding failure
검색은 맞았는데 답변 생성이 엉뚱함
원인:
- 프롬프트 문제
- 컨텍스트 정리 부족
- 너무 많은 chunk 전달
추천하는 발전 순서
처음부터 모든 걸 붙이지 말고 이렇게 가는 편이 좋습니다.
- vector retrieval만으로 시작
- metadata filter 추가
- BM25 결합
- query rewrite 추가
- reranker 추가
- duplicate suppression 추가
이 순서대로 가야 어느 단계가 실제 성능 개선을 만들었는지 판단하기 쉽습니다.
로그로 꼭 남겨야 할 것
- 원문 쿼리
- rewrite 결과
- retrieval 결과 top-k
- 최종 rerank 순위
- 선택된 chunk와 doc_id
- 누락된 expected 문서 여부
이 정보가 있어야 “왜 이 답변이 나왔는가”를 나중에 분석할 수 있습니다.
마무리
RAG 검색 품질을 높이는 핵심은 검색 단계를 하나로 보지 않는 것입니다.
- 사용자 질문을 정리하고
- 여러 검색 방식을 결합하고
- 필터로 범위를 좁히고
- reranking으로 최종 순서를 바로잡고
- 중복을 제어해야 합니다
다음 글에서는 이렇게 가져온 문서를 바탕으로 실제 답변을 어떻게 생성할지, 프롬프트, citation, fallback, answer formatting을 어떻게 설계해야 하는지 다루겠습니다.