---
title: "BM25-only가 정답인지 MES·SCM 검색을 질문 유형별로 다시 나눠 봤다"
slug: "bm25-only-mes-scm-retrieval"
category: "RAG"
topic: "rag"
subtopic: "retrieval"
tags: ["BM25","Hybrid Search","Dense Retrieval","Reranker","MES","SCM","Korean Tokenizer"]
status: "published"
created: "2026-07-21"
updated: "2026-07-21"
summary: "BEIR·MIRACL·BGE-M3와 검색 엔진 공식 문서를 대조해, MES·SCM 검색을 BM25 하나가 아니라 질문 유형별 경로로 설계해야 하는 이유를 정리했다."
kind: "Paper Review"
evidence: "BEIR·MIRACL·BGE-M3 원문과 Elasticsearch·Nori·ParadeDB·Lindera 공식 문서 대조"
---

MES·SCM 문서 검색을 생각하면서 처음 붙잡은 질문은 단순했다.

> BM25-only로 가도 되는가?

품번, LOT, W/O, PO, 화면 ID, 에러 코드가 많은 도메인이라면 BM25가 잘 맞을 것 같았다. 실제로 `MAT-2048-A`, `WO-20260721-0031`처럼 표면형 자체가 중요한 값은 의미가 비슷한 문장을 찾는 것보다 같은 문자열을 놓치지 않는 일이 먼저다.

그런데 논문과 공식 문서를 대조하고 나니 질문부터 잘못 잡았다는 생각이 들었다. **BM25와 Dense 중 하나를 고르는 문제가 아니라, 질문이 어느 데이터 경로로 가야 하는지를 먼저 정하는 문제**에 가까웠다.

지금 내 결론은 이렇다.

> 전체 MES·SCM 질문에 BM25-only를 적용하는 것은 권고하기 어렵다. 다만 BM25는 반드시 측정해야 할 baseline이고, 코드·품번 검색에서는 exact lookup과 함께 가장 먼저 실행할 fast path다.

이 결론은 아직 사내 MES 문서로 검증한 결과가 아니다. 아래에서 논문으로 확인한 사실과 실제 시스템에 적용하려는 설계 판단을 분리해서 적는다.

## 먼저 바로잡은 것은 exact match와 BM25가 다르다는 점이다

처음에는 품번 검색에 BM25가 강하다는 문장으로 정리하려 했다. 이 표현은 절반만 맞다.

BM25는 query와 document에 나타난 term을 바탕으로 점수를 계산한다. 문서 안에서 term이 얼마나 나타나는지, 전체 문서 집합에서 얼마나 희귀한지, 문서 길이가 어떤지를 반영한다. 자주 등장할수록 점수가 계속 선형으로 커지지 않도록 포화시키고, 긴 문서가 무조건 유리해지는 것도 보정한다.

하지만 BM25는 exact lookup이 아니다. 분석기가 `WO-20260721-0031`을 하이픈 단위로 나누거나 소문자로 바꾸면 내가 기대한 코드 한 덩어리가 그대로 남지 않을 수 있다.

Elasticsearch의 `term` query 문서는 product ID 같은 정밀한 값의 조회를 예로 들며, 저장된 term과 정확히 일치하는 값을 찾는다고 설명한다. 반대로 `text` field는 indexing 과정에서 분석되므로 `term` query를 피하라고 경고한다. 즉 코드 검색의 첫 단계는 BM25 점수 조정이 아니라 field mapping이다.

```text
part_no.keyword  → exact term lookup
part_no.text     → 분석된 검색 또는 BM25 후보
description      → BM25 / Dense 후보
```

품번과 화면 ID가 정확히 존재하면 바로 반환하고, 일부만 입력했거나 오타·별칭이 섞였을 때 BM25나 다른 검색 경로로 내려가는 편이 역할에 맞다. **Exact Registry → keyword/term → BM25**라는 순서는 논문이 제안한 표준 구조가 아니라, 공식 문서의 exact semantics를 MES 식별자에 적용한 내 설계 판단이다.

## BEIR에서 확인한 BM25는 강한 baseline이지 항상 정답은 아니었다

BEIR는 서로 다른 task와 domain의 영어 검색 데이터셋에서 lexical, sparse, dense, late-interaction, reranking 계열을 zero-shot으로 비교했다. 이 논문에서 가장 먼저 가져갈 문장은 “BM25가 Dense를 이겼다”가 아니다.

> 하나의 접근이 모든 데이터셋에서 일관되게 이기지 못했다.

논문의 표 2에서 BM25는 강한 기준선이었다. 당시 Dense 모델인 ANCE, TAS-B, GenQ는 데이터셋에 따라 BM25보다 크게 낮아졌고, domain이나 task가 학습 데이터와 달라질 때 특히 불안정했다. 반면 BM25 상위 100개를 cross-encoder로 다시 정렬한 `BM25+CE`는 18개 zero-shot 데이터셋 중 16개에서 BM25를 넘었고, 논문이 계산한 BM25 대비 평균 성능은 11% 높았다.

이 결과는 2021년 당시 공개 모델을 비교한 것이다. 이것만으로 현재의 모든 Dense 모델이 BM25보다 약하다고 말할 수 없다.

품질이 높았던 reranking과 late interaction에는 비용도 붙었다. 논문의 DBPedia 100만 문서 실험에서 BM25의 CPU latency는 20ms, top-100을 다시 정렬한 `BM25+CE`는 GPU 450ms, CPU 6,100ms로 보고됐다. 오래된 장비와 특정 구현에서 측정한 수치라 현재 SLA에 그대로 대입할 수는 없지만, “후보 전체를 비싼 모델로 비교하지 말고 작은 후보 집합을 만든 뒤 재정렬한다”는 구조는 분명하다.

BEIR 자체의 한계도 중요했다.

- 평가 대상은 영어였다.
- neural model은 문서의 앞 512 word pieces만 사용했다.
- relevance judgment pool을 lexical 검색 결과로 만든 데이터셋은 Dense가 찾아낸 새로운 문서를 미관련으로 처리할 수 있다.
- 논문이 TREC-COVID의 누락 judgment를 추가했을 때 ANCE의 nDCG@10은 0.654에서 0.735로 올라갔다.

그래서 BEIR는 “BM25-only가 정답”이라는 근거보다 **새 검색 모델을 도입해도 BM25를 빼고 평가하면 안 된다**는 근거로 읽는 편이 안전하다.

## MIRACL에서는 다국어 결합이 강했지만 모든 언어에서 이기지는 않았다

MIRACL은 18개 언어의 Wikipedia passage를 대상으로 같은 언어의 query와 document를 찾는 monolingual retrieval dataset이다. 원 논문은 78,000개 query와 726,000개가 넘는 사람의 relevance judgment를 제공한다.

개발셋 평균 nDCG@10은 다음과 같았다.

| 방식 | 전체 평균 nDCG@10 | 한국어 nDCG@10 |
|---|---:|---:|
| BM25 | 0.385 | 0.419 |
| mDPR | 0.418 | 0.419 |
| BM25 + mDPR Hybrid | 0.566 | 0.609 |

Hybrid는 BM25와 mDPR 점수를 각각 0~1로 정규화한 뒤 0.5 대 0.5로 합쳤다. 별도 tuning 없이도 평균에서는 개별 모델보다 높았다. 한국어에서도 BM25와 mDPR이 같은 0.419였지만 결합 결과는 0.609였다.

이 숫자는 “한쪽이 놓친 후보를 다른 쪽이 보완한다”는 가설을 뒷받침한다. 다만 모든 언어에서 결합이 이긴 것은 아니다. Yoruba에서는 BM25 0.406, mDPR 0.396, Hybrid 0.374였다.

평가셋이 만들어진 과정도 봐야 한다. MIRACL은 BM25, mDPR, mColBERT가 가져온 후보를 결합해 상위 passage를 사람이 판정했다. 한 종류의 retriever만 사용한 pool보다는 다양하지만, 평가 대상이 Wikipedia이고 query도 사실형 정보 검색에 가깝다. 화면 조작 매뉴얼, 설비 알람, 품번, 표, 실시간 WIP가 섞인 MES corpus와는 데이터 생성 과정부터 다르다.

따라서 MIRACL에서 가져갈 수 있는 것은 “MES에서도 0.609가 나온다”가 아니라 **다국어 검색에서도 lexical과 Dense의 오류가 달라 결합할 여지가 있었다**는 정도다.

## BGE-M3에서는 dataset에 따라 Sparse와 Dense의 우열이 바뀌었다

BGE-M3는 하나의 model이 Dense, learned sparse, multi-vector retrieval을 모두 수행하도록 학습했다. 100개가 넘는 언어와 최대 8,192 token 입력을 목표로 했고, 세 방식의 score를 결합할 수 있게 설계했다.

MIRACL 개발셋에서 논문이 보고한 nDCG@10은 100점 척도로 다음과 같다.

| 방식 | MIRACL 평균 nDCG@10 |
|---|---:|
| BM25, XLM-R tokenizer | 31.9 |
| M3 Sparse | 53.9 |
| M3 Dense | 69.2 |
| M3 Dense + Sparse | 70.4 |
| M3 Dense + Sparse + Multi-vector | 71.5 |

이 데이터에서는 Dense가 Sparse보다 높았고 결합이 더 높았다. 반면 multilingual long-document retrieval dataset인 MLDR에서는 M3 Dense가 52.5, Sparse가 62.2, Dense+Sparse가 64.8이었다. Sparse가 Dense보다 9.7점 높았고 결합이 다시 둘을 넘었다.

여기에도 그냥 지나치면 안 되는 조건이 있다.

- M3의 fine-tuning data에는 MIRACL과 Mr. TyDi가 포함됐다. 따라서 MIRACL 결과를 zero-shot 일반화 성능으로 읽으면 안 된다.
- 논문의 `Sparse`는 전통적인 BM25가 아니라 model이 term weight를 학습한 learned sparse 방식이다.
- `All`은 단순한 1차 검색이 아니다. Dense 후보를 대상으로 multi-vector score까지 계산하는 reranking 단계가 포함된다.
- 저자들도 real-world와 더 다양한 dataset에서의 일반화, 언어별 성능 차이, 긴 문서의 계산 비용을 limitation으로 적었다.

BGE-M3에서 내 판단을 가장 많이 바꾼 부분은 appendix의 tokenizer 비교였다. 같은 BM25도 XLM-R tokenizer를 쓰면 MIRACL 31.9, MKQA 39.9, MLDR 53.6이었고, Lucene analyzer를 쓰면 각각 38.5, 40.9, 64.1이었다. BM25의 `k1`, `b`를 조정하기 전에 token이 어떻게 만들어지는지부터 확인해야 한다는 뜻으로 읽었다.

## 한국어 BM25는 tokenizer 실험 없이 평가할 수 없다

한국어는 조사와 어미가 붙고, 복합 명사와 업무 약어가 많다. MES 문서에는 자연어만 있는 것도 아니다.

```text
자재불출처리
비가동사유등록
설비종합효율(OEE)
WO-20260721-0031
MTR_A_2048
```

Elasticsearch Nori는 기본적으로 `mecab-ko-dic`을 사용한다. 복합어를 원형으로 둘지, 분해된 token만 남길지, 둘 다 남길지를 `decompound_mode`의 `none`, `discard`, `mixed`로 정한다. 사용자 사전에는 custom noun과 복합 명사의 분해 규칙을 추가할 수 있다.

예를 들어 `비가동사유등록`을 하나의 업무 용어로도 찾고 `비가동`, `사유`, `등록`으로도 찾으려면 `mixed`와 사용자 사전을 실험할 이유가 생긴다. 반대로 품번 내부의 하이픈과 underscore는 형태소 분석기에 맡기기보다 별도의 exact field로 보존하는 편이 안전하다.

ParadeDB 공식 문서의 Korean Lindera는 prebuilt KoDic으로 한국어를 token화한다고 설명한다. Lindera upstream은 ko-dic user dictionary build를 지원한다. 다만 현재 ParadeDB의 공개 Lindera 문서에는 이 user dictionary를 ParadeDB index 설정에 전달하는 방법이 보이지 않는다. **Lindera 자체의 지원과 ParadeDB integration의 지원을 같은 것으로 가정하면 안 된다.** 제품 후보를 정한 뒤 실제 노출 API를 다시 확인해야 한다.

아직 사내 corpus로 확인하지 않은 상태에서 “Nori가 Lindera보다 좋다”거나 그 반대를 말할 근거는 없다. 아래 조건을 같은 평가셋으로 비교해야 한다.

- whitespace 또는 기본 analyzer
- Nori 기본 설정
- Nori `mixed` + MES 사용자 사전
- Korean Lindera + KoDic
- exact field를 분리한 상태와 분리하지 않은 상태

## 논문을 읽은 뒤 다시 나눈 MES·SCM 검색 경로

논문은 MES architecture를 직접 다루지 않는다. 다음 표는 논문에서 확인한 retrieval 특성과 MES 데이터의 성격을 연결한 **설계 가설**이다.

| 질문 유형 | 우선 경로 | 이유 |
|---|---|---|
| 품번·LOT·W/O·PO·화면 ID·에러 코드 | Exact Registry → keyword/term → BM25 fallback | 식별자는 의미 유사도보다 원문 보존과 exact hit가 먼저다. |
| BOM·OEE·MRP 같은 용어 | Alias Registry → BM25 → 필요 시 Dense | 약어와 사내 별칭을 먼저 정규화한 뒤 설명 문서를 찾는다. |
| 화면 매뉴얼·SOP·절차 | BM25 baseline → BM25+Dense 후보 → 선택적 rerank | 메뉴명과 버튼명은 lexical signal이 강하지만 자연어 질문과 본문 사이에는 lexical gap이 생긴다. |
| 자연어 부품 탐색 | 구조화 field filter + BM25 + Dense | 규격·설비·공정 조건과 의미 설명을 함께 사용해야 한다. |
| 장애·현장 사례 | Hybrid 후보 → 필요 시 rerank | 증상 표현은 달라도 원인이 같을 수 있고, 에러 코드의 exact signal도 중요하다. |
| 실시간 재고·WIP·출하량 | API / SQL / Metric Layer | 최신 수치 계산은 문서 검색 문제가 아니다. |
| 지표 공식·정의 | Metric Definition DB | 공식, 단위, 집계 grain, version을 구조화해 관리해야 한다. |
| 표·양식 | 구조화 parser → field search / SQL | chunk text만으로 column과 row 관계를 보존하기 어렵다. |

이 표에서 Dense는 모든 query에 무조건 실행되는 기본값이 아니다. code pattern이 확실한 query는 exact fast path로 끝낼 수 있다. 반대로 사용자가 “프레스 2호기에서 소재가 밀려 불량이 난 사례”처럼 표현하면 품번이 없어도 비슷한 장애 기록을 찾을 수 있는 Dense 후보가 필요할 수 있다.

내가 생각하는 routing은 다음에 가깝다.

```text
user query
  ├─ identifier detected ─→ registry / exact field ─→ confident hit면 종료
  ├─ live value intent ───→ API / SQL / metric layer
  ├─ metric definition ───→ definition DB
  └─ document search
       ├─ BM25 candidates
       ├─ Dense candidates, 필요한 query만
       ├─ merge / deduplicate
       └─ rerank, 품질 이득이 비용을 넘을 때만
```

## 읽고 나서 달라진 생각

### Baseline은 임시 구현이 아니었다

처음에는 BM25를 “Dense를 붙이기 전 단계”처럼 생각했다. BEIR를 읽고 이 생각을 버렸다. BM25는 새 모델이 정말 나아졌는지 확인하는 통제군이다. 특히 product code, menu label, error code처럼 lexical overlap 자체가 relevance인 query에서는 운영 경로로 남을 수 있다.

### Hybrid는 모델 두 개를 켜는 기능이 아니었다

MIRACL의 Hybrid는 score normalization과 고정 가중치를 사용했고, BGE-M3의 결합은 후보 집합과 가중치가 따로 있었다. Multi-vector는 reranking으로 사용됐다. 어떤 후보를 몇 개 가져오고, score scale을 어떻게 맞추고, 중복을 어떻게 제거할지 정하지 않으면 “Hybrid”라는 이름만 남는다.

### 검색 모델보다 데이터 경계가 먼저였다

실시간 재고를 embedding한 문서에서 찾으면 데이터가 낡는다. OEE 공식을 매뉴얼 문장 여러 개에서 조합하면 version과 단위가 흔들린다. 품번을 형태소 분석기에만 맡기면 정확한 key를 잃는다.

검색 품질이 낮다고 바로 embedding model을 바꾸기 전에, 이 질문이 문서 검색으로 가는 것이 맞는지부터 확인해야 한다.

### 한국어에서는 tokenizer도 model의 일부였다

BGE-M3 appendix에서 BM25 analyzer만 바꿔도 점수가 달라졌다. Nori 문서에서는 복합어 처리 방식에 따라 실제 token stream이 바뀐다. 앞으로 BM25 실험 결과를 적을 때는 “BM25” 한 단어로 끝내지 않고 analyzer, user dictionary, field mapping, parameter를 같이 남겨야 한다.

## 아직 확인하지 못한 것과 다음 실험

논문을 다 읽어도 사내 MES에서 무엇이 이기는지는 알 수 없다. 공개 benchmark와 사내 corpus 사이의 차이가 너무 크다. 다음 순서로 직접 측정해야 한다.

1. query를 식별자, 용어, 절차, 자연어 부품, 장애 사례, 실시간 값, 지표 정의로 나눈다.
2. 각 유형에서 실제 사용자가 찾으려는 정답과 허용 가능한 관련 문서를 표시한다.
3. Exact-only, BM25, Dense, BM25+Dense, Hybrid+Reranker를 같은 candidate budget으로 비교한다.
4. 전체 평균만 보지 않고 유형별 `Hit@1`, `Recall@k`, `nDCG@10`, p95 latency, index size를 기록한다.
5. 한국어 analyzer와 사용자 사전은 retrieval model과 분리하지 말고 같은 실험 축에 넣는다.
6. reranker는 top-N과 latency budget을 바꿔 품질 증가가 비용을 넘는 구간만 찾는다.

특히 코드 query와 자연어 query를 한 평균으로 합치면 결과를 잘못 읽을 수 있다. exact lookup이 코드 query를 거의 해결하고 Dense가 장애 사례에서만 개선되더라도, 전체 평균에서는 둘 중 하나가 불필요해 보일 수 있다. 유형별 실패를 먼저 보고 마지막에 전체 비용을 계산해야 한다.

지금 단계에서 BM25-only를 채택하거나 폐기할 생각은 없다. 먼저 exact field와 BM25 baseline을 제대로 만들고, 그 baseline이 놓치는 query 유형에만 Dense와 reranker를 추가한다. 논문을 읽고 얻은 답은 특정 algorithm 이름이 아니라 이 순서였다.

## References

### Papers

1. Stephen Robertson, Hugo Zaragoza. [BM25 원문](https://doi.org/10.1561/1500000019). Foundations and Trends in Information Retrieval, 2009.
2. Nandan Thakur, Nils Reimers, Andreas Rücklé, Abhishek Srivastava, Iryna Gurevych. [BEIR 논문](https://openreview.net/forum?id=wCu6T5xFjeJ). NeurIPS Datasets and Benchmarks, 2021.
3. Xinyu Zhang et al. [MIRACL 논문](https://aclanthology.org/2023.tacl-1.63/). Transactions of the Association for Computational Linguistics, 2023.
4. Jianlv Chen et al. [M3-Embedding 논문](https://aclanthology.org/2024.findings-acl.137/). Findings of ACL, 2024.

### Official documentation

5. Elastic. [Term query](https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-term-query).
6. Elastic. [nori_tokenizer](https://www.elastic.co/docs/reference/elasticsearch/plugins/analysis-nori-tokenizer).
7. ParadeDB. [Lindera tokenizer](https://docs.paradedb.com/documentation/tokenizers/available-tokenizers/lindera).
8. Lindera. [CLI commands 문서](https://lindera.github.io/lindera/lindera-cli/commands.html).

공식 문서는 2026-07-21에 확인했다.
