跳至内容
15.7 实战:`pg36_shop` 商品混合检索 PoC

15.7 实战:`pg36_shop` 商品混合检索 PoC

本节把前六节压成一个可审计的 1.3-proposal

frozen inputs
  -> deterministic schema/index/ranking
  -> exact quality golden
  -> separate ANN probe
  -> application privilege failure
  -> reset guards
  -> exact reset
  -> rebuild and re-review

正式证据来自 Homebrew PostgreSQL 18.4 的受控开发数据库。Pigsty 4.4 的 声明与运维职责已经映射,但本地没有运行 L1,因此不把直接 PostgreSQL 结果 伪装成 Pigsty 集群验收。

破坏边界

task.sh all 会删除并重建带精确 marker 的 shop_ch15。它不删除 pg_trgmvector 或第 14 章 schema,只适合本书本地/开发夹具。 生产不得执行这条“删后重建”路径。

15.7.1 用冻结向量离线复现实验

前置状态

本章沿用前章 libpq service:

[pg36-admin]
host=/path/to/socket-or-host
port=5432
dbname=pg36_shop
user=postgres
chmod 600 /path/to/pg_service.conf
export PGSERVICEFILE=/path/to/pg_service.conf
export PGSERVICE=pg36-admin

不要把密码放进命令行、脚本或 evidence。

context.sql 要求:

database = pg36_shop
writable instance
PostgreSQL major = 14..18
session user = superuser
can SET ROLE pg36_owner
ch04-v1 physical model exists
pg36_app = constrained non-superuser LOGIN
shop_ch14 marker is exact
pg_trgm = 1.6 in shop_ch14 with exact marker
vector = 0.8.4 in shop_ch14 with exact marker

任何一项不符就停止。本章不会“顺便安装一个更接近的版本”,因为那会同时 改变扩展与检索两个变量。

输入清单

static/labs/ch15/
├── frozen-corpus.csv
├── frozen-queries.csv
├── frozen-judgments.csv
├── fixture.sql
├── fixture-manifest.json
├── setup.sql
├── ranking-views.sql
├── verify.sql
└── ...

fixture-manifest.json 固定:

corpus:
  17 rows
  sha256=7136d6f1705e560c5d564407926b44c455cd59b3883cd640f4f51599806a90c8

queries:
  8 rows
  sha256=4b54b0ee322bf52649b5c682d468ea024ae301d8cac40d63aaaed908c9d8a45d

judgments:
  24 rows
  sha256=657ddc4d82f9b42af589fd64a6326407d5cfb6536b2f2c3c28862279eca41438

loader:
  sha256=2548978f3652f816452f22aed0d560a0d0263234a2f62a8fb77d4009c5545787

这些 hash 识别的是仓库输入。proposal checksum 识别的是版本、方法、质量和 验收合同,二者不要混淆。

单步建立

./static/labs/ch15/task.sh setup

setup 先检查已有 shop_ch15

  • schema owner 必须是 pg36_owner

  • schema comment 必须是:

    pg36 ch15 search quality lab; safe to rebuild
  • relation/index/view 必须在 21 个对象白名单中;

  • 每个对象必须有同一 marker;

  • schema 不能有未知 routine/operator/opclass。

碰撞保护通过后,它按依赖顺序删除旧视图/表/schema,不用 CASCADE,再以 pg36_owner 创建。

核心表:

CREATE TABLE shop_ch15.product_search (
  product_id bigint PRIMARY KEY,
  sku text NOT NULL UNIQUE,
  category text NOT NULL,
  active boolean NOT NULL,
  title text NOT NULL,
  description text NOT NULL,
  embedding shop_ch14.vector(4) NOT NULL,
  embedding_model text NOT NULL,
  search_document tsvector
    GENERATED ALWAYS AS (...) STORED
);

CREATE TABLE shop_ch15.eval_query (...);
CREATE TABLE shop_ch15.relevance_judgment (...);
CREATE TABLE shop_ch15.fixture_meta (...);

索引:

product_search_fts_idx              GIN pg_catalog.tsvector_ops
product_search_title_trgm_idx       GIN shop_ch14.gin_trgm_ops
product_search_embedding_hnsw_idx   HNSW shop_ch14.vector_l2_ops
product_search_filter_idx           partial B-tree WHERE active

排名/质量视图:

lexical_ranking
fuzzy_ranking
vector_exact_ranking
hybrid_rrf_ranking
all_ranking
quality_per_query
quality_summary

完成摘要:

status=fixture-ready
products=17
queries=8
judgments=24

逐字节回读

PG36_EVIDENCE_DIR="$PWD/evidence/ch15-cycle" \
  ./static/labs/ch15/task.sh evaluate

它用 COPY ... TO STDOUT CSV HEADER 从数据库导出 corpus/query/judgment, 再执行:

cmp frozen-corpus.csv evidence/corpus.csv
cmp frozen-queries.csv evidence/queries.csv
cmp frozen-judgments.csv evidence/judgments.csv

这验证:

  • 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 比较全文、模糊、向量与混合结果

查看词法解析

psql "service=pg36-admin" \
  -f static/labs/ch15/fts-analysis.sql

固定结果:

queryparsed tsquerymatches
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:

querylexicalfuzzyexact vectorhybrid RRF
q0111,3,22,3,11,2,3
q023,1,22,3,13,2,1
q0321,2,32,3,12,1,3
q0444,6,145,6,44,6,5
q0555,6,45,6,45,6,4
q067,87,8,158,9,77,8,9
q0710,11,1211,10,1210,11,12
q081212,10,1112,11,1012,10,11

是无行,不是三条零分结果。

注意 q04:

fuzzy third = product 14 / Digital Coffee Scale / unjudged
vector first = product 5 / Home Espresso Machine / grade 1
hybrid       = 4,6,5 / all judged relevant

这是互补成功例。

注意 q02:

fuzzy puts grade-3 product 1 first
hybrid puts grade-2 product 3 first

这是融合退化例。两种都必须写进报告。

质量结果

SELECT *
FROM shop_ch15.quality_summary
ORDER BY strategy;

得到:

strategyqueriesP@3R@3MRR@3mean NDCG@3min NDCG@3
fuzzy8.916667.9166671.942881.842828
hybrid_rrf8111.962929.759192
lexical8.291667.291667.75.6130430
vector_exact8111.817314.631039

质量视图把未返回 query 保留为 0,避免 survivorship bias。

计划证据

psql "service=pg36-admin" \
  -f static/labs/ch15/fts-plan.sql

psql "service=pg36-admin" \
  -f static/labs/ch15/trigram-plan.sql

psql "service=pg36-admin" \
  -f static/labs/ch15/vector-exact-plan.sql

psql "service=pg36-admin" \
  -f static/labs/ch15/vector-hnsw-plan.sql

psql "service=pg36-admin" \
  -f static/labs/ch15/vector-filtered-plan.sql

断言:

FTS      Bitmap Index Scan product_search_fts_idx
trigram  Bitmap Index Scan product_search_title_trgm_idx
exact    Seq Scan + Sort
HNSW     Index Scan product_search_embedding_hnsw_idx
filtered HNSW Index Scan + Filter

这些文件通过禁用 planner 备选路径建立“能力证明”。正常 17 行查询走顺序 扫描很合理;不要把强制 index plan 贴成性能结果。

目录证据:

SELECT
  index_name,
  access_method,
  operator_class,
  is_valid,
  is_ready,
  is_live
FROM (... index-catalog.sql ...);

最终四个 index 都必须 valid/ready/live,opclass 与查询 operator 一致。

ANN 对照

psql "service=pg36-admin" \
  --csv \
  -f static/labs/ch15/ann-compare.sql

结果:

exact_ids,ann_ids,recall_at_3
"7,8,9","7,8,9",1.000000

这个 probe:

  • 固定 q06 与 outdoor/active filter;
  • exact 结果来自全量 window ranking;
  • ANN 强制 HNSW;
  • 用集合交集测 recall。

它没有报告毫秒,因为 tiny fixture 的时间不稳定且无业务意义。

应用权限

psql "service=pg36-admin user=pg36_app" \
  -f static/labs/ch15/app-query.sql

应用可读 q02/q08 混合结果和质量摘要。

未授权更新:

UPDATE shop_ch15.product_search
SET title = 'unauthorized mutation'
WHERE product_id = 1;

必须:

psql exit=3
SQLSTATE=42501
permission denied for table product_search

自动化不是只检查非零 exit;它同时检查精确 SQLSTATE,避免连接失败或语法 错误被误当成权限测试通过。

15.7.3 输出 ADR、质量证据、生产代价与退出路径

ADR 结论

search-adr.md 决定:

FTS:
  pg_catalog.english
  stored weighted tsvector
  title A / description B
  GIN

fuzzy:
  lower(title)
  similarity + word_similarity
  GIN candidate predicate
  production must add a qualified threshold

vector:
  versioned model identity
  L2
  exact for quality golden
  HNSW only as measured serving candidate

fusion:
  equal-weight RRF
  k=60
  source depth=4
  final depth=3

它还明确否决:

  • 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:

bf92a6ad0f60dc3e125b39dbf67bf4d6c5e50275192bd01a7ca4c50d142f822e

更改 key 顺序或空白不会改变 canonical checksum;更改合同值会改变。

最终数据库状态:

release=1.3-proposal
fixture=ch15-search-v1
embedding_model=pg36-handcrafted-topic-4d-v1
products=17
active_products=16
queries=8
judgments=24
business_checksum=c637abf09edba88b7793f91201a57c34

business checksum 不包含运行时间、OID、index bytes 或绝对路径。

evidence 目录

一轮完整采集包含:

manifest.txt
setup.txt
corpus.csv
queries.csv
judgments.csv
document-catalog.csv
fts-analysis.csv
index-catalog.csv
size-catalog.csv
security-catalog.csv
quality-summary.csv
quality-detail.csv
ranking-results.csv
ann-compare.csv
fts-plan.txt
trigram-plan.txt
vector-exact-plan.txt
vector-hnsw-plan.txt
vector-filtered-plan.txt
app-query.csv
app-write.{exit,stdout,stderr}
final-state.csv
verify.txt
review.txt

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 行不能回答:

latency/throughput under representative concurrency
heap/GIN/HNSW size at target scale
bulk and concurrent build duration
steady write/WAL cost
autovacuum/reindex cost
replica replay/failover
clean backup restore
real model quality and generation cost

迁移到业务数据后,ADR 需要附:

领域最小证据
质量frozen + fresh queries,分段 P/R/MRR/NDCG
ANNexact-relative recall curve
查询P50/P95/P99、timeout、buffers、CPU
写入rows/s、WAL/row、索引增长、vacuum
HAreplica query、lag、switchover/failback
恢复clean environment restore
模型version/input/license/cost/failure
安全tenant/ACL canaries、secret/data boundary

精确退出

手工 reset 需要两个一致的显式确认:

PG36_RESET_TOKEN=RESET_CH15_SEARCH_LAB \
PG36_RESET_TARGET=pg36_shop/shop_ch15 \
PG36_EVIDENCE_DIR="$PWD/evidence/ch15-reset" \
  ./static/labs/ch15/task.sh reset

还会检查:

  • context/database/writable instance;
  • schema owner 与 marker;
  • 21 个 relation 的精确白名单与 marker;
  • 未知 routine/operator/opclass;
  • application_name LIKE 'pg36-ch15-%' 的其他活跃 worker。

错误分别用自定义 SQLSTATE:

P3660 invalid action token
P3661 invalid target
P3662 identity/inventory collision
P3663 active workers

删除顺序:

quality views
-> ranking views
-> relevance/query/product/meta tables
-> shop_ch15 schema

没有 CASCADE。最终必须:

remaining_schema=0
preserved_extensions=pg_trgm:1.6,vector:0.8.4

生产退出不是 DROP schema。生产要先:

stop/read-switch vector path
export and verify vectors/model identity
remove async generation traffic
drop/rebuild indexes online
observe fallback quality/capacity
retain rollback window
then remove no-longer-needed objects/packages

15.7.4 验收采用 checklist:evidence,不设脱离场景的性能线

为什么不给“必须 10 ms”

延迟取决于:

rows and dimensions
document/vector width
cache state
hardware/storage
concurrency
filters and selectivity
candidate K
index params
quality/recall target
write workload
network/application path

在 17 行、热缓存、本地 socket 上测到的微秒/毫秒,既不能预测一亿行,也 不能作为读者机器失败线。硬写一个数字只会诱导为过测试而牺牲质量或关闭 安全过滤。

因此本章把 15.7.4 的规则定义为:

checklist:evidence

即每一类能力都必须有可复核证据;业务上线再给每项填入自己的 SLO。

本地机制验收

检查证据固定结论
输入身份manifest + byte cmp17/8/24,三份一致
FTS 行为parsed query/match tableq02/q07 零命中
fuzzy 行为ranks/qualitymean NDCG .942881
vector qualityexact rankingRecall@3 1
fusionRRF ranks/qualitymean NDCG .962929,有 q02 退化
ANN 机制exact vs HNSWq06 Recall@3 1,仅单点
index 机制catalog + forced plansGIN/GIN/HNSW 路径存在
filteringcanary 17所有排名均不出现
ACLcatalog + expected failureread-only,UPDATE 42501
resettoken/target/active testsP3660/P3661/P3663
rebuildsecond full cyclechecksum 相同
PigstymanifestL1 not run

这一表中没有任何一项可被“SQL 返回了三行”替代。

业务场景性能卡

移植时创建一份场景卡:

dataset:
  products: ...
  active_ratio: ...
  dimensions: ...
  update_rate: ...
query:
  segments: ...
  filters/selectivity: ...
  top_k: ...
quality:
  min_recall_at_k: ...
  min_ndcg_at_k: ...
  max_segment_regression: ...
ann:
  min_exact_relative_recall: ...
latency:
  p50: ...
  p95: ...
  p99: ...
capacity:
  peak_qps: ...
  write_rate: ...
operations:
  max_build_window: ...
  max_replica_lag: ...
  restore_rto/rpo: ...
cost:
  monthly_model_budget: ...

数字必须来自业务 owner/SLO 和代表性测试,而不是本书替读者决定。

L1 验收增量

在 Pigsty L1 上补:

inventory commit and rendered config
package resolution for target OS/PG major
control/library hashes on every node
pg_extension catalog in target database
primary and replica search query
monitoring dashboards and alerts
planned switchover and failback
backup and clean restore
extension/model/index upgrade rehearsal

若某项未运行,报告应写 not-run,而不是 pass

双周期正式运行

evidence_dir="$(mktemp -d /tmp/pg36-ch15-evidence.XXXXXX)"

PG36_EVIDENCE_DIR="$evidence_dir" \
  ./static/labs/ch15/task.sh all

只把新建的专用 evidence 路径传给实验;不要把仓库根或广泛目录当目标。

all 的内部顺序:

cycle-1 collect + review
-> wrong token reset must fail P3660
-> wrong target reset must fail P3661
-> active worker reset must fail P3663
-> exact reset
-> verify extensions preserved
-> cycle-2 collect + review

正式结果:

status=ok
fixture=frozen-byte-identical
quality=precision+recall+mrr+ndcg
ranking=fts+trigram+exact-vector+rrf
ann=q06-exact-vs-hnsw-recall-1.000000
guards=P3660+P3661+P3663
extensions=ch14-preserved
pigsty_l1=not-run
release_candidate_checksum=bf92a6ad0f60dc3e125b39dbf67bf4d6c5e50275192bd01a7ca4c50d142f822e

第二轮完成后数据库保留可查询的 shop_ch15 最终状态,便于继续第 16 章; evidence 保留两个独立 cycle,证明 reset 后不是依赖第一次残留才通过。

最终评审句

本章可以得出的最强结论是:

1.3-proposal 在固定 PostgreSQL 18.4、本章扩展版本与合成 fixture 上, 两轮可重复;全文、模糊、精确向量、RRF、HNSW 测量路径、权限与精确复位 均有证据。它可以进入真实语料与 Pigsty L1 的下一阶段试点,尚未获得生产 性能与真实模型质量批准。

这比一句“PostgreSQL 可以做混合搜索”更窄,也更有用。


上一节:扩展部署与运行代价 · 返回本章目录 · 下一章:经天纬地:时序、空间与时空查询 · 查看全书目录 · 查看索引中心

最后更新于