HNSW 向量索引为什么又快又准?M、efConstruction、efSearch 调优实战
向量库已经建好,查询也从秒级降到了毫秒级,但召回率忽高忽低;把参数调大,又发现内存和 P95 延迟一起上涨——这通常不是模型问题,而是 HNSW 的三个核心参数没有按阶段调。
本文不背参数表,而是从一次查询如何在图中移动讲起,再给出一套可复现的 pgvector 建索引、检索与 Recall@K 调优流程。
一、HNSW 到底解决了什么问题?
有 N 条 d 维向量时,精确检索需要计算查询向量与大量候选的距离,朴素代价近似为 O(Nd)。它结果最准确,却很难在千万级数据和高并发下保持低延迟。
HNSW(Hierarchical Navigable Small World)把向量组织成“可导航的小世界图”:相似向量互相连边,再叠加多层结构。上层节点少、边跨度大,用来快速接近目标区域;越往下节点越密,最终在 Layer 0 做精细搜索。它牺牲极少量精确性,换取显著减少的距离计算次数。
图 1:HNSW 的“高速路 + 街区”结构。原创教学图(Image2 生成),用于解释层级、入口点和下降路径。
一个好记的类比是:上层像城市高速路,负责跨区域跳转;底层像街区道路,负责找到具体门牌。没有上层,搜索容易在局部图里走很久;底层不够连通,又可能错过真正的近邻。
pgvector 官方也明确说明,HNSW 创建的是多层图;与 IVFFlat 相比,它通常有更好的速度—召回率权衡,但建索引更慢、占用更多内存,而且不需要预先训练。
图 2:pgvector README 中的 HNSW 说明。来源:pgvector 项目,原始页面:https://github.com/pgvector/pgvector
二、一次查询是怎样跑完的?
查询从最高层入口点开始,每一步比较当前节点的邻居,向距离查询向量更近的节点移动;当本层无法继续改善时,就从当前节点下降一层。到 Layer 0 后,不再只保留单一路径,而是维护一个候选队列,扩展更多近邻,最后返回距离最小的 Top-K。
图 3:从入口点到 Top-K 的搜索路径。原创教学图(Image2 生成)。
这里最关键的是:HNSW 不是“沿一条线走到底”。上层采用近似贪心导航,底层通过候选队列保留多条可能路径。候选越多,越不容易被局部最优误导,但需要计算的距离也越多。
三、M、efConstruction、efSearch 分别控制什么?
1. M:图的连接密度
M控制每层节点允许保留的最大连接数。M 较大时图更连通,搜索有更多路线,召回上限通常更高;代价是索引更大、构建和查询时需要处理更多边。它属于索引结构参数,修改后通常需要重建索引。
2. efConstruction:建图时看多远
efConstruction是插入节点、选择邻居时使用的动态候选列表大小。数值越大,建图时比较的候选越多,图质量通常更高,但建索引时间和写入成本也会上升。它影响的是图的先天质量,不能靠查询时临时修改来补全所有结构缺陷。
3. efSearch:每次查询看多少候选
efSearch是查询阶段的动态候选列表大小,也是最适合在线调节的旋钮。增大它通常提高召回率,同时增加查询延迟。pgvector 默认值为 40,可在事务中用SET LOCAL只调整当前查询。
图 4:pgvector 官方给出的 M、ef_construction 与 hnsw.ef_search 含义及默认值。来源:pgvector 项目,原始页面:https://github.com/pgvector/pgvector#hnsw
图 5:三个参数的生效阶段与主要代价。原创教学图(Image2 生成)。
一句话区分:M 决定路网密度,efConstruction 决定修路认真程度,efSearch 决定这次导航愿意探索多少条路。
四、用 pgvector 跑通一个可调优示例
下面以 768 维余弦距离为例。索引操作符类与查询运算符必须匹配:vector_cosine_ops对应余弦距离运算符<=>。
CREATEEXTENSIONIFNOTEXISTSvector;CREATETABLEdocuments(id bigserialPRIMARYKEY,titletextNOTNULL,embedding vector(768)NOTNULL);CREATEINDEXdocuments_embedding_hnswONdocumentsUSINGhnsw(embedding vector_cosine_ops)WITH(m=16,ef_construction=100);ANALYZEdocuments;查询时把目标向量绑定为参数,不要把长向量直接拼接进 SQL:
BEGIN;SETLOCALhnsw.ef_search=100;SELECTid,title,1-(embedding<=>$1::vector)AScosine_similarityFROMdocumentsORDERBYembedding<=>$1::vectorLIMIT10;COMMIT;注意:要让 HNSW 索引参与近邻查询,通常需要ORDER BY使用距离运算符本身并配合LIMIT。不要只写ORDER BY 1 - distance DESC后就默认规划器一定能识别为同一种索引排序。
五、正确调参:先建立精确基准,再扫 efSearch
没有“标准答案参数”。向量分布、维度、数据量、Top-K、过滤比例、并发量与硬件都会改变最优点。可靠方法是用业务查询集画出召回率—延迟曲线。
定义 Recall@K:
Recall@K = |近似检索 Top-K ∩ 精确检索 Top-K| / K建议按以下顺序测试:
- 从真实流量抽取有代表性的查询向量,至少覆盖热门、长尾与过滤查询。
- 用精确检索生成每个查询的 Top-K 基准答案。
- 固定 M 和 efConstruction,依次测试 efSearch=40、80、120、200。
- 同时记录 Recall@K、P50/P95/P99、QPS、CPU、索引大小和构建时间。
- 找到满足召回目标的最小 efSearch;若继续增大仍到不了目标,再提高 M 或 efConstruction 并重建索引。
可作为第一轮实验起点,而不是生产环境定论:
| 场景 | M | efConstruction | efSearch 扫描区间 |
|---|---|---|---|
| 百万级、内存敏感 | 16 | 64–100 | 40 / 80 / 120 |
| 更重视召回率 | 24–32 | 100–200 | 80 / 120 / 200 |
| Top-K 较大 | 先从 16–24 开始 | 100–200 | 不低于 K,并继续向上扫描 |
六、带过滤条件时,为什么结果可能突然变少?
普通 HNSW 先沿向量图找候选,再应用属性过滤时,候选可能大量被过滤掉。过滤越严格,最终不足 K 条的概率越高。此时单纯增加 efSearch 有时有效,但会增加延迟;更稳妥的方案还包括为过滤字段建立 payload/关系索引、使用分区,以及采用支持过滤感知或迭代扫描的实现。
图 6:Qdrant 官方对过滤搜索与 HNSW 图连通性问题的说明。来源:Qdrant Documentation,原始页面:https://qdrant.tech/documentation/manage-data/indexing/#filterable-hnsw-index
在 pgvector 中,过滤通常在索引扫描后应用。新版 pgvector 提供 iterative index scans,可在结果不足时继续扫描;但启用前仍应先给高选择性过滤字段建立普通索引,并检查执行计划。
七、最常见的五个误区
- 只看延迟,不测召回率:10 ms 的错误结果没有业务价值。
- efSearch 小于 Top-K 还期待稳定结果:至少让候选规模覆盖 K,再用实测决定余量。
- 修改 M 后不重建索引:M 和 efConstruction 是建图阶段参数。
- 把参数调大当成必然线性收益:收益会递减,延迟和内存却继续增加。
- 忽略过滤条件与数据更新:过滤、删除、写入和数据分布漂移都会改变曲线。
总结
HNSW 的速度来自分层导航,准确性来自底层候选扩展。调优时牢记三个层次:
- M 决定图的连接能力与内存成本;
- efConstruction 决定建图质量与构建成本;
- efSearch 决定单次查询的召回率—延迟取舍。
最实用的顺序是:**精确检索建立基准 → 扫描 efSearch → 观察召回与 P95 → 召回上限不足时再调整 M、efConstruction 并重建。**这样得到的不是“网上推荐参数”,而是适合自己数据的可验证配置。