# 混合检索与排序验证

LLMS 索引： [llms.txt](/llms.txt)

---

混合检索的价值来自失败互补，不来自方法数量。每加一路候选，都同时增加
召回机会、查询成本和排序污染。

本节先保留每路独立排名，再用只依赖名次的 RRF 合并；最后用同一标注集找出
收益与退化。

## 15.5.1 词法、模糊和语义候选合并 {#item-15-5-1}

### 每一路先独立成立

本章先建三个排名视图：

```text
lexical_ranking
fuzzy_ranking
vector_exact_ranking
```

共同输出：

```text
query_id
product_id
result_rank
score
```

向量视图还保留 distance。每路都使用相同资格条件：

```sql
p.active
AND (
  q.category_filter IS NULL
  OR p.category = q.category_filter
)
```

如果三路过滤不同，融合后的差异既可能来自检索能力，也可能来自候选资格，
指标无法解释。授权、租户和生命周期过滤应在每路都保持同一语义。

### source rank 是融合接口

本章把每路前四名规范成：

```sql
SELECT
  query_id,
  product_id,
  'lexical' AS source_name,
  result_rank,
  1.0 AS source_weight
FROM shop_ch15.lexical_ranking
WHERE result_rank <= 4

UNION ALL
...
```

为什么用 `UNION ALL`：

- 同一商品被多路召回是有价值的支持证据；
- 普通 `UNION` 会按整行去重，但 source/rank 不同，本来也不会合并；
- 后续应显式按 `(query_id, product_id)` 聚合，而不是让 set operator
  隐式决定。

每路只提供 candidate，不应在视图内部偷偷调用另一路。这样才能：

- 单独关闭某一路；
- 比较增量质量；
- 观察每路延迟；
- 定位失败；
- 给最终结果解释 source。

### 候选深度是资源预算

本章：

```text
source_depth = 4
final_depth  = 3
```

它只适合 17 行夹具。生产候选深度影响：

```text
recall ceiling
union cardinality
dedup/sort work_mem
re-ranker cost
network payload
tail latency
```

应按 query segment 做曲线：

```text
depth = 10,20,50,100
  -> Recall@final_K
  -> NDCG@final_K
  -> P95/P99
  -> candidate count after dedupe
```

当一条 source 加深只增加重复/低质量对象而不提高最终 recall，就没有继续
扩大的理由。

### 空结果与低分结果不同

全文视图只输出 `@@` 命中对象，所以 q02/q07 没有行。

模糊视图对全部合格对象排名，即使低分也有行。这是评估 fixture 的有意设计，
生产则应加候选门槛。否则：

```text
no lexical evidence
+ zero fuzzy evidence
+ weak vector evidence
```

仍可能被融合成一个看似精确的总排名。

候选接口应区分：

```text
source unavailable
source returned no qualified candidate
source timed out
source returned candidates
```

超时不能被当作“该方法认为没有相关结果”，否则线上质量诊断会混淆能力与
故障。

### 去重后的 evidence 仍要保留

一个商品多路出现时，最终记录应能表达：

```text
product 1:
  lexical rank 1
  fuzzy rank 1
  vector rank 3
```

本章 `hybrid_rrf_ranking.sources` 用 text array 保存 source 名称，完整排名
仍可回查原视图。生产可把细节写进响应 trace、审计表或离线日志，不必塞进
业务表永久保存。

### SQL 内混合的边界

在 PostgreSQL 内完成候选和融合有明显优势：

- 与事务快照、状态和权限过滤同处一处；
- 少一次跨系统网络与数据同步；
- SQL 可以直接被 `EXPLAIN`、统计和审计；
- 小中规模问题结构简单。

但如果模型推理、复杂学习排序、跨多源索引或独立扩缩容占主导，融合可能更
适合应用/检索服务。决策标准不是“SQL 能不能写”，而是：

```text
failure domain
data freshness
latency budget
operational ownership
quality evidence
exit cost
```

## 15.5.2 分数归一、倒数排名融合与重排 {#item-15-5-2}

### 为什么不能直接加原始分数

本章三路原始值：

```text
ts_rank_cd(..., 32)  -> bounded cosmetic score
trigram similarity  -> character overlap in 0..1
vector L2 distance  -> non-negative distance, lower is better
```

即使都变换到 0–1，也没有共同概率含义。下面只是拍脑袋：

```sql
0.4 * lexical_score
+ 0.3 * fuzzy_score
+ 0.3 * (1 / (1 + vector_distance))
```

它会隐含：

- 三种尺度的形状可比；
- 一单位变化意义相同；
- 权重对所有 query segment 相同；
- 极值与分布稳定；
- 模型升级后仍适用。

若要线性加分，应使用标注数据做 calibration/learning-to-rank，并在 fresh
holdout 上验证。

### RRF 只比较名次

Reciprocal Rank Fusion 对 document \(d\)：

\[
RRF(d)=\sum_{s \in sources}
\frac{w_s}{k+rank_s(d)}
\]

其中：

- \(rank_s(d)\) 是 document 在 source \(s\) 中的名次；
- 未出现在 source candidate depth 内就没有该项；
- \(w_s\) 是可选 source 权重；
- \(k\) 平滑头部名次差。

本章：

```text
k = 60
w_lexical = w_fuzzy = w_vector = 1
source_depth = 4
```

SQL：

```sql
SELECT
  query_id,
  product_id,
  sum(
    source_weight / (60.0 + result_rank)
  )::double precision AS score,
  array_agg(source_name ORDER BY source_name) AS sources
FROM source_rank
GROUP BY query_id, product_id;
```

再：

```sql
row_number() OVER (
  PARTITION BY query_id
  ORDER BY score DESC, product_id
)
```

pgvector 官方 Hybrid Search 小节也把 RRF 或 cross-encoder 列为融合路径；
参见
[pgvector 0.8.4 Hybrid Search](https://github.com/pgvector/pgvector/blob/v0.8.4/README.md#hybrid-search)。

### `k` 不等于返回 K

RRF 中的小写 \(k\) 是平滑常数，不是最终结果数。增大它会让头部第 1 与
第 4 的单路差距变小，多路共同支持相对更重要；减小它会放大头部名次差。

不要因为经典示例常用 60 就把它当数学常数。本章用 60 是显式 proposal
参数。调参时要同时版本化：

```text
rrf_k
source weights
source candidate depths
tie breaker
final K
```

并报告每个 query segment 的变化。

### RRF 的优点与盲区

优点：

- 不要求原始 score 同尺度；
- 一个 document 被多路高排会自然加分；
- 实现与解释简单；
- source 新旧版本可独立。

盲区：

- 丢失 source 内分数差距：第 1 比第 2 好很多或只好一点都看不出来；
- candidate depth 外信息完全消失；
- 弱 source 也会投票；
- 不直接使用业务新鲜度、库存、质量、安全等 feature；
- 不会自动学会 query-dependent weight。

所以 RRF 是可靠 baseline，不是排序终点。

### source weight 要谨慎

加权 RRF：

```sql
source_weight / (k + rank)
```

很容易实现，但“vector 权重 1.5”必须由指标支持。若某一路只在错拼 query
有帮助，更合理的方向可能是：

- query segment 路由；
- 以置信门槛启用；
- 学习排序；
- 或扩大/收窄该路 candidate depth。

全局权重会把局部收益和局部伤害平均掉。

### 两阶段重排

当候选生成需要快、最终质量需要更多 feature：

```text
stage 1:
  FTS/trigram/ANN -> union top 100

stage 2:
  exact vector
  + structured business features
  + cross-encoder or learned ranker
  -> top 10
```

关键合同：

- 第一阶段 recall 必须足够；被漏掉的对象无法被重排救回；
- 第二阶段有独立超时与 fallback；
- 重排模型有版本、输入与许可证；
- fallback 次序仍经过评估；
- 在线日志保留 candidate pool 与最终选择。

外部 cross-encoder 可能最贵、最慢、最敏感，不要让数据库事务长时间等待
网络推理。常见做法是在数据库取候选后结束事务，再用版本化快照数据重排；
若新鲜度要求更高，需要明确一致性策略。

### 精确重排近似候选

ANN 常用模式：

```sql
WITH candidate AS MATERIALIZED (
  SELECT product_id, embedding
  FROM product
  ORDER BY embedding <-> :q
  LIMIT 100
)
SELECT product_id
FROM candidate
ORDER BY embedding <-> :q, product_id
LIMIT 10;
```

第二层用原向量精确重排 candidate，可以修正量化/子向量/relaxed ordering
内部次序，却不能召回第一层没选中的对象。最终仍要用 exact-relative recall
评估 candidate generator。

## 15.5.3 用标注集比较质量，不用单个“好看案例” {#item-15-5-3}

### 结果表只是起点

运行：

```bash
PG36_EVIDENCE_DIR="$PWD/evidence/ch15" \
  ./static/labs/ch15/task.sh evaluate
```

得到 `quality-summary.csv`：

```text
strategy,queries,precision@3,recall@3,mrr@3,mean_ndcg@3,min_ndcg@3
fuzzy,8,0.916667,0.916667,1.000000,0.942881,0.842828
hybrid_rrf,8,1.000000,1.000000,1.000000,0.962929,0.759192
lexical,8,0.291667,0.291667,0.750000,0.613043,0.000000
vector_exact,8,1.000000,1.000000,1.000000,0.817314,0.631039
```

平均值显示 hybrid 在本集合上 mean NDCG 最高，但不能到此结束。

### 看每个 query 的退化

`quality-detail.csv` 暴露：

```text
q02:
  fuzzy NDCG@3      = 0.842828
  hybrid NDCG@3     = 0.759192

q02/q07:
  lexical result_count = 0
  lexical NDCG@3       = 0
```

q02 的前三名：

```text
fuzzy: 1,3,2
hybrid: 3,2,1
```

商品 1 是 grade 3，纯 fuzzy 放第一；混合受到另外两路名次影响，把 grade 2
商品 3 放第一。因此：

```text
mean improved
does not imply every intent improved
```

生产 release gate 应同时有：

- aggregate threshold；
- 关键 query segment threshold；
- worst-case/低分位保护；
- 不允许退化的 canary query；
- filter/security hard assertions。

### 四种策略各告诉了什么

**全文**

```text
high precision when exact lexical evidence exists
zero results for two typo queries
```

它不是“差”，而是 candidate role 较窄。单独 NDCG 低不等于应删除；它可能
提供最可解释、最精确的头部证据。

**模糊**

```text
excellent typo recovery on this title corpus
misses one relevant result in q04 and q06
```

它证明字符相似的价值，也暴露业务邻近但字面不近的盲区。

**精确向量**

```text
all three relevant objects recalled for every query
mean NDCG lower because grade ordering is imperfect
```

这是“召回好、排序不够好”的典型 candidate generator。

**RRF**

```text
full recall and best mean NDCG
one typo query worse than fuzzy
```

它是当前 proposal baseline，而不是普遍胜者。

### 8 个查询没有统计外推力

本章数字可以证明：

- SQL 指标计算可复现；
- fixture 改动会被发现；
- 策略之间确有可解释差异；
- 反例和 filter guard 可持续回归。

不能证明：

- 真实流量的总体质量；
- 各语言/用户/类别公平；
- 指标提升具有统计显著性；
- 点击、转化或任务完成提高；
- 线上 P95/P99；
- embedding 模型优劣。

真实评估集应从代表性查询日志和业务任务构建，保留 fresh holdout。对线上
A/B：

```text
pre-register primary metric and guardrails
randomize at a safe unit
control novelty and carry-over
track no-result and latency
audit exposure/authorization
set stop conditions
```

离线相关性是上线前门槛，不是用户价值的最终替代。

### 指标 SQL 也要审校

本章 `quality_per_query` 明确建立所有策略 × 所有查询的 grid，再 LEFT JOIN
返回结果。这样没有返回行的 query 仍有 0 分；若从 ranking 表直接 GROUP BY，
空 query 会消失，平均值被悄悄抬高。

它还：

- 固定前三；
- 分母使用 3，不使用实际 result count；
- 对未标注 result 记 grade 0；
- 用 grade 计算 DCG 与 ideal DCG；
- 稳定按 grade/product id 生成理想排序。

审查 retrieval 指标实现时，重点找：

```text
missing queries silently dropped
precision denominator changed
unjudged treated inconsistently
ties unstable
ideal ranking calculated from retrieved set
approximate results used as truth
```

这些 bug 往往比检索算法差异更大。

### 发布结论的正确句式

可以写：

> 在 `ch15-search-v1` 的 8 个冻结查询、24 条分级标注和精确向量 golden
> 下，等权 RRF 的 mean NDCG@3 为 0.962929，并覆盖全部标注相关对象；
> q02 相对纯 fuzzy 退化，必须保留分段监控。结果不外推到真实语料。

不要写：

> 混合检索准确率 96%，优于其他方案。

后一句把 NDCG 当 accuracy，把小合成集当总体，也隐去了反例。

---

[上一节：可复现的向量检索](../04/) · [返回本章目录](../) · [下一节：扩展部署与运行代价](../06/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
