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,向量量级百万以内,想快速做 RAG | pgvector,不要引入新组件 |
| 需要"向量 + 业务数据强一致"(如权限过滤后必须精确) | pgvector,JOIN 与事务是硬需求 |
| 千万级以上、或需要复杂的多租户与分布式 | 评估专用向量库 |
| 只是做原型验证 | pgvector,成本最低 |
💡 一条经验:不要为了"可能上规模"提前引入专用向量库。 从 pgvector 起步,规模真的到了再迁移——迁移成本远低于早期多维护一套系统的成本。
二、类型选择:精度换内存
| 类型 | 每维字节 | 特点 | 用途 |
|---|---|---|---|
vector(d) | 4(float32) | 标准精度 | 小数据量;重排时用它 |
halfvec(d) | 2(float16) | 内存减半,召回损失很小 | 默认推荐 |
bit(d) | 1/8 | 内存减到 1/32,精度损失大 | 超大规模,必须配合重排 |
sparsevec(d) | 稀疏 | 稀疏向量 | SPLADE 类的学到的稀疏检索 |
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 是特例
| HNSW | IVFFlat | |
|---|---|---|
| 构建速度 | 慢 | 快 |
| 查询性能 | 快、召回高 | 调好了也快,但默认参数召回很差 |
| 内存占用 | 大(图要常驻内存) | 较小 |
| 能不能在空表建 | ✅ 可以 | ❌ 必须有代表性数据,否则质心偏斜 |
| 插入后 | ✅ 增量友好 | 召回会漂移,需定期重建 |
-- 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);三个调参旋钮
| 参数 | 默认 | 作用 |
|---|---|---|
m | 16 | 每层最大连接数 ↑ = 召回↑ 内存↑ |
ef_construction | 64 | 构建时候选宽度 ↑ = 图质量↑ 构建慢 |
hnsw.ef_search | 40 | 查询时候选宽度 ↑ = 召回↑ 延迟↑,应 ≥ 你的 LIMIT |
-- ⚠️ 用 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 的两个必知:
lists经验值:行数 ≤ 100 万时rows / 1000;超过后sqrt(rows)ivfflat.probes默认是 1——只搜一个簇,召回会很差,而且不会有任何提示。 建了 IVFFlat 觉得效果一般,先查这个值。
四、最容易踩的坑:索引静默失效
运算符类(opclass)必须与查询运算符匹配,否则 Postgres 会静默退化成全表扫描——不报错,只是变慢。
| 距离 | 运算符 | 对应 opclass |
|---|---|---|
| 余弦 | <=> | vector_cosine_ops / halfvec_cosine_ops |
| 欧氏(L2) | <-> | vector_l2_ops |
| 内积 | <#> | vector_ip_ops |
-- 用了 hnsw(… halfvec_cosine_ops) 建的索引,就必须用 <=> 查询
SELECT id FROM chunks ORDER BY embedding <=> $1::halfvec(1536) LIMIT 10;怎么确认索引真的在用:
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。
SET LOCAL hnsw.iterative_scan = strict_order; -- 严格按距离顺序
-- 或 relaxed_order(允许轻微乱序,更快)
SET LOCAL hnsw.max_scan_tuples = 20000;| 模式 | 特点 |
|---|---|
strict_order | 结果严格按距离排序,较慢 |
relaxed_order | 允许轻微乱序,更快,通常够用 |
0.8 之前的变通做法:过度取回(over-fetch)再在子查询里过滤。
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 做粗筛,再用原始向量精排。
-- 生成列存二进制向量
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);-- 两阶段:宽取 + 精排(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 就能拿到两路结果,不用维护两个系统。
-- 两路都在 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 索引通常与向量数据同量级甚至更大 |
-- 盯索引体积与是否被使用
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 还是二进制量化。
相关文档
本区
- DB · 数据库与存储计划 —— DB01~DB10 待写(关系模型、索引原理、事务…)
AI 区(pgvector 的主要用途)
- 实操 02 · RAG 实战:可溯源问答系统 —— 完整链路
- 实操 08 · 检索进阶:混合检索与重排 —— ANN 选型、RRF、评测
- G12 · 上下文工程 —— 取回之后怎么装配
后端区
- 01 · 运行时选型
- 03 · 认证与授权 —— 权限过滤的正确做法