Skip to main content
임베딩 기반 Dense 검색이 “의미”를 이해한다면, BM25는 “단어 자체”를 정확하게 매칭합니다. 제품번호, 오류 코드, 고유명사 검색에서 BM25는 여전히 Dense 검색을 압도합니다. 이 페이지에서는 TF-IDF부터 BM25까지의 발전 과정과 수식의 의미를 자연어로 풀어 설명합니다.

TF-IDF: BM25의 조상

BM25를 이해하려면 먼저 TF-IDF를 알아야 합니다. TF-IDF는 “단어의 중요도”를 두 가지 관점에서 측정합니다.

TF (Term Frequency) — “이 문서에서 얼마나 자주 나오는가?”

문서 A: "Databricks는 Databricks 플랫폼에서 Databricks를 활용한..."
→ "Databricks" TF = 3 (3번 등장)

문서 B: "클라우드 플랫폼에서 Databricks를 사용하여..."
→ "Databricks" TF = 1 (1번 등장)

→ TF 관점: 문서 A가 "Databricks"에 더 관련 있음
직관: 검색어가 문서에 많이 등장할수록 해당 문서가 그 주제에 관련될 가능성이 높습니다.

IDF (Inverse Document Frequency) — “전체에서 얼마나 드문가?”

전체 문서 100만 개 중:
- "데이터"  → 80만 개 문서에 등장 → IDF = log(100만/80만) = 0.22 (낮음, 흔함)
- "Anomalib" → 50개 문서에만 등장 → IDF = log(100만/50) = 9.90 (높음, 희소)
- "그리고"  → 95만 개 문서에 등장 → IDF = log(100만/95만) = 0.05 (매우 낮음)
직관: 모든 문서에 나오는 단어(“그리고”, “있다”, “the”)는 검색 가치가 낮습니다. 희소한 단어일수록 검색 판별력이 높습니다.

TF-IDF 점수

TF-IDF(단어, 문서) = TF × IDF
“Anomalib”이 3번 등장하는 문서: 3 × 9.90 = 29.7 (매우 높음) “데이터”가 10번 등장하는 문서: 10 × 0.22 = 2.2 (낮음)
참고 핵심 통찰: TF-IDF의 강력함은 IDF 부분 에 있습니다. 흔한 단어의 영향을 줄이고, 희소한(= 의미 있는) 단어의 영향을 극대화합니다.

TF-IDF의 한계

  1. 문서 길이 미보정: 10페이지 문서는 단순히 길기 때문에 단어가 더 많이 등장합니다. TF-IDF는 이를 보정하지 않아 긴 문서가 항상 유리합니다.
  2. 포화 효과 없음: “Databricks”가 3번 나온 문서와 300번 나온 문서의 점수 차이가 100배입니다. 하지만 실제로 3번이든 300번이든 관련성 차이는 크지 않습니다.
TF-IDF에서:
TF=3   → 점수 3 × IDF
TF=300 → 점수 300 × IDF  ← 100배 차이 (비현실적)

BM25: TF-IDF의 진화

BM25 (Best Matching 25)는 TF-IDF의 두 가지 한계를 우아하게 해결합니다.

BM25 수식

BM25(q, d) = Σ IDF(t) × [ TF(t,d) × (k1 + 1) ] / [ TF(t,d) + k1 × (1 - b + b × |d|/avgdl) ]

각 항의 의미:
- q: 검색 쿼리
- d: 평가 대상 문서
- t: 쿼리의 각 단어 (term)
- TF(t,d): 문서 d에서 단어 t의 등장 횟수
- IDF(t): 단어 t의 역문서 빈도
- |d|: 문서 d의 길이
- avgdl: 전체 문서의 평균 길이
- k1: TF 포화 정도 조절 (기본값: 1.2~2.0)
- b: 문서 길이 보정 강도 (기본값: 0.75)

개선점 1: 포화 함수 (Saturation)

BM25는 TF가 증가해도 점수가 상한선에 수렴 합니다. 수식에서 이것이 어떻게 작동하는지 보면, TF 관련 부분은 TF × (k1 + 1) / (TF + k1) 형태입니다. TF가 아무리 커져도 분자의 TF와 분모의 TF가 함께 커지므로, 이 값은 (k1 + 1)이라는 상한에 수렴합니다. 이것은 x / (x + c) 형태의 포화 함수(saturation function) 로, x가 0일 때 0이고 x가 무한대일 때 1에 수렴하는 S자 곡선입니다:
TF=1  → BM25 기여: 0.55
TF=3  → BM25 기여: 0.83
TF=10 → BM25 기여: 0.95
TF=100 → BM25 기여: 0.99  ← 거의 차이 없음!

(k1=1.5 가정)
이것은 “3번 나왔든 100번 나왔든, 이 문서가 해당 단어와 관련 있다는 신호는 비슷하다” 는 직관을 반영합니다. k1 파라미터의 역할:
  • k1이 작으면(0.5): 빠르게 포화 → TF 차이가 거의 무시됨
  • k1이 크면(3.0): 천천히 포화 → TF 차이가 더 반영됨

개선점 2: 문서 길이 정규화

보정 계수: (1 - b + b × |d|/avgdl)

문서가 평균보다 긴 경우: |d|/avgdl > 1 → 보정 계수 증가 → 점수 하락 (페널티)
문서가 평균보다 짧은 경우: |d|/avgdl < 1 → 보정 계수 감소 → 점수 상승 (보너스)
b 파라미터의 역할:
  • b=0: 문서 길이를 완전히 무시
  • b=1: 문서 길이를 강하게 보정
  • b=0.75 (기본값): 적당한 수준의 보정
참고 수식을 자연어로 요약: BM25는 이렇게 말합니다: “이 단어가 문서에 나왔어? 좋아, 점수를 줄게. 많이 나왔어? 약간 더 줄게, 하지만 무한히 올라가진 않아. 그리고 이 문서가 너무 길어서 단어가 많이 나온 거라면, 좀 깎을게.”

Dense vs Sparse: 시나리오별 비교

시나리오Dense (임베딩)Sparse (BM25)승자
”자동차 보험 가입” → “차량 보험 신청”의미적으로 유사 인식키워드 불일치Dense
”오류코드 ERR-5012”의미를 이해하지 못함정확한 문자열 매칭BM25
”Anomalib 설치 방법""Anomalib”을 일반 단어로 취급고유명사 정확 매칭BM25
”고장이 잦은 부품” → “결함률이 높은 컴포넌트”동의어/유의어 인식키워드 불일치Dense
”Delta Lake 3.2.1 릴리즈 노트”버전 번호 구분 어려움정확한 버전 매칭BM25
주의 핵심: Dense와 BM25는 경쟁 관계가 아니라 보완 관계 입니다. 실무에서는 둘 다 사용하는 하이브리드 검색이 최선입니다.

한국어에서 BM25: 공백 분절의 한계

BM25는 텍스트를 단어 단위로 분리(토큰화) 해야 합니다. 영어는 공백으로 단어가 깔끔하게 나뉘지만, 한국어는 그렇지 않습니다.
영어: "Databricks is great" → ["Databricks", "is", "great"] ← 깔끔
한국어: "데이터브릭스에서 벡터검색을 사용합니다"
  공백 분절: ["데이터브릭스에서", "벡터검색을", "사용합니다"] ← 조사가 포함됨!
“데이터브릭스에서”와 “데이터브릭스를”은 공백 분절 기준으로 다른 단어 가 됩니다. 이러면 “데이터브릭스”를 검색해도 매칭되지 않습니다.

해결: Kiwi 형태소 분석기

Kiwi 는 한국어 형태소 분석기(문장을 의미의 최소 단위인 형태소로 분리하는 도구)입니다. 내부적으로 통계 기반 모델과 사전을 결합하여, 조사/어미를 분리하고 원형(lemma)을 복원합니다. Python에서 pip install kiwipiepy로 설치할 수 있으며, C++ 기반이라 속도가 빠릅니다.
"데이터브릭스에서 벡터검색을 사용합니다"
→ Kiwi: ["데이터브릭스", "에서", "벡터", "검색", "을", "사용", "하", "ㅂ니다"]
                          ↑ 명사만 추출하여 BM25 인덱스 구축 가능!
형태소 분석을 거치면 “데이터브릭스에서”, “데이터브릭스를”, “데이터브릭스의”가 모두 “데이터브릭스” 라는 동일한 토큰으로 정규화되므로, 조사 변화에 관계없이 정확한 키워드 매칭이 가능해집니다.
참고 실무 권장: 한국어 BM25를 구현할 때는 반드시 Kiwi 같은 형태소 분석기를 전처리로 적용하세요. 공백 분절만으로는 한국어 BM25의 성능이 영어 대비 30-50% 떨어집니다. 자세한 내용은 한국어 RAG 최적화를 참고하세요.

다음: Dense와 Sparse를 어떻게 결합할까요? → 하이브리드 검색 & RRF