Skip to content

feat: 검색 3축 v7 재측정 — 임베딩 gpl3 채택, BM25 미채택, 리랭커 R1 유지 - #30

Merged
beomjinkim2000 merged 47 commits into
mainfrom
feat/goldenset-300
Aug 2, 2026
Merged

feat: 검색 3축 v7 재측정 — 임베딩 gpl3 채택, BM25 미채택, 리랭커 R1 유지#30
beomjinkim2000 merged 47 commits into
mainfrom
feat/goldenset-300

Conversation

@beomjinkim2000

Copy link
Copy Markdown
Collaborator

검색 부품 3축(임베딩 · 후보 구성 · 리랭커)을 v7 정답지(251문항 / 490 gold)에서 다시 재고, 임베딩만 교체하기로 판정했다.

최종 구성

gpl3 임베딩 + R1 리랭커 ON + BM25 OFF (바뀐 것은 임베딩 하나)

축별 판정

맞댄 것 결과 MRR@10 델타 95% CI
임베딩 e5-small ↔ bge-m3 · KURE-v1 · gpl3 gpl3 채택 +0.0437 [+0.0097, +0.0807]
후보 구성 dense만 ↔ dense+BM25 (E·F·G_doc) BM25 미채택 +0.0007 0을 걸침
리랭커 ON/OFF 리랭커 없음(A_doc) ↔ R1 켠다 +0.0651 [+0.0203, +0.1108]
리랭커 종류 R1 ↔ R2(한국어판) R1 유지 −0.0885 [−0.1199, −0.0578]

bge-m3(+0.0511)·KURE-v1(+0.0578)도 승격 기준을 통과했으나, 118M / 384D로 e5-small과 동일 비용인 gpl3를 택했다.

판정 규격

전 축 모두 수치를 보기 전에 사전등록했다 (scripts/verdict_*.py + 회귀 테스트).
승격 기준: CI 하한 > 0 AND 점추정 ≥ +0.03 AND 유형별 −0.03 이하 회귀 없음 AND 부수 지표 유의 음수 없음.
부트스트랩은 문서 단위 클러스터링(95 클러스터, 평균 2.64) · 10,000 리샘플 · seed 20260729.

정답지 v6 → v7

BM25 전용 후보 중 미판정 734건(26.0%) 이 검색기를 구조적으로 불리하게 만들던 문제를 라벨링으로 제거했다(결과: gold 4건, 0.54%). 라벨링은 라벨러 → 독립 검증자 2단계.

알려진 한계

  • 이 실험은 전부 문서 스코프다 — 대상 문서를 안다고 두고 그 안에서만 잰다. 라우팅(100문서 중 선택) 효과는 미측정.
  • E·F·G × R2 셀은 미측정 (두 축 모두 개별 기각돼 잴 이유가 없다).
  • 절대값을 v5와 나란히 놓지 않는다 — 정답 수가 달라 시험지가 다르다.

정정

V5_SECONDARY_M0_G 상수가 실제로는 KURE(M2)의 값을 들고 있었다. 이 오라벨 탓에 "M0 nDCG 부호 역전"이라는 없던 현상이 미해결 사항으로 기록돼 있었고, 철회했다. 판정 로직은 이 상수를 쓰지 않아 채택 판정에는 영향이 없다.

관련

  • 설계: 볼트 「검색 실험 설계 (4모델·BM25)」
  • 결과: 볼트 「검색 실험 결과 (4모델·BM25)」
  • ADR-010(임베딩 gpl3), ADR-011(BM25 미채택)

beomjinkim2000 and others added 30 commits July 30, 2026 00:19
임베딩 실행 위치(맥 vs GPU VM) 판정용 측정 스크립트다. 기존
bench_embed_throughput.py(CUDA 전용)와 두 가지가 다르다.

1. 디바이스 판정이 목적이다. mps.is_available()과 모델이 실제로 올라간
   디바이스를 둘 다 기록하고, 여러 디바이스를 한 번에 잰다. 리랭커가
   맥에서 조용히 CPU로 폴백해 L4의 49배 느렸던 전례 때문에, 어느 쪽이
   실제로 쓰였는지가 수치보다 중요하다.
2. 표본이 문서를 가로지른다. 이전 벤치마크는 저장 순서 앞 200개를 써서
   사실상 문서 1건만 봤다. seed 고정 무작위 표본으로 바꾸고 커버 문서
   수와 토큰 길이 분포를 함께 보고한다.

정본 인덱스(data/chroma_v2)는 sqlite를 읽기 전용으로 직접 열어 어떤
쓰기도 하지 않는다.

메모리 가드를 넣은 이유: 8GB 통합 메모리 맥에서 bge-m3 fp32를 batch 32로
돌렸다가 워밍업 한 배치도 못 끝내고 18분간 스왑을 6GB까지 밀어붙인 끝에
OOM 킬을 맞았다. 같은 머신의 다른 작업까지 멈춘다. 그래서 배치 기본값을
8로 낮추고, 적재 전에 여유 메모리와 가중치 크기를 비교해 들어가지 않으면
중단한다. 느린 것보다 시작하지 않는 것이 낫다.
신규 골든셋 85문항(g300 × 검색 4유형)의 정답 라벨링 대상을 만든다. 정답지를
모델별로 만들면 서로 다른 시험이 되므로, 정답지는 하나로 두고 그 하나를 세
임베딩의 후보를 합쳐 구성한다.

dense_doc만 필요하므로 전체 30,002청크를 다시 인덱싱하지 않는다. 대상 27문서
9,323청크만 다루고 결과는 Chroma가 아니라 numpy로 든다.

주의해서 다룬 지점 셋:

1. Chroma에 저장된 documents에는 프리픽스도 [사업명/발주기관/섹션] 메타 헤더도
   없다(build_index가 embed엔 embed_text를, 저장엔 text를 쓴다). 저장 원문을
   그대로 M1/M2에 넣으면 e5와 다른 텍스트를 임베딩하게 되므로 헤더를 복원하고,
   복원분을 e5로 재임베딩해 저장 벡터와 대조하는 게이트를 뒀다.
2. _norm_doc_id는 strip을 먼저 하고 확장자를 뒤에 떼서, `... 상하원 .hwp`처럼
   확장자 앞에 공백이 있으면 후행 공백이 남아 인덱스와 매칭되지 않는다. 대상
   문서 2종이 여기 걸렸다. 채점 경로 전반이 쓰는 함수라 건드리지 않고 래퍼에서
   흡수했다.
3. 9,323건을 encode() 한 번에 넘기면 8GB 머신에서 OS가 프로세스를 죽인다.
   조각내어 인코딩하고 조각마다 체크포인트를 남겨 재개 가능하게 했다.

후보 드롭·중복은 세어서 0이 아니면 중단한다 — 조용히 줄어든 후보는 특정 모델을
부당하게 낮게 만든다.
strip을 먼저 하고 확장자를 나중에 떼는 순서라 '... 상하원 .hwp' 같은
파일명이 '... 상하원 '(후행 공백)으로 정규화됐다. 인덱스 쪽 doc_id는
확장자가 없어 '... 상하원'이 되므로 둘이 영영 안 맞았다.

골든셋 300의 대상 문서 2종(문항 6건)이 여기 걸렸다. 기존 200문항은
target_doc_id에 확장자가 없어 이 경로를 타지 않았다.

이 실패는 예외가 아니라 빈 후보로 나타난다 — _resolve_with가 warning
한 줄만 찍고 넘어가므로 로그를 안 보면 해당 문항만 조용히 죽는다.
1) 질의 구성: make_rerank_labels.py:79의 규약은 ' '.join([*선행_턴, question])
   인데 질문만 넣고 있었다. 후속 유형의 대명사('그 사업')는 선행 질문 없이
   해소되지 않아, 그 문항들만 다른 후보를 받아 기존 164문항 캐시와 잣대가
   갈린다. 6.8.6에서 같은 실수로 후속 29문항 측정이 무효가 된 전례가 있다.
   질의 임베딩·BM25·산출물 3곳 모두 query_of()로 통일. 영향 16문항.

2) 범위: 신규 g300 85문항만 잡고 있었으나, 12.5에서 라벨을 새로 붙인 g200
   5건이 기존 캐시(164문항 기준)에 없다. 12.7의 300문항 재채점에 그 5건의
   M1/M2 dense_doc도 필요하므로 대상을 90문항/32문서/10,643청크로 넓힌다.
   라벨링 풀 대상은 pool_items()로 신규 85문항만 따로 추린다.
디바이스 선택이 mps > cpu 뿐이라 CUDA 머신에서 무조건 CPU로 적재됐다.
에러가 안 나므로 로그의 device= 를 봐야만 알아챈다 (VM 실행에서 실제로
device=cpu 로 뜬 것을 확인). cuda > mps > cpu 순으로 고치고 --device 로
강제 지정할 수 있게 했다. empty_cache 도 백엔드별로 분기.
임베딩 실행처가 GPU VM으로 확정돼, 맥에서 끝낼 수 있는 몫과 VM에서 돌 몫을
단계로 갈랐다.

  --stage local : 부분집합·복원검증·e5·bm25_doc·VM 인계 패키지 (맥)
  --stage embed : 청크·질의 임베딩 → npz (GPU VM)
  --stage merge : npz를 받아 합집합 완성

인계 패키지에서 특히 조심한 지점: embed_text에는 인덱싱 시점의 e5 `passage: `가
이미 붙어 있다. 받는 쪽이 모델 프리픽스를 덧붙이면 bge-m3·KURE 텍스트에
`passage: `가 남아 e5와 다른 문자열을 임베딩하게 되고, 그 차이가 에러 없이
"모델 성능 차이"로 오인된다. MANIFEST에 치환(retarget_embed_text)임을 명시했다.

포장은 python tarfile로 한다 — macOS tar czf는 한글 자소를 분리시켜 chunk_id
대조가 깨진다. 파일명·내용 NFC와 SHA256 목록을 함께 낸다.

bm25_doc은 임베딩과 무관하므로 1회만 계산한다. 코퍼스는 전체 30,002청크를 쓰고
결과만 대상 문서로 거른다 — 부분집합만 색인하면 IDF가 달라져 기존 산출물과
비교가 성립하지 않는다.

라벨링 풀(신규 85문항)과 후보 캐시(90문항)를 분리해 낸다. 캐시가 못 덮는 기존
g200 5건은 임베딩·캐시 대상이지만 라벨이 이미 있어 판정 대상은 아니다.

모델 벡터가 아직 없으면 그 자리를 비운 채 골격을 만들고 미완 모델을 메타에
남긴다.
- lc.py: ChatOpenAI use_responses_api=True + reasoning={effort, summary: auto}
  (effort는 기존 GENERATION_REASONING_EFFORT 그대로, max_completion_tokens는
  max_output_tokens로 매핑됨 — 페이로드 확인)
- generate.py: content가 블록 리스트가 되는 Responses 경로 대응 — 답변은 .text로
  추출(_text_of), 빈 답변 재시도 판정에 status==incomplete 추가, reasoning 요약
  추출(_reasoning_summary, additional_kwargs/content 블록 양쪽 수용) 후 반환값에 추가
- core.py run_turn: 생성 경로에서만 reasoning 채움 — 즉답·반문·무근거는 None
- server.py /ask: AskResponse에 reasoning(str|None) 추가, 스텁 turn_fn 호환 위해 get

실호출 검증(:8001): 생성 경로 요약 수신(534자)·즉답 경로 null 확인.
UX 스펙 §4 (프론트 접이식 "생각 과정" 블록의 백엔드 몫).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dht21ZNa9LJs6q5pdXjyfn
내부 파일명이 답변에 그대로 보여 신뢰감을 깎는다는 사용자 피드백.
spec §10-4의 '직답 출처 명시'는 유지하되 라벨만 사용자 언어로 바꿨다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dht21ZNa9LJs6q5pdXjyfn
hwp_hwpx_parser 1.0.0은 표 셀(LIST_HEADER)을 level 무시하고 개수로만 세서,
셀 안 중첩 표의 셀까지 바깥 표 셀로 오인 카운트한다. 그 결과 바깥 표가
조기 종료되고 남은 셀·중첩 셀이 표 밖 낱개 문단으로 흩어졌다 (PyPI 최신에도
미수정). 서브클래스 리더로 표 레코드와 같은 level의 LIST_HEADER만 바깥 셀로
세고, 중첩 표는 to_inline(행 구조 소실) 대신 행(<br>)·셀(|) 구분을 보존해
셀 안에 직렬화한다.
업스트림 _build_table_data는 셀 텍스트를 행 수대로 순차 채움만 해서 병합
정보가 통째로 버려진다 — 간트형 일정 표는 병합 헤더가 1칸으로 줄어 월
라벨과 데이터 열이 어긋난 채 마크다운이 됐다.

LIST_HEADER 셀 레코드의 공통부 8바이트 뒤 colAddr·rowAddr·colSpan·rowSpan
(각 UINT16, 오프셋 8·10·12·14)을 그대로 읽어 격자를 재구성한다. 오프셋
근거는 실문서 실측: colSpan=3 셀의 폭(오프셋 16 UINT32)이 위 행 3개 셀 폭
합과 정확히 일치, rowSpan=2 아래 행은 해당 열 셀 레코드 결손.

병합이 있는 표만 <table><tr><td colspan rowspan> 텍스트 전용 HTML 한 줄로
직렬화하고(다운스트림 줄 단위 블록 분리에 안 갈라지게), 병합 없는 표·주소
정보가 없는 레코드(합성·손상)는 기존 마크다운 경로 그대로다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCDZppk5btUJfzW4S9p3uf
hwp5 병합 셀 HTML 직렬화(fb928c7) 이후 <table> 안 담당자 성명이
markdown 파이프 행 전용 마스킹을 통과해 실명 그대로 노출되던 문제.

- <table>/<tr>/<td|th> 스택 파싱 + colspan·rowspan 점유 그리드 전개 후
  기존 직함-이름 규칙(옆·아래·자간 이름) 적용 — 태그·속성은 보존
- 중첩 <table>·셀 안 파이프 직렬화 중첩행도 동일 처리
- 실측 잔존 보완: 이름 셀 다음 셀이 전화/이메일이면 치환(직함 비인접 연락처 표),
  인라인 '이름[직함], 연락처' 쉼표·무공백 직함 패턴 허용, 성씨 '용' 추가
- 오탐 보정: 조직 접미(측·팀·실)·기술사/기능사·용역* 제외
- 실측: 추출본 99건 재검수 — 확인된 노출 실명 잔존 0, 신규 치환 전건 육안 검수
라벨 확정 전에 M3를 합류시킨다. 라벨을 3모델로 붙인 뒤 넣으면 M3 후보 상당수가
미판정으로 남고 현행 채점이 미판정을 오답으로 세어 새 모델이 구조적으로 불리해진다
(6.8.8에서 실제로 발생, 신규 판정 2,105개 재작업).

- M3는 허브 모델이 아니라 로컬 state_dict라 베이스 e5-small SentenceTransformer에
  가중치만 얹는다. AutoModel로 풀링을 새로 구현하면 그 순간 §10(동일 엔진)이 깨진다.
  missing/unexpected 키가 1개라도 있으면 중단 (일부만 얹히면 "덜 학습된 모델"이 되어
  성능 차이로 오인된다)
- 프리픽스 부착 여부는 12.2a 판정 대기 중 — config 플래그(EMBEDDING_M3_USE_E5_PREFIX)로만
  분기하고 코드 어디에도 하드코딩하지 않는다
- M0/M1/M2 벡터는 재계산 없이 재사용 (3모델 평균 49.59로 기존 기록 49.6 재현)
- 게이트 3종 자동화: --stage gate (§9 chunk_id 양방향 차집합 · §11 실제 입력 문자열 ·
  §10 엔진 대조), 실패 시 하드페일
- 증분 보고를 풀 생성과 같은 자리에서 센다 (사후 추정 금지)

실측: chunk_id 0건/0건 · 10,643=10,643 · 32문서=32문서 / 드롭 0 · 중복 0 /
라벨링 85문항 후보 49.59 → 53.52 (+334, M3 고유 7.3%)
리드 리뷰 보완 2건.

1) pooling 이 "?"로 남았다. sentence-transformers 버전에 따라 표기가
   pooling_mode / get_pooling_mode_str 로 갈리는데 후자만 봤다. 실제 모듈에서
   읽어 "mean"으로 기록한다 — 이 메타는 나중에 "왜 이 수치가 나왔나"를 재현할
   유일한 근거라 추정으로 두면 안 된다.

2) M3 벡터는 12.2a 판정 전 기본값으로 만든 **잠정치**다. config에
   EMBEDDING_M3_PREFIX_DECIDED 를 두고, 산출물 메타에 prefix_decision_pending
   으로 흘린다. 풀 메타에는 최상단 `프리픽스_판정_대기`로 띄운다 — 모델별 메타
   안쪽에만 있으면 12.3 라벨링에서 아무도 보지 않고, 그 뒤엔 되돌릴 수 없다.

기존 M3 npz는 벡터를 건드리지 않고 메타만 갱신했다(청크 벡터 sha256 앞16
88a608fa09a14144 동일 확인). 풀 재생성 결과도 동일: 49.59 → 53.52 / 드롭 0 / 중복 0.
gpl3 학습 시 `query: `/`passage: ` 부착 여부를 작성자에게 확인할 수 없어
실측으로 정했다. 프리픽스 오류는 예외를 던지지 않고 성능만 조용히 떨어지므로
(설계 §11) 추정으로 넘길 수 없는 항목이다.

v5 164문항·리랭커 OFF·engine exact(numpy 전수, 세 조건 동일):

| 조건 | MRR@10 | nDCG@10 | Recall@20 |
|---|---|---|---|
| (a) 원본 e5 + 프리픽스 | 0.6402 | 0.5764 | 0.7608 |
| (b) gpl3 + 프리픽스 | 0.7052 | 0.6447 | 0.8305 |
| (c) gpl3 프리픽스 없음 | 0.5219 | 0.4846 | 0.7225 |

- (b) 채택. (b)−(c) = +0.1833 CI [+0.1398, +0.2297]로 프리픽스를 떼면
  원본 e5보다도 나쁘다((c)−(a) = −0.1182 CI [−0.1756, −0.0615]).
  gpl3는 e5 프리픽스 관례를 유지한 채 학습됐다
- (b)−(a) = +0.0650 CI [+0.0193, +0.1134] (paired bootstrap 1만 회·시드 20260729).
  nDCG +0.0684 [+0.0294, +0.1074] · Recall@20 +0.0697 [+0.0274, +0.1122]로
  세 지표 모두 하한 > 0
- **이 수치는 리랭커 OFF의 dense 단독값이라 승격 판정이 아니다.** 판정은 종단
  성적으로 한다(설계 §3). KURE도 Recall은 올랐으나 종단에서 +0.0297로 깎였다.
  종단·CI는 v6 300문항으로 12.7에서 낸다

재현 검증: 조건 a가 검색 실험 결과 §M0 실측 MRR@10 0.6402를 넷째 자리까지
재현했다 — dense_doc 경로가 원본과 동일하다. nDCG·Recall이 문서 표(0.6061/
0.8163)와 다른 것은 정답지 v4(310) → v5(353) 차이이며 파이프라인 문제가 아니다.

§11 게이트는 `embed_text_복원_cos`(min 0.9999999 / mean 1.0)로 대체·강화했다.
재구성 입력의 벡터가 원본 인덱스와 코사인 1.0이면 입력 문자열이 바이트 동일이라는
전수 증명이라, 표본 육안 확인보다 강하다. drops 0 / 팀 OpenAI API 호출 0.
기존 169문항의 라벨은 v5(3모델) 기준이라 M3 후보가 판정 풀 밖에 있다. 그대로
승격하면 254문항의 66%에서 M3만 미판정이 오답으로 세어져 12.7 비교가 무효가 된다.

- scripts/build_m3_delta_v6.py 신설. `--stage plan`은 임베딩 없이 재사용/신규
  건수만 센다(5초). 실행 시점을 수치로 정하기 위한 단계다
- 12.2b의 pool_v6_vec_M3.npz를 **행 위치로** 꽂아 재사용한다. 한 칸만 밀려도
  다른 청크의 벡터로 검색하게 되고 에러가 나지 않으므로 재사용 경로를 테스트로
  못 박았다
- 산출은 기존 retrieval_cache_200*.json 과 같은 모양이다. 델타 계산은
  build_pool_delta.py 가 --caches M3=... 로 그대로 받는다 — 같은 규칙을 두 곳에
  두면 반드시 갈린다
- 검색은 build_pool_v6.dense_doc 을 import 해서 쓴다(§10 동일 엔진).
  프리픽스는 config 플래그 경유, 드롭 0건 하드페일, §9 양방향 대조 포함
- 판정 이력 없는 문항은 자동 처리하지 않는다(--allow-missing-pool 명시 필요).
  전부 미판정으로 보면 라벨링 부담이 부풀고, 버리면 그 문항만 M3가 불리해진다

실측 계수: 169문항 / 대상 문서 81종 / 청크 23,725 = 재사용 4,646(19.6%) +
신규 임베딩 19,079. pytest 14건 신규.
12.2a 실측으로 확정: 리랭커 OFF · v5 164문항 기준 gpl3+프리픽스 MRR@10 0.7052 /
프리픽스 없음 0.5219 / 원본 e5 0.6402. 미부착은 원본보다도 낮아 gpl3가 e5의
query:/passage: 관례를 유지한 채 학습됐음이 드러났다.

- EMBEDDING_M3_PREFIX_DECIDED = True. 근거·수치·조건을 EMBEDDING_M3_PREFIX_DECISION에
  두고 산출물 메타로 흘린다. 나중에 "왜 프리픽스를 붙였나"에 답하는 유일한 기록이다
- 정답지 버전 주석을 함께 박았다. 이 수치는 v5(353개) 기준이라 「검색 실험 결과」 문서
  표(v4 310개, nDCG 0.6061 / Recall 0.8163)와 직접 대조하면 어긋난다
- 기존 M3 벡터는 이미 부착 기준으로 계산돼 있어 **재계산 불필요**. 벡터를 건드리지 않고
  메타만 갱신했다(청크 벡터 sha256 앞16 88a608fa09a14144 동일 확인).
  풀 메타의 `프리픽스_판정_대기` 표식도 해제됐다

풀 재생성 결과 불변: 49.59 → 53.52 / 드롭 0 / 중복 0. pytest 37 passed.
기존 169문항의 M3 dense_doc 후보를 뽑아 미판정분만 델타 작업목록으로 냈다.

12.5에서 라벨을 새로 붙인 g200 5문항은 **어느 후보 캐시에도 판정 이력이 없다**
(골든셋·라벨 파일에만 존재). 기존 코드는 이 경우 KeyError로 죽었고, 조용히
'전부 미판정'으로 처리하면 라벨링 부담이 부풀고 버리면 그 문항만 M3가 불리해진다.
--allow-missing-pool 을 명시했을 때만 골든셋에서 문항 정보를 채워 진행한다.

- resolve_meta: 후보 풀 항목이 정본, 없으면 골든셋 폴백(질의는 선행_턴+질문 규약 유지)
- 전량 미판정으로 실은 문항은 실행 로그에 경고로 남긴다

실측: 신규 판정 598개 / 문항당 3.54
  판정이력 보유 164문항 498개(문항당 3.04) + 이력 없는 5문항 100개(문항당 20.0)
게이트 §9 chunk_id 0건/0건 · 23,725=23,725 · 81문서=81문서 / 드롭 0 · 중복 0
재사용 4,646 + 신규 임베딩 19,079 (62청크/초, 8분)
§11 육안 확인은 앞 200자만 본다. 이 게이트는 복원 입력을 **원본 e5**로 다시 임베딩해
인덱스 저장 벡터와 대조하므로, 문자열 전체가 인덱싱 시점과 바이트 동일함을 표본 전수로
증명한다. 메타 헤더 규약이 조금이라도 달라지면 코사인이 즉시 떨어진다.
12.2a에서 검증된 방식을 그대로 적용했다.

M3는 e5와 같은 프리픽스를 쓰므로(12.2a 조건 b 확정) 이 검증이 M3 입력에도 그대로 유효하다.

실측: 23,725청크 중 128표본 코사인 min=0.9999999 / mean=1.0000000 (12.2a와 동일 수준).
임계 0.999 미만이면 하드페일. pytest 3건 추가.
## 1) paired_bootstrap 에 clusters 옵션 (Plans 12.7 DoD)

같은 문서의 문항들은 독립이 아니다. 문항 단위로 재표집하면 유효표본을 과대평가해
신뢰구간이 실제보다 좁아진다 — 유의성을 부풀리는 방향의 오차다.

- clusters(qid -> 문서 키)를 주면 재표집 단위가 문서가 된다
- **기존 문항 단위 경로는 난수 소비 방식까지 그대로 뒀다.** 6.8.9 수치를 재현하지
  못하면 "판정이 유지되는지" 대조하는 12.7의 목적 자체가 성립하지 않는다.
  기능 추가 전 구현으로 뽑은 골든값을 회귀 테스트로 고정했다
- 군집이 문항 1:1이면 두 경로가 완전히 같은 결과를 낸다(테스트로 고정)
- 군집 키 없는 문항은 ValueError — 조용히 버리면 그 문항만 채점에서 빠진다
- 결과에 method / n_clusters / cluster_size_mean 기록

## 2) 델타에서 기존 정답(gold) 제외 — 12.3b 실측 결함

pool_delta_v6_M3.jsonl 155문항 중 2문항에 **기존 정답 청크가 신규 후보로 섞였다**
(로봇산업진흥원 ::0011, 축산물품질평가원 ::0009). 둘 다 판정 이력이 없는 문항이라
뺄 판정분이 없어 gold가 그대로 통과했다.

라벨러는 당연히 gold로 붙이는데 이건 "새 모델이 새로 회수한 정답"이 아니다. 그대로
집계하면 신규 회수율이 부풀려진다 — 12.3b에서 채택률 0.84% vs 실제 0.50%로 갈렸다.
12.7 채택 판정의 근거 수치가 뒤집힐 수 있는 크기다.

unjudged()로 판정분과 gold를 함께 뺀다. 산출 파일은 **덮지 않았다** — 12.3b 라벨링이
130/155 진행 중이라 재생성하면 진행분이 무효가 된다. 혼입 2건은 집계에서 제외한다.

pytest 40건 통과 (신규 클러스터 6 + gold 제외 3).
임베딩 0건. 254문항의 dense_doc은 이미 두 블록(164 구캐시 + 90 pool_v6_candidates)에
나뉘어 있고 겹침이 0이라 붙이기만 하면 된다.

## §10 — M0 두 벌 (리드 제안 채택)

불변식은 "전 모델 동일 엔진"이 아니라 **"한 문항에서 비교하는 모델끼리 엔진이 같을 것"**이다.
- M0_exact ↔ M3        : 전건 exact (핵심 판정)
- M0_fast ↔ M1 ↔ M2    : 164 근사 / 90 exact, 세 모델이 문항 단위로 동일 (6.8.9 재확인)
check_engine_alignment가 비교쌍별로 문항 단위 대조하고 어긋나면 중단한다.
M0_exact는 재계산 산출물이 있어야 조립되며, 없으면 건너뛰고 그 게이트를 경고로 남긴다.

## 질문 원문 — §2-1 재발 방지

선행_턴 + 질문 병합본을 `query` 키로 낸다. 후속 문항인데 선행_턴이 비면 중단.
추가로 rerank_on_cache.load_questions에도 게이트를 넣었다 — `query` 없이 `선행_턴`만 있는
행을 만나면 그건 병합본이 아니라 골든셋 원본이므로 멈춘다. 전례에서는 파일 이름도 문항 수도
그럴듯해서 에러 없이 수치만 틀어졌다.

## C_doc 한정

--variants 로 범위를 좁힌다(기본 전부). 통합 캐시엔 bm25/dense_full이 없어 전 변형을
강제하면 KeyError가 난다. 오타는 조용히 무시하지 않는다.

## gold

구 캐시의 v4/v5 gold는 버리고 --labels 것만 넣는다. 254 전건이 아니면
gold_complete:false로 각인 — 현재 169/254(신규 85는 라벨링 중)이라 채점 불가 상태다.

실측: 4태그 × 254문항 / M3 중복 5(169파일 우선) / 질문 254(선행 턴 병합 45) /
캐시 각 0.65MB / C_doc 후보 문항당 20. pytest 16건 신규.
85문항 4,549후보를 한 워커가 들고 판정하면 컨텍스트가 터진다. 실제로 오늘
워커 하나가 약 400k 토큰에서 죽어 교체했다. 작업을 문항 단위로 쪼개고 결과를
그때그때 파일에 붙이는 경로가 필요했다.

- shard_pool_v6.py: pool_v6.jsonl을 4샤드로 분할. 문항 수와 유형 분포가 한쪽에
  쏠리지 않게 나눈다(실측 22/21/21/21, 유형별 편차 ±1)
- view_q.py / scan_q.py: 문항 하나와 그 후보만 꺼내 읽는다. 전건을 메모리에
  올리지 않는다
- append_label.py: 판정 결과를 한 건씩 append. 중간에 끊겨도 이어서 할 수 있어
  워커 교체 비용이 0이다 — 오늘 세 번의 교체에서 판정분 손실이 없었다

산출(data/ 아래, gitignore): pool_v6_shard{1..4}_labeled.jsonl 85문항 전건,
정답 122건(문항당 1.44), chunk_id 오류 0건, confidence 높음49·보통28·낮음8.
유형별 문항당 정답은 탐색비교 1.92 · 단일세부 1.47 · 후속 1.33 · 직답 0.92로
직답만 1 미만이고 빈 정답 4건 중 2건이 직답이다 — 12.4 우선 확인 대상.
임베딩 없음. 청크 벡터는 chroma_v2의 fp32를 그대로 쓰고, 질의 254건만 e5로 인코딩한 뒤
numpy 전수 계산으로 dense_doc 상위 20을 만든다(17초).

## 자기 검증 두 벌

① 90문항은 12.2b가 exact로 만든 pool_v6_candidates.json#M0와 **완전 일치**해야 한다.
   실측: 집합 0 / 순서 0 / 거리차 0.0 → 계산 경로가 12.2b와 같음이 증명됐다.
   어긋나면 하드페일한다.
② 164문항 vs 근사 캐시: 집합 4건(2.4%) / 순서 9건(5.5%) 차이.
   설계 문서의 "정확 계산과 대조하니 164문항 중 9문항(5.5%)에서 상위 20이 달랐다"와
   순서 기준으로 정확히 일치한다 — 근사 캐시를 그대로 쓰면 안 되는 이유의 재확증이다.

## 게이트

- embed_text 복원 코사인 min 0.9999999 / mean 1.0 (12.2a·12.2c와 같은 방식)
- 질의는 build_questions 재사용 — 선행_턴 병합과 후속 게이트가 그대로 적용된다
- 대상 문서 미해석·후보 0건은 하드페일

## 결과

§10 비교쌍 게이트 두 개가 모두 닫혔다.
- 핵심(M0_exact↔M3): 254문항 전건 exact, 불일치 0
- 6.8.9 재확인(M0_fast↔M1↔M2): 254문항 전건 일치(164 fast / 90 exact를 셋이 동일하게)

v6_scope()를 build_rescore_cache_v6로 올려 범위 산정 규칙을 한 곳에 뒀다.
pytest 3건 추가.
240문항(신규 85 + 델타 155)을 검증자 둘이 병렬로 봤다. 문항당 후보가 shard는
53건, 델타는 3.86건으로 무게가 달라 조회 경로를 나눴다.

- verify_q.py / dump_shard4.py / dump_delta.py: 문항 하나와 후보만 꺼낸다.
  전건을 메모리에 올리면 컨텍스트가 터진다 — 오늘 워커 하나가 약 400k 토큰에서 죽었다
- doc_grep.py: 같은 문서의 다른 청크를 찾는다. 빈 정답의 사유를 "본문 부재"와
  "후보 미포함"으로 가르는 데 쓴다. g300:efe1ba7f는 총괄표 합계 청크가 후보에
  없었는데 같은 문서의 그 청크가 다른 문항 후보엔 있어 본문 부재가 아님이 드러났다
- screen_shard4.py / triage.py: 명백한 오답을 먼저 걸러 판단이 필요한 후보만 남긴다
- append_verify.py / append_verified.py / append_s4.py: 결과를 한 건씩 붙인다

검증 결과(data/ 아래, gitignore): 신규 85문항 통과77·수정8, 유지120·제거2·추가8로
최종 정답 128건(라벨러 122 대비 +6). 델타 155문항 통과155, 유지5·제거0·추가0에
보류 1건. 제거보다 추가가 많은 것은 라벨러 프로필(재현율 0.863/정밀도 0.750)과
방향이 맞고, 2단계 파이프라인이 설계대로 작동했다는 뜻이다.

검증자 간 공유 기준 2건을 확정했다.
- 기준 V-1(발주기관): 역할 정의가 명시된 청크만 인정. 표지 기관명 단독·서식
  서명란은 GOLD_CRITERIA §2로 제외
- 식별자 조각 오염: 값 조각이 자기완결이면 R8 식별자 짝은 오염으로 본다
정답 486 = 기존 356 + 신규문항 128 + 델타신규 2 (중복 2 제외). 리드 예측과 일치.

## 병합은 집합 연산이다

델타 최종 gold 4건 중 2건(g200:37cd7e06 ::0011, g200:9f071cc8 ::0009)이 기존 gold와
겹친다. 리스트로 이으면 같은 chunk_id가 두 번 들어가 그 문항의 nDCG·Recall 분모가 부풀고
에러 없이 조용히 채점이 틀어진다. 한 출처 안의 중복도 상류가 깨진 신호라 중단한다.

## 채점 성립 251문항 (리드 예측 250과 1건 차이)

검증 전 gold 빈 문항은 4건이었으나 12.4 검증에서 g300:979f6a74가 채워져 3건이 됐다
(검증 추가 8 · 제거 2). 남은 3건은 검증자가 빈 목록을 실증으로 지지한 문항이다.
251문항은 설계 §6.8 측정 해상도 기준(+0.03 판정에 236문항)을 넘는다.

## 보류 1건은 gold에 넣지 않았다

g200:1f4ec3a0 ::0210 — 같은 문항의 기존 gold가 배점 없이 선정방식만 진술하고도 채택돼
있어 이것만 빼면 문항 내 잣대가 갈린다. 사유를 MANIFEST에 기록해 나중에 기존 라벨을
손볼 때 함께 재판정하도록 남겼다.

## 파일 분리는 v4 규약을 따른다

rerank_labels.jsonl = gold 보유 251, _full = 전건 300. 전건을 실험 입력으로 주면 분모를
착각한다(v4를 만든 이유).

pytest 9건 신규. 산출물은 data/(gitignore).
## 1) gold 주입 — 5태그 전건 254문항 / 486건 일치

구 캐시의 v4/v5 gold를 버리고 v6 정본을 넣었다. check_gold_alignment가 문항별로
라벨 정본과 대조하고 하나라도 어긋나면 중단한다 — 옛 정답지로 다른 시험을 치르는 것은
에러 없이 조용히 틀어지는 종류다.

주입 원천은 **rerank_labels_full.jsonl(전건)**이다. gold 보유분만 담긴 파일을 주면
gold가 빈 3문항이 "라벨 정본에 없는 문항"으로 걸려 중단된다(실제로 걸렸고, 그게 맞는 동작).
gold_complete의 정의도 "대조 통과"로 바로잡았다 — gold가 빈 문항은 정상이므로
"모든 문항에 gold가 있는가"로 정의하면 안 된다.

## 2) 채점 분모 보정 — 254행 캐시, 251문항 채점

gold가 빈 문항은 어떤 순위를 내놔도 0점이라 남겨 두면 분모만 늘어 모든 구성의 절대
수치가 함께 낮아지고 6.8.9·12.2a와 비교가 성립하지 않는다. drop_goldless로 제외하고
제외 건수를 로그에 남긴다.

## 3) 판정 규격을 코드로 박았다 (scripts/verdict_12_7.py)

수치를 보고 나서 기준을 고르면 어떤 결과든 정당화된다. 그래서 실행 전에 고정한다.
- 승격 = CI 하한>0 AND 점추정>=+0.03 AND 어떤 유형도 -0.03 이하 회귀 없음(경계 포함)
- 문서 클러스터 부트스트랩(1만 회·seed 20260729). 군집 키는 첫 대상 문서 기본 —
  실측 251문항 → 95군집·군집당 2.64(Plans 2.96과 같은 자릿수). 대상 문서 집합 키는
  129군집·1.97로 잘게 쪼개져 클러스터 보정 효과가 약해진다
- 유형별 분해 필수 — 평균이 오르며 한 유형이 망가지는 걸 잡는 게 회귀 조건의 목적
- 6.8.9 대조: KURE 하한(-0.0042) 부호가 바뀌면 재검토 필요로 표시(ADR-010·spec §2 파생)
- 해석 주의를 산출물에 박는다: 12.2a +0.0650은 리랭커 OFF의 dense 단독값이라 승격 판정이
  아니다 / M3 고유 정답 0.49%이므로 이득은 순위 상승이며, 종단 유지 여부는 추론이지 결론이 아니다

pytest 신규 18건(누계 134 passed).
VM 디스크가 100%까지 갔던 원인이 중복 다운로드였다(공유본 2.2GB + 홈 HF 캐시 2.2GB).
리드가 홈 캐시를 지웠으므로 허브 id를 그대로 넘기면 2.2GB를 다시 받아 도로 찬다.
--local-files-only 로 HF_HUB_OFFLINE/TRANSFORMERS_OFFLINE 을 걸어 조용히 받지 말고
에러로 멈추게 한다.

--require-cuda 는 torch.cuda.is_available()이 아니라 **실제 파라미터 디바이스**를 본다.
CUDA 분기 누락으로 GPU를 두고 조용히 CPU로 떨어진 전례(56f19b3)를 그 자리에서 잡는다.
VM에서 CPU면 49배 느려 사실상 멈춘 것과 같다. pytest 2건 추가.
채점기 기본 Recall은 RETRIEVAL_TOP_K(=5)라 @20이 산출물에 없다. --dump-ranked 20이 남긴
순위로 계산한다(재채점 불필요, chunk_id만이라 NDA 안전). ranked가 없으면 중단 —
조용히 0으로 채우면 모든 모델이 똑같이 0이 되어 차이가 사라진다.

pytest 3건 추가(누계 16).
리드가 보고한 KeyError: 'C_doc'은 3772022에서 이미 수정됐다(로더가 "variants" 한 겹을
벗기지 않았던 것). 재발을 막는 구조 회귀 테스트를 추가한다 — 판정 스크립트가 안 돌면
사전등록의 의미가 없다.

스크립트가 리드의 수동 판정치를 소수점 4자리까지 재현함을 확인했다:
  M3 ↔ M0_exact +0.0437 [+0.0097, +0.0807]
  M1 ↔ M0_fast  +0.0511 [+0.0103, +0.0959]
  M2 ↔ M0_fast  +0.0578 [+0.0210, +0.0972]
전부 승격 기준 통과, 유형별 회귀 없음, 군집 95·평균 2.64.

pytest 2건 추가(누계 18).
- MRR@10 판정 + nDCG@10·Recall@5·@20 부수 지표 CI 필수 산출
- 부수 지표가 유의 음수면 MRR 통과해도 채택 차단 (v5에서 실제로 놓칠 뻔한 손해)
- paired bootstrap 1만회 seed 20260729 · 문서 클러스터
- v5 대조값(e5 +0.0222 / bge -0.0035 / KURE -0.0127, G_doc) 상수로 고정
- build_bm25_doc_v6: 전체 30,002청크 코퍼스로 색인 후 대상 문서 필터, k=20
  드롭 0건 하드페일 / doc_id는 정규화 공간에서 매칭(후행 공백으로 23문항이 조용히 0건이던 함정)
  선행 90문항분과 전건 일치 확인
- audit_bm25_unjudged_v6: BM25 전용 후보의 판정 이력 유무를 문항별로 집계
  dense 대조군·변형(E/F/G_doc)별 노출까지 산출
- build_pool_delta_bm25: 감사와 동일한 판정 이력 정의(load_judged 재사용)로 델타 산출
  BM25 전용 ∩ 미판정 ∩ gold 아님, BM25 순위 유지, 기존 정답은 맥락으로만 동봉
  --expect 하드페일 + 작업목록 적재 건수·중복 재검사 (드롭 0)
- shard_pool_v6: --mode doc 추가 — 같은 문서를 같은 샤드로 묶고 후보 수로 균등 배분
  기존 유형 라운드로빈(--mode type)은 기본값으로 불변, 분할 누락·중복 게이트 추가
- promote_goldenset_v7: v6 정본에 BM25 델타 채택분을 집합 연산으로 병합(486→490)
  v6 디렉터리 덮어쓰기 차단 / 문항 수 불변 하드게이트(300·251) / 판정 대상 밖 chunk_id 차단
  MANIFEST에 풀 구성 모델(4모델+BM25)과 '문항 불변, 정답만 증가' 근거 명시
- verdict_bm25_v6: v5 대조 조회 키를 엔진 접미사(M0_exact/M0_fast) 정규화
  (채택 조건 로직은 불변 — 판정·CI 전건 동일함을 대조로 확인)
- verdict_12_7: --labels/--recompute 추가. 기본값은 v6 정본 그대로라 정본 수치 재현 불변
  라벨만 바꾸고 재계산을 안 하면 '바꿨는데 수치가 그대로'인 무효 대조가 되므로 게이트로 둠
`V5_SECONDARY_M0_G`가 들고 있던 nDCG −0.0364 / Recall@5 −0.0492는
M0(e5)가 아니라 M2(KURE)의 v5 값이다. v5 정본이 "KURE-v1 기준"이라
명시하고, 같은 표의 MRR −0.0127도 KURE의 v5 G_doc 값과 일치한다
(M0는 +0.0222).

이름 하나 때문에 "M0의 nDCG가 v5 −0.0364 → v7 +0.0459로 부호가
뒤집혔다"는 없는 현상이 미해결 사항으로 기록되고 문서 세 곳에
퍼졌다. M0의 v5 부수 지표는 애초에 기록이 없어 대조가 불가능하다.

판정 로직은 이 상수를 쓰지 않으므로(대조 표시 전용) 12.8의 채택
판정은 영향받지 않는다. 상수명·출력 키를 M2로 바로잡고 정정 사유를
주석에 남겼다. Plans.md 12.8과 볼트 문서 2건도 철회 서술로 정정.

tests 21 passed.
임베딩만 gpl3로 바꾸고 청크 경계는 동결한다. chunk_id가 바뀌면 v6·v7 정답지가
통째로 무효가 되므로 재청킹 없이 기존 인덱스에서 id·본문·메타를 읽어 벡터만
새로 계산해 새 디렉터리(chroma_v3)에 쓴다. 기존 chroma_v2는 읽기 전용.

- src/index/encoder.py: 로컬 state_dict 로더를 인덱싱·실험 공용 정본으로 분리.
  베이스 e5 SentenceTransformer에 트랜스포머 가중치만 얹어 풀링·정규화를 보존하고,
  missing/unexpected 키가 1개라도 있으면 중단한다 (일부만 얹혀도 에러가 안 난다).
  build_pool_v6.load_encoder는 이 함수로 위임 — 사본을 두면 인덱싱과 실험이 다른
  가중치를 볼 수 있다.
- config: EMBEDDING_MODEL을 gpl3로, DATA_CHROMA를 chroma_v3로. 베이스 e5는
  EMBEDDING_MODEL_E5로 분리해 프리픽스 규약·토크나이저 출처를 고정한다.
- scripts/reindex_gpl3.py: 게이트 4종(청크 경계 동결·프리픽스 전수 복원·드롭 0건·NFC)
  전부 하드페일.
- tests: e5 세대 인덱스(chroma_v2)를 재현하는 대조 테스트는 질의 모델을
  EMBEDDING_MODEL_E5로 못박는다. 프로덕션 기본값을 따라가면 e5 인덱스에 gpl3 질의를
  쏘게 되는데, 384차원이 같아 에러 없이 순서만 틀어진다.
gate_parity가 read_collection(ids/metadatas)을 summarize(chunk_ids/doc_ids)에
넘겨 KeyError로 죽었다. 임베딩 30,002건을 다 돌린 뒤 게이트에서 죽어 GPU 시간을
통째로 버리는 자리라, dump_index 규약으로 맞추고 실제 인덱스 두 벌을 읽는
회귀 테스트를 붙였다.

scripts/verify_reindex_repro.py: 재구축 인덱스가 12.7 판정 수치를 재현하는지
검사한다. 12.7의 gpl3 수치는 인덱스가 아니라 풀 벡터(npz)로 낸 값이라, 구조
게이트 4종을 다 통과하고도 검색 품질이 갈릴 수 있다. 문항·정답지·질의·리랭커를
12.7이 실제로 쓴 값으로 고정하고 **후보 출처만** 새 인덱스로 바꿔, 후보 층위
(집합·순서·거리)와 지표 층위(MRR@10·nDCG@10·Recall@5·@20)를 함께 본다.
R2(dragonkue/bge-reranker-v2-m3-ko)는 v5에서 기각됐으나 그 판정은 v5 라벨링
이전에 확정됐고 정답지는 이후 v6→v7로 바뀌었다 — 현행 정답지로는 잰 적이 없다.
채택된 gpl3(M3)는 v5 시점에 없던 모델이라 R2 대조값 자체가 없다.

- 판정 MRR@10 / 부수 nDCG@10·Recall@5·Recall@20
- paired bootstrap 95% CI, 1만회, seed 20260729, 문서 클러스터(첫 대상 문서)
- 채택: CI 하한>0 AND 점추정>=+0.03 AND 유형 회귀 없음 AND 부수 유의 음수 없음
  (verdict_bm25_v6.py와 동일 규격 — 축마다 기준이 다르면 비교가 성립하지 않는다)
- v5 대조값 상수 박제(e5 -0.0638 / bge -0.0707 / KURE -0.0587, M3는 None)
- R1 기준선도 ranked에서 v7 정답지로 재채점 — 박제 지표를 읽으면 무효 대조
- 정답지 기본값을 goldenset_v7로 지정 (v6 기본값은 조용한 오채점 경로)
R1↔R2는 '어느 리랭커인가'만 묻는다. 그 앞의 '리랭커를 켤 것인가'(A↔C)는 v7
재측정에서 빠져 있었다 — v5의 +0.104(이 실험 최대 이득)를 현행 정답지로
확인한 적이 없다. 축이 반만 닫히므로 같이 잰다.

- 방향 C − A (리랭커 ON이 챌린저) — v5와 부호를 맞춘다
- A_doc은 리랭킹을 안 한다: 캐시 dense_doc 순위를 그대로 채점 (GPU 불필요)
- A·C가 같은 per_q_metric을 타게 해 차이를 리랭커로 좁힌다
- v5 대조 상수 박제: A 0.6402 → C 0.7443 (+0.1041, v5/164문항/e5)
- 안 재는 변형 근거 각인: B_doc(프로덕션 구성 아님)·D_doc(v5 설계 결함, 32.6%
  가 대상 문서 밖 — G_doc이 대체)
- 판정 기준·통계는 R1↔R2와 동일 규격, 별도 비교쌍으로 분리

기존 R1↔R2 판정 로직 무수정 — --skip-a-doc 실행 결과가 ccfa8f9 산출물의
비교/규격/재검토_필요와 완전 일치함을 대조 확인
라벨링 보조 스크립트에 누적된 E741(모호한 변수명 l)·E702/E701(세미콜론·한 줄 if)
·UP006/UP035/UP045(구형 타이핑 표기)·I001(임포트 정렬)을 정리했다.
동작을 바꾸는 변경은 없다 — zip(strict=False)는 기존 관대한 동작을 그대로 고정한 것이다.

테스트 805 passed / 1 skipped.
PR #31(lcwmodel)이 먼저 머지되면서 main의 프로덕션 임베딩 설정이 gpl3(ADR-010)에서
Qwen3-Embedding-4B로 덮였다. 그런데 인덱스는 재구축되지 않아 main 단독으로는 검색이
동작하지 않는다 — 질의 벡터 2560차원 vs 컬렉션 384차원으로
"Collection expecting embedding with dimension of 384, got 2560" 예외가 나고,
추천·채팅이 함께 죽는다.

충돌 2건은 gpl3 쪽을 취했다:

- src/config.py — EMBEDDING_MODEL = EMBEDDING_MODEL_M3(gpl3), DATA_CHROMA = chroma_v3.
  Qwen은 이 값을 바꾸면서 인덱스를 같이 갈지 않아 성립하지 않는 조합이었다.
- src/index/build_index.py — _get_model이 encoder.load_encoder를 타야 한다.
  main 쪽 구현은 허브 id로만 로드해 로컬 가중치(gpl3)를 못 읽는다.

main에서 온 변경(knockout 판정, 배치 묶음 4→2, search_text 분리 등)은 그대로 유지된다.
aa9381f가 추가한 model/ 실험 스크립트도 건드리지 않았다.

검증: gpl3 가중치 로드 후 chroma_v3 doc_cards 질의 성공, 후보 5건 정상 반환.
- assets/: 서비스 흐름 스크린샷 6장 + 데모 mp4(2.1MB, 25.7초)
- 파싱·청킹 문제/해결 8건, 검색 실험(임베딩·BM25·리랭커) 판정 표 추가
- 핵심 기능·시스템 아키텍처·폴더 구조·한계점 표로 정리
- 운영 절차(셋업·인덱싱·서버·VM 검증·API)는 OPERATIONS.md로 분리
- 팀원 협업일지·보고서 링크 블록은 main 원본 유지
main에서 들어온 모델 탐색 코드로 lint 112건이 재발해 CI가 막혀 있었다.
model/ 은 노트북 성격이라 스타일 게이트 대상에서 제외하고,
src/index/build_index2.py 의 E402(중간 import) 1건만 실제로 고쳤다.
@beomjinkim2000
beomjinkim2000 merged commit ed5aed2 into main Aug 2, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant