外观
实测对比:500万向量下HNSW召回与速度权衡
问题背景:HNSW 不是建完索引就万事大吉
在 PostgreSQL 中使用 pgvector 做语义检索时,很多团队第一次压测都会遇到同一个矛盾:数据量涨到几百万后,精确最近邻查询(顺序扫描)延迟不可接受;切到 HNSW 索引后,P95 延迟明显下降,但业务又反馈“相似结果不稳定”“边缘案例召回变差”。
HNSW(Hierarchical Navigable Small World)的核心思想,是在向量集合上构建多层近邻图。查询时从稀疏的上层快速定位,再在下层逐步搜索候选邻居。它天然是“近似最近邻”算法:速度来自提前停止,召回率来自扩大候选集合。因此,调优不是单纯把某个参数调大,而是在以下四个指标之间找平衡:
- 召回率:Top-K 结果与精确检索结果的重合程度;
- 查询延迟:尤其关注 P95/P99,而不是只看平均耗时;
- 构建成本:索引创建时间、CPU、WAL 与维护开销;
- 存储空间:图边数量越多,索引体积越大。
本文基于 500 万条 768 维向量的模拟压测,总结 pgvector HNSW 在 PostgreSQL 实战中的参数权衡方法。示例 SQL 可直接运行,实际数值会受 CPU、磁盘、缓存、向量分布和过滤条件影响,建议重点复现实验方法,而不是照搬数字。
方案对比:HNSW 与 IVFFlat 应该怎么选
pgvector 常见索引类型主要有 HNSW 与 IVFFlat。IVFFlat 先把向量聚类成多个 list,查询时只扫描最近的若干簇;HNSW 则维护近邻图,通过图遍历获得更高的查询质量。
在 500 万向量规模下,如果业务追求低延迟、高召回,并且能够接受更高的构建成本,HNSW 通常是更稳妥的默认选择。IVFFlat 更适合预算敏感、数据分布较均匀、可以接受通过 probes 调节召回的场景。
简单概括:
- HNSW:查询召回高、延迟稳定,适合在线语义搜索、RAG 召回、推荐候选集;
- IVFFlat:索引较轻,但召回依赖聚类质量和 probes,数据分布偏斜时容易漏簇;
- 顺序扫描:可作为评测基线,不适合在线高并发路径。
实操步骤:构造可复现的 500 万向量测试集
先安装扩展并建立测试表。为了让示例更容易运行,下面使用 32 维向量;正式压测建议替换为真实 embedding 维度,例如 768 或 1024 维。
sql
-- 1. 安装 pgvector 扩展
CREATE EXTENSION IF NOT EXISTS vector;
-- 2. 删除旧测试表(重复执行时使用)
DROP TABLE IF EXISTS doc_vector_test;
-- 3. 创建向量表
CREATE TABLE doc_vector_test (
id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
tenant_id int NOT NULL,
embedding vector(32) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
-- 4. 生成 500 万条随机向量
-- 正式评测时建议替换为真实 embedding,随机均匀数据通常比真实数据更容易召回
INSERT INTO doc_vector_test(tenant_id, embedding)
SELECT
(random() * 10)::int + 1,
(
SELECT COALESCE(
string_agg((ROUND((random() * 2 - 1)::numeric, 6))::text, ','),
''
)
FROM generate_series(1, 32)
)::vector
FROM generate_series(1, 5000000) AS s(n);
-- 5. 分析表统计信息
ANALYZE doc_vector_test;随机向量只能验证索引机制,不代表真实业务分布。真实 embedding 往往存在主题聚集、热点簇和离群点,召回率可能明显不同。因此,最好从生产脱敏数据中抽取一份固定评测集,并冻结查询向量。
建立 HNSW 索引:m 与 ef_construction 的影响
pgvector 创建 HNSW 索引时,最关键的两个参数是 m 和 ef_construction:
m:每个节点在图中的最大连接数,影响图的连通性和索引大小;ef_construction:建索引时维护的候选队列大小,值越大,建图质量通常越高,但构建越慢;vector_cosine_ops、vector_l2_ops、vector_ip_ops必须与查询距离算子一致。
sql
-- 平衡型配置:在线业务常见起点
SET maintenance_work_mem = '4GB';
CREATE INDEX idx_doc_vector_hnsw
ON doc_vector_test
USING hnsw (embedding vector_cosine_ops)
WITH (
m = 16,
ef_construction = 128
);在同一台测试机上,500 万条 768 维向量的大致经验是:m=16、ef_construction=128 往往是较均衡的起点;将 m 提高到 32 或 48,召回上限会提升,但索引体积和构建时间也会明显增加;ef_construction 提到 256 能改善图质量,却不一定能线性改善所有查询的召回。
一个常见误区是只在建索引时把 ef_construction 调得很大,却不调整查询期 hnsw.ef_search。实际上,ef_construction主要决定图的构建质量,运行时能搜索多深,还受 hnsw.ef_search 控制。
查询调优:用 hnsw.ef_search 交换召回率与延迟
查询参数可以在会话级设置,不需要重建索引:
sql
-- 关闭顺序扫描,强制观察 HNSW 路径(仅压测使用)
SET enable_seqscan = off;
-- 查询候选队列,值越大通常召回越高、延迟越高
SET hnsw.ef_search = 80;
EXPLAIN (ANALYZE, BUFFERS)
SELECT id
FROM doc_vector_test
ORDER BY embedding <=> (
SELECT embedding
FROM doc_vector_test
WHERE id = 123456
)
LIMIT 20;在 500 万向量压测中,可以按阶梯方式测试 hnsw.ef_search = 40、80、160、320。经验上,Top-20 查询从 40 提到 160,召回率提升通常最明显;继续提到 320 后,边际收益变小,但延迟和共享缓冲区访问量仍会上升。若在线接口要求 P95 低于 50ms,而 ef_search=160 已满足 95% 以上召回,就没有必要盲目调到 320。
还可以针对不同业务路径设置不同参数:
sql
-- 在线搜索:优先低延迟
BEGIN;
SET LOCAL hnsw.ef_search = 80;
SELECT id
FROM doc_vector_test
ORDER BY embedding <=> $1
LIMIT 20;
COMMIT;
-- 离线评测或高召回任务:优先准确性
BEGIN;
SET LOCAL hnsw.ef_search = 320;
SELECT id
FROM doc_vector_test
ORDER BY embedding <=> $1
LIMIT 100;
COMMIT;如何科学测量召回率
不要凭“结果看起来差不多”评估 HNSW。应先使用精确查询生成基线,再比较近似查询结果。pgvector 中可以通过提高代价阈值、临时关闭索引扫描或使用独立评测表获得精确结果。简单压测时,可对少量查询使用顺序扫描。
sql
-- 生成精确 Top-20 基线
EXPLAIN (ANALYZE, BUFFERS)
SELECT id
FROM doc_vector_test
ORDER BY embedding <=> (
SELECT embedding FROM doc_vector_test WHERE id = 123456
)
LIMIT 20;建议使用固定的 100 到 1000 条查询向量,分别记录:
- 精确 Top-K 的 ID 集合;
- HNSW Top-K 的 ID 集合;
- 交集数量除以 K,得到 Recall@K;
- 平均延迟、P95、P99;
- 索引大小、构建时长和缓冲命中率。
如果业务还有 tenant_id、时间范围或状态过滤,要评测的是“过滤后的召回率”,而不是无过滤召回率。HNSW 返回的是向量近邻,再由执行阶段过滤不满足条件的记录;当过滤条件选择率很低时,可能需要扫描更多候选才能补足 Limit。
sql
-- 带租户过滤的真实查询形态
BEGIN;
SET LOCAL hnsw.ef_search = 160;
SELECT id
FROM doc_vector_test
WHERE tenant_id = 3
AND created_at >= now() - interval '90 days'
ORDER BY embedding <=> $1
LIMIT 20;
COMMIT;参数选择建议:从默认值到业务最优值
在多数 500 万级向量场景中,可以按下面路径推进:
- 初始建索引:
m=16、ef_construction=128; - 在线查询:从
hnsw.ef_search=80开始压测; - 召回不足:先提高
ef_search到 160,再评估是否重建为m=24/32; - 延迟过高:优先降低
ef_search,并检查过滤条件、内存命中和执行计划; - 索引构建慢:合理提高
maintenance_work_mem,但不要超过机器可用内存; - 数据持续写入:关注增量写入后的图质量,必要期使用
REINDEX重建。
参数没有全局最优值。客服机器人这类短查询链路可能更看重 P95;法务、医疗或知识库问答可能更看重召回;推荐系统还可能用向量检索只做粗排,再由业务规则重排。
避坑总结:HNSW 调优最容易踩的 5 个坑
1. 距离算子建错,索引没有被使用
错误现象:ORDER BY embedding <=> $1 使用余弦距离,但索引建成了 vector_l2_ops,执行计划退化为顺序扫描,延迟从几十毫秒变成数秒。修正方法是保持索引算子与排序算子一致:
sql
-- 余弦距离对应 vector_cosine_ops
CREATE INDEX idx_doc_vector_cosine
ON doc_vector_test USING hnsw (embedding vector_cosine_ops);
-- L2 距离对应 vector_l2_ops
CREATE INDEX idx_doc_vector_l2
ON doc_vector_test USING hnsw (embedding vector_l2_ops);2. 只调 ef_construction,不调 hnsw.ef_search
错误现象:建索引时设置了很高的 ef_construction,但在线召回仍不稳定。原因是建图质量和查询搜索深度是两件事。修正方法是先用固定评测集测试不同 hnsw.ef_search,找到召回与延迟的拐点;只有当提高 ef_search 仍无法达到召回目标时,再考虑提高 m 或重建索引。
3. 带高选择性过滤后仍直接套用无过滤参数
错误现象:无过滤查询 P95 很低,加上 tenant_id 和时间范围后,结果数量不足或延迟升高。这是因为近邻候选中大量记录被过滤,索引需要访问更多候选。修正方法是按租户大小分层评测,小租户可提高 ef_search,热点大租户可考虑分区、局部索引或业务上缩小召回范围,而不是无限增大全局参数。
4. maintenance_work_mem 设置过大导致生产实例内存压力
错误现象:重建索引时构建速度变快,但数据库出现内存抖动、缓存被挤压,甚至影响在线请求。修正方法是在低峰期重建,并根据实例内存、并发维护任务和 work_mem 综合设置,而不是直接把 maintenance_work_mem 调到机器内存的 80%。构建完成后继续观察缓存命中率与磁盘 IO。
5. 用随机向量结果代替真实业务结论
错误现象:随机数据上召回率接近满分,上线后面对真实文本 embedding 却出现主题误召回。随机均匀向量缺少真实数据的簇结构和离群点,只能验证功能。修正方法是使用生产脱敏样本、固定查询集和真实过滤条件,并同时记录 Recall@K、P95、索引体积与构建耗时。
最终结论
pgvector 的 HNSW 调优,本质是用图连接数、建图候选集和查询候选集,换取不同阶段的成本收益。500 万向量下,推荐先用 m=16、ef_construction=128 建索引,以 hnsw.ef_search=80 建立压测基线,再围绕真实 Top-K 和过滤条件逐步调整。
真正可靠的参数不是来自某个“万能配置”,而是来自可复现的召回评测、P95 延迟监控和持续的索引维护。只要把精确基线、阶梯压测和业务分层策略固定下来,HNSW 就能在 PostgreSQL 中同时承担好用、可运维且性能够强的向量检索角色。
免责声明
本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。