15.7 实战:`pg36_shop` 商品混合检索 PoC
本节把前六节压成一个可审计的 1.3-proposal:
正式证据来自 Homebrew PostgreSQL 18.6 的受控开发数据库。Pigsty 4.5 的 声明与运维职责已经映射,但本地没有运行 L1,因此不把直接 PostgreSQL 结果 伪装成 Pigsty 集群验收。
破坏边界
task.sh all会删除并重建带精确 marker 的shop_ch15。它不删除pg_trgm、vector或第 14 章 schema,只适合本书本地/开发夹具。 生产不得执行这条“删后重建”路径。
15.7.1 用冻结向量离线复现实验
前置状态
本章沿用前章 libpq service:
不要把密码放进命令行、脚本或 evidence。
context.sql 要求:
任何一项不符就停止。本章不会“顺便安装一个更接近的版本”,因为那会同时 改变扩展与检索两个变量。
输入清单
ch15 关键实验输入
fixture-manifest.json 固定:
这些 hash 识别的是仓库输入。proposal checksum 识别的是版本、方法、质量和 验收合同,二者不要混淆。
单步建立
setup 先检查已有 shop_ch15:
-
schema owner 必须是
pg36_owner; -
schema comment 必须是:
-
relation/index/view 必须在 21 个对象白名单中;
-
每个对象必须有同一 marker;
-
schema 不能有未知 routine/operator/opclass。
碰撞保护通过后,它按依赖顺序删除旧视图/表/schema,不用 CASCADE,再以
pg36_owner 创建。
核心表:
索引:
排名/质量视图:
完成摘要:
逐字节回读
它用 COPY ... TO STDOUT CSV HEADER 从数据库导出 corpus/query/judgment,
再执行:
这验证:
- SQL loader 没有手工录错;
vector::text可稳定导出;- 布尔、字符串、顺序和 rationale 都一致;
- 重建后数据身份没有漂移。
如果 CSV 行顺序没有显式 ORDER BY,逐字节比较没有意义。本章三条 export
分别按 product id、query id、query/product id 排序。
为什么不调用真实模型
若测试运行时调用外部 embedding API:
- provider 可能改变输出;
- 网络/限流会让数据库实验不稳定;
- 凭据和费用进入教学流程;
- 无法区分模型漂移与 SQL 漂移;
- 离线读者无法复现。
所以本章把真实模型评估留作迁移任务。读者要替换为真实向量,应复制
ch15-search-v1 为新 fixture/version,保留旧 golden,不要覆盖四维输入后
继续沿用本章 checksum。
15.7.2 比较全文、模糊、向量与混合结果
查看词法解析
固定结果:
| query | parsed tsquery |
matches |
|---|---|---|
| q01 | 'wireless' & 'headphon' |
1 |
| q02 | 'wirel' & 'hedphon' |
0 |
| q03 | 'music' & 'go' |
1 |
| q04 | 'coffe' & 'bean' & 'grinder' |
1 |
| q05 | 'make' & 'espresso' & 'home' |
1 |
| q06 | 'trail' & 'hydrat' |
2 |
| q07 | 'postgr' & 'databs' & 'tune' |
0 |
| q08 | 'semant' & 'nearest' & 'neighbor' |
1 |
q02/q07 的 lexeme 并不会因为“看起来像错拼”自动改正。这个证据把 FTS 零召回定位在语言处理层,不是 GIN index 故障。
前三名全景
固定 product id:
| query | lexical | fuzzy | exact vector | hybrid RRF |
|---|---|---|---|---|
| q01 | 1 | 1,3,2 | 2,3,1 | 1,2,3 |
| q02 | — | 3,1,2 | 2,3,1 | 3,2,1 |
| q03 | 2 | 1,2,3 | 2,3,1 | 2,1,3 |
| q04 | 4 | 4,6,14 | 5,6,4 | 4,6,5 |
| q05 | 5 | 5,6,4 | 5,6,4 | 5,6,4 |
| q06 | 7,8 | 7,8,15 | 8,9,7 | 7,8,9 |
| q07 | — | 10,11,12 | 11,10,12 | 10,11,12 |
| q08 | 12 | 12,10,11 | 12,11,10 | 12,10,11 |
— 是无行,不是三条零分结果。
注意 q04:
这是互补成功例。
注意 q02:
这是融合退化例。两种都必须写进报告。
质量结果
得到:
| strategy | queries | P@3 | R@3 | MRR@3 | mean NDCG@3 | min NDCG@3 |
|---|---|---|---|---|---|---|
| fuzzy | 8 | .916667 | .916667 | 1 | .942881 | .842828 |
| hybrid_rrf | 8 | 1 | 1 | 1 | .962929 | .759192 |
| lexical | 8 | .291667 | .291667 | .75 | .613043 | 0 |
| vector_exact | 8 | 1 | 1 | 1 | .817314 | .631039 |
质量视图把未返回 query 保留为 0,避免 survivorship bias。
计划证据
断言:
这些文件通过禁用 planner 备选路径建立“能力证明”。正常 17 行查询走顺序 扫描很合理;不要把强制 index plan 贴成性能结果。
目录证据:
最终四个 index 都必须 valid/ready/live,opclass 与查询 operator 一致。
ANN 对照
结果:
这个 probe:
- 固定 q06 与 outdoor/active filter;
- exact 结果来自全量 window ranking;
- ANN 强制 HNSW;
- 用集合交集测 recall。
它没有报告毫秒,因为 tiny fixture 的时间不稳定且无业务意义。
应用权限
应用可读 q02/q08 混合结果和质量摘要。
未授权更新:
必须:
自动化不是只检查非零 exit;它同时检查精确 SQLSTATE,避免连接失败或语法 错误被误当成权限测试通过。
15.7.3 输出 ADR、质量证据、生产代价与退出路径
ADR 结论
search-adr.md 决定:
它还明确否决:
ILIKE替代完整检索;- 只用 FTS;
- 只用向量;
- 未校准原始分数直接相加;
- 用 HNSW 结果生成质量 golden。
proposal 是版本化合同
baseline-v1.3-proposal.json
固定:
- PostgreSQL/extension/Pigsty 参考版本;
- fixture/model/距离;
- index 与权限;
- 四种质量指标;
- ANN probe;
- business checksum;
- evidence 文件清单;
- reset 边界;
- 明确 limitation。
canonical JSON checksum:
更改 key 顺序或空白不会改变 canonical checksum;更改合同值会改变。
最终数据库状态:
business checksum 不包含运行时间、OID、index bytes 或绝对路径。
evidence 目录
一轮完整采集包含:
manifest.txt 记录 server、database、in-recovery、扩展版本、模型、Pigsty
证据边界、proposal checksum、fixture manifest checksum 与全部实验源文件
SHA-256。
review.py 不信任脚本“跑完了”,它重新解析证据并断言:
- 三份 export 与 source byte-identical;
- manifest hash 与真实文件一致;
- 17/8/24 行数;
- 唯一 inactive 商品为 17;
- 四种索引 AM/opclass/状态/marker;
- 应用 ACL;
- FTS match counts;
- 32 个策略×查询质量格;
- 79 条实际 top-3 ranking 记录;
- hybrid 每个 query 的 id 次序;
- ANN exact/approx intersection;
- 五份计划的关键 node;
42501权限失败;- final checksum 与 verify summary。
生产代价清单
当前 proposal 未给出性能线,因为本地 17 行不能回答:
迁移到业务数据后,ADR 需要附:
| 领域 | 最小证据 |
|---|---|
| 质量 | frozen + fresh queries,分段 P/R/MRR/NDCG |
| ANN | exact-relative recall curve |
| 查询 | P50/P95/P99、timeout、buffers、CPU |
| 写入 | rows/s、WAL/row、索引增长、vacuum |
| HA | replica query、lag、switchover/failback |
| 恢复 | clean environment restore |
| 模型 | version/input/license/cost/failure |
| 安全 | tenant/ACL canaries、secret/data boundary |
精确退出
手工 reset 需要两个一致的显式确认:
还会检查:
- context/database/writable instance;
- schema owner 与 marker;
- 21 个 relation 的精确白名单与 marker;
- 未知 routine/operator/opclass;
application_name LIKE 'pg36-ch15-%'的其他活跃 worker。
错误分别用自定义 SQLSTATE:
删除顺序:
没有 CASCADE。最终必须:
生产退出不是 DROP schema。生产要先:
15.7.4 验收采用 checklist:evidence,不设脱离场景的性能线
为什么不给“必须 10 ms”
延迟取决于:
在 17 行、热缓存、本地 socket 上测到的微秒/毫秒,既不能预测一亿行,也 不能作为读者机器失败线。硬写一个数字只会诱导为过测试而牺牲质量或关闭 安全过滤。
因此本章把 15.7.4 的规则定义为:
即每一类能力都必须有可复核证据;业务上线再给每项填入自己的 SLO。
本地机制验收
| 检查 | 证据 | 固定结论 |
|---|---|---|
| 输入身份 | manifest + byte cmp | 17/8/24,三份一致 |
| FTS 行为 | parsed query/match table | q02/q07 零命中 |
| fuzzy 行为 | ranks/quality | mean NDCG .942881 |
| vector quality | exact ranking | Recall@3 1 |
| fusion | RRF ranks/quality | mean NDCG .962929,有 q02 退化 |
| ANN 机制 | exact vs HNSW | q06 Recall@3 1,仅单点 |
| index 机制 | catalog + forced plans | GIN/GIN/HNSW 路径存在 |
| filtering | canary 17 | 所有排名均不出现 |
| ACL | catalog + expected failure | read-only,UPDATE 42501 |
| reset | token/target/active tests | P3660/P3661/P3663 |
| rebuild | second full cycle | checksum 相同 |
| Pigsty | manifest | L1 not run |
这一表中没有任何一项可被“SQL 返回了三行”替代。
业务场景性能卡
移植时创建一份场景卡:
数字必须来自业务 owner/SLO 和代表性测试,而不是本书替读者决定。
L1 验收增量
在 Pigsty L1 上补:
若某项未运行,报告应写 not-run,而不是 pass。
双周期正式运行
只把新建的专用 evidence 路径传给实验;不要把仓库根或广泛目录当目标。
all 的内部顺序:
正式结果:
第二轮完成后数据库保留可查询的 shop_ch15 最终状态,便于继续第 16 章;
evidence 保留两个独立 cycle,证明 reset 后不是依赖第一次残留才通过。
最终评审句
本章可以得出的最强结论是:
1.3-proposal在固定 PostgreSQL 18.6、本章扩展版本与合成 fixture 上, 两轮可重复;全文、模糊、精确向量、RRF、HNSW 测量路径、权限与精确复位 均有证据。它可以进入真实语料与 Pigsty L1 的下一阶段试点,尚未获得生产 性能与真实模型质量批准。
这比一句“PostgreSQL 可以做混合搜索”更窄,也更有用。
上一节:扩展部署与运行代价 · 返回本章目录 · 下一章:经天纬地:时序、空间与时空查询 · 查看全书目录 · 查看索引中心