# Document Server 검색 평가셋 (search_eval)

> Phase 1 산출물 — graded relevance baseline. peppy-hugging-nest.md Phase 1.
> Phase 2 모델 점검 (embedding / reranker / OCR-marker pipeline / STT) 의 객관 측정 도구.

## 개요

`tests/search_eval/queries.yaml` 은 Document Server 의 검색 품질을 정량 측정하기 위한 평가셋이다. 두 버전 존재:

- **v0.1** (2026-04-07) — binary relevance (relevant_ids set + top3_ids 보조). 23 case, 9 카테고리. corpus 753 documents 기준.
- **v0.2** (2026-05-23) — graded relevance (0~3 등급) + 사용자 권장 7 카테고리 + language / ocr_derived metadata. v0.1 호환 유지 (legacy_category + relevant_ids + top3_ids 컬럼 보존).

본 PR (PR-Eval-V0_2-Schema-Harness) = v0.1 23 case 의 v0.2 schema swap + run_eval graded harness 만. 신규 28 case (50+ 목표) + baseline 박제 + 약점 분석은 후속 PR (PR-Eval-V0_2-Baseline-Analysis).

## Graded relevance 등급 (4단계)

| Grade | 의미 | 기준 |
|---|---|---|
| **3** | highly relevant (top-3 강제) | direct match — 본법/본 chapter/본 article. 검색 결과 top-3 에 반드시 포함돼야 함. |
| **2** | relevant (정답 후보) | 보조 정답 — 시행령/규칙/관련 chapter. top-10 안 = OK. |
| **1** | marginal | 약한 관련 — 같은 법령군이지만 다른 chapter, 같은 주제지만 다른 문서 유형. top-10 안 = 가점, 밖 = 무감점. |
| **0** | irrelevant | 무관 — 검색 결과 잡히면 감점 후보 (현재는 표시만, 감점 미구현). |

failure_expected 케이스는 graded_relevance 비움 (`{}`). 검색 결과 0건 = correct.

### Graded score 계산식 (`run_eval.py`)

```python
# graded NDCG@k — gain = 2^grade - 1
dcg = sum((2 ** grade - 1) / log2(rank + 1) for rank, doc_id ...)
idcg = sum((2 ** g - 1) / log2(rank + 1) for rank, g in sorted(grades, desc)[:k] ...)
ndcg = dcg / idcg

# graded recall@k — grade >= threshold 만 정답
recall_t2 = |{returned[:k]} ∩ {grade>=2}| / |{grade>=2}|
recall_t3 = |{returned[:k]} ∩ {grade>=3}| / |{grade>=3}|
```

## 카테고리 (7개)

| Category | 의미 | 측정 목적 |
|---|---|---|
| **standards** | 법령/규칙/기준 (산업안전보건법, ASME, KGS, NIST 등) | exact_keyword + structured doc lookup |
| **korean_only** | 한국어 자연어 query → 한국어 doc | base semantic_search 품질 |
| **english_only** | 영어 자연어 query → 영어 doc | base 영어 자료 검색 |
| **mixed** | 한국어+영어 혼합 OR ko query → en doc OR vice versa | crosslingual 능력 (현재 0.53 미달 root cause 검증) |
| **exam** | 가스기사 study 도메인 (`study_questions` 기반) | 시험 문제 → 학습 자료 매칭 |
| **ocr_derived** | marker/OCR pipeline 통한 scanned PDF chunk 검색 | Phase 2C 입력 — OCR 추출 품질이 검색에 미치는 영향 |
| **failure_expected** | 결과 0건 expected | precision 측정 (no-result correctness) |

`legacy_category` 컬럼으로 v0.1 9 카테고리 호환 보존 (분석 시 옛 분포 재현 가능).

## v0.2 schema 컬럼

```yaml
- id: kw_001
  query: "산업안전보건법 제6장"
  category: standards               # v0.2 7 카테고리 중 하나
  legacy_category: exact_keyword    # v0.1 카테고리 보존
  intent: fact_lookup               # semantic_search / fact_lookup / filter_browse
  domain_hint: document             # document / news / mixed
  language: ko                      # ko / en / mixed
  ocr_derived: false                # marker/OCR chunk 검색 케이스 여부
  failure_expected: false           # 결과 0건 expected
  graded_relevance:                 # {doc_id: grade} (0~3)
    3856: 3
    3868: 2
    3879: 2
  relevant_ids: [3856, 3868, 3879]  # v0.1 호환 (--eval-version v0.1 용)
  top3_ids: [3856]                  # v0.1 호환
  notes: |
    Act(3856) = grade 3 (본법, top-3 강제).
    Decree(3868)/Rule(3879) = grade 2 (보조 정답, 본법 우선).
```

## 신규 case 작성 가이드

1. **id 명명**: 카테고리 prefix + 3-digit (예: `std_006`, `ko_006`, `en_001`, `mix_006`, `exam_001`, `ocr_001`, `fail_004`)
2. **graded_relevance 채점 사유 notes 필수** — 왜 그 등급 부여했는지 1~3줄 사람용 audit trail
3. **failure_expected = true** 인 case 는 `graded_relevance: {}` (빈 dict)
4. **language 결정**:
   - 순수 한국어 query + 한국어 doc → `ko`
   - 순수 영어 query + 영어 doc → `en`
   - 혼합 (한국어+영어 keyword OR ko query → en doc) → `mixed`
   - 다른 언어 (불어 등) 도 일단 `mixed` (PR-2 에서 enum 확장 검토)
5. **ocr_derived 결정**: 정답 doc 중 `documents.source_type='scanned'` 또는 marker 처리 chunk 가 1개 이상이면 `true`
6. **정답 doc_id 식별**: GPU DB 의 `documents` 테이블에서 `title ILIKE '%...%'` 또는 metadata grep 으로 식별. graded 등급은 사람 판단.

### Self-check (PR review)

- [ ] notes 에 채점 사유 명시?
- [ ] graded grade 분포 합리적 (모든 정답이 grade 3 = 의심)?
- [ ] failure_expected case 의 graded_relevance 가 `{}` 인가?
- [ ] legacy_category 보존 (v0.1 호환)?
- [ ] language / ocr_derived 일관성?

## v0.1 호환성

`--eval-version v0.1` 으로 옛 점수 (Recall@10 / MRR@10 / NDCG@10 / Top-3 hit-rate) 재현 가능. swap 후 옛 baseline 대비 ±0.001 이내여야 함.

`load_queries()` 의 v0.1 fallback:
- yaml 에 `graded_relevance` 없고 `relevant_ids` 만 있으면, `top3_ids` 는 grade 3 / 나머지 relevant_ids 는 grade 2 로 자동 매핑.
- 이 fallback 으로 v0.1 yaml 도 v0.2 mode 에서 graded score 산출 가능.

## CLI 사용 예

```bash
export DOCSRV_TOKEN="eyJ..."

# 단일 평가, default = 두 mode 다 출력
.venv/bin/python tests/search_eval/run_eval.py \
    --base-url https://docs.hyungi.net \
    --output reports/baseline_v0_2_2026-05-23.csv

# v0.2 graded only
.venv/bin/python tests/search_eval/run_eval.py \
    --base-url https://docs.hyungi.net \
    --eval-version v0.2 \
    --output reports/v0_2_only.csv

# v0.1 mode (옛 점수 회귀 확인)
.venv/bin/python tests/search_eval/run_eval.py \
    --base-url https://docs.hyungi.net \
    --eval-version v0.1

# A/B 비교 (baseline vs candidate)
.venv/bin/python tests/search_eval/run_eval.py \
    --baseline-url https://docs.hyungi.net \
    --candidate-url http://localhost:8000 \
    --output reports/phase2a_vs_baseline.csv
```

### Output (v0.2 mode 예)

```
=== single (n=23, scored=20, graded=20) ===
  -- v0.2 graded --
    NDCG@10 (graded)        : 0.812
    Recall@10 (grade>=2)    : 0.756
    Recall@10 (grade>=3)    : 0.900
  Latency p50: 240 ms
  Latency p95: 412 ms
  Failure-case precision: 3/3 (1.00) — empty result expected
  by category:
    failure_expected      n= 3  recall=0.00  ndcg=0.00  gndcg=0.00
    korean_only            n= 9  recall=0.73  ndcg=0.78  gndcg=0.81
    mixed                  n= 5  recall=0.53  ndcg=0.61  gndcg=0.65
    standards              n= 5  recall=0.90  ndcg=0.95  gndcg=0.95
    english_only           n= 1  recall=0.67  ndcg=0.75  gndcg=0.78
  by language:
    en         n= 1  recall=0.67  gndcg=0.78
    ko         n=15  recall=0.81  gndcg=0.85
    mixed      n= 4  recall=0.55  gndcg=0.63
```

(위는 예시 — 실제 baseline 박제는 후속 PR)

## Baseline 박제 정책

위치: `tests/search_eval/baselines/v{version}_{date}.{json,md}`

각 baseline = 2 파일:
- **`.json`** — 점수 raw (overall + by_category + by_language + by_ocr_derived)
- **`.md`** — 약점 카테고리 분석 보고서 (사람용 audit)

박제 시점:
- 평가셋 schema 변경 시 (v0.1 → v0.2 swap)
- 검색 시스템 주요 변경 commit 직후 (모델 swap / chunks 재생성 / reranker 교체)
- Phase 2A~D Diagnose PR 의 비교 baseline

박제 명명 컨벤션: `v0_2_baseline_YYYY-MM-DD[_label].{json,md}`. label 은 선택 (예: `_after_embedding_swap`, `_chunks_md_content`).

## Phase 1 closure 조건 ([[feedback_quant_expectation_not_hard_gate]])

본 PR (PR-Eval-V0_2-Schema-Harness):
- [x] v0.2 schema swap (23 case)
- [x] run_eval graded 함수 + --eval-version flag
- [x] README.md (본 파일)

후속 PR (PR-Eval-V0_2-Baseline-Analysis):
- [ ] 50+ graded case 확정 (신규 28 case 작성)
- [ ] baseline json + analysis md 박제
- [ ] 약점 카테고리 보고서 — embedding-sensitive failure pattern 4 카테고리 식별:
  - candidate recall@k (embedding 후보 selection)
  - crosslingual miss (ko↔en mismatch)
  - Korean-English mismatch (한자/영문 fallback)
  - OCR-derived chunk miss

closure gate: **0.70 미달 OK** — 원인 분해 보고서 (analysis md) 가 있으면 Phase 1 closure.

## 관련

- Phase 1 plan: `~/.claude/plans/phase-1-graded-eval-v0-2.md`
- Parent plan: `~/.claude/plans/peppy-hugging-nest.md` § Phase 1
- 건강도 평가 (graded relevance 1년 미작성 gap): `~/.claude/projects/-Users-hyungi/memory/project_document_server_health_assessment.md:24`
- 발주건 단위 baseline (별 트랙, Phase 0 / merry-yawning-owl): `queries_order_baseline.yaml` + `order_groups.yaml` — 본 PR 영향 없음
