검색 2차 실험 결과 (임베딩 교체)
아카이브됨 — ** 검색 2차 실험 (임베딩·BM25·리랭커)**로 계획·결과가 합쳐졌다 (2026-07-29)
이 문서의 내용은 전부 합친 문서로 옮겼다. 값 정본도 그쪽이다. 사료로서 보존하되 현행 상태로 읽지 말 것 — 특히 후보 구성 변형 D의 설명이 틀렸다 (D는 리랭커 뒤에서 C와 동치라 아무것도 재지 못한다 → 합친 문서 §9-1).
2차 판정 정본. 계획·측정 조건은 검색 2차 실험 계획 (임베딩·BM25·리랭커) §0-1이 정본이다. 1차 판정 → 검색 실험 결과 (164문항) · 정답지를 고친 기록 → 정답지 편향 제거 (전체 풀링 라벨링)
결론 — 아무것도 안 바꾼다
축 판정 왜 임베딩 ❌ e5-small 유지 KURE가 +0.0297 앞서지만 신뢰구간 하한이 −0.0042. 확실한 비용 vs 증명되지 않은 이득 → §2 리랭커 ✅ bge-reranker-v2-m3(R1) 유지한국어 튜닝판 R2는 세 채점 가정 어디서도 유의하게 진다 → §4 BM25 ⚠️ 미채택이나 재측정 중 지금까지의 기각은 후보 확장 효과를 안 잰 반쪽 실험이었다 → §3 즉 현행
e5-small + R1이 그대로 남는다. 단 §3은 열려 있다.
0. 세 줄 요약
- dense 단독으로는 새 임베딩이 크게 이긴다 — MRR 0.6402 → 0.7417/0.7416(+0.10). 그런데 리랭커를 켜면 그 이득이 대부분 흡수된다 → §5.
- 정답지 편향을 걷어내고 다시 재도 결론은 같았다. 편향 제거 후 KURE 이득이 +0.0068 → +0.0297로 4배 커졌지만, 164문항으로는 이 크기를 우연과 구분할 수 없다 → §2.
- 대신 다른 문이 열렸다. BM25 기각은 어휘 검색이 후보를 넓히는 효과를 한 번도 재지 않은 판정이었다. e5가 못 찾은 17문항 중 11문항을 BM25가 건진다 → §3.
1부 · 판정
1. 최종 성적표 ★ 값 정본
164문항 · C_doc(문서범위 + 리랭커) · MRR@10 · 골든셋 v5(편향 제거 후).
기준선은 1차에서 채택한 e5-small + R1. 승격 기준은 측정 전에 정해둔 신뢰구간 하한 > 0(계획 §13).
| 조합 | MRR@10 | 기준선 대비 | 95% CI (paired bootstrap 1만 회) | 판정 |
|---|---|---|---|---|
| e5-small + R1 (현행) | 0.7443 | 기준선 | — | ✅ 유지 |
| bge-m3 + R1 | 0.7640 | +0.0197 | [−0.0151, +0.0578] | ❌ |
| KURE-v1 + R1 | 0.7740 | +0.0297 | [−0.0042, +0.0654] | ❌ 아슬아슬하게 기각 |
| e5-small + R2 | 0.6805 | −0.0638 | [−0.1005, −0.0275] ✱ | ❌ |
| bge-m3 + R2 | 0.6736 | −0.0707 | [−0.1169, −0.0243] ✱ | ❌ |
| KURE-v1 + R2 | 0.6856 | −0.0587 | [−0.1035, −0.0140] ✱ | ❌ |
✱ = 95% 구간이 0을 포함하지 않음
1-1. 참고 — dense 단독 (리랭커 없이, 골든셋 v4 기준)
임베딩 자체의 실력. 판정용이 아니라 진단용이다(계획 §7).
| 임베딩 | MRR@10 | +BM25 | nDCG@10 | Recall@5 | Recall@20 | 정답이 후보에 아예 없음 | |
|---|---|---|---|---|---|---|---|
| M0 | multilingual-e5-small (118M/384d) | 0.6402 | 0.6096 | 0.6061 | 0.6188 | 0.8163 | 17 |
| M1 | BAAI/bge-m3 (568M/1024d) | 0.7417 | 0.6179 | 0.6973 | 0.7132 | 0.8557 | 14 |
| M2 | nlpai-lab/KURE-v1 (0.6B/1024d) | 0.7416 | 0.6491 | 0.7082 | 0.7335 | 0.8631 | 13 |
M1 ≈ M2 — dense 단계에서 한국어 튜닝의 이득은 없다. 계획 §3의 판정표대로 읽으면 **“계열이 관건”**이다. M0의 Recall@20 0.8163은 1차 실험값과 정확히 재현됐다.
2. 왜 임베딩을 안 바꾸나 — “차이가 없다”가 아니다
KURE는 +0.0297로 승격 최소 폭(+0.03)에 사실상 닿았다. 그런데 95% 구간 하한이 −0.0042다. 164문항으로는 이 크기를 우연과 구분할 수 없다.
교환 조건이 이렇다.
| 치러야 하는 것 (확실) | 얻는 것 (불확실) |
|---|---|
| 재인덱싱 28.5분 | MRR +0.0297 — 다만 구간이 0을 품는다 |
| 모델 5배 (118M → 0.6B) | |
| HF 캐시 4.3GB (VM 여유 7GB) |
**정직하게 쓰면 “교체해도 손해는 없지만 이득이 증명되지 않는다”**이다. “차이가 없다”가 아니다. 점추정은 계속 KURE가 앞섰고, 정답지 편향을 걷어낼수록 격차가 벌어졌다.
2-1. 재판정 조건 — 이 셋 중 하나면 다시 본다
- 골든셋이 300문항 이상으로 늘어난다 — 구간이 좁아져 +0.03이 유의해질 수 있다
- 다른 이유로 재인덱싱이 필요해진다 — 교체 비용이 사라지면 교환 조건 자체가 바뀐다
- Recall@20이 병목인 기능이 생긴다 — e5 0.8163 vs KURE 0.8631(§1-1). 후보를 넓히는 게 중요한 기능이면 판단이 달라진다
3. BM25 판정은 반쪽이었다 ★ 재측정 중
"BM25 미채택"을 어휘 검색 전체를 기각한 것으로 읽으면 안 된다
지금까지 잰 건 BM25가 순서를 바꾸는 효과뿐이고, 후보를 넓히는 효과는 한 번도 재지 않았다.
rerank_on_cache.py의 D 변형이 RRF 융합 후 if c in seen으로 BM25만 찾은 청크를 전부 버렸다.
그래서 D는 dense 후보 20개의 순서만 바꾼 것이 됐다. 검색 실험 결과 (164문항) §4에서 C ≡ D가 수렴한 것도 이 때문이다.
코드에 적힌 이유는 *“BM25 전용 청크는 본문이 없어 추가 Chroma 왕복 필요 — 이득 대비 비용이 커 v1 범위 밖”*이었는데, 실측하니 20개 일괄 조회 0.6ms다. dense 검색 2.05초의 0.03%도 안 된다. 근거가 성립하지 않는다.
3-1. 버려온 것의 크기 (v5 라벨 기준 실측)
| 임베딩 | dense_doc가 정답을 하나도 못 찾은 문항 | 그중 BM25가 찾아준 문항 |
|---|---|---|
| M0 e5-small (현행) | 17 | 11 |
| M1 bge-m3 | 10 | 3 |
| M2 KURE-v1 | 9 | 3 |
e5가 못 찾은 17문항 중 11문항을 BM25가 건진다. 이 17문항은 spec.md §10-17이 **“리랭커로 도달 불가능한 상한”**이라고 명시한 바로 그 문항들이다 — 임베딩을 바꿔도 13문항이 남았던(§1-1) 그 영역이다.
3-2. 왜 6.8.5에서는 손해였나
리랭커 없이 RRF 순서를 그대로 썼기 때문이다. 리랭커를 얹으면 나쁜 BM25 후보를 밑으로 내릴 수 있어, 재현율은 얻고 정밀도 손실은 리랭커가 흡수하는 구조가 된다.
6.8.5의 수치(§1-1의
+BM25열)는 폐기하지 않는다. *“리랭커 없는 조건에서 RRF 순서를 그대로 쓰면 손해”*라는 측정으로 여전히 유효하다.
진짜 하이브리드를 측정 중이다 — E(융합 상위 20) · F(융합 상위 30). 결과가 나오면 이 절을 판정으로 고쳐 쓴다.
2부 · 근거
4. R2는 왜 지나 — 점수가 포화한다
R2(dragonkue/bge-reranker-v2-m3-ko)가 M0·M1·M2 어디에 붙어도 R1보다 −0.06 ~ −0.08이다. 근소한 차이가 아니다.
원인을 실측으로 잡았다 — R2는 점수가 1.0으로 포화한다.
| 한 문항의 R2 점수 | |
|---|---|
| 후보 20개 중 고유 점수 | 10개뿐 |
| 상위 3개 점수 | 전부 정확히 1.0 |
점수가 다 같으면 순위를 매길 수 없다. 변별을 못 하는 것이다 → 지표 포화
4-1. 그 포화가 C ≡ D까지 흔들었다
M0 × R2에서만 C_doc 0.6805 vs D_doc 0.6815로 0.001 어긋났다.
계획 §6이 *“확인용 조합이 다르면 결과가 아니라 버그 신호”*라고 못박아 둔 자리라 멈추고 봤는데, 버그가 아니었다.
C와 D는 후보 집합은 같고 순서만 다르다(§3의 if c in seen). 리랭커가 그 순서를 자기 점수로 덮어쓰는데, 점수가 동점이면 안정 정렬이 입력 순서를 그대로 유지한다.
164문항 중 1문항(g200:4406b63d)에서 정답 청크가 C는 3등, D는 2등이 됐다. 그게 0.001이다.
R1에서는 한 건도 발생하지 않았다. R2를 쓰면 안 되는 이유가 하나 더 나온 셈이다.
5. 임베딩 교체 이득이 리랭커에 흡수된다
dense 단독: M0 0.6402 → M2 0.7416 +0.10 ← 임베딩 실력 차이는 크다
+ 리랭커 R1: M0 0.7443 → M2 0.7740 +0.0297 ← 종단에서는 3분의 1 이하로 줄어든다
계획 §2-2가 나눠 적어둔 두 능력으로 읽으면 정확히 맞아떨어진다.
| 임베딩을 바꿔서 얻는 것 | 예상 | 실측 |
|---|---|---|
| 정답을 20개 안에 넣는 능력 | 리랭커가 못 함 → 값어치 크다 | Recall@20 0.8163 → 0.8631, 후보 밖 17 → 13 ✅ |
| 20개 안에서 위로 올리는 능력 | 리랭커가 이미 함 → 작다 | 종단 +0.0297에 그침 ✅ |
계획 §14의 예상 1·2번이 둘 다 맞았다. §14가 미리 적어둔 대로 이건 실패가 아니라 “리랭커가 이미 그 몫을 하고 있다”는 유용한 결론이다.
그리고 §3이 바로 여기서 이어진다 — **리랭커가 못 하는 일(후보를 넓히는 것)**을 BM25가 할 수 있다면, 그건 임베딩 교체와 겹치지 않는 이득이다.
6. 한국어 튜닝은 양쪽에서 이득이 없었다
| 축 | 원본 → 한국어 튜닝판 | 결과 |
|---|---|---|
| 임베딩 | bge-m3 → KURE-v1 | 무승부 (0.7417 vs 0.7416, dense 단독) |
| 리랭커 | R1 → R2 | 크게 짐 (−0.06 ~ −0.08) |
계획 §4가 *“둘 다 미미하면 우연이 아니라 패턴”*이라고 미리 적어둔 자리다. 다만 방향은 예상(§14-5 “근소하다”)보다 나빴다 — 리랭커 쪽은 근소한 게 아니라 크게 졌다.
3부 · 하다가 바로잡은 것
7. 검색과 재정렬이 서로 다른 질문을 보고 있었다 ★
리랭킹을 처음 돌렸을 때 0.7747이라는 값이 나왔다. 기준선 대비 +0.03을 훌쩍 넘는 수라 승격처럼 보였는데, 가짜 상승이었다.
원인은 질문 파일이었다.
| 단계 | 무엇을 봤나 |
|---|---|
| 1단계 — dense 후보 캐시 | qid_query_200.jsonl (병합본) |
| 2단계 — 리랭킹 | goldenset_200.jsonl (원본) ← 여기가 틀렸다 |
두 파일은 후속 질문 29문항의 텍스트가 다르다.
원본: "그 사업의 안정화기간은?"
병합본: "한영대학교 …사업기간은 어떻게 되나요? 그 사업의 안정화기간은?"
그래서 검색은 맥락이 붙은 질문으로 후보를 뽑았는데, 리랭커는 “그 사업”이 뭔지 모르는 채로 그 29문항의 순서를 매겼다. 그 어긋남이 0.7747이라는 상승으로 나타났다.
골든셋·정답지·채점 기준에는 문제가 없었다. 잘못은 전적으로 실행 인자 쪽이다.
교훈 — 1단계(후보 캐시)와 2단계(리랭킹)는 반드시 같은 질문 파일을 봐야 한다
이것도 검색 실험 결과 (164문항) §8(인덱스 혼동)과 같은 조용한 실패다. 파일 이름이 둘 다 그럴듯하고, 문항 수도 맞고, 에러도 안 난다. 성적이 올라서 발견됐다. 성적이 내려갔으면 “새 임베딩이 별로네”로 넘어갔을 수도 있다.
7-1. 두 질문 파일의 관계
| 파일 | 무엇인가 | 필드 | 문항 |
|---|---|---|---|
goldenset_200.jsonl | 정본 골든셋 | qid / question / 유형 / 선행_턴 / 대상_doc_id / expected_answer_기준 | 200 |
qid_query_200.jsonl | 위에서 파생된 검색 실험용 질문 텍스트 | qid / query 둘뿐 (정답 정보 없음) | 164 |
rerank_labels_200.jsonl | 정답 라벨. VM 골든셋의 rerank_labels.jsonl과 gold 청크 전건 일치 | 164 |
goldenset_200.jsonl (200문항)
│ 선행_턴 + question 을 이어붙임
│ − 모른다 15 − 반문 15 − 빈 정답 6
▼
qid_query_200.jsonl (164문항, qid + query) ← 검색·리랭킹은 항상 이걸 본다
goldenset_200.jsonl을 검색 인자로 넣으면 문항 수 필터링과 선행 턴 병합이 둘 다 빠진다.
8. 그 밖에 바로잡은 것
| 무엇 | 어디에 |
|---|---|
| 정답지가 e5에 편향돼 새 모델을 과소평가하던 것 | 정답지 편향 제거 (전체 풀링 라벨링) |
| 정답률 1.3%를 보고 “미채택”으로 판단했다가 철회한 경위 | 같은 문서 §7 |
| 캐시 생성 1,887초 → 136초 · Chroma 필터 검색이 근사라는 발견 | 후보 캐시 |
| chunk_id 게이트 (임베딩을 바꿔도 청크 경계가 안 밀림) | 검색 2차 실험 계획 (임베딩·BM25·리랭커) §8 |
9. 관련 문서
| 문서 | 무엇을 보러 |
|---|---|
| 검색 2차 실험 계획 (임베딩·BM25·리랭커) | §0-1 = 측정 조건 정본 · 설계 근거 · 승격 기준(§13) |
| 정답지 편향 제거 (전체 풀링 라벨링) | 정답지를 어떻게 고쳤나 — 6.8.7·6.8.8 전 과정 |
| 검색 실험 결과 (164문항) | 1차 판정 — 이 실험의 기준선(0.7443) |
| 후보 캐시 | dense_doc/dense_full/bm25 구분, 검색을 한 번만 돌리는 법 |
| 개념 색인 | MRR·nDCG·BM25·리랭커 개념 |
코드: scripts/build_retrieval_cache.py · scripts/rerank_on_cache.py · scripts/rerank_bias_bounds.py · scripts/promote_decision.py
정본: Plans.md 6.8 · spec.md §10-17