Files
xsl aa1287d83d feat(retrieval): hybrid search over pgvector with hard filters and gap tracking
DeterministicEmbedder(可换 BGE-M3 的接口位)+ kb_chunk 双索引写入
+ 硬过滤→向量→关键词→RRF 重排管线 + kb_gap 缺口埋点(T-P2-03/04/06,TD-08)。

实现:
- embedding.py:EmbeddingProvider Protocol + DeterministicEmbedder
  (sha256 种子→归一化 1024 维假向量,同文本同向量/异文本近似正交)
  + build_embedding_provider 工厂(未知模型名抛 CONFIG_ERROR)。
- repo.py:upsert_chunks(整实体先删后插,逐行带 embedding_model/version,
  可重跑判据 T-P2-03);hybrid_search(§8.3 CTE:硬过滤 published/
  entity_types/occasion/avoid_body_shapes → <=> 余弦 topN → ts_rank topN
  → RRF);record_gap(咨询锁串行化同 query+filter_ctx 的 hit_count 递增)。
- rerank.py:RRF/权重/新鲜度纯数学(独立于 SQL 以便单测逐项断言)。
- service.py:RetrievalService.search 编排 embed→检索→(空结果)gap 埋点,
  SearchOutcome.degraded 标记降级。

测试:单测 +30(test_embedding 确定性/维度/归一化/工厂拒绝、test_rerank
公式逐项断言、test_retrieval_service 编排打桩);集成 +5
(test_hybrid_retrieval:禁忌实体同文最近向量仍零出现、draft 零出现、
关键词路径、换模型整组替换、kb_gap hit_count=2、partial 索引 EXPLAIN 验证)。
全绿:ruff / mypy(retrieval+shared+domain) / lint-imports / 268 单测 /
21 集成。

偏差说明:
1) 新增 rerank.py(任务文档只点名 embedding/repo/service,但重排数学必须
   不连库可单测,独立模块是最低成本实现,仍属 packages/retrieval/**)。
2) upsert_chunks 的 upsert 粒度是实体(先删后插):kb_chunk 无业务自然键,
   行级 ON CONFLICT 不可行;换模型名=新版本整组替换,不留旧版本残向量。
3) 禁忌过滤用 COALESCE(attrs->avoid_body_shapes, '[]')兜底:jsonb NULL
   的 ? 运算结果是 NULL,直接 NOT 会把未声明禁忌的好实体一并排除。
4) 关键词路过滤用 tsv @@ tsquery 而非 ts_rank>0:ts_rank 对不匹配文本返回
   ~1e-20 正 epsilon,阈值过滤会把无关 chunk 全放进关键词路。
5) RRF 分量按 1/(k+1) 归一到 (0,1],使 0.45/0.2/0.25/0.1 权重量纲可比
   (原始倒数 0.45×1/61 与规则项 0.25×1 完全不可比)。
2026-08-14 20:12:27 +08:00

35 lines
1.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# `packages/retrieval/` — 向量与混合检索
架构 §8.3。进程内包,CPU + PostgreSQL,无 HTTP 路由(被 agent / report 的
service 调用)。
## 模块
- `embedding.py`:`EmbeddingProvider` Protocol + `DeterministicEmbedder`
(测试/dev 的确定性假实现,sha256 种子 → 归一化 1024 维向量)+ 工厂。
真实 BGE-M3 ONNX 加载是后续任务(`kb_embedding_device` 已预留)。
- `repo.py`:`kb_chunk` / `kb_gap` 数据访问。一条 CTE 完成硬过滤 +
向量召回(`<=>` 余弦)+ 关键词召回(`ts_rank`)。
- `rerank.py`:RRF 融合重排的纯数学(单测逐项断言,不连库)。
- `service.py`:编排 embed → 检索 →(无结果)gap 埋点。
管线:
```
硬过滤(SQL WHERE) → 向量召回(pgvector) → 关键词召回(tsvector)
→ RRF 融合 + 权重重排 → 输出 ScoredChunk;空结果落 kb_gap
```
重排权重(可配置,需用黄金集调参):
`最终分 = 0.45×向量 + 0.2×关键词 + 0.25×规则权重 + 0.1×新鲜度`
## 两条不能妥协的要求
- **硬过滤零漏**(FR-KB-06 / T-P2-04):draft/禁忌实体必须在 SQL 的
WHERE 里排除,不能靠权重压低 —— 这是过滤问题,不是排序问题。
集成测试 `tests/integration/test_hybrid_retrieval.py` 断言"禁忌实体
即使向量最近也绝不出现"。
- **规则命中要有明显加权**:专业规则的可信度高于语义相似度(`rule_boost`
接口已留,规则引擎联动属后续任务)。
- p95 < 200ms(NFR-PERF-06)。