"RAG 핵심 학습 (11/26) — Sparse Retrieval과 BM25 깊이 다이브"
Dense가 잘 못하는 정확 매칭·고유명사·코드를 채우는 검색이 Sparse — 그 표준이 BM25다.
Sparse Retrieval은 문서를 단어 출현 빈도 벡터로 표현하고 키워드 일치로 답을 찾는다. 단순해 보이지만 TF-IDF의 정보량 가중, BM25의 길이 정규화, Inverted Index의 검색 속도가 어우러져 고유명사·코드·약어에 Dense보다 강함. 본 편은 Robertson 2009 BM25 식을 풀고, Elasticsearch/OpenSearch와 RAG의 결합 패턴, 그리고 한국어 형태소 분석기가 결정적인 이유까지 다룬다.
0. Prerequisites
- 10편 Dense Retrieval — 두 검색의 상보성.
- 5편 청킹 — 청크가 BM25 토큰화 단위와 어떻게 만나는지.
- 7편 메타데이터 — pre-filter 통합.
1. 학습 목표
- TF, IDF, BM25를 식과 직관으로 안다.
- Inverted Index가 빠른 이유를 안다.
- 한국어 형태소 분석기가 BM25에 결정적인 이유를 안다.
- BM25의 한계와 12편 Hybrid의 자리를 안다.
2. 핵심 요약
Sparse Retrieval은 벡터의 대부분이 0인 단어-빈도 벡터로 검색. TF-IDF는 빈도(TF) × 정보량(IDF). BM25(Robertson & Zaragoza 2009)는 TF-IDF에 문서 길이 정규화와 TF 포화를 추가한 사실상의 표준. Inverted Index는 단어 → 문서 ID 리스트의 사전으로 O(query 토큰 수) 속도. Elasticsearch / OpenSearch가 운영 표준 — RAG와는 BM25 candidate → Reranker 또는 Dense와 Hybrid. 한국어는 형태소 분석기(Mecab, Kiwi, Nori) 없이 BM25를 쓰면 조사·어미가 본문에서 잘려 매칭이 깨진다. 조사 분리 + 어간 추출이 한국어 BM25의 최소 위생.
3. 직관 — 같은 query, BM25가 잡고 Dense가 놓침
질문: "PR-2024-Q3 보고서의 5장 핵심 결론?"
같은 코퍼스에서:
- Dense: "PR-2024-Q3"의 임베딩이 흐림(희귀 토큰). top-K가 비슷한 주제의 다른 보고서로 흩어짐.
- BM25: "PR-2024-Q3"라는 정확한 토큰이 있는 청크에 매우 높은 점수. top-1이 목표 청크 자체.
반대 케이스: "통화 환산 기준" → BM25는 "환율 적용" 청크를 못 잡는다(10편 3장). 두 방식은 서로 다른 종류의 query에 강함.
4. 정의 — Sparse의 핵심 용어
| 용어 | 정의 |
|---|---|
| TF (Term Frequency) | 문서 내 단어의 출현 횟수 |
| IDF (Inverse Document Frequency) | 전체 코퍼스에서 드물수록 가중 높음 |
| TF-IDF | \(\text{TF} \times \text{IDF}\). 자주 등장하면서 코퍼스에선 드문 단어가 핵심 |
| BM25 | TF-IDF + 문서 길이 정규화 + TF 포화 곡선. 1990년대 후반부터 표준 |
| Inverted Index | 단어 → \([(doc\_id, tf), ...]\) 사전. 검색 시 query 토큰만 lookup |
| Stemming / Lemmatization | 어간 추출 / 표제어 변환 (영어: running → run, 한국어: 갑니다 → 가다) |
| Morphological Analyzer | 형태소 분석. 한국어·일본어 BM25의 전제 |
| Stopwords | "는", "이", "그", "the", "of" 등 정보량 낮은 단어. 보통 제거 |
5. 식 — BM25를 그대로 풀기
문서 \(D\)에 query \(Q = \{q_1, q_2, ..., q_n\}\)의 BM25 점수:
$$\text{BM25}(D, Q) = \sum_{i=1}^{n} \text{IDF}(q_i) \cdot \frac{f(q_i, D) \cdot (k_1 + 1)}{f(q_i, D) + k_1 \cdot \left(1 - b + b \cdot \frac{|D|}{\text{avgdl}}\right)}$$
각 항의 의미:
- \(f(q_i, D)\) = 문서 \(D\) 내 토큰 \(q_i\)의 빈도 (TF)
- \(\text{IDF}(q_i) = \log\left(\frac{N - n_i + 0.5}{n_i + 0.5} + 1\right)\), \(N\) = 전체 문서 수, \(n_i\) = \(q_i\)가 등장하는 문서 수
- \(|D|\) = 문서의 토큰 수, \(\text{avgdl}\) = 코퍼스 평균 토큰 수
- \(k_1\) = TF 포화 파라미터 (보통 1.2~2.0). 클수록 TF가 점수에 더 크게 기여.
- \(b\) = 길이 정규화 파라미터 (보통 0.75). 1에 가까울수록 짧은 문서 가중.
TF 포화의 직관: 같은 단어가 10번 → 11번이 되는 효과가 1번 → 2번보다 훨씬 작음. 무한 반복 spam을 막는다.
길이 정규화의 직관: 짧은 문서에서 단어가 1번 나오는 게 긴 문서에서 1번 나오는 것보다 더 중요.
6. 원리 워크스루 — BM25 직접 구현부터 ES 통합까지
6.1 rank_bm25로 한 줄 검색 (Python, 영어)
from rank_bm25 import BM25Okapi
docs = [
"The quarterly sales report consolidates in USD after applying FX rates.",
"Inventory SKU-2024-04: 30 units in the Incheon warehouse.",
"Exception filings require department-head approval and form SEC-EX-04.",
]
tokenized = [d.lower().split() for d in docs]
bm25 = BM25Okapi(tokenized)
query = "SKU-2024-04 inventory".lower().split()
scores = bm25.get_scores(query)
6.2 Elasticsearch로 동일 검색
from elasticsearch import Elasticsearch
es = Elasticsearch("http://localhost:9200")
es.indices.create(index="rag", body={
"mappings": {
"properties": {
"chunk_text": {"type": "text", "analyzer": "standard"}, # 영어 standard
"version": {"type": "keyword"},
"security_level": {"type": "keyword"},
}
}
})
for i, d in enumerate(docs):
es.index(index="rag", id=f"c{i:03d}", document={
"chunk_text": d,
"version": "3.2",
"security_level": "internal",
})
results = es.search(index="rag", body={
"query": {
"bool": {
"must": [{"match": {"chunk_text": "SKU-2024-04 inventory"}}],
"filter": [
{"term": {"version": "3.2"}},
{"terms": {"security_level": ["public", "internal"]}},
]
}
},
"size": 5
})
6.3 한국어 — Nori analyzer가 결정적
es.indices.create(index="rag_ko", body={
"settings": {"analysis": {"analyzer": {
"ko_analyzer": {
"type": "custom",
"tokenizer": "nori_tokenizer",
"filter": ["nori_part_of_speech", "lowercase"],
}
}}},
"mappings": {"properties": {
"chunk_text": {"type": "text", "analyzer": "ko_analyzer"},
"version": {"type": "keyword"},
}}
})
es.index(index="rag_ko", id="c001", document={
"chunk_text": "분기 매출 보고는 환율 적용 후 USD로 통합한다.",
"version": "3.2",
})
results = es.search(index="rag_ko", body={
"query": {"match": {"chunk_text": "환율 적용 통화 환산"}}
})
Nori는 내장 형태소 분석기 — nori_part_of_speech 필터로 조사·어미를 자동 제거. 이 한 줄이 한국어 BM25 품질의 절반 이상.
6.4 BM25를 Dense와 결합 (Hybrid 미리보기)
bm25_hits = es.search(...)
dense_hits = vectordb.similarity_search(...)
def rrf(rankings, k=60):
scores = {}
for ranking in rankings:
for rank, doc_id in enumerate(ranking, start=1):
scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank)
return sorted(scores.items(), key=lambda x: -x[1])
7. 변형과 사례
7.1 BM25F — 다중 필드 가중
- 무엇이 바뀌나: 한 문서가 제목·본문·태그 여러 필드를 가질 때 필드별 가중치.
- 왜 쓰나: 제목 매칭이 본문 매칭보다 훨씬 강한 신호.
- 무엇이 가능해졌나: 정밀한 필드 가중 검색.
- 어디에 적합한가: 정형화된 문서(상품·블로그·논문).
- 한계: 가중치 튜닝 필요. 평가셋(14편) 동반.
7.2 SPLADE — 학습 기반 sparse
- 무엇이 바뀌나: 학습된 토큰 가중치가 BM25의 통계 가중을 대체.
- 왜 쓰나: BM25보다 의미 신호를 일부 포착. 그러면서 sparse의 해석 가능성 유지.
- 무엇이 가능해졌나: BM25와 Dense의 중간 지대.
- 어디에 적합한가: 도구 통합(BGE-M3의 sparse 모드, SPLADE++).
- 한계: 학습된 가중치라 Inverted Index가 가변 — 일반 ES보다 운영 복잡.
7.3 Elasticsearch + vector field — 단일 시스템 통합
- 무엇이 바뀌나: ES 8.x부터 dense_vector 필드 + BM25를 같은 인덱스에서 동시 운영.
- 왜 쓰나: 두 시스템 운영 부담 회피.
- 무엇이 가능해졌나: BM25와 Dense를 한 query DSL로.
- 어디에 적합한가: 기존 ES 운영 + 새 RAG 통합.
- 한계: ES의 HNSW는 전용 벡터DB(Qdrant 등)보다 약간 약함. 대규모(\(>\) 100M)에선 분리 권장.
7.4 한국어 형태소 분석기 비교 — Mecab vs Kiwi vs Nori
- 무엇이 바뀌나: 분석 정확도·속도·관리 부담.
- 왜 쓰나: BM25의 토큰 단위가 분석기에 의존.
- 무엇이 가능해졌나: 한국어 RAG의 검색 품질 결정.
- 어디에 적합한가:
- Mecab: 가장 빠름, C++. 사용자 사전 관리 부담.
- Kiwi: Python 친화, 한국어 신조어·고유명사에 강함.
- Nori: Elasticsearch 내장, 운영 단순.
- 한계: 분석기 변경 시 전체 재인덱싱.
7.5 BM25 + 도메인 사전 — 고유명사 사전 등록
- 무엇이 바뀌나: SKU 코드, 약어, 사내 용어를 형태소 분석기의 사용자 사전에 등록.
- 왜 쓰나: 기본 분석기가 고유명사를 잘못 분리 막음.
- 무엇이 가능해졌나: PR-2024-Q3 같은 코드가 하나의 토큰으로 유지.
- 어디에 적합한가: 사내 RAG.
- 한계: 사전 관리 부담. 신조어 발생 시 수동 업데이트.
8. 한계와 실패 양상
8.1 유사어·의미 매칭 부족
- 왜 본질적인가: BM25는 문자열 일치. "환율 적용"과 "통화 환산"의 의미적 연결을 모름.
- 진단: 유사어 query의 recall이 Dense보다 현저히 낮음.
- 완화: Dense와 Hybrid(12편). 또는 동의어 사전을 ES analyzer에 등록.
- 다음 편: 12편.
8.2 한국어 조사·어미로 토큰 깨짐
- 왜 본질적인가: 분석기 없이는 "환율을"·"환율이"·"환율은"이 모두 다른 토큰. BM25가 세 토큰을 다른 단어로 셈.
- 진단: 같은 명사의 다양한 활용형이 top-K를 분산.
- 완화: 형태소 분석기로 어간 추출 (Mecab/Kiwi/Nori).
- 다음 편: 외전 D(한국어 RAG).
8.3 고빈도 stopword 점수 오염
- 왜 본질적인가: "the", "of", "는", "이" 같은 단어가 IDF로 어느 정도 억제되지만 완전 0은 아님. 짧은 query에서 잡음.
- 진단: top-K가 stopword 매칭으로 흩어짐.
- 완화: stopword 사전을 analyzer에 등록(ES
stopfilter). - 다음 편: 운영 위생 — 16편.
8.4 문서 길이 편차 큰 코퍼스
- 왜 본질적인가: BM25의 \(b\) 파라미터가 길이 정규화하지만, 편차가 극단적(10토큰 vs 10K토큰 혼재)이면 정규화 곡선이 어느 쪽도 만족 못함.
- 진단: 짧은 청크가 일관되게 top-K 상위 또는 하위로 쏠림.
- 완화: 청크 크기를 균일화(5편), 또는 BM25 파라미터를 코퍼스별 튜닝.
- 다음 편: 5편 cross-ref, 16편(튜닝).
8.5 오탈자·표기 차이 누락
- 왜 본질적인가: "Pinecone"과 "pinecone"은 lowercase로 같지만 "AI"와 "에이아이"는 다름. BM25는 표면형 일치만.
- 진단: 같은 의미의 다른 표기에서 누락.
- 완화: analyzer의 folding / normalization 필터. 또는 ngram("sku20240Q3" 같은 매칭).
- 다음 편: 12편 Hybrid.
8.5 Common Pitfalls
- "BM25는 옛날 거니까 안 써도 된다." — 10편 3장, 본 편 3장. 고유명사·코드 query에 결정적.
- "한국어도 ES default analyzer로." — 8.2절. Nori/Mecab/Kiwi 필수.
- "BM25는 튜닝이 필요 없다." — \(k_1\), \(b\), stopword, analyzer 모두 코퍼스별 튜닝.
- "Hybrid는 Dense + BM25 가중 합이면 끝." — 12편 RRF가 표준. 단순 가중 합은 점수 스케일 차이로 깨짐.
- "형태소 사용자 사전 없이도 사내 약어 잘 잡힌다." — 7.5절. 사전 등록이 매칭률 결정.
9. 정리된 결론
Q1. BM25가 TF-IDF에 더한 두 가지 개선은?
문서 길이 정규화 (\(b\)), TF 포화 곡선 (\(k_1\)). Chapter: 5장.
Q2. BM25가 Dense보다 강한 query 종류는?
고유명사, 코드, 약어, 정확 표현, 희귀 어휘. 임베딩이 흐려지는 토큰들. Chapter: 3장, 8.1절(반대 케이스), 10편 8.1절.
Q3. Inverted Index가 빠른 이유는?
단어 → 문서 ID 리스트의 사전 구조. 검색이 query 토큰 수에 비례하고 전체 문서 수와 무관. Chapter: 4장, 5장.
Q4. 한국어 BM25의 최소 위생 두 가지는?
형태소 분석기로 조사 분리 + 어간 추출. 사용자 사전에 사내 고유명사 등록. Chapter: 6.3절, 7.5절, 8.2절.
Q5. BM25와 Dense를 결합하는 표준 방식은?
각자 top-K' 추출 후 RRF(Reciprocal Rank Fusion). 단순 가중 합은 점수 스케일 차이로 불안정. Chapter: 6.4절, 12편 cross-ref.
10. 추가 학습
1차 자료
- Robertson, S., Zaragoza, H. The Probabilistic Relevance Framework: BM25 and Beyond. Foundations and Trends in IR 2009. (BM25 표준 정리)
- Robertson, S. et al. Okapi at TREC-3. NIST Special Publication 1994. (BM25 원안)
- Formal, T. et al. SPLADE: Sparse Lexical and Expansion Model for First Stage Ranking. SIGIR 2021. arXiv:2107.05720.
- Lin, J. A Few Brief Notes on DeepImpact, COIL, and a Conceptual Framework for Information Retrieval Techniques. arXiv:2106.14807 (2021). (Sparse-Dense 통합 관점)
- Park, E. Mecab-ko: Korean fork of MeCab. (한국어 형태소 표준)
공식 docs
- Elasticsearch BM25:
https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-similarity.html - OpenSearch BM25:
https://opensearch.org/docs/latest/query-dsl/full-text/match/ - Elasticsearch Nori:
https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-nori.html - rank-bm25 (Python):
https://github.com/dorianbrown/rank_bm25 - Kiwi:
https://github.com/bab2min/Kiwi
보조 자료
- 사용자 노트 10장 — BM25.
- 사용자 노트 35장 6절 — Retriever Routing (BM25와 Dense의 분리·결합).
Cheat Sheet
| 항목 | 표준값 / 권장 |
|---|---|
| \(k_1\) | 1.2 ~ 2.0 (Elasticsearch 기본 1.2) |
| \(b\) | 0.75 (대부분 코퍼스에 안정) |
| Stopword | 영어 standard, 한국어는 analyzer 내장 사전 |
| Analyzer (한국어) | Nori (ES 내장) > Kiwi (Python 친화) > Mecab (속도 최고) |
| 사용자 사전 | 사내 약어·고유명사 반드시 등록 |
| candidate 수 | top 50~200 (Hybrid·Reranker 이전) |
| Hybrid 결합 | RRF (k=60 기본) — 12편 |
원칙 한 줄: BM25는 고유명사·코드의 정확 매칭, Dense는 의미 유사도 — 한국어는 형태소 분석기 + 사용자 사전이 BM25 품질의 절반.
Bridge — 다음 편
다음 — RAG 핵심 학습 (12/26) Hybrid Search와 Score Fusion.
Dense와 BM25를 어떻게 결합할 것인가. Reciprocal Rank Fusion(Cormack 2009), Weighted Fusion, Score Normalization, 그리고 가중치 실험의 표준 패턴까지. 두 검색의 분업이 RAG 정확도를 한 단계 끌어올린다.
시리즈 전체 안내: 시리즈 목차
댓글
댓글 쓰기