Skip to content

11 · pgvector 向量检索 ​

速览卡 ​

项内容
一句话定位不引入新组件就能做 RAG——什么时候 pgvector 够用,什么时候必须换专用向量库
核心内容类型与索引选型、调参旋钮、过滤与 ANN 的冲突、与 RAG 的集成、运维代价
读完能回答我这规模该用 pgvector 吗?索引为什么没生效?加了 WHERE 之后结果变少怎么办?
相关板块实操 02 · RAG 实战 | 实操 08 · 检索进阶 | C01 · 运行时选型

本篇假设你已经有 Postgres。关系模型与 SQL 基础见 DB01(待写),本篇只讲向量检索这一层。


一、先做判断:pgvector 还是专用向量库 ​

维度pgvector专用向量库(Qdrant / Milvus / Weaviate 等)
引入成本零(已有 PG 就装个扩展)多一个组件要部署、监控、备份
事务与关联✅ 向量与业务数据同一事务、能 JOIN❌ 要自己维护一致性
混合检索与 tsvector(全文)天然同库通常要两个系统再融合
规模上限百万级舒适;千万级要精心调优 [易变]设计目标就是大规模
运维成熟度取决于你的 PG 运维能力需单独积累

判断框架 ​

你的情况建议
已有 PG,向量量级百万以内,想快速做 RAGpgvector,不要引入新组件
需要"向量 + 业务数据强一致"(如权限过滤后必须精确)pgvector,JOIN 与事务是硬需求
千万级以上、或需要复杂的多租户与分布式评估专用向量库
只是做原型验证pgvector,成本最低

💡 一条经验:不要为了"可能上规模"提前引入专用向量库。 从 pgvector 起步,规模真的到了再迁移——迁移成本远低于早期多维护一套系统的成本。


二、类型选择:精度换内存 ​

类型每维字节特点用途
vector(d)4(float32)标准精度小数据量;重排时用它
halfvec(d)2(float16)内存减半,召回损失很小默认推荐
bit(d)1/8内存减到 1/32,精度损失大超大规模,必须配合重排
sparsevec(d)稀疏稀疏向量SPLADE 类的学到的稀疏检索
sql
CREATE EXTENSION IF NOT EXISTS vector;   -- 需要 0.7+ 才有 halfvec / bit / sparsevec

CREATE TABLE chunks (
  id          bigserial PRIMARY KEY,
  tenant_id   uuid NOT NULL,
  content     text NOT NULL,
  embedding   halfvec(1536) NOT NULL,      -- 存半精度
  created_at  timestamptz DEFAULT now()
);

默认用 halfvec:同样的内存能装下两倍数据,召回损失通常可以忽略。 真要精确重排时,再单独存一份 vector 用于 TOP-K 精算。


三、索引:HNSW 是默认,IVFFlat 是特例 ​

HNSWIVFFlat
构建速度慢快
查询性能快、召回高调好了也快,但默认参数召回很差
内存占用大(图要常驻内存)较小
能不能在空表建✅ 可以❌ 必须有代表性数据,否则质心偏斜
插入后✅ 增量友好召回会漂移,需定期重建
sql
-- HNSW:默认选择
CREATE INDEX ON chunks USING hnsw (embedding halfvec_cosine_ops)
  WITH (m = 16, ef_construction = 64);

-- IVFFlat:只在特定情况下用
CREATE INDEX ON chunks USING ivfflat (embedding halfvec_cosine_ops)
  WITH (lists = 1000);

三个调参旋钮 ​

参数默认作用
m16每层最大连接数 ↑ = 召回↑ 内存↑
ef_construction64构建时候选宽度 ↑ = 图质量↑ 构建慢
hnsw.ef_search40查询时候选宽度 ↑ = 召回↑ 延迟↑,应 ≥ 你的 LIMIT
sql
-- ⚠️ 用 SET LOCAL(事务内),不要用 SET
BEGIN;
SET LOCAL hnsw.ef_search = 100;
SELECT id, content FROM chunks ORDER BY embedding $1::halfvec(1536) LIMIT 10;
COMMIT;

为什么必须用 SET LOCAL:带连接池时,普通 SET 会泄漏到下一个拿到这条连接的请求。 这和事务池化下的 session 状态是同一类坑。

IVFFlat 的两个必知:

  1. lists 经验值:行数 ≤ 100 万时 rows / 1000;超过后 sqrt(rows)
  2. ivfflat.probes 默认是 1——只搜一个簇,召回会很差,而且不会有任何提示。 建了 IVFFlat 觉得效果一般,先查这个值。

四、最容易踩的坑:索引静默失效 ​

运算符类(opclass)必须与查询运算符匹配,否则 Postgres 会静默退化成全表扫描——不报错,只是变慢。

距离运算符对应 opclass
余弦<=>vector_cosine_ops / halfvec_cosine_ops
欧氏(L2)<->vector_l2_ops
内积<#>vector_ip_ops
sql
-- 用了 hnsw(… halfvec_cosine_ops) 建的索引,就必须用 <=> 查询
SELECT id FROM chunks ORDER BY embedding <=> $1::halfvec(1536) LIMIT 10;

怎么确认索引真的在用:

sql
EXPLAIN (ANALYZE, BUFFERS)
SELECT id FROM chunks ORDER BY embedding <=> $1::halfvec(1536) LIMIT 10;
-- 想看到:Index Scan using chunks_embedding_idx
-- 不想看到:Seq Scan  /  Sort

另外两个静默失效的常见原因: 查询向量没显式 cast($1::halfvec(1536))导致隐式转换失败; 索引和查询的维度不一致。


五、过滤:ANN 与 WHERE 的冲突 ​

这是 pgvector 在生产里最常见的诡异问题:

现象:加了 WHERE tenant_id = ? 之后,明明有几千条匹配,却只返回 3 条(甚至 0 条)

原因:HNSW 先按 ef_search 取回候选,再由 WHERE 过滤掉一部分, 剩下的不够 LIMIT 就直接返回不足,不会再去索引里找更多。

解法(pgvector 0.8+):开启迭代扫描,让索引继续取候选直到满足 LIMIT。

sql
SET LOCAL hnsw.iterative_scan = strict_order;   -- 严格按距离顺序
-- 或 relaxed_order(允许轻微乱序,更快)
SET LOCAL hnsw.max_scan_tuples = 20000;
模式特点
strict_order结果严格按距离排序,较慢
relaxed_order允许轻微乱序,更快,通常够用

0.8 之前的变通做法:过度取回(over-fetch)再在子查询里过滤。

sql
WITH candidates AS (
  SELECT id, content FROM chunks
  ORDER BY embedding <=> $1::halfvec(1536)
  LIMIT 500                       -- 多取
)
SELECT * FROM candidates WHERE tenant_id = $2 LIMIT 10;

⚠️ 无论用哪种方式,租户隔离不能只靠这里的过滤—— 权限校验要在数据层也做一次。参见 03 · 认证与授权。


六、超大规模:二进制量化 + 重排 ​

内存装不下时,用 bit 做粗筛,再用原始向量精排。

sql
-- 生成列存二进制向量
ALTER TABLE chunks
  ADD COLUMN embedding_bq bit(1536)
  GENERATED ALWAYS AS (binary_quantize(embedding)::bit(1536)) STORED;

CREATE INDEX ON chunks USING hnsw (embedding_bq bit_hamming_ops);
sql
-- 两阶段:宽取 + 精排(80x 过采样是个合理的起点)
SET LOCAL hnsw.ef_search = 800;
WITH q AS (SELECT binary_quantize($1::halfvec(1536))::bit(1536) AS qb),
candidates AS (
  SELECT i.id, i.content, i.embedding
  FROM chunks i, q
  ORDER BY i.embedding_bq <~> q.qb     -- 用二值索引粗筛
  LIMIT 800
)
SELECT id, content FROM candidates
ORDER BY embedding <=> $1::halfvec(1536)  -- 用原始向量精排
LIMIT 10;

这个"便宜地宽取 → 精确地窄排"的两阶段形态,是所有压缩索引的通用套路 (与 RAG 里的"召回 → 重排"是同一思想)。


七、与 RAG 集成:最小链路 ​

文档 → 分块 → embedding → 存入 chunks 表(halfvec + 业务字段)
                                    ↓
用户提问 → embedding → pgvector 召回 Top-50
                     → 与 BM25/tsvector 融合(混合检索)
                     → Rerank 精排到 Top-5
                     → 拼进上下文 → LLM

在 PG 里做混合检索的好处:全文检索(tsvector)和向量检索在同一个库, 一次 SQL 就能拿到两路结果,不用维护两个系统。

sql
-- 两路都在 PG 里
WITH dense AS (
  SELECT id, embedding <=> $1::halfvec(1536) AS d_score
  FROM chunks ORDER BY d_score LIMIT 50
),
sparse AS (
  SELECT id, ts_rank_cd(content_tsv, plainto_tsquery($2)) AS s_score
  FROM chunks WHERE content_tsv @@ plainto_tsquery($2)
  ORDER BY s_score DESC LIMIT 50
)
SELECT id FROM (
  SELECT id, 1.0/(60 + row_number() OVER (ORDER BY d_score)) AS r FROM dense
  UNION ALL
  SELECT id, 1.0/(60 + row_number() OVER (ORDER BY s_score DESC)) AS r FROM sparse
) t GROUP BY id ORDER BY SUM(r) DESC LIMIT 20;   -- RRF 融合

融合策略的选择与评测方法见 实操 08 · 检索进阶。


八、运维:三个必须盯的点 ​

问题现象对策
索引装不进内存CPU 空闲但 p95/p99 延迟飙升转 halfvec;再不行上二进制量化
删除不回收大量更新/删除后召回缓慢下降定期 REINDEX INDEX CONCURRENTLY
索引体积膨胀索引比数据本身还大监控 pg_relation_size;HNSW 索引通常与向量数据同量级甚至更大
sql
-- 盯索引体积与是否被使用
SELECT indexrelname,
       pg_size_pretty(pg_relation_size(indexrelid)) AS size,
       idx_scan
FROM pg_stat_user_indexes WHERE relname = 'chunks';
-- idx_scan = 0 说明你的查询根本没走索引(回到第四节)

另外两个习惯:

  • 不要 SELECT *:一个 1536 维向量就是几 KB,白传。枚举你真正要的列
  • 重嵌文档用"删+插"而不是原地 UPDATE:HNSW 的删除是标记,频繁原地更新会累积膨胀

九、自检清单 ​

  • [ ] 能说出 pgvector 与专用向量库的分界线(什么规模该换)
  • [ ] 默认用 halfvec 而不是 vector
  • [ ] 索引用 HNSW,且知道 IVFFlat 只在什么情况下用
  • [ ] 知道 ivfflat.probes 默认为 1 这个坑
  • [ ] 调参用 SET LOCAL,不用 SET
  • [ ] 用 EXPLAIN 确认过索引真的在用(opclass 匹配)
  • [ ] 知道过滤 + LIMIT 会导致结果不足,且知道 iterative_scan 这个解法
  • [ ] 监控索引体积与 idx_scan
  • [ ] 知道 HNSW 删除不回收,有 REINDEX 计划

十、常见误区 ​

误区事实
"装了扩展建了索引就完事"opclass 不匹配会静默全表扫描,必须 EXPLAIN 验证
"IVFFlat 和 HNSW 差不多"IVFFlat 的 probes 默认是 1,且必须在有数据后建
"SELECT * 方便"向量列几 KB 一条,白白拖慢网络与序列化
"加个 WHERE 不影响"ANN + 过滤会导致结果不足,这是最隐蔽的坑
"向量列放主表就行"高频更新主表会带累 HNSW 索引;量大时考虑分表
"pgvector 什么规模都能扛"千万级以上要认真评估;内存是硬约束
"二进制量化直接替代"必须配合重排,单独用召回损失明显

十一、动手练习 ​

练习 A:验证你的索引真的在跑 ​

造 1 万条向量数据,建 HNSW 索引,然后故意用错误的运算符查询一次, 对比 EXPLAIN 结果与耗时。感受一下"静默退化"有多安静。

练习 B:复现"过滤导致结果不足" ​

让 90% 的数据属于 tenant A,10% 属于 tenant B。 查询 tenant B 的 Top-10,观察返回条数。然后开启 iterative_scan 再试一次。

练习 C:估算内存 ​

1536 维、m=16 的 halfvec 约 4~6 KB/向量(量级估算 [易变])。 算一下你 100 万条需要多少内存,判断该用 halfvec 还是二进制量化。


相关文档 ​

本区 ​

AI 区(pgvector 的主要用途) ​

后端区 ​