15.4 可复现的向量检索
向量列并不自带“语义”。它只是一种有维度的数值,配合一个距离/相似度函数 产生次序。语义来自向量生成过程;查询性能来自精确或近似执行路径;质量来自 外部标注。
把三者分开,才能复现,也才能退出。
15.4.1 维度、距离度量与归一化
一列必须有模型身份
最小表不是:
embedding vector(1536)而是至少:
embedding vector(1536) NOT NULL,
embedding_model text NOT NULL,
embedded_at timestamptz NOT NULL真实系统还可能需要:
input_template_version
source_text_hash
normalization
generation_status / error
provider/model revision
dimension同一维度不代表同一空间。两个 1536 维模型生成的向量不能因为类型相容就放进 同一个近邻索引比较。一次模型升级应视为数据迁移,而不是悄悄覆盖列。
本章用:
embedding shop_ch14.vector(4) NOT NULL,
embedding_model text NOT NULL
CHECK (
embedding_model =
'pg36-handcrafted-topic-4d-v1'
)四维分别是 audio、kitchen、outdoor、books 的人工主题坐标。它的价值是每个 距离可手算、没有 API 漂移;它不证明真实 embedding 的语义能力。
四个常见距离/相似合同
pgvector 0.8.4 对 vector 提供:
| 运算符 | 含义 | HNSW opclass |
|---|---|---|
<-> | L2 / Euclidean distance | vector_l2_ops |
<#> | negative inner product | vector_ip_ops |
<=> | cosine distance | vector_cosine_ops |
<+> | L1 / taxicab distance | vector_l1_ops |
还有 binary vector 的 Hamming/Jaccard,以及 halfvec、sparsevec 的相应
能力;本章不展开。
pgvector 让“越小越近”符合 PostgreSQL ascending index scan:
-- L2:小者优先
ORDER BY embedding <-> :query
-- inner product:返回负内积,所以仍按 ASC
ORDER BY embedding <#> :query
-- cosine similarity 若要展示
1 - (embedding <=> :query)不要把 <#> 的负数直接叫“相似度”。用于展示内积时要乘 -1,用于索引
排序则保留升序负内积。
选距离要回到模型合同
L2:
[ d_{L2}(x,y)=\sqrt{\sum_i(x_i-y_i)^2} ]
它同时受方向与向量长度影响。
cosine similarity:
[ \cos(x,y)=\frac{x\cdot y}{|x||y|} ]
更强调方向,cosine distance 通常为 (1-\cos(x,y))。
inner product:
[ x\cdot y=\sum_i x_i y_i ]
同时受方向与模长影响。某些模型明确训练为用 dot product 排序。
若所有向量都被归一化为单位长度:
[ |x-y|_2^2 = 2 - 2(x\cdot y) ]
此时 L2、cosine 与 inner product 的次序存在紧密关系;但仍应按模型文档 和性能目标选 opclass,不能因为数学关系就混用未归一化数据。
模型 ADR 要回答:
does the producer normalize?
does the database verify normalization tolerance?
which operator is used by every query?
which matching opclass indexes that operator?
how are zero vectors handled?一个常见错误:
CREATE INDEX ... (embedding vector_cosine_ops);
SELECT ... ORDER BY embedding <-> :q LIMIT 10;索引是 cosine,查询却用 L2;二者 operator family 不匹配,不能期待该索引 支持查询。目录与执行计划必须同时复核。
维度是存储和索引合同
本章使用 vector(4),数据库拒绝不同维度的值。pgvector 0.8.4 的 HNSW
vector 索引支持最多 2000 维;halfvec、bit、sparsevec 有各自上限。
这些是当前版本事实,升级前重查官方文档。
“维度越高语义越好”不是规律。维度会影响:
- 每行存储;
- index tuple 与图内存;
- 构建和查询计算;
- WAL、备份和网络;
- 模型能力与压缩损失。
先以模型要求为输入,再用真实规模测成本。不要为了迎合数据库索引上限随意 截断向量;降维、量化或子向量都需要新的质量基线。
15.4.2 精确近邻与近似索引
默认是精确搜索
没有近似索引时:
SELECT product_id, title
FROM shop_ch15.product_search
WHERE active
AND category = 'outdoor'
ORDER BY
embedding
OPERATOR(shop_ch14.<->)
'[0,0,0.9,0]'::shop_ch14.vector(4),
product_id
LIMIT 3;PostgreSQL 对所有合格行算距离、排序并取前三。这是 exact nearest neighbor: 在当前快照和距离合同下,召回是完备的。它的成本随参与比较的行数、维度与 并行度增长。
本章质量视图不是直接执行一个可能被 HNSW 接管的 ORDER BY ... LIMIT。
它先产生所有 pair 的 distance,再用 window function 排名:
row_number() OVER (
PARTITION BY query_id
ORDER BY distance, product_id
)这使质量 golden 明确来自 exact 全集,与线上 planner 是否选择 ANN 无关。
强制 exact plan:
SET enable_indexscan = off;
SET enable_bitmapscan = off;得到:
Seq Scan on product_search
Sort Key: ((embedding <-> ...)), product_id这是本章的 reference path。
ANN 用召回换速度
pgvector 支持 HNSW 与 IVFFlat:
exact search:
computes all eligible distances
perfect recall under the metric
approximate search:
visits a selected part of an index
lower work, potentially different results官方 README 明确指出,增加 approximate index 后,同一查询可能得到不同 结果;必须把这种差异视为算法合同,而不是数据库 bug。
本章建:
CREATE INDEX product_search_embedding_hnsw_idx
ON shop_ch15.product_search
USING hnsw (
embedding shop_ch14.vector_l2_ops
)
WITH (
m = 8,
ef_construction = 32
);默认值在 pgvector 0.8.4 是 m=16、ef_construction=64;本章用较小值
只是让夹具声明显式,并非生产建议。
HNSW 建多层图:
- 通常有较好的 speed/recall trade-off;
- 构建更慢、内存更多;
- 不需要像 IVFFlat 那样先训练 lists,所以空表也能先建;
- 持续写入仍要维护图和 WAL。
IVFFlat 把向量分到 lists,查询探测部分 lists:
- 构建更快、内存较少;
- 通常查询 speed/recall trade-off 低于 HNSW;
- 建索引前应有代表性数据;
- lists/probes 选择直接影响 recall。
这不是永久排名。数据规模、更新率、过滤、内存和 SLO 会改变选择。
索引查询形状必须匹配
让 ANN index 生效,典型查询要:
ORDER BY embedding <-> :query
LIMIT K只写距离范围:
WHERE embedding <-> :query < :radius不一定形成同样的 ordered index path。pgvector 官方建议把范围条件与
ORDER BY、LIMIT 结合。
还要避免把 indexed expression 包进不等价表达式:
-- 可能破坏路径匹配
ORDER BY 1 - (embedding <=> :query) DESC
-- 直接按 index operator 升序
ORDER BY embedding <=> :query展示 similarity 可以在外层计算;候选扫描保持与 opclass/operator 一致。
ANN recall 必须相对 exact 定义
对于同一 query 与 filters:
[ Recall@K_{ANN} = \frac{|ANN_K \cap Exact_K|}{K} ]
本章 ann-compare.sql 在同一事务中:
- 从 exact 质量视图取 q06 前三;
- 强制 HNSW path 并取同样过滤后的前三;
- 求集合交集。
结果:
exact_ids = 7,8,9
ann_ids = 7,8,9
recall@3 = 1.000000一次查询、17 行、四维向量上的 1.0 不是生产结论。正式测量至少按:
query segment
filter selectivity
K
ef_search/probes
concurrency
data freshness
model version报告分布,并把 exact 抽样任务长期保留。pgvector 官方 monitoring 建议同样 是关闭 index scan 取得 exact 结果,再与近似结果比较。
15.4.3 索引参数、过滤条件与召回代价
HNSW 有构建参数和查询参数
构建参数:
| 参数 | 作用 | 增大通常带来的影响 |
|---|---|---|
m | 每层最大连接数 | 图更密、潜在召回更好、空间/构建更贵 |
ef_construction | 构图候选列表大小 | 潜在召回更好、构建/写入更慢 |
查询参数:
| 参数 | 作用 | 增大通常带来的影响 |
|---|---|---|
hnsw.ef_search | 查询动态候选列表 | recall 上升机会、延迟与工作量上升 |
pgvector 0.8.4 默认 ef_search=40。单次实验应使用事务局部设置:
BEGIN;
SET LOCAL hnsw.ef_search = 100;
SELECT ... ORDER BY embedding <-> :query LIMIT 10;
COMMIT;不要把 session pool 中遗留的 GUC 当作服务配置;也不要只测一个值。需要 绘制:
ef_search -> recall distribution
-> P50/P95/P99 latency
-> buffers/CPU参数发布要与 index build identity、模型、数据快照和查询集一起版本化。
过滤为什么会“吃掉”结果
pgvector 0.8.4 官方说明,approximate index scan 的普通过滤是在扫描后应用。 假设只有 10% 行满足过滤,初次探索 40 个候选,平均可能只留下约 4 个;需要 K=10 时就会短缺。
这不是 SQL 三值逻辑错,而是 ANN 没继续探索足够候选。
可选策略:
- 过滤列 B-tree + exact search 当过滤后集合很小,先用普通索引缩小到少量行,再 exact 排序,常常更快且 recall 完整。
- iterative index scan 过滤后不足时继续探索 ANN。
- partial HNSW index
少数稳定类别各有索引,例如
WHERE category_id=123。 - partitioning 过滤维度离散且能形成真实数据生命周期/裁剪边界时分区。
- 扩大 candidate/ef_search 简单但增加每次查询成本,仍要测。
本章同时创建:
CREATE INDEX product_search_filter_idx
ON shop_ch15.product_search(category, product_id)
WHERE active;它提醒读者:向量检索仍然是 PostgreSQL 查询,普通关系索引和选择性统计并未 失效。
iterative scan 的 strict 与 relaxed
pgvector 从 0.8.0 起支持 iterative index scan:
SET LOCAL hnsw.iterative_scan = 'strict_order';
-- or
SET LOCAL hnsw.iterative_scan = 'relaxed_order';strict 保持距离严格次序;relaxed 允许轻微乱序,可能获得更好的 recall。 relaxed 结果若要重新严格排序,官方示例使用 materialized CTE 后在外层排序。
迭代不会无限进行,还受:
hnsw.max_scan_tuples
ivfflat.max_probes
memory limits等边界影响。返回 K 行、返回顺序与 recall 都要分别断言。
本章用 strict:
SET hnsw.iterative_scan = 'strict_order';只为让证据顺序稳定。它不能让 approximate graph 等价于 exact scan。
partial index 不是“每个租户建一个”
官方建议在过滤值很少时考虑 partial HNSW:
CREATE INDEX ...
USING hnsw (embedding vector_l2_ops)
WHERE category_id = 123;若有成千上万租户或高基数 ACL,给每个值建索引会造成:
- index 数量爆炸;
- DDL/catalog/autovacuum 管理困难;
- 写放大;
- planner 规划开销;
- 备份恢复和升级时间增长。
高基数过滤更可能需要:
- exact on a selective conventional index;
- 合理分区;
- 分片/路由层;
- 两阶段候选与业务过滤;
- 或重新选择搜索架构。
任何方案都必须验证权限过滤不会因“为了 recall”被放宽。
构建与维护的安全线
HNSW 构建图若能放入 maintenance_work_mem 会更快;官方也警告不要把该参数
设到耗尽 server 内存。生产建索引:
CREATE INDEX CONCURRENTLY ...可以减少阻塞写入,但会更慢、产生更长资源占用,失败还可能留下 invalid index。要监控:
SELECT
phase,
blocks_done,
blocks_total
FROM pg_stat_progress_create_index;并检查:
SELECT indisvalid, indisready, indislive
FROM pg_index
WHERE indexrelid = 'schema.index_name'::regclass;本章 setup 是离线教学重建,所以用普通 CREATE INDEX;不得把这个选择直接
搬到在线主表。
何时退回 exact
以下场景 exact 可能更好:
- 过滤后只剩少量行;
- K 较大,ANN 需要探索大部分集合;
- 质量要求不允许近似漏召回;
- 数据规模尚小;
- 向量更新频繁,ANN 维护成本超过收益;
- exact 可以在可接受延迟内并行完成。
先测 crossover,再引入 ANN。拥有 HNSW 扩展能力不构成使用理由。
15.4.4 冻结文本、模型标识、许可证、向量文件与校验和
“同一个模型”仍可能生成不同向量
可复现输入至少包括:
exact source text bytes
field concatenation/template
Unicode and whitespace normalization
truncation/chunking
model/provider/revision
tokenizer revision
output dimension
post-normalization
generation parameters只记录营销名,例如 embedding-v3,不够。云服务可能滚动更新后端;本地模型
可能因权重、tokenizer、运行库或量化不同而变化。
本章如何消除外部变量
{
"model_id": "pg36-handcrafted-topic-4d-v1",
"dimensions": 4,
"method": "manually assigned deterministic topic coordinates",
"external_model": false,
"external_api": false,
"distance": "L2"
}商品与 query 的向量都保存在冻结 CSV 中。setup 载入后,数据库导出:
COPY (
SELECT
product_id,
sku,
category,
active::text,
title,
description,
embedding::text
FROM shop_ch15.product_search
ORDER BY product_id
) TO STDOUT WITH (FORMAT csv, HEADER true);再与源文件逐字节 cmp。这比“查询结果差不多”严格得多:向量文本一个数字
变化就失败。
文件 hash 与数据库 checksum 各负责什么
fixture manifest 固定:
frozen-corpus.csv SHA-256
frozen-queries.csv SHA-256
frozen-judgments.csv SHA-256
fixture.sql SHA-256最终数据库 business_checksum 则覆盖:
all products and vectors
all queries and vectors
all judgments
all top-3 ranks and rounded scores
all quality summaries二者互补:
- file hash 证明输入资产没变;
- database checksum 证明装载、计算和最终行为没变。
运行时间、OID、绝对路径、索引物理页大小不进入 business checksum,因为它们 不是跨重建稳定的业务事实。
许可证和数据边界不能等上线后再补
向量可能泄露源数据特征,也可能受模型服务条款约束。ADR 至少记录:
source text ownership and lawful basis
whether text leaves the trust boundary
provider retention/training policy
region and transfer mechanism
model/output usage license
secrets and service account scope
deletion propagation
backup retention
incident and audit trail“只传向量,不传原文”也不是自动匿名。是否能从向量推断敏感信息要按威胁 模型评估。
本章文本和数字都是项目自有合成 fixture,不调用外部服务:
text_license = project-owned synthetic fixture
vector_license = handcrafted numeric fixture这使仓库可重复,不替真实项目完成法务评审。
模型升级要双版本迁移
不要:
UPDATE product
SET embedding = new_model(text);在同一列原地混写。更稳健的流程:
1. create new vector column/table with model_id v2
2. backfill from frozen source-text hash
3. build matching v2 opclass/index
4. evaluate v1 vs v2 on frozen + fresh sets
5. shadow query / guarded traffic
6. switch read version explicitly
7. retain rollback window
8. export or retire v1 under data-retention rules若维度改变,类型和索引自然需要新对象;即使维度相同,也应保持逻辑隔离。
退出路径
pgvector 列可转文本导出:
COPY (
SELECT
product_id,
embedding_model,
embedding::text
FROM shop_ch15.product_search
ORDER BY product_id
) TO STDOUT WITH (FORMAT csv, HEADER true);但“能导出字符串”只是技术出口第一步。还要证明目标系统:
- 能按同一精度解析;
- 保留 document/model identity;
- 使用同一距离与归一化;
- 重建索引后质量不变;
- 在切换窗口双读校验;
- 能回放迁移期间更新。
本章精确 reset 删除 shop_ch15,却保留第 14 章持有的扩展;它演示了对象
边界,不是生产数据迁移演练。
上一节:模糊匹配与拼写容错 · 返回本章目录 · 下一节:混合检索与排序验证 · 查看全书目录 · 查看索引中心