# 检索一致性指标（v1）

这套指标回答的是一个问题：**当前引擎检索到的页，和 python 基线检索到的页，有多一致。**

它不回答「检索得准不准」。`search.py` 的排序不是标准答案，只是这套库上人类已经用了一阵的参照系。
真正的质量评估需要人工标注相关性（每个查询标出哪些页确实该被召回），那是另一件事，不在本文范围内。

因此本文所有指标都叫**一致性指标**，不叫质量指标。名字选错，后面所有人都会拿它当正确答案。

---

## 三个指标

记查询集合为 $Q$，基线对查询 $q$ 的 Top-5 为 $B_q$，当前引擎的 Top-5 为 $M_q$。

### 1. Recall@5（micro）

$$
\mathrm{Recall@5}=\frac{\sum_{q\in Q}|B_q\cap M_q|}{\sum_{q\in Q}|B_q|}
$$

- 分母是**基线槽位总数**，不是查询数。
- 基线里点名、但当前语料里已经不存在的页，**仍然计入分母，仍然算 miss**，同时单独累计
  `missingFromLibrary`。不从分母里剔除 —— 剔除会让 Recall 因为库内容变化而虚高。
- 基线槽位不足 5 条的查询，分母按实际条数算，不补空位。

### 2. Top-1 一致率

$$
\mathrm{Top1}=\frac{|\{q:|B_q|\ge 1 \land B_q[0]=M_q[0]\}|}{|\{q:|B_q|\ge 1\}|}
$$

- 页名整串精确相等，不做大小写、别名、去后缀归一。
- 只比首位，不接受「首位在对方的 Top-5 里」这种放宽。

### 3. Top-5 集合一致率（Jaccard，macro）

$$
J(B_q,M_q)=
\begin{cases}
1 & B_q\cup M_q=\varnothing\\
\dfrac{|B_q\cap M_q|}{|B_q\cup M_q|} & \text{otherwise}
\end{cases}
$$

$$
\mathrm{Top5Jaccard}=\frac{1}{|Q|}\sum_{q\in Q}J(B_q,M_q)
$$

- 空并集定义为 1：两边都没结果，视为一致。不定义会得到 `NaN`，静默污染均值。
- 用 Jaccard 而不是除以 5：基线槽位 3～5 不等，除以 5 会让「完美召回但基线只有 3 条」的查询也只拿 60%。
- 用 macro（每查询等权），因为查询是实验单元。Recall@5 用 micro，是总量问题；
  两者聚合方式不同是**故意的**，不是疏漏。

---

## 前置条件（缺一条就不出数）

1. **基线指纹**：`tests/local/ab_baseline.meta.json` 里的 `library.fingerprint`。
   计算方式与导出时一致：对 `name|body.length` 排序后取 sha256 前 16 位。
2. **引擎版本**：报告里打印当次 `lib/engine.js` 的 git commit。
3. **基线版本**：v3，14 查询 / 68 槽位。不与其他历史快照混合统计。
   v3 于 2026-11-04 重导（库 90 → 91 页，两条期望页名随库内容变化），v2 快照留在
   `tests/local/history/` 作对照：拿 v2 跑今天这个库只能到 66/68，差的 2 项是库漂移，不是检索退化。

## 判定顺序（严格按序，先中先停）

| 顺序 | 条件 | 结论 | 含义 |
| --- | --- | --- | --- |
| 1 | 没有 `WIKI_ROOT` | 跳过 | 本地回归，不是 CI 用例 |
| 2 | 元数据缺失 / 无指纹 | `BASELINE_STALE` | 无法证明基线对应哪份库 |
| 3 | 指纹不一致 | `BASELINE_STALE` | **库变了**，先重新导出基线；不判算法失败 |
| 4 | 指纹一致，Recall@5 < 100% | `GATE_FAIL` | **算法退步**，必须逐条解释漏项 |
| 5 | 其余 | `PASS` | |

指纹不一致时拒绝出数，是为了防止把「知识库自然演化」误判成「引擎回归」。
反过来，指纹一致还漏槽，就没有库变化这个借口了。

## 门禁

- **Recall@5 = 100% 硬门禁。** 它是回归红线。
- **Top-1 / Top-5 不设门禁，只记 delta。** 排序变化可能是改进；用阈值卡住，等于把
  「和 python 一样」当成正确性。

## 报告字段

| 字段 | 用途 |
| --- | --- |
| `missingFromLibrary` | 基线页名在当前语料中不存在的数量 |
| `recallMisses` | 未召回的基线槽位数 |
| `top1Matches` | 首位完全一致的查询数 |
| `top5Jaccard` | 各查询 Jaccard 的均值（macro） |
| `gate` | `PASS` / `GATE_FAIL` / `BASELINE_STALE` |

## 退出码

| 码 | 含义 |
| --- | --- |
| 0 | PASS，或没有 `WIKI_ROOT` 而跳过 |
| 2 | `GATE_FAIL` |
| 3 | `BASELINE_STALE` |

## 本文不测什么

- 不测检索质量。没有相关性标注，就没有质量指标 —— 只有一致性。
- 不用这些数字调 `termScore` / `compareHits` 的权重。基线是测量工具，不是优化目标。
- 不覆盖 `page` / `list` / `overview` / `lint` / `draft`。它们没有「Top-5 召回」这个概念。
