diff --git a/docs/features/retrieval/F05-coarse-ranker.md b/docs/features/retrieval/F05-coarse-ranker.md new file mode 100644 index 00000000..7a8c9891 --- /dev/null +++ b/docs/features/retrieval/F05-coarse-ranker.md @@ -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: #`) | + +> 本文档归档「粗排层」特性的设计决策:在检索层新增 **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 的模块地图与铁律更新,随实现提交一同落地(第三提交)。 +- **测试基线回填**:实现提交后更新本文元信息。