跳至内容

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 distancevector_l2_ops
<#>negative inner productvector_ip_ops
<=>cosine distancevector_cosine_ops
<+>L1 / taxicab distancevector_l1_ops

还有 binary vector 的 Hamming/Jaccard,以及 halfvecsparsevec 的相应 能力;本章不展开。

pgvector 让“越小越近”符合 PostgreSQL ascending index scan:

-- L2:小者优先
ORDER BY embedding <-> :query

-- inner product:返回负内积,所以仍按 ASC
ORDER BY embedding <#> :query

-- cosine similarity 若要展示
1 - (embedding <=> :query)

不要把 <#> 的负数直接叫“相似度”。用于展示内积时要乘 -1,用于索引 排序则保留升序负内积。

参见 pgvector 0.8.4 Querying

选距离要回到模型合同

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 维;halfvecbitsparsevec 有各自上限。 这些是当前版本事实,升级前重查官方文档。

“维度越高语义越好”不是规律。维度会影响:

  • 每行存储;
  • 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=16ef_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 BYLIMIT 结合。

还要避免把 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 在同一事务中:

  1. 从 exact 质量视图取 q06 前三;
  2. 强制 HNSW path 并取同样过滤后的前三;
  3. 求集合交集。

结果:

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 没继续探索足够候选。

可选策略:

  1. 过滤列 B-tree + exact search 当过滤后集合很小,先用普通索引缩小到少量行,再 exact 排序,常常更快且 recall 完整。
  2. iterative index scan 过滤后不足时继续探索 ANN。
  3. partial HNSW index 少数稳定类别各有索引,例如 WHERE category_id=123
  4. partitioning 过滤维度离散且能形成真实数据生命周期/裁剪边界时分区。
  5. 扩大 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、运行库或量化不同而变化。

本章如何消除外部变量

fixture-manifest.json 明确:

{
  "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 章持有的扩展;它演示了对象 边界,不是生产数据迁移演练。


上一节:模糊匹配与拼写容错 · 返回本章目录 · 下一节:混合检索与排序验证 · 查看全书目录 · 查看索引中心

最后更新于