关于 pgvector-semantic-search
面向使用 PostgreSQL 和 pgvector 构建语义搜索的开发者,提供向量存储、索引、查询与调优指南。内容涵盖 HNSW、量化及过滤查询等实践。
主要功能
- 01配置 halfvec 与余弦距离
- 02创建和调优 HNSW 索引
- 03使用二进制量化与重排
- 04处理过滤条件下的向量搜索
- 05批量加载与索引维护
适用场景
- 01为文本嵌入构建语义搜索
- 02调优大型向量数据集的检索
- 03改善过滤查询的召回率
- 04排查向量索引性能问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ timescale/ pg-aiguide --skill pgvector-semantic-search安装后在 Claude Code 中输入/pgvector-semantic-search确认已加载。
详细介绍
pgvector 语义搜索
语义搜索根据含义而非精确关键词查找内容。嵌入模型会将文本转换为高维向量,其中含义相似的内容会映射到彼此相近的点。pgvector 在 PostgreSQL 中存储这些向量,并使用近似最近邻(ANN)索引快速查找最接近的匹配项,无需离开数据库即可扩展到数百万行。将文本与其嵌入一起存储,然后将搜索文本转换为向量进行查询,并返回距离最小的行。
本指南介绍 pgvector 的设置和调优,不涉及嵌入模型选择或文本分块;这两者会显著影响搜索质量。所有功能(halfvec、binary_quantize、迭代扫描)均要求 pgvector 0.8.0+。
推荐默认配置
除非有特定原因,否则请使用此配置。
- 嵌入列的数据类型:
halfvec(N),其中N为嵌入维度(所有位置必须一致)。示例使用 1536;请替换为你的维度N。 - 距离:余弦(
<=>) - 索引:HNSW(
m = 16,ef_construction = 64)。使用halfvec_cosine_ops,查询时使用<=>。 - 查询时的召回率:
SET hnsw.ef_search = 100(根据已发布的基准测试,这是一个不错的起点;提高该值可在延迟增加的情况下提升召回率) - 查询模式:
ORDER BY embedding <=> $1::halfvec(N) LIMIT k
对于大多数文本嵌入工作负载,此配置能在速度和召回率之间取得良好平衡。
核心规则
- 在每个数据库中启用扩展:
CREATE EXTENSION IF NOT EXISTS vector; - 默认使用 HNSW 索引——速度与召回率的平衡更佳,可在空表上创建,无需训练步骤。只有在写入密集型或受内存限制的工作负载中才考虑使用 IVFFlat。
- 默认使用
halfvec——以halfvec存储并建立索引,可将存储和索引大小减少 50%,且召回率损失极小。 - 初始数据批量加载后再创建索引,以获得最佳构建性能。
- 在生产环境中并发创建索引:
CREATE INDEX CONCURRENTLY ... - 默认使用余弦距离(
<=>):对于未归一化的嵌入,请使用余弦距离。对于单位归一化的嵌入,余弦距离和内积的排序结果相同;默认使用余弦距离。 - 查询运算符必须与索引运算符类匹配:使用
halfvec_cosine_ops建立的索引要求查询中使用<=>;halfvec_l2_ops要求使用<->;运算符不匹配时不会使用索引。 - 始终显式转换查询向量(
$1::halfvec(N)),以避免预备语句中的隐式转换失败。 - 数据和查询始终使用相同的嵌入模型。只有生成向量的模型相同时,相似度搜索才能正常工作。
类型规则
- 将嵌入存储为
halfvec(N) - 将查询向量转换为
halfvec(N) - 将二进制量化向量作为
bit(N)存储在生成列中 - 不要混用
vector/halfvec/bit,除非显式转换 - 切勿在
ORDER BY中对表列调用binary_quantize();应改为存储其结果 - 维度必须匹配:
halfvec(1536)列要求查询向量转换为::halfvec(1536)。
标准模式
-- Store and index as halfvec
CREATE TABLE items (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
contents TEXT NOT NULL,
embedding halfvec(1536) NOT NULL -- NOT NULL requires embeddings generated before insert, not async
);
CREATE INDEX ON items USING hnsw (embedding halfvec_cosine_ops);
-- Query: returns 10 closest items. $1 is the embedding of your search text.
SELECT id, contents FROM items ORDER BY embedding <=> $1::halfvec(1536) LIMIT 10;
有关其他距离运算符(L2、内积等),请参阅 pgvector README。
HNSW 索引
推荐使用的索引类型。它会创建多层可导航图,在速度与召回率之间提供更佳平衡。可在空表上创建(无需训练步骤)。
CREATE INDEX ON items USING hnsw (embedding halfvec_cosine_ops);
-- With tuning parameters
CREATE INDEX ON items USING hnsw (embedding halfvec_cosine_ops) WITH (m = 16, ef_construction = 64);
HNSW 参数
| 参数 | 默认值 | 描述 |
|---|---|---|
m | 16 | 每层的最大连接数。数值越高,召回率越好,占用内存越多 |
ef_construction | 64 | 构建时的候选列表。数值越高,图质量越好,构建速度越慢 |
hnsw.ef_search | 40 | 查询时的候选列表。数值越高,召回率越好,查询速度越慢。应 ≥ LIMIT。 |
ef_search 调优(粗略指南,实际结果因数据集而异):
| ef_search | 近似召回率 | 相对速度 |
|---|---|---|
| 40 | 较低(部分基准测试中约为 95%) | 1x(基准) |
| 100 | 较高 | 慢约 2x |
| 200 | 很高 | 慢约 4x |
| 400 | 接近精确 | 慢约 8x |
-- Set search parameter for session
SET hnsw.ef_search = 100;
-- Set for single query
BEGIN;
SET LOCAL hnsw.ef_search = 100;
SELECT id, contents FROM items ORDER BY embedding <=> $1::halfvec(1536) LIMIT 10;
COMMIT;
IVFFlat 索引(通常不推荐)
默认使用 HNSW。只有当 HNSW 的运维成本比最高召回率更重要时,才使用 IVFFlat。
以下情况可选择 IVFFlat:
- 写入密集或数据持续变化,且愿意频繁重建索引
- 经常重建索引,并希望构建时间和内存用量可预测
- 内存有限,无法让 HNSW 图基本常驻内存
- 数据经过分区或分层,且此索引位于较冷的分区
如果需要以下特性,请避免使用 IVFFlat:
- 低延迟下的最高召回率
- 尽量少的调优
- “设置后无需再管”的索引
注意:
- 创建 IVFFlat 索引前,必须先有数据。
- 召回率取决于
lists和ivfflat.probes;probes 越高,召回率越好,查询速度越慢。
入门配置:
CREATE INDEX ON items
USING ivfflat (embedding halfvec_cosine_ops)
WITH (lists = 1000);
SET ivfflat.probes = 10;
量化策略
- 量化是内存方面的选择,而非召回率方面的选择。
- 默认使用
halfvec进行存储和索引。 - 估算 HNSW 索引占用空间时,可按每个 1536 维
halfvec约 4–6 KB(m=16)计算(数量级估算);3072 维约为其 2 倍;m=32 时,HNSW 链接和图的开销大致翻倍。 - 如果 p95/p99 延迟上升,而 CPU 大多处于空闲状态,HNSW 索引可能已无法常驻内存。
- 如果
halfvec无法装入内存,请使用二进制量化并重新排序。
1536 维向量指南
m=16、1536 维时 halfvec 的近似容量(假设 RAM 大部分可用于索引缓存):
| RAM | halfvec 向量数近似上限 |
|---|---|
| 16 GB | 约 2–3M 个向量 |
| 32 GB | 约 4–6M 个向量 |
| 64 GB | 约 8–12M 个向量 |
| 128 GB | 约 16–25M 个向量 |
对于 3072 维嵌入,将这些数字除以约 2。
对于 m=32,容量也要除以约 2。
如果此规模下索引无法装入内存,请使用二进制量化。
这些是范围,不是保证值。请在负载下监控缓存驻留情况和 p95/p99 延迟,以验证效果。
二进制量化(适用于超大型数据集)
内存占用减少 32 倍。搭配重新排序使用,可获得可接受的召回率。
-- Table with generated column for binary quantization
CREATE TABLE items (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
contents TEXT NOT NULL,
embedding halfvec(1536) NOT NULL,
embedding_bq bit(1536) GENERATED ALWAYS AS (binary_quantize(embedding)::bit(1536)) STORED
);
CREATE INDEX ON items USING hnsw (embedding_bq bit_hamming_ops);
-- Query with re-ranking for better recall
-- ef_search must be >= inner LIMIT to retrieve enough candidates
SET hnsw.ef_search = 800;
WITH q AS (
SELECT binary_quantize($1::halfvec(1536))::bit(1536) AS qb
)
SELECT *
FROM (
SELECT i.id, i.contents, i.embedding
FROM items i, q
ORDER BY i.embedding_bq <~> q.qb -- computes binary distance using index
LIMIT 800
) candidates
ORDER BY candidates.embedding <=> $1::halfvec(1536) -- computes halfvec distance (no index), more accurate than binary
LIMIT 10;
80 倍过采样比例(为 10 个结果检索 800 个候选项)是一个合理的起点。二进制量化会损失精度,因此重新排序时需要更多候选项,才能找到真正的最近邻。若召回率不足,可增加候选项;若重新排序延迟过高,可减少候选项。
不同数据集规模下的性能
| 规模 | 向量数 | 配置 | 备注 |
|---|---|---|---|
| 小型 | <100K | 默认值 | 索引可选,但能改善尾延迟 |
| 中型 | 100K–5M | 默认值 | 监控 p95 延迟;这是最常见的生产环境规模 |
| 大型 | 5M+ | ef_construction=100+ | 内存驻留至关重要 |
| 超大型 | 10M+ | 二进制量化 + 重新排序 | 如有可能,优先增加 RAM 或进行分区 |
首先调整 ef_search 以改善召回率;只有在召回率趋于平稳且内存允许时,才增加 m。在并发负载下,如果索引无法装入内存,尾延迟会飙升。二进制量化是最后的备选方案;优先考虑增加 RAM 或进行分区。
筛选最佳实践
筛选式向量搜索需要谨慎处理。根据筛选条件的选择性和查询形态,筛选可能导致提前终止(行数不足、结果缺失),也可能增加工作量(延迟)。
迭代扫描(筛选条件较严格时推荐)
默认情况下,如果存在 WHERE 子句,HNSW 可能会提前停止,从而导致结果数量少于预期。迭代扫描允许 HNSW 继续搜索,直到找到足够多符合筛选条件的行。
当筛选条件显著缩小结果集时,请启用迭代扫描。
-- Enable iterative scans for filtered queries
SET hnsw.iterative_scan = relaxed_order;
SELECT id, contents
FROM items
WHERE category_id = 123
ORDER BY embedding <=> $1::halfvec(1536)
LIMIT 10;
如果结果仍然较少,请增加扫描预算:
SET hnsw.max_scan_tuples = 50000;
权衡:增大 hnsw.max_scan_tuples 可以提高召回率,但可能会显著增加延迟。
无需迭代扫描的情况:
- 筛选条件匹配表中的很大一部分数据(选择性低)
- 通过 B-tree 索引进行预筛选
- 查询单个分区或部分索引
选择合适的筛选策略
筛选条件选择性高(少于约 10k 行) 在筛选列上使用 B-tree 索引,以便 Postgres 在 ANN 之前进行预筛选。
CREATE INDEX ON items (category_id);
基数低(不同值较少) 针对每个筛选值使用部分 HNSW 索引。
CREATE INDEX ON items
USING hnsw (embedding halfvec_cosine_ops)
WHERE category_id = 11;
筛选值多或数据集较大 按筛选键分区,使每个 ANN 索引保持较小。
CREATE TABLE items (
embedding halfvec(1536),
category_id int
) PARTITION BY LIST (category_id);
关键规则
- 匹配行数较少的筛选条件需要预筛选、分区或迭代扫描。
- 始终在真实负载下,通过测量 p95/p99 延迟和已访问元组数来验证筛选查询。
替代方案:使用 pgvectorscale 按标签筛选
对于带标签筛选的大型数据集,pgvectorscale 的 StreamingDiskANN 索引支持对 smallint[] 列建立筛选索引。标签与向量一同建立索引,从而无需承担 HNSW 后置筛选的准确率权衡,即可高效执行筛选搜索。有关设置详情,请参阅 pgvectorscale 文档。
批量加载
-- COPY is fastest; binary format is faster but requires proper encoding
-- Text format: '[0.1, 0.2, ...]'
COPY items (contents, embedding) FROM STDIN;
-- Binary format (if your client supports it):
COPY items (contents, embedding) FROM STDIN WITH (FORMAT BINARY);
-- Add indexes AFTER loading
SET maintenance_work_mem = '4GB';
SET max_parallel_maintenance_workers = 7;
CREATE INDEX ON items USING hnsw (embedding halfvec_cosine_ops);
维护
- 定期执行 VACUUM:在更新或删除后执行——过期条目可能会一直保留,直到清理完成
- 执行 REINDEX:如果高频变更后性能下降,可从头重建图
- 对于频繁删除的写入密集型工作负载,可考虑使用 IVFFlat,或通过 hypertables 按时间分区
监控与调试
-- Check index size
SELECT pg_size_pretty(pg_relation_size('items_embedding_idx'));
-- Debug query performance
EXPLAIN (ANALYZE, BUFFERS) SELECT id, contents FROM items ORDER BY embedding <=> $1::halfvec(1536) LIMIT 10;
-- Monitor index build progress
SELECT phase, round(100.0 * blocks_done / nullif(blocks_total, 0), 1) AS "%"
FROM pg_stat_progress_create_index;
-- Compare approximate vs exact recall
BEGIN;
SET LOCAL enable_indexscan = off; -- Force exact search
SELECT id, contents FROM items ORDER BY embedding <=> $1::halfvec(1536) LIMIT 10;
COMMIT;
-- Force index use for debugging
BEGIN;
SET LOCAL enable_seqscan = off;
SELECT id, contents FROM items ORDER BY embedding <=> $1::halfvec(1536) LIMIT 10;
COMMIT;
常见问题(症状 → 修复方法)
| 症状 | 可能原因 | 修复方法 |
|---|---|---|
| 查询未使用 ANN 索引 | 缺少 ORDER BY + LIMIT、运算符不匹配或发生隐式类型转换 | 使用与索引操作符类匹配的距离运算符进行 ORDER BY;显式转换查询向量 |
| 结果少于预期(筛选查询) | HNSW 因筛选条件而提前停止 | 启用迭代扫描;增大 hnsw.max_scan_tuples;或进行预筛选(B-tree)、使用部分索引或分区 |
| 结果少于预期(未筛选查询) | ANN 召回率过低 | 增大 hnsw.ef_search |
| CPU 使用率低但延迟高 | HNSW 索引未驻留在内存中 | 使用 halfvec,减小 m/ef_construction,增加 RAM,进行分区,或使用二进制量化 |
| 索引构建缓慢 | 构建内存或并行度不足 | 增大 maintenance_work_mem 和 max_parallel_maintenance_workers;在批量加载后构建 |
| 内存不足错误 | 索引过大,超出可用 RAM | 使用 halfvec,减小索引参数,或改用二进制量化并重新排序 |
| 结果为空或缺失 | 向量为 NULL 或零向量 | 避免使用 NULL 嵌入;使用余弦距离时不要使用零向量 |