0057: BM25 backend을 bm25s로 추가 분석 변형
0057: BM25 backend을 bm25s로 추가 분석 변형
- Status: accepted
- Date: 2026-05-18
- Deciders: hskim
- Related: ADR 0001 (naive_baseline 불변식), ADR 0010 (하이브리드 BM25 기준선), ADR 0011 / ADR 0013 / ADR 0023 / ADR 0031 (재사용되는 additive opt-in 백엔드 패턴), issue #988, context7 audit sweep 2026-05-18 (
~/.claude/plans/context7-fizzy-glade.md)
Context
context7 audit Tier 2 finding — 현재 rag_retrieval.py:80 가 rank_bm25.BM25Okapi (pure Python BM25) 를 lazy-import 한다. bm25s 는 numpy sparse matrix 기반 BM25 구현으로 큰 corpus (1000+ docs) 에서 100-500x 빠르고, method 다양성 (robertson / lucene / atire / bm25+ / bm25l) + IDF mixing + idf_method 옵션을 제공한다.
100-doc 도메인에선 latency 차이가 sub-ms 수준이라 즉각적 가치는 작다. 그러나 향후 scale-out (1000+ docs 또는 다른 corpus 추가) 시 의미가 있고, modern API ergonomics (bm25s.BM25(...).index(corpus).retrieve(query, k=N)) 가 사용자 코드를 단순화한다. 더 중요한 가치는 opt-in additive pattern 강화 — ADR 0011 (LLM synthesis), ADR 0023 (HyDE), ADR 0031 (kiwi tokenizer) 모두 같은 패턴 (default 미변경, opt-in 만 추가) 을 따른다.
사전 검증 (2026-05-18, ~/.claude/plans/context7-fizzy-glade.md A8 sub-plan §사전 검증) 에서 bm25s.BM25(method="robertson", k1=1.5, b=0.75) 가 rank_bm25.BM25Okapi 와 동일 corpus 토큰에 대해 ranking 100% 일치 함을 확인했다 (한국어 RFP-ish corpus, multi-hit / single-term / IDF-effect / OOV 4 query). 절대 점수는 IDF 처리 차이로 다르지만, RRF fusion 은 ordering 만 사용하므로 fusion 후 결과는 bit-equal 이다. lucene (bm25s default), atire 는 마지막 두 위치 swap — 사용하지 않는다.
Decision
rag_retrieval.py 에 bm25_backend: "okapi" | "bm25s" 파이프라인 config 키 추가. 네 프리셋 (naive_baseline, agentic_full, agentic_full_llm, agent_react) 모두 기본값 "okapi". eval/config.yaml 에 신규 분석 변형 행 full_bm25s 추가 (bm25_backend: bm25s + retrieval_backend: hybrid). requirements-bm25s.txt 신규 opt-in (bm25s>=0.2,<1.0); 기본 requirements.txt 에는 추가하지 않는다.
BIDMATE_BM25_BACKENDenv var (defaultokapi) 도 fallback 지원 — config 키가 우선, env 가 process-wide fallback.rag_retrieval._make_bm25_instance(corpus, backend)신규 factory —backend="okapi"시_BM25Okapi(corpus),backend="bm25s"시_bm25s.BM25(method="robertson", k1=1.5, b=0.75).index(corpus).rag_retrieval.get_or_build_bm25cache key 가(profile, tokenizer)→(profile, tokenizer, backend, schema_version, chunk_count). okapi / bm25s 캐시 격리.rag_retrieval.bm25_scores_for_index도backendkwarg 받음.bm25.get_scores(...)인터페이스가 두 backend 통일 (사전 검증).
Never-raise vs typed-raise 계약
bm25s 경로는 typed-raise 다 (ADR 0031 의 kiwi silently degrade 와 다름):
_make_bm25_instance가_bm25s is None이면RuntimeErrorraise + 설치 hint- 이유:
bm25_backend="bm25s"는 명시적 opt-in 이므로 사용자가 의도해서 켠 것. silent fallback 은 측정 의도를 가린다 (full_bm25s행이hybrid_bm25와 byte-equal 이 되어버려 lift 측정 불가능). - 기본
requirements.txt만 설치한 CI 에서full_bm25s행을 실행하면 build step 에서 실패하지만, defaultokapi경로는 영향 받지 않음 —_bm25s is None이어도_BM25Okapi(corpus)는 정상 동작.
계약 보존
- ADR 0001 (naive_baseline): 네 프리셋 모두
bm25_backend: "okapi"보유.naive_baseline골든 (tests/data/naive_baseline_top_k.json,tests/test_naive_baseline_ranking_invariance.py) 이 bit-identical —naive_baseline은retrieval_backend: dense라 BM25 호출 자체가 없다. 명시적"okapi"값은 BM25 를 silently bm25s 로 swap 하는 미래 변경으로부터 보호. - ADR 0010 (hybrid BM25): 기존 하이브리드 분석 변형 행 (
hybrid_bm25,hybrid_bm25_extra_stopwords,hybrid_bm25_k30_*등) 은 기본값으로 암묵적bm25_backend: "okapi"→ 기존 eval delta 수치 byte-equal. - ADR 0031 (kiwi tokenizer):
full_kiwi행은bm25_tokenizer: kiwi+ 기본bm25_backend: okapi. 신규full_bm25s는bm25_tokenizer: regex+bm25_backend: bm25s. 두 축 독립 —full_bm25s_kiwi같은 조합 행은 본 ADR 범위 밖 (별도 ablation 필요 시 추가). - ADR 0003 (answer/citation 계약): 스키마 변경 없음.
bm25_backend키는eval_summary.json행 메타데이터에 노출되지만answer.claims/answer.citations는 변경 없음.
Re-open 조건
additive opt-in 백엔드 (bm25_backend: bm25s) 추가 자체는 accepted — 구현·머지 완료 (PR #988), opt-in 으로 운영 중. 아래는 default flip (네 프리셋 기본값을 okapi → bm25s 로 변경) 만의 deferred 조건이다. 이 ADR 이 re-open 되어 bm25_backend 기본값이 bm25s 로 flip 되는 조건은 다음 세 가지 모두 충족:
- 메인테이너가 공개 fixture smoke eval surface (n=42) 또는 비공개 real eval (n=100) 에서
bm25_backend: bm25s+bm25s설치 상태로 실측 —eval_summary.json에 실제full_bm25s행 (build-fail 이 아닌) 생성. full_bm25s가hybrid_bm25(자연스러운 control — 같은retrieval_backend: hybrid,bm25_backend만 차이) 대비 다음 중 하나 이상 충족:accuracyORcitation_precision에서 ≥ +3pp lift, 95% bootstrap CI 비중첩 (ADR 0026 / ADR 0031 임계값 일치)latency_p95≥ -30% 단축, ANDaccuracy/citation_precision동급 이상tests/test_bm25_backend_parity.py의top-N overlap ≥ 95%가 real corpus 에서도 유지 (작은 fixture 와 다를 수 있음)
- 후속 ADR (
005x이상 번호) 이 열려bm25_backend기본값 flip — CI 설치 footprint 영향 (bm25s+ numpy sparse 추가) 및 baserequirements.txt에bm25s추가할지 opt-in 유지할지 결정 문서화.
조건 1 충족 + 조건 2 미충족 시 (ADR 0019/0021/0031 이 임베딩/토크나이저에서 발견한 0pp-on-hybrid 패턴이 BM25 backend 에도 성립), 이 ADR 은 accepted 상태 유지 (default flip 만 deferred) 하고 공개 fixture smoke eval surface 에 측정 부록만 추가 — 측정 폐루프 작동.
Measurement
2026-05-22 — retrieval-channel A/B (top-N overlap, real-100)
PR #1303 (issue #1299) 이 bm25_backend 를 make_plan → plan dict → retrieve_candidates 로 threading 하기 전까지 full_bm25s 행은 plan 에 backend 키가 도달하지 않아 silently okapi 로 fallback 했다 (bm25s 라벨이지만 실제 okapi 측정). #1303 머지 + requirements-bm25s.txt 설치 후, 비공개 real-100 corpus 에서 Re-open 조건 (1)+(2)-③ 을 실측했다.
방법 — backend 변수만 격리한 retrieval-channel A/B. rag_retrieval.bm25_scores_for_index (파이프라인의 BM25 채널 그대로) 를 backend="okapi" / backend="bm25s" 두 번 호출, 동일 query tokens (regex tokenizer, shared stopword profile) 로 top-N chunk 집합 overlap 을 측정. corpus = real-100 인덱스 (26376 chunks, hashing 임베딩 빌드), queries = eval/real_config.local.yaml 의 221 케이스 질의. 풀 agentic 파이프라인 (planner / verifier / answer / rerank) 은 backend 와 무관하므로 우회 — 같은 run 안 inter-backend 비교. aggregate-only (ADR 0005; per-case chunk 텍스트 미노출).
| top-k | mean overlap | median | min | overlap≥0.95 비율 | exact 순서 일치 |
|---|---|---|---|---|---|
| 10 | 0.9819 | 1.00 | 0.60 | 91.4% | 89.6% |
| 30 | 0.9836 | 1.00 | 0.70 | 91.0% | 88.2% |
| 50 | 0.9854 | 1.00 | 0.62 | 90.5% | 88.2% |
(n=221, empty-token 질의 0건, bm25s.BM25(method="robertson", k1=1.5, b=0.75))
판정
- 조건 (1) 충족 — real n=221,
bm25s설치 상태에서 실제bm25s스코어링 실행. divergence 존재 (exact 순서 일치 88–90%, min overlap 0.60) 자체가 okapi silent fallback 이 아님을 증명한다 (fallback 이었다면 overlap 이 전부 1.0). #1303 plumbing + backend dispatch 가 진짜로bm25s경로를 행사. - 조건 (2)-③ 은 aggregate 로만 유지 — mean overlap 98.2–98.5% ≥ 0.95 이나, toy fixture 의 100% ranking parity 는 real corpus 에서 약화된다 (약 9–10% 질의가 overlap < 0.95, top-k 순서 완전 일치 ~88–90%).
tests/test_bm25_backend_parity.py주석이 예고한 “larger fixtures or real corpora may erode this” 가 26376-chunk corpus 에서 실제 발생 — robertson vs okapi 의 IDF 분포 차이가 큰 corpus 의 tie-breaking 을 일부 바꾼다. 이 overlap 은 swap 의 안전성 신호이지 개선 신호가 아니다. - 조건 (2)-①② 는 별도 측정하지 않음 — rankings 가 거의 동일 (mean 98%+) 한 데다 RRF fusion + rerank 가 다운스트림을 추가로 평탄화하므로
accuracy/citation_precision의 ≥ +3pp lift 가능성은 희박하다. 100-doc 도메인 latency 차는 sub-ms (bm25s의 100–500x 이득은 1000+ docs 에서만 발현). end-to-end accuracy delta 의 풀 파이프라인 측정은 parity 가 이미 높아 ROI 가 낮아 deferred.
결정 — 조건 (1) 충족 + 조건 (2) 실질 미충족 (parity 는 안전성 신호일 뿐 lift 아님; ADR 0019/0021/0031 의 0pp-on-hybrid 패턴이 BM25 backend 에도 성립). 위 “Re-open 조건” 의 line — 조건 1 충족 + 조건 2 미충족 시 측정 부록만 추가 — 경로를 따라 bm25_backend 기본값 flip 보류, bm25s 는 opt-in additive 백엔드로 유지. 후속 ADR (조건 3) 은 열지 않는다. 측정 폐루프 작동.
2026-05-22 — 풀 파이프라인 end-to-end 측정 (비공개 real-100, n=221)
2026-05-22 의 retrieval-channel A/B (위 항목) 는 top-N overlap 만 측정하고 end-to-end accuracy / citation_precision / latency delta 는 deferred 였다. 이번에 그 deferred 분을 실측하여 re-open 조건 (1) + (2)-①② 충족 여부를 확정한다.
- Surface: 비공개 real-100 eval, n=221 cases (answerable 118 / abstention 103). subset 없이 전체 — full bootstrap 검정력 유지.
- Index: prebuilt
data/index/real100(26376 chunks,EMBEDDING_BACKEND=hashing오프라인 기본 경로). 재빌드 없이 prebuilt 재사용. - Control:
full(hybrid + okapi, metadata_first + rerank + verifier 풀 agentic 파이프라인).full_bm25s와bm25_backend만 차이 (둘 다retrieval_backend: hybrid,bm25_tokenizer: regex) — 자연스러운 control.bm25s0.3.9 설치 상태에서full_bm25s행이 build-fail 아닌 실제 행으로 생성됨 (조건 (1) ✓).
| metric | full (okapi) |
full_bm25s |
delta |
|---|---|---|---|
| accuracy | 0.1610 CI[0.0932, 0.2288] | 0.1610 CI[0.0932, 0.2288] | +0.00 pp (CI 완전 중첩) |
| citation_precision | 0.1045 CI[0.0593, 0.1582] | 0.1059 CI[0.0593, 0.1610] | +0.14 pp (CI 중첩) |
| latency_p95 | 4249.6 ms | 14500.8 ms | +241% (느려짐) |
| retrieve_ms (mean / p95) | 1168 / 2017 ms | 2627 / 7041 ms | +125% / +249% |
(accuracy / citation_precision CI 는 answerable n=118 기반.)
판정:
- 조건 (2)-① (≥ +3pp lift, 95% CI 비중첩): ✗ 미충족. accuracy +0.00pp, citation_precision +0.14pp — 둘 다 임계값 3pp 미달이고 95% bootstrap CI 가 완전히 중첩한다.
- 조건 (2)-② (latency_p95 ≥ -30% 단축): ✗ 미충족. 단축은커녕 +241% 악화. 26376-chunk 스케일에서
bm25ssparse-matrix 경로의 per-queryretrieve_ms가 okapi 대비 mean 2.25배 / p95 3.5배 느리다 — ADR Context 의 “100-doc 도메인에선 latency 차이가 sub-ms, 향후 1000+ docs scale-out 시 의미” 예측과 정합 (현 스케일에선 고정 오버헤드가 우세). - 조건 (2)-③ (top-N overlap ≥ 95% on real corpus) 은 2026-05-22 retrieval A/B (overlap mean 98.2%@10) 로 이미 충족이나, 이는 parity 신호이지 lift 가 아니므로 default flip 의 근거가 되지 못한다.
결론: re-open 조건 (1) 충족 + (2)-①② 미충족 → bm25_backend 기본값 flip 보류 유지. ADR 0019 / 0021 / 0031 이 임베딩 / 토크나이저 축에서 발견한 0pp-on-hybrid 패턴 (RRF fusion + cross-encoder rerank 가 채널 ranking 차이를 평탄화) 이 BM25 backend 축에서도 성립함이 end-to-end 로 확정. 2026-05-22 retrieval-overlap 기반 0pp 예상과 일치. bm25s 는 okapi 와 동등 품질이되 현 스케일에선 더 느리므로 opt-in additive 변형으로 유지.
Caveats: single run (seed 미평균), full → full_bm25s 순차 실행 (동일 머신). latency 절대값은 머신 부하 노이즈를 포함한다 — backend 무관 축인 query_analysis_ms 도 412 → 813ms 로 변동 (run-order / load 변동 시사). 단 latency 방향성 (bm25s 가 -30% 개선의 정반대로 명백히 느림) 은 robust 하여 조건 (2)-② 판정은 결정적. 결과는 aggregate-only (ADR 0005 경계 — per-case text / doc id 미노출).
Consequences
이득
- 향후 scale-out (1000+ docs) 시 latency 100-500x 개선 가능 표면 확보. 100-doc 도메인에선 측정 가치 작지만 backend abstraction 자체가 future-proof.
- eval 매트릭스 1행 증가;
full_bm25s의hybrid_bm25대비 delta 가 항상 가시화 (CI install 시). - ADR 0001 불변식이 기본값 선택 (
"okapi") + 네 프리셋 명시로 보존. 추후 bm25s 제거는eval/config.yaml한 줄 삭제 +requirements-bm25s.txt삭제로 가능; 스키마 bump 없음. - 저장소 관용구에 네 번째 구체적 “default 키 기반 분석 변형” 사례 추가 (
query_expansionADR 0023,bm25_stopword_profileissue #150,bm25_tokenizerADR 0031 에 이어) — 측정 게이팅을 가진 additive Protocol 백엔드 dispatch.
비용
- 사용자가 이해해야 할 파이프라인 config 키 1개 추가. 기본값
"okapi"(동작 변경 없음) + typed-raise 계약 (silent fallback 없음) 으로 완화. requirements-bm25s.txt신규 — opt-in install layer 1개 추가 (m3/graph/lora/observability 패턴 inherits, 사용자 cognitive load minimal).bm25s경로 활성화 시 추가 ~10MB 설치 footprint (bm25s+numpy(이미 base) +scipy(이미 base)). minimal 환경에서 opt-in 으로 격리.- kiwipiepy / mecab / khaiii 토크나이저와
bm25sbackend 조합 행 (full_bm25s_kiwi등) 은 본 ADR 범위 밖. 필요 시 후속 추가 (eval row 1개 추가 비용).
제약 (불변)
- ADR 0001:
naive_baseline골든 bit-identical (tests/test_naive_baseline_ranking_invariance.py검증). - ADR 0003: answer / citation 계약 불변;
schema_versionbump 없음. - ADR 0010: 기존 하이브리드 BM25 분석 변형 행 byte-equal — 신규
full_bm25s행만 새 경로 행사. - ADR 0031:
bm25_tokenizer와bm25_backend는 직교 축.full_kiwi행은bm25_backend: okapi(기본),full_bm25s는bm25_tokenizer: regex(기본).
Alternatives considered
- rank_bm25 를 bm25s 로 전면 교체. 기각: ADR 0001 “기준선 보존” 불변식과 충돌. 모든 설치가
bm25s+ scipy 의존성을 강제로 끌어와 minimal-footprint 배포 스토리도 깨진다. 추가 행 패턴이 ADR 0019/0026/0031 deferred-then-closed 루프와 정확히 일치 — 측정 후 default flip 결정. bm25s.BM25(method="lucene")사용 (bm25s default). 사전 검증에서 ranking 이BM25Okapi와 마지막 두 위치 swap —robertson만큼 안전하지 않다. ES (Elasticsearch) 와의 absolute score parity 가 필요하면 향후 별도 ADR 로 결정.bm25s.BM25(method="atire"). Academic IR 표준이지만lucene과 같은 ranking 차이. 사용 안 함.bm25_backend를 env var 만으로 제어 (config key 추가 안 함). 같은 eval run 안에서okapi(hybrid_bm25 행) +bm25s(full_bm25s 행) 동시 측정 불가능 — env 는 process 전역. 본 ADR 의 측정 표면 핵심이 같은 run 안 inter-row 비교라 config key 필수.requirements.txt에bm25s추가하고 default backend swap. 기각: scale-out 효과 측정 전 default 변경은 기준선 위반. 측정 게이트 후 후속 ADR 에서 결정.
Verification
tests/test_bm25_backend_parity.py 가 세 contract 를 lock:
VALID_BM25_BACKENDS == {"okapi", "bm25s"}— narrowed 명시- 네 프리셋 모두
bm25_backend: "okapi"명시 (ADR 0001 + 0057 invariant) bm25s.BM25(method="robertson")가rank_bm25.BM25Okapi와 ranking 100% 일치 + top-10 overlap ≥ 95%get_or_build_bm25캐시 격리 (okapi / bm25s 인스턴스 분리)
bm25s 미설치 환경에서는 pytest.importorskip("bm25s") 로 contract 3+4 가 skip — minimal CI 정상 동작. contract 1+2 는 항상 실행.
ADR 0057 의 Re-open 조건 (1)+(2) 충족은 eval_summary.json::ablation.runs[name="full_bm25s"] vs name="hybrid_bm25" 의 metric delta 비교로 추적 — scripts/distinguishing_power.py 가운지 floor 보다 lift 가 크면 후속 ADR 가 default flip 검토.
See also
rag_retrieval._make_bm25_instance— backend factory.rag_retrieval.get_or_build_bm25— cache + dispatch.rag_pipeline_presets.VALID_BM25_BACKENDS— validator surface.requirements-bm25s.txt— opt-in install layer.eval/config.yamlfull_bm25s— 분석 변형 행.- ADR 0019 → ADR 0021 / ADR 0031 — 이 ADR 이 따르는 측정 기반 deferral 패턴.
- context7 audit sweep 2026-05-18 (
~/.claude/plans/context7-fizzy-glade.md§ A8 Sub-Plan). - Issue #988.