Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 177 additions & 0 deletions docs/features/retrieval/F05-coarse-ranker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# F05 — 粗排抽象层 CoarseRanker 与 FusionStore 粗排契约扩展(src/retrieval + src/storage + src/construction)

## 元信息

| 项 | 值 |
|---|---|
| 日期 | 2026-08-05 |
| 影响范围 | `src/retrieval/`(新增 `coarse_ranker.py`、`coarse_ranker_impl/`,`types.py` 新增 `CoarseCandidate`,`retriever_impl/pipeline_retriever.py` 瘦身)、`src/storage/`(`types.py` 新增 `RankRule`/`ScoredFusionRecord`,`fusion.py` 新增 `search_records`,`fusion_impl/` 各实现补齐)、`src/construction/`(新增 `fusion_index_builder`);规约 `docs/specs/S04-retrieval.md`、`docs/specs/S06-storage.md`;模块文档 `src/retrieval/AGENTS.md`、`src/storage/AGENTS.md`、`src/construction/AGENTS.md` |
| 测试基线 | 待回填(设计先行归档,实现落地后补 pytest 基线) |
| Refs | —(如有 issue 补 `Refs: #<n>`) |

> 本文档归档「粗排层」特性的设计决策:在检索层新增 **CoarseRanker** 抽象(多路召回 + 粗排 +
> 单元物化 + 有效性复核),并把 FusionStore 的检索契约扩展为可携带粗排规则、可回带
> `FusionRecord`,使华为云 CSS、lakebase 等融合存储后端能在存储侧一次完成召回与粗排。
> 接口契约归 `docs/specs/S04-retrieval.md` / `docs/specs/S06-storage.md`;本文只记录 why / why-not。

---

## 背景

### 1. 检索链路缺少「粗排」这一层抽象

现有链路(`PipelineRetriever`)把「并行多路召回 → Fuser 融合 → 截断预算 → UnitReader
点读 MemoryUnit → 三道复核」作为编排步骤散落在 Retriever 内部,面向的是**分离存储形态**
(fulltext + vector + kv 各自独立)。当存储后端本身是融合形态(CSS、lakebase、现有
FusionStore)时,「多路召回 + 融合排序」本可在存储侧一次完成,但检索层没有一个抽象能
对接它——Recaller 是单路粒度,Fuser 只吃不带单元信息的 `ScoredUnit`。

### 2. FusionStore 检索契约表达力不足

`FusionStore.search` 只返回 `list[ScoredID]`(id + score),`FusionQuery` 固定为
vector/text/scalar_filters/top_k/vector_weight 五个字段,粗排规则被写死为「向量分与
文本分按 vector_weight 线性混合」。CSS 的 function_score、lakebase 的 hybrid search
等后端原生粗排能力没有透传通道;命中行的正排内容(`FusionRecord`)也不会随结果回带,
调用方拿到 id 后必须再回 KV 点读 MemoryUnit,多一次往返。

### 3. 构建侧没有 FusionStore 的写入方

`src/construction/` 现有 vector/fulltext/hybrid 三种 IndexBuilder,均不写 FusionStore;
`FusionRecord.value`(正排原始值)没有写入方。fusion 检索路径即使落地,也只能回退
KV 点读 MemoryUnit,「存储侧一次拿全」的价值落空。

---

## 决策

### 决策 1:新增 `CoarseRanker` 算子,作为检索链路的粗排阶段

在 retrieval 层新增与 Recaller/Fuser/Discloser 平级的算子(`retrieval/coarse_ranker.py`
+ `coarse_ranker_impl/`):

```python
class CoarseRanker(RetrievalOperator):
def coarse_rank(self, scope: Scope, query: ParsedQuery, top_n: int) -> list[CoarseCandidate]: ...
```

```python
@dataclass
class CoarseCandidate:
unit_id: str
score: float # 粗排得分(越大越相关)
unit: MemoryUnit | None # 已物化、已复核的记忆单元
evidence: list[ChannelEvidence] # 通道证据明细
```

粗排阶段的职责边界:**多路召回 → 粗排融合 → top_n 截断 → 单元物化 → 有效性复核**,
产出的 `CoarseCandidate` 即「合格候选」,直接作为精排(Reranker)、阈值过滤与披露的输入。

### 决策 2:两个实现,分别对应两种存储形态

- **`LocalCoarseRanker`(默认,组合式)**:接管现 `PipelineRetriever` 的步骤 3–6——
并行多路召回(keyword/vector/graph/L0/L1,通道失败隔离降级)、超采样 recall_k、
`Fuser.fuse` 融合(含 layered_merge 分层归并)、budget 截断、`UnitReader` 点读、
`passes`/`in_event_window`/`matches_filters` 三道复核。现有 Recaller/Fuser 抽象与
实现**原样保留**,从「被 Retriever 直接编排」降级为「被 LocalCoarseRanker 组合」,
既有单测与分层召回逻辑零重写。
- **`FusionCoarseRanker`(存储侧粗排)**:组装 `FusionQuery`(vector/text 取自
ParsedQuery,scalar_filters 下推,`rank` 规则透传),单次调用
`FusionStore.search_records`;MemoryUnit 优先从 `ScoredFusionRecord.record.value`
反序列化,`value` 缺失或索引↔真源不一致时回退 `UnitReader` KV 点读;复核三件套
同样执行(纵深防御,不假设后端下推完整)。

### 决策 3:图召回并入粗排层

GraphRecaller 作为 LocalCoarseRanker 组合的一路 Recaller 保留;fusion 后端的图扩展
(如 milvus_graph 的「向量种子 → 图邻居」)在存储侧 search 内完成。粗排层对上是统一
语义,不再在 Retriever 里为图通道留独立并行分支。

### 决策 4:有效性复核收进粗排层

lifecycle/as_of、event-time 窗、调用方 filters 三道复核在粗排阶段尾部执行(此时单元
已物化,是天然的复核点)。`CoarseCandidate` 因此是「已复核合格」的候选;Retriever
只做 rerank → 阈值 → top_k → disclose,不再持有 recallers/fuser/unit_reader。

### 决策 5:FusionStore 契约扩展——新增 `search_records`,原 `search` 不动

```python
@dataclass
class RankRule:
strategy: str = "linear" # linear(vector_weight 混合)| rrf | backend(后端原生)
params: dict[str, Any] # rrf 的 k、通道权重等;backend 模式承载原生 DSL

@dataclass
class ScoredFusionRecord:
id: str
score: float
record: FusionRecord | None # 后端支持时回带正排行
```

- `FusionQuery` 扩展:`rank: RankRule | None`(None = 后端默认规则)、
`return_records: bool`、`extensions: dict[str, str]`(CSS/lakebase 等后端特定透传,
内核核心不解释——与检索层 extensions 同一惯例)。
- 新增 `search_records(scope, query) -> list[ScoredFusionRecord]`;`search` 保持返回
`list[ScoredID]` 不变。memory/milvus_graph 实现各自补齐 `search_records`;CSS/lakebase
后端把 `RankRule` 翻译成原生粗排(`strategy="backend"` + `params`/`extensions` 承载
翻译不了的自定义规则)。
- 「分越大越相关」语义不变:距离型度量的 fusion 后端在装配期拒绝(沿用
VectorRecaller 的既有做法)。

### 决策 6:构建侧新增 `FusionIndexBuilder`

把 MemoryUnit 写成 `FusionRecord`(vector/text/scalars + **`value` = 序列化单元**),
scope 隔离与冲突/缺失语义沿用 FusionStore 既有契约。这是 FusionCoarseRanker 免去
KV 往返的前置条件,纳入本特性同一闭环。

### 决策 7:装配形态

`coarse_ranker` 配置项(`local` 默认 / `fusion`)注入 Retriever;`local` 实现复用现有
recallers/fuser 装配参数;`rank_rule` 可配置到 retriever params 或经调用级 extensions
透传。scope 仍是显式首参,不进 filters(架构铁律不变)。

---

## 拒绝的方案

- **直接改 `search` 返回 `ScoredFusionRecord`**:接口更干净,但所有现有实现
(memory/milvus_graph)与调用方必须同步修改,向后不兼容。新增 `search_records`
让扩展纯增量,旧路径零破坏。
- **图召回留在 Retriever 独立编排**:链路多一层并行分支,粗排语义在「粗排层内」与
「Retriever 内」两处分裂;且 fusion 后端的图扩展天然在存储侧,两种形态无法统一。
- **复核留在 Retriever**:粗排返回未复核候选会把精排预算浪费在无效单元上(superseded
旧版本、落窗外的 t_event);且 fusion 路径的物化发生在粗排内,复核与物化拆开反而
要在 Retriever 重新引入单元字典的传递。
- **粗排层整体替换 Recaller/Fuser 抽象**:改动面大,分层召回(L0/L1)、MaxP 聚合、
通道隔离等已验证逻辑要全部重写迁移。组合式复用让 LocalCoarseRanker 只是既有组件的
再编排,风险最小。
- **`RankRule` 做成强类型枚举 + 固定参数字段**:后端原生粗排 DSL(CSS function_score、
lakebase hybrid 参数)表达力不一,强类型契约会不断被迫加字段。`strategy` 三档 +
开放 `params`/`extensions` 透传,内核不解释,与 `RetrievalQuery.extensions` 的既有
惯例一致。
- **不做 `FusionIndexBuilder`,fusion 路径永远 KV 回退**:检索侧契约先行的做法会让
`FusionCoarseRanker` 失去存在价值(与 local 路径相比只剩一次 search 调用,正排
能力空转),且后续补构建侧时检索侧行为还会再变一次。

---

## 验证

设计先行归档,实现尚未落地。落地后须回填:

- `uv run --frozen pytest tests/unit/retrieval tests/unit/storage tests/unit/construction -q`
- `ruff check`(行长 100,target py3.11)
- LocalCoarseRanker 行为基线:与现 PipelineRetriever 全链路结果一致(同一 query 集合
对比 items 与得分顺序)。

## 已知遗留

- **CSS / lakebase 后端实现**:本期只定义 `RankRule`/`extensions` 契约与翻译点,
具体后端适配后续单独立项。
- **分层召回(L0/L1)在 fusion 路径的映射**:local 路径靠分层分表 + layered_merge;
fusion 后端如何在单表/多表上承载 L0/L1 粗排未定义,落地 FusionCoarseRanker 时需
明确(可先只支持 L2,分层退化为 local 路径独占能力)。
- **milvus_graph 的 `search_records`**:图扩展结果如何回带 `FusionRecord`(邻居的
正排值)需实现时定夺。
- **specs 与 AGENTS.md 同步**:`S04-retrieval.md`/`S06-storage.md` 修订、三个模块
AGENTS.md 的模块地图与铁律更新,随实现提交一同落地(第三提交)。
- **测试基线回填**:实现提交后更新本文元信息。