이전에 Hybrid Search 전반적인 흐름과 개념적인 내용을 공부했었다.
오늘은 그 연장선에서, 실제 RAG를 구현하면서 검색모듈 부분에서 주로 사용하는 라이브러리와 기술 스택에 대해 다뤄보려고 한다. 오늘은 Milvus와 BM25 각각이 어떤 역할을 하는지, 그리고 두 가지를 결합하여 어떻게 Hybrid Search를 구현하는지에 정리하려 한다.

1. BM25: Sparse Search
BM25는 키워드 기반 문서 검색 알고리즘이다. 간단히 말하면 “질문에 들어간 단어가 얼마나 많이, 또 얼마나 중요한 위치에 등장했는가”를 점수화한다.
"어떻게 하면 사용자의 질의(query)와 문서(document) 사이의 연관성을 정량적으로 계산할 수 있을까?"
단순히 문서에 단어가 포함되어 있냐/없냐만 본다면, 너무 단순해서 실제 검색 품질이 좋지 않다. 그래서 등장한 것이 TF-IDF 계열 알고리즘이고, 그중에서도 가장 널리 쓰이는 방식이 바로 BM25이다.
엘라스틱서치(ElasticSearch)에서도 5.0 버전 이후 BM25가 기본 랭킹 알고리즘으로 채택될 만큼 표준처럼 사용된다!
BM25 핵심 개념 요약
BM25 알고리즘은 크게 세 가지 아이디어를 사용한다.
- 단어 빈도(TF, Term Frequency)
- 문서 안에 검색어가 여러 번 등장하면 그 문서가 더 관련 있을 확률이 높다.
- 단, 같은 단어가 지나치게 많이 등장하면 오히려 중요도가 떨어진다(광고성 스팸 방지).
- 역문서 빈도(IDF, Inverse Document Frequency)
- 흔한 단어(예: "그리고", "합니다")는 검색어로서 의미가 약하다.
- 반대로 특정 문서에만 등장하는 드문 단어는 검색 신호로서 강하다.
- 문서 길이 보정(Document Length Normalization)
- 같은 단어가 등장하더라도, 긴 문서보다는 짧은 문서에 등장할 때 더 중요한 단서가 된다.
- 그래서 BM25는 평균 문서 길이와 비교해 보정값을 적용한다.
이제 알고리즘을 수식으로 확인해보자!
쿼리 Q={q1,q2,…,qn}가 주어지고, 특정 문서 D에 대해 BM25 점수는 다음과 같이 계산한다.

여기서,
- f(q,D): 단어 가 문서 D에 등장한 횟수
- |D|: 문서 D의 전체 길이(토큰 개수)
- avgdl: 전체 문서의 평균 길이
- k1: TF에 대한 스무딩 파라미터 (보통 1.2~2.0)
- b: 문서 길이 보정 파라미터 (보통 0.75)
- IDF(q): 역문서 빈도
- 단어가 많을수록 점수 ↑
- 너무 길면 점수 ↓
- 희귀한 단어일수록 점수 ↑
직접 BM25를 구현하면 내부 동작을 훨씬 쉽게 이해할 수 있다. 아래는 Mecab 형태소 분석기를 활용한 간단한 구현 예시이다.
from konlpy.tag import Mecab
import math
import numpy as np
from collections import defaultdict
mecab = Mecab()
class BM25:
def __init__(self, documents, k1=1.2, b=0.75):
self.documents = documents
self.k1 = k1
self.b = b
self.doc_count = len(documents)
self.doc_len = []
self.doc_freqs = []
self.idf = {}
self.avgdl = 0
self._calc_idf()
def _calc_idf(self):
tokenized_docs = [mecab.morphs(doc) for doc in self.documents]
total_len = 0
word_freq = defaultdict(int)
for doc in tokenized_docs:
self.doc_len.append(len(doc))
total_len += len(doc)
freqs = defaultdict(int)
for word in doc:
freqs[word] += 1
self.doc_freqs.append(freqs)
for word in freqs:
word_freq[word] += 1
self.avgdl = total_len / self.doc_count
for word, freq in word_freq.items():
self.idf[word] = math.log(1 + (self.doc_count - freq + 0.5) / (freq + 0.5))
def get_scores(self, query):
query_tokens = mecab.morphs(query)
scores = np.zeros(self.doc_count)
for q in query_tokens:
if q not in self.idf:
continue
idf = self.idf[q]
for i, freqs in enumerate(self.doc_freqs):
f = freqs.get(q, 0)
denom = f + self.k1 * (1 - self.b + self.b * self.doc_len[i] / self.avgdl)
scores[i] += idf * (f * (self.k1 + 1)) / denom
return scores
def get_top_n(self, query, n=5):
scores = self.get_scores(query)
top_n = np.argsort(scores)[::-1][:n]
return [self.documents[i] for i in top_n]
실행예시
docs = ["오늘 날씨가 참 좋다",
"내일 날씨는 흐릴 것 같다",
"서울의 미세먼지가 심하다",
"오늘은 미세먼지가 없다"]
bm25 = BM25(docs)
query = "날씨"
print(bm25.get_top_n(query, n=2))
- 장점
- 특정 키워드가 정확히 들어간 문서를 잘 찾아낸다.
- 구현이 빠르고, 인덱싱/검색 속도가 빠르다.
- 법률, 규정집처럼 전문 용어가 중요할 때 특히 강력하다.
- 단점
- 동의어나 표현 변형에 취약하다.
- 예: 질문에 자동차라고 했는데 문서에는 차량이라고 적혀 있으면 점수를 낮게 준다.
- 단순 빈도 기반이기 때문에, 문맥적 유사도(semantic similarity)는 잡아내지 못한다.
- 동의어나 표현 변형에 취약하다.
즉, BM25는 "딱 떨어지는 키워드 매칭"에 사용하는 알고리즘이다. TF-IDF를 개선한 문서 랭킹 알고리즘이며 문서 길이 보정과 단어 빈도 포화 효과를 고려해 실제 검색 엔진에 훨씬 잘 맞음.
직접 구현해보면 수식이 단순한 원리에서 나왔다는 걸 알 수 있다.
검색 품질을 높이는 첫걸음으로 BM25를 이해하고 적용해보면 좋다.
2. Milvus: Dense Search (시멘틱 검색)
Milvus는 벡터 데이터베이스(Vector DB)다. 여기서는 문서와 질문을 임베딩(Embedding)으로 바꿔서 벡터 공간에 저장하고, 벡터 간 거리를 비교해서 "가장 의미가 비슷한 문서"를 찾아낸다.
- 장점
- 동의어, 유사한 표현까지 잡아낼 수 있다.
- 예: 자동차와 차량은 임베딩 공간에서 가깝게 매핑된다.
- 문맥을 반영하므로, 키워드가 직접 안 들어있어도 의미가 맞으면 검색 가능하다.
- 동의어, 유사한 표현까지 잡아낼 수 있다.
- 단점
- 모델이 학습한 임베딩 품질에 크게 좌우된다.
- 특정 키워드를 정확히 집어내는 데는 오히려 약하다.
- 계산량이 많아서 BM25보다 느릴 수 있다.
즉, Milvus는 "문맥적으로 비슷한 것까지 찾아주는 시멘틱 검색에 사용되는 vector DB공간"이다.
3. Hybrid Search
실무에서 써보니, BM25만 쓰면 놓치는 경우가 있고, Milvus만 쓰면 잡음이 많은 경우가 있었다. 예를 들어
- BM25만 쓰면 →
질문: 5월 초 경제 보고서
문서: "5월 상순 경제동향" → 키워드 매칭이 애매해서 못 잡음. - Milvus만 쓰면 →
질문: AI 인력 수급 방안
문서: "AI 윤리 가이드라인" → 의미가 조금이라도 비슷하면 가져와서 정확도가 떨어짐.
그래서 Hybrid Search를 쓴다. 방법은 간단하다.
이렇게 BM25 점수와 Milvus 점수를 가중 평균하거나,
아니면 RRF(Reciprocal Rank Fusion) 같은 기법으로 순위를 다시 합친다.
- 키워드 정확도는 BM25로,
- 의미적 유사도는 Milvus로 보완하는 셈이다.
사용자 질문
↓
[BM25] 키워드 매칭 점수 ┐
├─ Hybrid Scoring → Top N 문서
[Milvus] 임베딩 유사도 ┘
그 후에는 보통 Reranker 모델을 붙여서 LLM이 보기 좋은 순서로 정렬한다.
이 부분에 대한 자세한 내용은 이전 글 참고,,
2025.08.19 - [인공지능/자연어처리] - RAG 구축하기 - 3.2 성능 최적화 : Hybrid Search(CC& RRF) 와 Rerank
글을 쓰면서 느낀 건, 검색기는 결국 정확도(precision)와 재현율(recall) 사이에서 줄타기를 하는 게임이라는 점이다. BM25와 Milvus는 각자 한쪽 극단을 맡고 있고, Hybrid Search는 그 사이의 균형을 잡아주는 전략이라고 볼 수 있다.
'AI > Natural language processing' 카테고리의 다른 글
| [NLP] RAG 성능 평가방법 : RAGAS를 이용해 데이터셋 생성부터 평가까지 (4) | 2025.08.26 |
|---|---|
| [NLP] LangChain을 활용하여 RAG 구축하기 (2) | 2025.08.25 |
| RAG 구축하기 - 3.2 성능 최적화 : Hybrid Search(CC& RRF) 와 Rerank (0) | 2025.08.19 |
| RAG 구축하기 - 3.1 성능 최적화 : Chunking 전략 제대로 파헤치기 (0) | 2025.08.19 |
| RAG 구축하기 - 2. Retrieval과 Generation (1) | 2025.08.19 |