검색 실험 구도

아카이브됨 — 실행 결과는 검색 개선이 정본 (2026-07-29)

이 문서는 측정 전 사전등록 설계서다. 사료로서 보존하되 현행 상태로 읽지 말 것.

이 문서실제
진행 상황 “1단계 A·B·B′ ← 지금 여기”1~3차 측정 완료
7변형(A~D + B′·E·F)실행은 검색 범위(전체 vs 문서 한정) 축으로 진행 — B′·E·F 미실행
리랭커 모델 스윕미착수. 유효 잔여분은 검색 개선 §18-1로 이관 완료

측정 대상 내역 기존 26 · 어휘형 9오기였고 기존 27 · 어휘형 8로 고쳤다검색 실험 골든셋 40문항 부록 원자료 기준(미검출 4 = 기존 2·어휘형 2, 대상 아님 1 = 기존 #21). 40/36/35 관계는 검색 개선 §6 참조.

BM25 하이브리드와 리랭커를 넣을지 말지 숫자로 판정하는 실험. 좋아 보여서 넣는 게 아니라, 미리 정한 선을 넘겨야 넣는다.

재는 것검색 순위 (MRR@10 주 지표)
대고 재는 것35문항 / 정답 청크 72개
판정선A 대비 MRR@10 +0.05
지금1단계 측정 직전

문항 전수 → 검색 실험 골든셋 40문항 · 데이터 손상 → 인덱스 데이터 손상 진단 · 경위 → 검색 개선 · 구조도 → 검색 품질 개선 (BM25·리랭커)

왜 다시 설계했나

Plans 6.1에서 6개 변형을 비교했는데 hit@5전부 1.000이었다. 검색을 바꿔도 숫자가 안 움직이면 판정이 불가능하다.

원인은 지표가 문서 단위라는 것. 청킹이 모든 청크 앞에 사업명+발주기관을 붙이니 어떤 청크가 뽑혀도 “정답 문서에서 나왔다”가 항상 참이 된다. 지표가 검색기가 아니라 청킹 접두어를 재고 있었다.

그래서 실험 코드보다 지표를 먼저 새로 만들었다. 지표가 변별을 못 하면 그 뒤는 전부 무의미하다.

무엇을 비교하나

뼈대 — 2×2 (사전등록)

BM25와 리랭커는 독립 축이라 하나씩이 아니라 네 칸을 전부 돈다. 그래야 “둘 다 켜야 좋아지는지, 하나로 충분한지”가 갈린다.

리랭커 OFF리랭커 ON
BM25 OFFA 베이스라인C
BM25 ONBD

라벨링 뒤 3개 추가 (탐색)

실패 4건을 실측하니 A~D만으로는 답 못 하는 질문이 생겼다.

실측답 못 하는 질문
전체 질문 160위 → 핵심어만 2위BM25가 나쁜가, 질문을 잘못 넣었나
정답이 dense 23·35·94위인데 top-20에서 잘림알고리즘인가, 후보 깊이인가
정답이 표면 dense 10위, 줄글이면 4위표 문항에서만 이득 날 가능성

최종 7변형

BM25리랭커후보 K쿼리
A_dense20원문
B_hybrid20원문
C_rerank20원문
D_hybrid_rerank20원문
B′_kw20핵심어
E_deep40원문
F_all40핵심어

ᵉ = 탐색(exploratory). A~D는 사전등록.

  • 핵심어 쿼리 = BM25에 넣기 전 저IDF 토큰 제거. dense 쿼리는 원문 유지(임베딩은 문맥이 필요).
  • 후보 K는 RERANK_CANDIDATE_K. 최종 반환은 전부 top-5.
  • 전 변형이 한 프로세스에서 config 플래그만 토글해 돈다. 인덱스·질문·정답이 동일하므로 차이는 검색 경로에서만 온다.

승격 판정은 A~D로만 한다. B′·E·F는 라벨링 결과를 본 뒤 추가했다. 이걸 판정에 넣으면 결과를 보고 기준을 만든 셈이 되어 실험이 무의미해진다. 셋은 “다음에 뭘 고칠지” 참고용이다.

리랭커 모델은 별도 축

한국어 전용 리랭커가 나은지도 봐야 하지만, 위 표에 곱하면 14변형이 된다. 별도로 뺄 수 있는 이유는 후보를 고정한 채 점수만 다시 매기면 되기 때문이다.

검색 1회 → 후보 20개 확정      ← 모델 바꿔도 불변
      ↓ 이 20개를 모델별로 재배열
문항 35 × 후보 20 = 모델당 700쌍
모델역할
BAAI/bge-reranker-v2-m3기준선 (현행)
upskyy/ko-reranker-8k한국어 파인튜닝본

둘 다 2.27GB · 최대 8194토큰 · Apache-2.0. ko-reranker-8k는 모델 카드상 v2-m3를 한국어 데이터로 파인튜닝한 것이라, 구조·토크나이저·길이가 같고 변수가 “한국어 학습” 하나뿐인 통제된 비교가 된다. 코드는 RERANK_MODEL 한 줄만 바뀐다.

탈락한 셋: Dongjin-kr/ko-reranker(최대 514), bge-reranker-base(514 + 저성능), jina-reranker-v2(CC-BY-NC 비상업).

514가 왜 탈락 사유인가 — 리랭커 입력 토큰 길이 실측:

입력중앙값512 초과
청크 본문 (현행)1480%
parent_text (표 전문, 개선 후보)45446%

지금 입력이면 514 모델도 무해하다. 그러나 표 전문을 넣는 개선을 하면 절반이 잘린다. 실패 사례 상당수가 표 문항이라 그 길을 막을 수 없다.

모델 교체 기준: 동일 후보 위에서 MRR@10 +0.03 이상이면 교체, 미만이면 v2-m3 유지. 이 축도 A~D 판정에는 넣지 않는다.

융합은 RRF

코사인 유사도(0~1)와 BM25 점수(상한 없음)는 스케일이 달라 더할 수 없다. RRF는 점수를 버리고 순위만 쓴다.

score(청크) = Σ 1 / (60 + 그 검색기에서의 순위)

정규화 파라미터 튜닝이 없어서 “융합을 잘못 맞춰 졌다”는 변명이 안 나온다.

무엇으로 재나

지표보는 것
MRR@10주 지표. 첫 정답이 몇 등인가 (1/등수)
nDCG@10정답 여러 개가 위쪽에 몰렸나 (정답 2개 이상 22문항)
Recall@5실제로 생성에 넘어가는 top-5의 품질
hit@5회귀 감시용. 1.0에서 안 떨어지는지만

구현은 src/eval/ir_metrics.py. 순수 함수 + stdlib.

무엇에 대고 재나

라벨링 전수40기존 30 · 어휘형 10
− 검색 미검출4정답이 후보 풀 밖
− 검색 대상 아님1문서에 근거 자체가 없음
측정 대상35기존 27 · 어휘형 8

정답 청크 72개(문항당 2.1개). 인덱스는 data/chroma_v2 — 30,002청크, 표 헤더 승격 반영본.

어휘형 10문항은 BM25를 이기게 하려고 만든 게 아니라, BM25가 이겨야 정상인 구간이 실제로 있는지 보려고 만들었다. 고유번호·법령 조항·정확한 기관명처럼 임베딩이 뭉개기 쉬운 표층 문자열 질문들이다. 여기서도 못 이기면 도입 근거가 없다.

보고는 두 축으로 쪼갠다

분할이유
출처전체 35 / 기존 27 / 어휘형 8어휘형만 좋아지고 기존이 나빠지는 건 개선이 아니라 트레이드오프. 합치면 숨는다
정답 소재표 / 줄글dense는 표에서 약하고(10위·70%) 줄글에서 강하다(4위·87%). 하이브리드 효과가 여기서 갈릴 것

추가로 중복 청크 문항 19개를 뺀 재계산도 함께 낸다. 그 문항들은 정답과 오답의 텍스트가 거의 같아 점수 차이가 운에서 나온다.

정답은 어떻게 정했나

30,000개 중에서 사람이 정답을 찾는 건 불가능하므로, 두 검색기가 건진 것만 후보로 놓고 고른다(TREC 풀링).

후보 = dense top-20 ∪ BM25 top-20    → 문항당 30~40개, 총 1,438개
후보 출처
dense만638
BM25만638
둘 다162

겹침이 11%다. BM25가 dense는 못 보던 청크를 실제로 건져 올리고 있다 — 실험에 변별력이 있다는 뜻이다.

라벨링은 기계가 기준답변과의 내용어 겹침으로 상위 3개를 제안하고, 사람은 승인하거나 번호로 고치기만 했다. 겹침 점수는 질문이 아니라 기준답변에 대고 잰다. 질문에 대고 재면 BM25가 찾기 쉬운 청크가 정답으로 뽑히는 자기충족 편향이 생긴다.

라벨링 중 드러난 문제와 교훈은 검색 실험 골든셋 40문항.

언제 채택하나

A 대비 ① 전체 MRR@10 +0.05 이상, ② 기존 문항 hit@5 = 1.0 유지 → 둘 다 만족하면 프로덕션 반영 (플래그 ON) → 하나라도 미달하면 미채택으로 기록, 코드는 OFF 유지 → 여러 변형이 통과하고 차이가 0.02 이내면 더 단순한 쪽 (A < B,C < D)

결과 보기 전에 못 박은 이유 — 숫자를 본 뒤 기준을 정하면 어떤 결과든 성공으로 해석된다. +0.03이 나왔을 때 “그래도 개선이니까”라고 말하지 않기 위한 사전 구속.

단순한 쪽 우대 — 리랭커는 공짜가 아니다. 568M 모델을 매 질의마다 20개 청크에 돌리는 비용·지연·GPU 메모리를 0.02 차이로 정당화할 수 없다.

실행 순서

1단계 · 로컬 CPUA B B′. 리랭커가 없어 GPU 불필요. BM25의 순수 효과와 쿼리 처리 효과를 먼저 본다.

  • B′ ≫ B → 문제는 BM25가 아니라 쿼리 처리. 알고리즘 교체 불필요
  • B′ ≈ B ≈ A → BM25가 이 코퍼스에서 힘을 못 쓴다. 대안 검토

2단계 · VM L4C D E F. 리랭커 1,600~3,200회 추론. CUDA fp16, 맥 MPS는 쓰지 않는다(수치가 갈리면 비교가 무너진다).

선결: VM 디스크가 96G 중 1.7G만 남아 모델을 못 받는다. 두 모델이면 4.5GB 필요. 차선책은 맥 CPU 측정(20~40분).

3단계 · 모델 스윕 — 2단계 최고 후보 구성을 고정하고 두 모델만 갈아끼워 700쌍씩 재채점. 몇 분.

4단계 · 해석

관측해석행동
E ≫ C병목은 후보 깊이RERANK_CANDIDATE_K 상향
B′ ≫ B병목은 쿼리 처리저IDF 필터 프로덕션 반영
ko-8k > m3 +0.03한국어 학습이 유효RERANK_MODEL 교체
F ≈ D추가 조치 효과 없음원안대로 판정
전부 A와 비슷순위 문제가 아님인제스트로 투자 전환

필요한 코드

작업규모
핵심어 쿼리 필터 — 코퍼스 df 기반 저IDF 제거, hybrid.py
변형 스윕 확장 — VARIANTS에 후보 K·쿼리 모드 필드
표/줄글 분할 집계 (출처 3분할은 이미 있음)
모델 스윕 — 고정 후보 재채점, RERANK_MODEL 인자화

플래그 기본값은 계속 OFF. 측정 스크립트가 토글할 뿐 프로덕션 동작은 안 바뀐다.

비용 — OpenAI 호출 0건. 임베딩·BM25·리랭커 전부 로컬 모델이라 팀 키 $20 한도와 무관.

이 실험이 답하지 못하는 것

결과 보고서에 반드시 함께 적는다.

  1. 풀링의 한계 — 두 검색기 어느 쪽도 못 건진 청크는 영원히 정답이 될 수 없다. 실제 Recall은 여기서 재는 값보다 낮다.
  2. BM25는 승격만 시킨다 — BM25만 찾은 청크는 본문이 없어 리랭킹 대상에서 빠진다. 기여는 “dense가 이미 본 청크를 위로 올리는 것”까지다. 신규 청크를 넣으려면 Chroma 왕복이 한 번 더 필요한데 v1 범위 밖.
  3. 답변 품질은 안 잰다 — 검색 순위만 본다. 검색이 좋아져도 생성이 좋아진다는 보장은 없다(별도 골든셋 평가 몫).

보류 — BGE-M3 전환

spec.md/ADR-006이 임베딩을 multilingual-e5-small로 고정하고 있다. BGE-M3로 바꾸면 e5와 BM25를 한 모델로 대체할 수 있고 한국어 성능도 낫지만:

  • 30,002청크 재인덱싱 + 라벨 전량 재생성
  • 모델 2.2GB (VM 디스크 문제 동일)
  • 임베딩·sparse·인덱스가 한꺼번에 바뀌어 무엇 때문에 좋아졌는지 구분 불가

→ 1단계 결과를 보고 결정한다. B′가 잘 나오면 바꿀 이유가 없다.

진행 상황

✅ 지표·하이브리드·리랭커 구현     PR #22 머지, 403테스트, 플래그 OFF
✅ 재인덱싱                        chroma_v2, 30,002청크
✅ 후보 풀 재생성                  1,438후보
✅ 라벨링 40문항 + 감사            측정 대상 35 확정
□  코드 4건 (위 표)
□  1단계 A·B·B′                   ← 지금 여기
□  2단계 C·D·E·F                  VM 디스크 선결
□  모델 스윕 m3 ↔ ko-8k
□  결과 기록 · 승격 판정 · PR

관련

검색 실험 골든셋 40문항 · 인덱스 데이터 손상 진단 · 검색 품질 개선 (BM25·리랭커) · 검색 개선 · 쿼리 파이프라인 개선안 · 쿼리 파이프라인