들어가기 전에
RAG 논문을 처음 공부할 때, 검색 방식은 대부분 벡터 검색(dense retrieval)을 전제로 합니다. 질문을 임베딩 벡터로 변환하고, 코사인 유사도로 가장 가까운 문서를 찾는 방식이죠. 그런데 실무에서 RAG 시스템을 만들어보면 벡터 검색만으로는 놓치는 케이스가 생각보다 많습니다.
이번 글에서는 키워드 기반 검색의 대표 알고리즘인 BM25, 두 검색 결과를 합치는 방법인 RRF(Reciprocal Rank Fusion), 그리고 이를 RAG에 적용한 RAG-Fusion 논문을 정리해보았습니다.
왜 벡터 검색만으로는 부족할까?
벡터 검색은 의미적으로 유사한 문서를 잘 찾습니다. '자동차 사고'를 검색해도 '차량 충돌'이 담긴 문서를 찾아낼 수 있습니다.
하지만 약점이 있습니다.
정확한 키워드 매칭이 중요한 경우에 취약합니다. 예를 들어 'GPT-4o'나 'BM25'처럼 고유한 이름이나 특수한 기술 용어를 검색할 때, 임베딩 벡터는 그 단어의 정확한 철자보다는 의미적 유사성에 의존하기 때문에 오히려 관련 없는 문서를 가져오기도 합니다. 'K8s'를 검색했을 때 Kubernetes 문서가 나와야 하지만, 임베딩 모델이 두 표현을 같은 의미로 학습하지 않았다면 찾지 못합니다. 버전 번호, 모델 이름, 제품 코드처럼 표현 자체가 중요한 경우가 여기에 해당합니다.
희귀한 단어나 전문 용어에도 약합니다. 임베딩 모델은 학습 데이터에서 자주 등장한 단어와 문맥을 잘 표현하지만, 특정 도메인의 전문 용어나 신조어는 학습 데이터에 충분히 포함되어 있지 않을 수 있습니다. 이 경우 벡터 검색은 그 단어의 의미를 제대로 표현하지 못해 검색 정확도가 크게 떨어집니다.
반대로 키워드 기반 검색은 정확한 단어 매칭에는 강하지만, 표현이 다른 동의어나 문맥적 의미를 이해하지 못합니다. '자동차 사고'를 검색하면 'vehicle collision'이 담긴 영어 문서는 찾지 못합니다. 사용자가 '집 구하기'라고 검색했는데 문서에는 '주거 임차' 또는 '부동산 계약'이라고 적혀 있다면 이 문서는 검색 결과에 나오지 않습니다.
여기까지 정리하면 각 방식의 강점과 약점은 서로 정반대입니다.
| 벡터 검색 | 키워드 검색(BM25) | |
| 강점 | 동의어, 문맥 이해 | 정확한 단어 매칭 |
| 약점 | 고유명사, 전문 용어 | 표현 다양성, 의미 이해 |
둘을 함께 쓰는 하이브리드 검색이 실무에서 자주 사용되는 이유가 바로 여기에 있습니다.
하나가 놓치는 케이스를 다른 하나가 잡아주는 구조입니다. 이를 이해하려면 먼저 키워드 검색의 핵심 알고리즘인 BM25를 알아야 합니다.
BM25 - 키워드 검색이 30년 동안 살아남은 이유
우선 TF-IDF의 한계부터..
BM25를 이해하려면 TF-IDF에서 시작하는 게 좋습니다. TF-IDF는 두 가지 개념을 곱해 문서의 관련도를 계산합니다.
- TF(Term Frequency): 쿼리 단어가 문서에 얼마나 자주 등장하는지
- IDF(Inverse Document Frequency): 해당 단어가 전체 문서에서 얼마나 희귀한지
직관적으로 맞는 방식입니다. 자주 등장하고(TF) 희귀한 단어일수록(IDF) 관련성이 높다는 논리입니다.
예를 들어 'the'는 모든 문서에 등장하므로 IDF가 낮고 '양자역학'은 드물게 등장하므로 IDF가 높습니다.
그런데 TF-IDF에는 두 가지 구조적 문제가 있습니다.
첫 번째로 TF가 선형으로 증가합니다. 'elephant'가 100번 등장한 문서가 10번 등장한 문서보다 정확히 10배 관련성이 높을까요? 100번 나왔다면 이미 충분히 관련 있는 문서입니다. 200번 나온다고 두 배 더 관련 있지는 않습니다. 어느 순간부터는 추가 등장이 관련성을 높이지 않는데 TF-IDF는 이를 무시하고 계속 점수를 올립니다. 이 문제를 TF 포화(saturation) 문제라고 합니다.
두 번째로 문서 길이를 고려하지 않습니다. 300페이지 책에서 'elephant'가 10번 등장한 것과, 한 문단 메모에서 10번 등장한 것은 완전히 다른 의미입니다. 메모에서 10번 나왔다면 이 문서의 주제가 elephant일 가능성이 매우 높지만, 300페이지 책에서 10번은 그냥 지나가는 언급일 수 있습니다. 긴 문서는 단순히 길기 때문에 단어가 많이 등장할 수 있고, 이 경우 TF-IDF는 긴 문서에 불공정하게 높은 점수를 줍니다.
그렇다면 BM25가 이걸 어떻게 해결했을까요?
BM25(Best Matching 25)는 이 두 문제를 수식적으로 해결합니다.
BM25(D, Q) = Σ IDF(qᵢ) · [f(qᵢ,D) · (k₁+1)] / [f(qᵢ,D) + k₁·(1 - b + b·|D|/avgdl)]
계산식이 복잡해 보이지만 핵심은 두 가지입니다.
1. TF 포화 처리
분모에 f(qᵢ,D)(term frequency)가 들어가면서 TF가 커질수록 분모도 커져 점수 증가가 둔화됩니다.
처음 몇 번의 등장은 점수에 크게 기여하지만, 반복될수록 추가 기여가 점점 작아집니다. 결국 점수는 특정 값에 수렴하게 되는 것이죠.
그리고 k₁ 파라미터(기본값 1.2)가 이 포화 속도를 조절합니다. k₁이 클수록 TF의 영향이 더 오래 지속되고, 0에 가까울수록 TF를 거의 무시하고 IDF만으로 점수를 계산합니다.
2. 문서 길이 정규화
|D|/avgdl은 현재 문서 길이를 전체 문서 평균 길이로 나눈 값입니다. 문서가 평균보다 길면 분모가 커져 점수가 낮아지고 짧으면 점수가 높아집니다. b 파라미터(기본값 0.75)가 길이 정규화의 강도를 조절합니다.
b=0으로 설정하면 문서 길이를 전혀 고려하지 않고 b=1이면 완전히 정규화합니다. 기본값 0.75는 수십 년간 다양한 데이터셋에서 실험적으로 검증된 값입니다.
이 두 가지 개선 덕분에 BM25는 TF-IDF가 가졌던 '단어를 많이 우겨넣은 긴 문서가 무조건 유리한' 문제를 해결했습니다. Elasticsearch는 2016년 Lucene 6으로 전환하면서 기본 유사도 알고리즘을 TF-IDF에서 BM25로 바꿨고 지금도 Elasticsearch, Solr, Lucene의 기본 검색 알고리즘입니다.
BM25의 한계점
BM25는 키워드가 정확히 일치하는 경우에 강하지만 쿼리와 문서가 다른 표현을 사용할 때는 약합니다.
예를 들어'심근경색'을 검색하면 'heart attack'이 담긴 문서는 점수를 받지 못합니다. 두 단어가 아무리 같은 의미라도 BM25 입장에서는 완전히 다른 단어입니다.
그리고 BM25는 단어의 순서를 고려하지 않습니다. '개가 사람을 문다'와 '사람이 개를 문다'는 의미가 전혀 다르지만 BM25 입장에서는 같은 단어들이 등장한 동일한 문서입니다. 이것이 벡터 검색과 BM25를 함께 쓰는 하이브리드 검색이 필요한 이유입니다.
RRF - 그럼 점수 대신 순위로 합치면 어떨까?
BM25와 벡터 검색을 함께 쓰기로 했다면 두 결과를 어떻게 합쳐야 할까요? 단순히 점수를 더할 수는 없습니다.
BM25 점수와 벡터 유사도 점수는 단위가 다르고 분포도 다릅니다. BM25는 0에서 수십까지 벡터 유사도는 0에서 1 사이일 수 있습니다. 이 둘을 더하면 절대적으로 큰 수치를 가진 BM25가 벡터 검색 결과를 압도합니다.
min-max 정규화로 두 점수를 모두 0~1 범위로 맞추는 방법도 있지만 이 방식은 이상치에 취약합니다.
BM25 점수 중 하나가 유독 높으면 나머지 점수들이 모두 0에 가깝게 몰려버리게 되버리죠. 결국 정규화 이후에도 특정 문서 하나가 전체 랭킹을 왜곡할 수 있습니다.
RRF(Reciprocal Rank Fusion) 는 점수 자체를 쓰지 않고 순위(rank) 만 보는 방식으로 이 문제를 근본적으로 해결합니다.
점수가 아무리 다른 척도여도 순위는 언제나 1위, 2위, 3위로 공평하게 비교할 수 있기 때문입니다.
RRF 수식
RRF_score(d) = Σ 1 / (k + rank(d))
각 검색 방식에서 문서 d의 순위를 구하고 1/(k + 순위)를 모두 더합니다. 이 때 기본값 k=60이 널리 쓰입니다.
예를 들어 BM25 결과에서 3위, 벡터 검색에서 1위인 문서가 있다면 k=60을 적용하면
RRF score = 1/(60+3) + 1/(60+1)
= 1/63 + 1/61
≈ 0.0159 + 0.0164
= 0.0323
BM25에서는 1위, 벡터 검색에서는 5위인 문서라면
RRF score = 1/(60+1) + 1/(60+5)
= 1/61 + 1/65
≈ 0.0164 + 0.0154
= 0.0318
첫 번째 문서(두 검색 모두 상위권)가 두 번째 문서(한 검색에서만 1위)보다 점수가 높습니다.
두 시스템 모두에서 높게 평가된 문서일수록 최종 순위가 올라가는 구조입니다. 한 시스템에서 압도적 1위여도 다른 시스템에서 전혀 검색되지 않으면 최종 순위가 낮을 수 있습니다.
어떤 문서가 두 검색에 모두 등장하는 것 자체가 '이 문서는 여러 관점에서 관련 있다'는 신호입니다. RRF는 이 신호를 수식으로 포착합니다.
k=60인 이유
k 값이 낮을수록 1위와 2위의 점수 차이가 커져 순위가 결과에 더 강하게 영향을 미칩니다. 높을수록 차이가 줄어들어 더 많은 문서가 고르게 기여합니다.
실제 수치로 보면 아래와 같습니다.
| 순위 | k=1 | k=10 | k=60 |
| 1위 | 1/2 = 0.500 | 1/11 = 0.091 | 1/61 = 0.016 |
| 10위 | 1/11 = 0.091 | 1/20 = 0.050 | 1/70 = 0.014 |
| 100 | 1/101 = 0.010 | 1/110 = 0.009 | 1/160 = 0.006 |
k=1이면 1위와 10위의 점수 차이가 5배가 넘지만 k=60이면 1위와 10위의 차이가 거의 없습니다.
k=60은 상위권 문서들이 비슷하게 기여하면서 하위권으로 갈수록 자연스럽게 감소하는 균형을 만들어는 것이죠.
Cormack et al.의 2009 SIGIR 논문에서 다양한 데이터셋에 걸쳐 실험적으로 검증된 값입니다.
Elasticsearch에서 RRF 사용하기
GET example-index/_search
{
"retriever": {
"rrf": {
"retrievers": [
{
"standard": {
"query": {
"match": { "text": "심근경색 치료" }
}
}
},
{
"knn": {
"field": "embedding",
"query_vector": [0.1, 0.2, "..."],
"k": 10
}
}
],
"rank_constant": 60,
"rank_window_size": 100
}
}
}
BM25 키워드 검색(standard)과 kNN 벡터 검색(knn)을 동시에 실행하고 RRF로 결과를 합칩니다. rank_window_size는 각 검색에서 몇 개의 문서를 가져와 RRF 계산에 사용할지를 결정합니다. 값이 클수록 더 많은 후보를 고려해 품질이 올라가지만 그만큼 계산 비용도 증가합니다. Elasticsearch 8.x부터 rrf retriever를 기본 지원합니다.
RAG-Fusion - 검색 방식이 아니라 쿼리를 다양화한다
BM25 + 벡터 검색의 하이브리드가 '두 가지 검색 방식을 합치는' 접근이라면 RAG-Fusion(Rackauckas, 2024)은 다른 방향에서 접근합니다. 검색 방식을 다양화하는 대신 쿼리 자체를 여러 개로 만들어 각각 검색하고 RRF로 합칩니다.
출발점은 이런 질문입니다. 사용자가 입력한 쿼리는 원래 의도의 한 가지 표현일 뿐입니다. 'MEMs 마이크로폰의 주파수 응답은 어떻게 되나요?'라고 물었지만 실제로 필요한 정보는 주파수 범위일 수도 있고, 성능 특성일 수도 있고, 측정 방법일 수도 있습니다.
하나의 쿼리로 검색하면 그 표현에 가장 가까운 문서만 나오고 다른 관점의 문서는 놓칩니다. 같은 의도를 다른 방식으로 표현한 여러 쿼리로 검색하면 단일 쿼리로는 놓쳤던 관련 문서를 더 많이 포착할 수 있습니다.
RAG-Fusion의 동작 방식
사용자 쿼리: 'MEMs 마이크로폰의 주파수 응답은 어떻게 되나요?'
│
▼
┌─────────────────────────────────┐
│ LLM으로 다양한 쿼리 생성 │
│ ① MEMs 마이크로폰 주파수 범위 │
│ ② 실리콘 마이크로폰 성능 특성 │
│ ③ MEMs 센서 오디오 스펙 │
│ ④ 마이크로폰 주파수 응답 측정 │
└─────────────┬───────────────────┘
│
▼
각 쿼리로 벡터 검색 수행
│
▼
┌─────────────────────────────────┐
│ RRF로 결과 통합 │
│ → 여러 쿼리에서 공통으로 상위에 │
│ 랭크된 문서가 최종 상위에 올라옴│
└─────────────┬───────────────────┘
│
▼
최종 문서 → LLM → 응답 생성
여기서 RRF의 역할이 중요합니다. 각 쿼리로 검색한 결과를 단순히 합치면 중복이 많아지고 노이즈가 섞입니다.
RRF를 사용하면 여러 쿼리에서 공통으로 높게 랭크된 문서가 자연스럽게 최상위로 올라옵니다.
한 쿼리에서만 1위인 문서보다 네 쿼리 모두에서 3~5위권인 문서가 최종 순위가 더 높을 수 있습니다. 여러 관점에서 관련 있다고 판단된 문서를 우선하는 방식입니다.
코드로 보기
from langchain.chat_models import ChatOpenAI
llm = ChatOpenAI(temperature=0)
def generate_queries(original_query: str, n: int = 4) -> list[str]:
"""LLM으로 다양한 쿼리 생성"""
prompt = f"""다음 질문을 {n}가지 다른 방식으로 재작성하세요.
각 쿼리는 같은 정보를 찾되, 다른 표현을 사용해야 합니다.
원래 질문: {original_query}
쿼리 목록 (줄바꿈으로 구분):"""
response = llm.predict(prompt)
# 원래 쿼리 + 생성된 쿼리들
queries = [original_query] + response.strip().split('\n')
return queries[:n+1]
def rag_fusion_retrieve(query: str, retriever, k: int = 60) -> list:
"""RAG-Fusion 방식으로 문서 검색"""
queries = generate_queries(query)
# 각 쿼리로 검색
all_results = {}
for q in queries:
docs = retriever.get_relevant_documents(q)
for rank, doc in enumerate(docs):
doc_id = doc.page_content[:50] # 간단한 문서 ID
if doc_id not in all_results:
all_results[doc_id] = {"doc": doc, "score": 0}
# RRF 점수 누적: 여러 쿼리에서 상위권일수록 높은 점수
all_results[doc_id]["score"] += 1 / (k + rank + 1)
# 최종 RRF 점수 기준으로 정렬
sorted_docs = sorted(
all_results.values(),
key=lambda x: x["score"],
reverse=True
)
return [item["doc"] for item in sorted_docs]
generate_queries에서 temperature=0으로 설정한 이유는 쿼리 생성의 일관성을 위해서입니다.
너무 창의적인 쿼리가 생성되면 원래 의도에서 벗어날 수 있기 때문입니다. 생성된 쿼리와 원래 쿼리를 함께 사용한다는 점([original_query] + ...)도 중요합니다. 원래 쿼리가 가장 직접적인 표현이기 때문에 항상 포함시켜야 합니다.
RAG-Fusion의 장단점
장점: 원래 쿼리의 다양한 측면을 커버합니다. 'MEMs 마이크로폰의 주파수 응답'을 물었을 때 주파수 범위, 성능 특성, 측정 방법 등 다양한 관점에서 관련 문서를 수집하기 때문에 단일 쿼리보다 포괄적인 답변이 가능합니다. 논문에서는 정확도와 포괄성 측면에서 일반 RAG 대비 개선이 확인되었습니다.
단점: LLM을 두 번 호출합니다. 쿼리 생성에 한 번, 최종 답변 생성에 한 번, 단순 RAG보다 지연 시간이 늘어납니다.
또한 LLM이 원래 의도와 벗어난 쿼리를 생성하면 오히려 관련 없는 문서가 포함될 수 있습니다. 예를 들어 'MEMs 마이크로폰' 쿼리에서 LLM이 '스피커 출력 특성' 같은 엉뚱한 쿼리를 생성하면 오히려 노이즈가 증가합니다.
최근 프로덕션 환경에서의 추가 연구에 따르면 RAG-Fusion이 재랭킹(reranking) 이후 단계까지 고려하면 단일 쿼리 대비 개선 효과가 항상 보장되지는 않는다는 결과도 있습니다. 검색 재현율(recall)은 올라가지만 재랭킹과 컨텍스트 예산 제한이 적용되면 그 이득이 상쇄될 수 있다는 의미입니다. 도입 전에 실제 데이터로 직접 검증해보는 것이 좋습니다.
잠깐 정리
| 개념 | 역할 | 핵심 아이디어 |
| BM25 | 키워드 기반 검색 | 단어 빈도 + 희귀성 + 문서 길이 정규화 |
| RRF | 검색 결과 합치기 | 점수 대신 순위로 합산, k=60 |
| RAG-Fusion | 쿼리 다양화 | LLM으로 쿼리 확장 + RRF로 통합 |
실무 적용 순서를 추천하자면 먼저 BM25 단독으로 키워드 검색을 구성해보고, 벡터 검색과의 하이브리드를 RRF로 연결하고, 검색 재현율을 더 높여야 한다면 RAG-Fusion을 추가하는 흐름이 자연스럽습니다.
마치며
벡터 검색은 강력하지만 만능이 아닙니다. BM25와의 하이브리드는 지금 대부분의 프로덕션 RAG 시스템에서 사실상 기본 선택지가 되고 있습니다. RRF는 수식이 단순한 만큼 직접 구현해보면 이해가 빠릅니다. RAG-Fusion은 아이디어 자체는 직관적이지만 실제로 쓸 만한지는 직접 실험해봐야 압니다.
좋은 검색은 좋은 RAG의 절반입니다.
참고자료
'AI' 카테고리의 다른 글
| [바미] 텍스트가 아닌 정보를 RAG는 어떻게 읽을까? (2) | 2026.03.31 |
|---|---|
| [바미] AI가 거짓말을 하는지 어떻게 알 수 있을까? - LLM이 그 답변 봐봐! 혹시 사쿠라야? (2) | 2026.03.30 |
| [바미] 내가 크롤링한 결과물이 쓰레기가 되는 이유 (2) | 2026.03.28 |
| [바미] 어떻게 자르느냐가 검색 품질을 결정한다. (0) | 2026.03.27 |
| [바미] AI도 구글 검색을 한다면 어떨까요? (0) | 2026.03.26 |