본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 04. 00:17

나의 RAG 파이프라인은 CEO를 찾지 못했다 — 하이브리드 검색(Hybrid Retrieval)으로 해결한 방법

요약

순수 벡터 검색의 한계를 극복하기 위해 BM25 키워드 검색과 FAISS 밀집 검색을 결합한 하이브리드 검색 구현 방법을 소개합니다. RRF(Reciprocal Rank Fusion) 알고리즘을 사용하여 두 검색 결과의 순위를 효과적으로 병합하는 과정을 다룹니다.

핵심 포인트

  • 순수 벡터 검색은 키워드 매칭이 중요한 특정 정보 추출에 취약함
  • 하이브리드 검색은 밀집 검색(FAISS)과 희소 검색(BM25)을 병렬로 실행함
  • RRF 알고리즘을 통해 두 검색 결과의 순위를 수학적으로 결합함
  • RRF를 사용하면 단일 검색 결과보다 더 정교한 상위 청크 추출 가능

나의 지난 포스트에서, 나는 LangChain 없이 FastAPI + FAISS만 사용하여 처음부터 RAG 파이프라인을 구축했다. 테스트 세트에서 17/19점을 기록했다. 하지만 두 가지 질문에서 실패했다:

  • "CEO는 누구인가?" — 찾을 수 없음
  • "Zentara의 직원은 몇 명인가?" — 찾을 수 없음

두 답변 모두 1페이지에 바로 있었다. 무엇이 잘못되었고, 어떻게 해결했을까?

순수 벡터 검색(Pure Vector Search)이 실패한 이유

문제는 1페이지에 있는 밀집된 "회사 개요(Company snapshot)" 테이블이었다. CEO, CTO, 본사(HQ), 직원 수, 매출 등이 하나의 청크(Chunk)에 모두 담겨 있었다. 해당 청크의 임베딩(Embedding)은 8개 이상의 주제가 뒤섞인 모호한 평균값이 되었고, 그래서 "CEO는 누구인가?"라고 물었을 때 특정 쿼리에 대해 높은 순위를 차지하지 못했다.

이것이 **순수 의미론적 검색(Pure Semantic Search)**의 전형적인 약점이다. 문서에는 "CEO"라는 단어가 정확히 한 번 등장한다. 키워드 검색(Keyword search)이라면 즉시 찾아냈을 것이다. 하지만 벡터 검색은 의미론적 유사성(Semantic similarity)에 의존하며, 짧은 쿼리는 다른 내용이 대부분인 청크에 대해 충분히 강력한 매칭을 만들어내지 못한다.

해결책: 하이브리드 검색(Hybrid Retrieval)

해결책은 두 가지 검색을 병렬로 실행하고 결과를 결합하는 것이다:

  1. FAISS (밀집 검색, Dense) — 의미론적 유사성, "충전 시간은 얼마인가요?"와 같은 스타일의 질문에 적합
  2. BM25 (희소 검색, Sparse) — 키워드 매칭, "CEO는 누구인가?"와 같은 스타일의 질문에 적합

그 다음, 서로 다른 소스의 순위 목록을 결합하는 표준 알고리즘인 **상호 순위 결합(Reciprocal Rank Fusion, RRF)**을 사용하여 병합한다.

질문 ─► 임베딩(embed) ─► FAISS 검색 ──┐
                                    ├─► RRF 결합(fusion) ─► 상위 k개 청크 ─► LLM ─► 답변
질문 ─► 토큰화(tokenize) ─► BM25 검색 ┘

RRF의 작동 방식

RRF는 간단하다. 순위 목록 중 하나라도 나타나는 각 청크에 대해 다음을 계산한다:

rrf_score = 1/(k + rank_in_faiss) + 1/(k + rank_in_bm25)

여기서 k = 60(표준 상수)이다. 검색 모두에서 순위가 높은 청크는 한 곳에서만 1위를 차지한 청크보다 더 높은 점수를 얻는다.

예시: 청크 5가 BM25에서는 1위, FAISS에서는 4위로 선정된 경우:

FAISS 점수:  1/(60 + 4) = 0.0156
BM25 점수:   1/(60 + 1) = 0.0164
RRF 점수:                0.0320  ← FAISS 단독 1위 점수(0.0164)를 능가함

구현 (The implementation)

3개의 파일만 변경되었습니다. 핵심인 업데이트된 store.py는 다음과 같습니다:

from rank_bm25 import BM25Okapi

RRF_K = 60
...

main.py에서의 유일한 변경 사항은 매개변수 하나가 추가된 것입니다:

# 이전 (v1)
retrieved = store.search(query_vec, top_k=req.top_k)

...

이것이 전부입니다. 청킹 (Chunking), 임베딩 (Embedding), PDF 추출 (PDF extraction) 또는 LLM 로직에는 아무런 변경이 없습니다.

결과: 전과 후

질문v1 (FAISS 전용)v2 (하이브리드)
Zentara Robotics의 CEO는 누구인가요?실패성공
...

이제 CEO 질문은 기본값인 top_k=3에서 작동합니다. BM25가 "CEO"라는 키워드를 직접 매칭하고 RRF가 이를 상위로 끌어올렸기 때문입니다.

직원 수 질문은 top_k=5에서 작동합니다. 해당 청크는 많은 사실 정보가 밀집되어 있어 여전히 순위가 낮지만, 하이브리드 검색 (Hybrid retrieval)을 통해 검색 범위 내로 들어왔습니다. 리랭커 (Reranker) (Cross-encoder)를 사용한다면 top_k=3에서도 이를 해결할 수 있을 것입니다. 이것이 다음 계획입니다.

배운 점

  1. 순수 벡터 검색 (Pure vector search)에는 키워드 사각지대가 있습니다. 밀집된 청크 (Dense chunk) 내에 특정 용어가 한 번만 등장한다면, 의미론적 유사성 (Semantic similarity)만으로는 이를 안정적으로 찾아낼 수 없습니다. BM25는 이러한 경우를 즉시 잡아냅니다.

  2. RRF는 우아합니다. 점수 정규화 (Score normalization)도 필요 없고, 두 검색기 사이의 가중치 튜닝도 필요 없습니다. 오직 순위와 상수만 있으면 됩니다. 별도의 설정 없이 바로 작동합니다.

  3. LLM보다 검색기 (Retriever)가 더 중요합니다. v1에서의 두 가지 실패는 모두 LLM의 실패가 아닌 검색의 실패였습니다. LLM은 올바른 청크를 구경조차 하지 못했습니다. RAG를 개선하는 지점은 더 화려한 모델로 교체하는 것이 아니라, 검색 품질을 높이는 데 있습니다.

  4. 하이브리드 방식이 밀집된 청크 문제를 완전히 해결하지는 못했습니다. 직원 수 질문은 여전히 top_k=5가 필요합니다. 진정한 해결책은 더 나은 청킹 (더 밀집된 표를 더 작은 조각으로 나누는 것)을 적용하거나, 후보군을 더 정밀하게 재점수화할 수 있는 리랭커 (Reranker)를 도입하는 것입니다.

다음 단계 (What's next)

  1. Reranker (cross-encoder) — 더 나은 정밀도를 위해 상위 k개(top-k)를 재점수화(re-score)합니다.
  2. Evaluation harness (평가 하네스) — 수동으로 테스트하는 대신 19개 질문 테스트 세트를 자동화합니다.
  3. Streaming (스트리밍) — 긴 답변에 대해 더 나은 사용자 경험(UX)을 제공합니다.

직접 시도해 보세요

uv sync
cp .env.example .env   # API 키를 설정하세요
uv run uvicorn app.main:app --reload

http://localhost:8000/docs를 열고, 포함된 샘플 PDF(data/sample_test_file.pdf)를 업로드한 뒤 "Who is the CEO?"라고 질문해 보세요. 이제 잘 작동합니다.

만약 하이브리드 검색(hybrid retrieval)을 구현했거나 리랭커(reranker)를 사용해 본 경험이 있다면, 어떤 방식이 효과적이었는지 알려주세요.

저는 Santanu Mohanta입니다. LinkedIn에서 저와 연결되거나 GitHub에서 제 프로젝트들을 확인해 보세요.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0