검색 실험 결과 (164문항)

아카이브됨 — 이 문서의 수치는 무효다 (2026-07-29)

무효 사유: 골든셋 v4 기준이고 정답지 편향이 보정되지 않았다 — 새 임베딩의 상위 10개 중 M1 26% · M2 32%가 미판정이라 자동 오답으로 세어졌다. BM25 서술도 설계 결함(집합 불변 · 스코프 불일치)이 드러나기 전 판이다.

대체 정본 → 검색 실험 결과 (유효한 내용은 전부 이관했다) 본문은 지우지 않는다왜 그 판정이 틀렸는지가 남아야 같은 실수를 반복하지 않는다.

계획은 검색 실험 계획 (164문항). 이 문서는 실제로 재고 판정한 결과다. 개념이 낯설면 → 개념 색인

이 결과는 e5-small 임베딩 위에서 잰 값이다

베이스 임베딩을 바꾸면 일부 결론이 달라진다. 2차 실험 → 검색 2차 실험 (임베딩·BM25·리랭커) 2차 실측이 나왔다검색 2차 실험 (임베딩·BM25·리랭커). 무엇이 남고 무엇이 바뀌었는지는 바로 아래 표.

0-1. 2차 실험에서 뭐가 남고 뭐가 바뀌었나

2차 실측이 나왔다 → 검색 2차 실험 (임베딩·BM25·리랭커). 결론 대부분이 살아남았다.

내용2차 결과
리랭커 켜면 BM25 무효 (D≡C) — §4⚠️ 전제가 좁았다M1·M2에서도 일치하긴 했으나, D가 BM25 전용 청크를 버리기 때문에 성립한 것이다. 그 청크를 살리면 성립하지 않는다
측정 방법 (캐시·문서범위·상하한) — §10✅ 재사용됨
교훈 (소표본 뒤집힘·프로덕션 경로) — §5, §9✅ 영구
인덱스 함정 (chroma vs chroma_v2) — §8✅ 영구같은 종류가 또 났다 — 이번엔 질문 파일
e5 Recall@20 0.8163 · 17문항 후보 밖재현됨2차에서 소수점까지 같은 값. 새 임베딩은 0.8631 / 13문항
BM25 미채택 — §3⚠️ 절반만 확인됨순서 융합은 임베딩 3종 전부에서 순손해. 그러나 후보를 넓히는 효과는 미측정이었다 → 재측정 중
리랭커 +0.104 — §1⚠️ 폭이 줄었다예상대로. 새 임베딩을 써도 종단 격차가 +0.0297에 그치고, 신뢰구간이 0을 품어 교체하지 않았다
e5 + 리랭커 = 0.7443넷째 자리까지 재현2차의 기준선이자 파이프라인 재현성 근거
유형별 수치 — §6⚠️ 조건부

1차가 무효가 되는 게 아니라, 조건부 결론을 조건별로 다시 잰 것이다. 그리고 e5 + 리랭커 = 0.7443이 없으면 2차에서 “새 임베딩이 나은가”를 잴 기준이 없었다.


0. 세 줄 요약

  1. BM25는 안 쓴다. 어떤 조건에서도 지금 검색보다 나빴다. (단 이 실험이 잰 건 순서 융합뿐이다 — §4 경고 참조)
  2. 리랭커는 쓴다. 기준의 두 배를 넘겼고, 손해 본 문항 유형이 하나도 없다.
  3. 둘을 같이 켤 이유는 없다. 리랭커를 켜면 BM25는 수학적으로 무효가 된다. → §4

1부 · 결론

1. 최종 성적표

164문항. 기준선은 지금 돌아가는 검색(A_doc = 뜻으로만 찾기 + 문서 좁히기). 승격 기준은 측정 전에 정해둔 MRR +0.05다.

변형구성MRR@10기준선 대비판정
A_doc뜻으로 찾기 단독0.6402기준선
B_doc+ 글자로 찾기0.6096−0.031
B2_doc+ BM25(확신할 땐 미적용)0.6311−0.009
C_doc+ 리랭커0.7443+0.104채택
D_doc+ BM25 + 리랭커0.7443+0.104✅ (C와 동일)

MRR은 “정답이 몇 등에 있나”를 재는 값이다. 1등이면 1점, 2등이면 0.5점, 5등이면 0.2점.

+0.104가 얼마나 큰가: 164문항 총점으로 17점이다. 정답이 6등에 있던 문항 20개가 1등으로 올라온 것과 같은 크기다.

2. 무엇을 켜기로 했나

지금:   질문 → 문서 좁히기 → 뜻으로 20개 찾기 → 상위 5개 → LLM
바뀜:   질문 → 문서 좁히기 → 뜻으로 20개 찾기 → 리랭커가 재정렬 → 상위 5개 → LLM
                                                  ↑ 여기만 추가

RETRIEVAL_USE_RERANKER = True, RETRIEVAL_USE_BM25False 유지.


2부 · BM25는 왜 떨어졌나

3. 게이팅으로도 못 살렸다

계획 §8-2에서 **“뜻으로 찾기가 확신할 땐 BM25를 빼자”**는 변형(B2)을 넣었다. 1차 실험에서 BM25 때문에 순위가 내려간 문항이 전부 이미 1등을 맞히던 문항이었기 때문이다.

임계값을 여러 개 놓고 전부 재봤다.

게이트 조건BM25 미적용 문항MRR
없음 (B_doc)00.6096
거리 < 0.055420.6288
거리 < 0.064830.6208
거리 < 0.0751240.6311
전부 빼기 (= A_doc)1640.6402

BM25를 끌수록 좋아진다. 그리고 전부 껐을 때가 제일 좋다.

이건 “임계값을 잘못 골랐다”가 아니라 BM25 기여 자체가 순손실이라는 뜻이다. 게이트를 아무리 정교하게 만들어도 기준선에 아래에서 수렴할 뿐, 넘지 못한다. 그래서 B2 방향은 여기서 접는다.

4. C와 D가 소수점까지 똑같은 이유

164문항 전건이 완전히 같은 값이 나왔다. 처음엔 버그를 의심했는데, 구조적으로 그럴 수밖에 없다.

BM25가 후보를 못 바꾼다. 러너는 BM25만 찾아낸 청크를 버린다. 본문과 메타데이터가 없어서 리랭커에 넣을 수도, 채점할 수도 없기 때문이다 (retrieve.py의 계약과 동일 — 계획 §8 참조). 그래서 BM25가 하는 일은 후보 집합을 바꾸는 게 아니라 순서만 바꾸는 것이다.

리랭커가 그 순서를 지운다. 리랭커는 후보 20개 전체를 자기 점수로 다시 정렬한다. 들어온 순서는 결과에 아무 영향을 주지 않는다.

C:  [뜻으로 찾은 20개] ─────────────→ 리랭커 재정렬 → 결과
D:  [뜻으로 찾은 20개] → BM25로 순서만 바꿈 → 리랭커 재정렬 → 결과
                          ↑ 여기서 바뀐 순서를    ↑ 여기서 지움

리랭커를 켜는 순간 BM25는 무효가 된다. 둘을 같이 켤 이유가 없다.

단, 이건 "BM25 전용 청크를 버린다"는 계약 위에서만 참이다

그 계약(if c in seen)의 근거였던 *“본문 조회 비용이 크다”*는 실측으로 무너졌다 — 20개 일괄 조회 0.6ms. 청크를 살리면 BM25가 후보 집합을 바꾸므로 리랭커가 지울 수 없다. 그 효과는 한 번도 측정된 적이 없다검색 2차 실험 (임베딩·BM25·리랭커) §9

5. 1차 실험(40문항)의 +0.073은 노이즈였다

계획 §3에서 “BM25는 문서를 좁혔을 때 +0.073”이라고 확정했었다. 164문항에서는 −0.031이다. 부호가 뒤집혔다.

36문항으로 잰 값이었고, 그중 이득이 난 문항이 16개였다. 표본이 작으면 이 정도 폭은 얼마든지 흔들린다.

교훈: 40문항짜리 예비 측정은 “가설이 살아 있나”를 보는 용도이지, 확정 근거로 쓸 게 아니었다. 계획 문서에서 이걸 “확정 1”이라고 부른 건 과했다.


3부 · 리랭커는 어디서 이겼나

6. 유형별 — 손해 본 유형이 없다

유형문항A_docC_doc차이
단일세부900.69560.8322+0.137
직답200.52600.7146+0.189
탐색비교250.52670.5611+0.034
후속290.64490.6498+0.005

한 유형이 좋아지면서 다른 유형이 나빠지는 일이 없었다. 이게 중요하다. BM25는 직답에서만 올랐고(0.526→0.610) 탐색비교에서 크게 깎였다(0.527→0.441). 평균만 보면 가려지는 손해였다.

탐색비교·후속은 개선폭이 작다. 둘 다 문서를 여러 개 걸치거나 앞 대화에 의존하는 유형이라, 후보를 잘 정렬하는 것만으로는 한계가 있다. 후속 개선 과제로 남긴다.

7. 보조 지표도 같이 올랐다

변형MRR@10nDCG@10Recall@5
A_doc0.64020.60610.6188
C_doc0.74430.70130.7110

MRR만 오르고 Recall이 떨어졌다면 “1등은 잘 맞히는데 나머지를 놓친다”는 뜻이라 위험했을 텐데, 셋이 같이 올랐다. 문서범위 검색이라 doc_hit@5는 정의상 1.0으로 유지된다(지표 포화).


4부 · 하다가 바로잡은 것

8. 인덱스를 잘못 쓰고 있었다

앞선 A/B 측정은 data/chroma를 썼는데, 정답 청크 253개 중 251개만 들어 있었다. 정답지는 chroma_v2(253개 전부) 기준으로 만들어진 것이었다.

chunk_id는 양쪽 다 존재해서 에러 없이 조용히 2문항이 빠진다. 이게 이 함정의 고약한 점이다 (청크와 chunk_id 참조).

이번엔 전부 chroma_v2로 통일했다. VM /srv/GPTPilot/chroma도 같은 인덱스다 (청크 30,002 + doc_cards 100 = sqlite embeddings 30,102).

9. 프로덕션에 없는 경로를 재고 있었다

계획 §15의 교훈이 그대로 재발할 뻔했다. 러너가 build_retriever(k=...)만 호출해서 문서 필터 없이 검색하고 있었다.

이번엔 문서범위·전체범위를 둘 다 재서 나란히 뒀다.

변형전체범위문서범위
dense0.63590.6402
+BM250.60040.6096
+리랭커0.73130.7443

문서를 좁히면 어느 방식이든 조금씩 낫다. 그리고 결론은 양쪽에서 같다 — 이번엔 뒤집히지 않았다.

10. 측정 방법을 바꿨다 — 검색 1회, 조합은 계산

변형마다 검색을 다시 돌리면 BM25 융합에만 문항당 3초씩 든다. 변형 7개면 한 시간이 넘고, 게이팅 임계값 스윕은 아예 불가능하다.

그래서 검색은 한 번만 돌려 캐시하고 조합은 오프라인 계산으로 바꿨다.

스크립트역할소요
build_retrieval_cache.py문항당 dense(전체·문서범위)+BM25 후보를 1회 패스로 저장1,887초
eval_variants_offline.pyA/B/A_doc/B_doc + 게이팅 임계값 7종 스윕1초 미만
rerank_on_cache.pyC/D 리랭커 (VM L4)93초
promote_decision.py승격 기준 대조 판정표즉시

캐시에는 chunk_id와 거리만 담는다. 질문·청크 원문은 저장하지 않는다(NDA). 거리를 같이 저장한 덕분에 게이팅 판정을 재검색 없이 계산할 수 있었다.

개념 정리와 2차에서의 최적화 → 후보 캐시


5부 · 다음

11. 프로덕션 반영 — 켰다

RETRIEVAL_USE_RERANKER = True. 켜기 전 선결 과제 둘은 아래처럼 해소했다.

과제상태무엇을 했나
동시성 락✅ 해소ask()가 동기 함수라 FastAPI 스레드풀에서 568M 모델이 중복 로드될 수 있었다(→ L4 OOM). 이중검사 락(모델·코퍼스 지문별 key)으로 로더를 1회만 태우고, MODEL_LOAD_TIMEOUT_S=300으로 유한 대기를 둬 초과 시 폴백 강등한다. 8스레드 Barrier 테스트로 로더 1회 호출을 확인했다
지연시간✅ 해소아래 §11-1
거짓양성 검사⬜ 남음모른다·반문 30문항에서 없는 근거를 만들어내지 않는지 (계획 §7-3)

11-1. “맥은 MPS” 라는 전제가 틀렸다

이전 서술은 “맥(MPS)으로 운영하면 지연시간을 다시 재야 한다” 였는데, 코드가 애초에 MPS를 안 쓴다. _load_cross_encoderdevice="cuda" if use_cuda else "cpu"로 되어 있고, MPS 경로가 불안정해 일부러 배제한다는 주석이 붙어 있다. 맥에서는 CPU로 돈다.

그래서 잴 대상은 MPS가 아니라 맥 CPU였고, 그걸 쟀다.

환경문항당배수
VM L4 (GPU)0.27초
맥 CPU13.27초49×

후보 20개, 워밍업 분리, 3회 중앙값.

문항당 13초는 대화형으로 쓸 수 없는 값이다. 그래서 RERANKER_REQUIRE_GPU=True 가드를 넣어 GPU가 없으면 dense 단독으로 자동 강등한다 — spec §6-2의 “리랭커는 GPU에서만” 계약을 런타임에서 강제하는 것이다.

프로덕션은 VM(GPU 있음)이라 이 가드를 타지 않는다. 그런데도 넣은 이유는 맥에서 백엔드를 띄우는 워크플로가 실재하기 때문이다(8.8 로컬 통합 데모). 가드가 없으면 그 경로에서 조용히 49배 느려진다.

retrieve.pyuse_bm25 = ... and doc_id is None(BM25 데드코드)은 BM25를 안 쓰기로 했으니 그대로 둬도 된다. 다만 왜 죽어 있는지 모르고 지나치면 안 되는 코드라 주석으로 남길 가치는 있다.

2차 후속: 이 줄은 *“문서가 특정되면 BM25를 아예 끈다”*는 판단이다. 지금 재는 것이 정확히 그 판단이 옳았는지다 → 검색 2차 실험 (임베딩·BM25·리랭커) §9-6

12. 남은 숙제

  • 탐색비교·후속의 개선폭이 작다 — 여러 문서를 걸치는 검색은 별도 접근이 필요하다
  • BM25 단독 발굴 청크를 버리는 계약 — 고쳐도 리랭커를 켜면 어차피 무효라 실익이 없다이 판단이 틀렸다. 청크를 살리면 후보 집합이 달라져 리랭커가 지울 수 없고, e5가 못 찾은 17문항 중 11문항을 BM25가 건진다 → 검색 2차 실험 (임베딩·BM25·리랭커) §9

13. 관련 문서

문서무엇을 보러
검색 실험 계획 (164문항)이 실험을 왜 이렇게 설계했나
검색 2차 실험 (임베딩·BM25·리랭커)임베딩을 바꿔 이 결론을 다시 잰 값
검색 개선1차 실험 전 과정 + 검색 변경 이력
골든셋200문항 확장 설계 · 라벨러 성적
개념 색인MRR·BM25·RRF·리랭커 등 개념
쿼리 파이프라인검색이 전체 흐름에서 어디인가

코드: scripts/build_retrieval_cache.py · eval_variants_offline.py · rerank_on_cache.py · promote_decision.py 결과물: data/promote_decision_200.md · data/variant_results_200.json · data/rerank_variant_results_200.json (전부 gitignore) 커밋: 336d6a1 · 정본: Plans.md 6.7