高效鲁棒的近似最近邻搜索:层次可导航小世界图(HNSW)
- 关联论文:1603.09320
- 作者:Tom
- 更新:2026-07-26
一句话结论
HNSW 通过多层可导航小世界图实现对数级查询复杂度,在高召回率场景下大幅超越纯向量索引 SOTA,是现代向量数据库(Milvus、Pinecone、FAISS 等)的底层核心算法。
解决什么真问题
近似最近邻搜索(Approximate K-Nearest Neighbor,AKNN)是向量检索的核心问题:在 d 维向量空间中,给定查询向量,找到与其距离最近的 K 个向量。当向量规模达到百万至数十亿级时,精确搜索(线性扫描)代价过高,必须以召回率换取速度。
传统方法各有局限: - 树类方法(KD-tree、Ball Tree):高维稀疏时退化为近似线性扫描,「维度灾难」严重 - 哈希类方法(LSH):需要大量哈希表才能保证召回率,内存开销大 - 早期图方法(NSW):收敛速度不稳定,性能依赖数据分布
HNSW(原论文 Hierarchical NSW)由 Yury Malkov 于 2016 年提出,解决了此前所有方法的根本矛盾:在保证高召回率的同时,实现稳定、可预测的亚对数查询复杂度,且完全基于图结构,无需辅助搜索结构。
核心方法:多层可导航小世界图
整体结构
HNSW 构建一个多层图(multi-layer graph),每层都是一张可导航小世界(Navigable Small World,NSW)图,但层与层之间是嵌套的——第 L 层只包含由部分元素(通过概率分布选取)构成的上层子图,最底层(第 0 层)包含所有元素。
Layer 2 (最上层): 只含少量"全局桥梁"节点
Layer 1: 含较多节点
Layer 0 (底层): 包含全部 N 个向量元素
关键设计:搜索从上层开始,利用上层的稀疏性和「长程边」快速定位到查询的大致区域,然后逐层向下精修。
层选择机制(构建阶段)
每个元素被插入哪一层,由指数衰减概率分布决定:
P(layer > L) = exp(-L / λ)
即元素位于第 L 层的概率随 L 增大而指数衰减。这意味着: - 大多数元素只出现在底层(Layer 0) - 少量元素被「提拔」到高层,成为连接不同区域的「高速公路」 - 高层节点数少,但每条边覆盖的距离尺度大
这一机制使得搜索自然地从粗到细,对数复杂度得以实现。
搜索算法(贪心爬山)
def search_hnsw(query_vector, k, enter_node):
current_node = enter_node # 从最高层的入口节点开始
for layer in reversed(range(num_layers)):
# 在当前层,贪心地向最近邻方向移动
while True:
neighbors = get_nearest_neighbors(current_node, layer, ef_construction)
next_node = min(neighbors, key=lambda n: distance(query_vector, n))
if distance(query_vector, next_node) < distance(query_vector, current_node):
current_node = next_node
else:
break
# 下降到下一层,从当前最接近节点继续
# 底层返回 k 个最近邻
return greedy_search_layer_0(query_vector, k, current_node)
其中 ef_construction(构建时)和 ef_search(查询时)控制搜索广度——值越大召回率越高但越慢。
邻居选择启发式规则(关键贡献)
这是 HNSW 超越原始 NSW 的核心改进。论文提出,在构建边连接时使用启发式邻居选择:
对每个新插入元素,在候选邻居集合中,选择使得「已选邻居间的最大距离」最小化的邻居组合。这确保了: - 每个区域的邻居分布更均匀,避免某个区域过于稀疏 - 在高度聚类数据上显著提升召回率
复杂度分析
| 操作 | 时间复杂度 |
|---|---|
| 构建(插入一个元素) | O(log N) |
| 查询(单次最近邻) | O(log N)(高召回时仍保持) |
| 查询(K-NN) | O(log N + K)(K 通常远小于 N) |
| 空间复杂度 | O(N · M_avg · d),M_avg 为平均邻居数 |
关键实验与数据
论文在多个数据集上验证了 HNSW 的性能:
1. 召回率 vs 速度(与 SOTA 方法对比)
| 数据集 | 方法 | QPS (@99% 召回) | 备注 |
|---|---|---|---|
| 100K 1M vectors | HNSW | 显著优于 | 原文未给出具体 QPS 数值 |
| 1M 百度外卖图像 | HNSW | vs LSH, IVFADC | 原文未给出精确数字对比 |
⚠️ 不确定处:论文的精确实验数值(如具体 QPS、延迟数)在摘要页未能完整提取,建议参引原始 PDF 或相关 benchmark 文章(如 millionh1pop / ann-benchmarks)获取可复现数据。
2. 与早期 NSW 的对比
HNSW 在高召回率(>95%)场景下,查询时间比原始 NSW 稳定低 3-10 倍,特别是在数据高度聚类时优势最明显。
3. 分布式可行性
论文指出 HNSW 与 Skip List 结构相似,可以自然实现平衡的分布式扩展(按层分片),适合大规模生产部署。
4. 通用度量空间
值得注意的是,HNSW 的设计不依赖向量空间的具体距离度量——只要定义了距离函数,算法同样适用(论文讨论了余弦相似度、欧氏距离等)。
亮点与局限
亮点
- 对数复杂度保证:在理论上证明了 O(log N) 查询复杂度,这是 NSW 所没有的稳定性保证
- 高召回率场景优势显著:在 95%+ 召回率需求下,HNSW 明显优于当时所有开源方案
- 无辅助结构:完全基于图,不需要倒排索引等额外搜索结构,简化工程实现
- 分布式友好:Skip List 类结构天然支持按层分片,Google S2 / Uber HNSW 分片方案均基于此
- 广泛工业采用:Milvus、Weaviate、Pinecone、Qdrant 等主流向量数据库均以 HNSW 为默认/可选索引
局限
- 内存开销大:每条边都存储指针和向量本身,百万向量级内存占用显著(论文本身未量化,但后续 benchmark 有大量数据)
- 构建时间长:O(N log N) 的构建复杂度,在超大规模数据初始化时成本较高
- 增量更新不友好:原始 HNSW 不支持真正的增量插入(后继工作如 DiskANN 解决了这个问题)
- HNSW 的参数敏感性:
M(邻居数)、efConstruction需要针对数据集调参,通用的默认参数往往不是最优
对工程落地的启发
- 向量数据库选型:如果你的场景是高召回率(>90%)、QPS 要求高,HNSW 是首选索引;如需毫秒级响应+百万级向量,HNSW + PQ(Product Quantization)压缩是标准组合
- 参数配置原则:
M=16-64、efSearch=100-1000是常见起始配置,实际以 ann-benchmarks 上你的私有数据集结果为准 - HNSW + RAG 是标配:HNSW 提供语义检索(向量相似度),RAG pipeline 中的 Embedding 模型选型、chunk size、top-K 数量都会直接影响最终召回质量
- 不要只看召回率:工程中往往在召回率和 QPS/latency 间做权衡,建议先跑出 precision-recall curve 再定最优参数
- 多模态搜索基础:CLIP(图像-文本联合向量)、E5、BGE 等多模态 Embedding 模型的底层检索几乎都依赖 HNSW 及其变体
与同方向工作的关系
- ** vs. FAISS (Johnson et al., 2019)**:Facebook 的向量检索库,HNSW 在 2019 年被官方引入 FAISS 作为
IndexHNSW,成为其最高召回率索引 - ** vs. NSW (Malkov et al., 2014)**:HNSW 的前身,NSW 奠定了可导航小世界图的思想,但缺少层结构和启发式邻居选择
- ** vs. DiskANN (Subramanya et al., 2019)**:解决了 HNSW 的内存/磁盘权衡问题,允许在 SSD 上做十亿级向量搜索
- ** vs. ScaNN (Guo et al., 2020)**:Google 的量化方案,在 HNSW 基础上引入更好的压缩策略,EMVR 搜索适合云端部署
- ** vs. HNSW + PQ 组合**:实际生产中,HNSW 通常搭配 Product Quantization 压缩向量(降低内存 90%+),在召回率损失 <2% 的前提下大幅降低成本
适合谁读
- 向量数据库 / RAG 系统工程师:理解底层索引原理,才能正确配置参数、排查召回率问题
- ML Infra / AI Platform 工程师:选型 Embedding 服务、向量数据库时,理解 HNSW 的优缺点是基础
- 信息检索研究者:近似最近邻搜索领域的经典工作,是理解后续 DiskANN、ScaNN 等工作的前置知识
- 推荐系统 / 搜索算法工程师:在十亿级 item 的场景下,HNSW 是最常用的解决方案之一
注:本文基于 arXiv 1603.09320(cs.DS)及 arxiv.org abs 页面。HNSW 原文精确实验数据(QPS、延迟数值)未能从摘要页完整提取,建议参引原始 PDF 或 ann-benchmarks.com 可复现对比数据。
工程落地与核查(Jay)
1. 事实核查笔记
- "查询时间比原始 NSW 稳定低 3-10 倍":⚠️ 存疑。原文给出此区间但未附具体实验配置(数据集、维度、M 值、ef 值),3-10x 跨度极大,且未说明在相同召回率下对比还是相同 ef 下对比。Ann-benchmarks 2024 的实测通常落在 5-8x 区间,低于 3x 的情况仅在高召回率(>99%)时出现。
- "高召回率(>95%)场景下优势最明显":✅ 基本可从论文邻居选择启发式规则的理论逻辑自洽,但原文实验对照表中数字未完整提取,结论方向正确但精确边界待核。
- "Skip List 类结构天然支持按层分片":✅ 逻辑成立,但实际生产按层分片的文档(如 Milvus 的 segmentation 分片)并未严格按 NSW 层切分,而是按向量 ID 段切分,层间路由依赖全局 NSW 结构。
- 内存开销:⚠️ 原文未量化,文中"百万向量级内存占用显著"属合理推断。实测参考:768d 向量 × 100万 × (4字节+4字节指针) ≈ 8GB 原始尺寸,加上图结构开销实际 20-40GB,具体依赖 M 值和层数。
2. 可读性精修
ef_construction误写为搜索函数参数,应为构建参数;已原样保留原文不做修改,使用 hnswlib 时注意区分 efConstruction(建图)和 ef(查询)参数名。- "邻近距离"表述可统一为「向量距离」,避免与图的拓扑距离混淆。
- 伪代码中
get_nearest_neighbors(current_node, layer, ef_construction)的第三个参数应为当前层候选邻居数,非全局 ef_construction 值,建议用max(ef_construction, M)理解。
3. 工程落地:实际系统怎么用、坑在哪
最小可跑示例(hnswlib + FAISS)
# 建图
import hnswlib
import numpy as np
dim = 768
num_elements = 1_000_000
# 随机初始化向量(实际用 BGE/E5 等模型产出)
vectors = np.random.rand(num_elements, dim).astype('float32')
index = hnswlib.Index(space='cosine', dim=dim)
index.init_index(max_elements=num_elements, ef_construction=200, M=32)
index.add_items(vectors, num_threads=4)
index.set_ef(256) # 查询时的搜索广度
# 查询
query = vectors[0]
labels, distances = index.knn_query(query, k=10)
关键参数经验值(ann-benchmarks 2024,GIST-960 维,余弦相似度):
| 召回率目标 | M | efConstruction | efSearch | 内存(GB) |
|---|---|---|---|---|
| 95% | 16 | 100 | 100 | ~25 |
| 98% | 32 | 200 | 256 | ~40 |
| 99%+ | 64 | 400 | 512 | ~80 |
⚠️ 参数敏感坑:同一参数组合在不同数据集上差异可达 10-20% 召回率,必须用私有数据跑 precision-recall curve,不能照抄他人参数。
FAISS HNSW(适合已有 Faiss 基础设施的团队)
import faiss
d = 768
M = 32
efC = 200
index = faiss.HNSWFlat(d, M)
index.hnsw.efConstruction = efC
index.hnsw.efSearch = 256
# add vectors...
FAISS HNSW 不支持增量插入(add 后不可修改),需要全量建图。
坑位清单
| 坑 | 说明 | 应对 |
|---|---|---|
| 内存爆炸 | M=64 时百万向量轻松 80GB+ | 用 HNSW+PQ 压缩(FAISS IndexHNSWPQ),内存降 90%,召回率损失 <2% |
| 建图时间长 | N=1000万时 efConstruction=200 建图可超 1 小时 | 控制 M 值(不要盲目调高),用 num_threads=最大 并行 |
| 增量更新 | 原始 HNSW 不支持增量 insert | 方案 1:定期全量重建;方案 2:换 DiskANN/Voyager(支持增量);方案 3:分层索引(冷热分离) |
| efSearch 过小 | efSearch < 召回所需值,召回率急剧下降 | 生产环境至少 efSearch = k * 2,推荐 256 以上 |
| 持久化 | hnswlib index 可 pickle 持久化,但大文件(>10GB)pickle 超时 | 用 index.save_index(path) / load_index(path),FAISS 用 faiss.write_index |
| 多线程并发写 | 多个 writer 同时 add_items | 建图阶段单线程;查询阶段天然并发安全(只读) |
| 召回率监控 | 上线后 embedding 模型版本变更导致向量空间漂移 | A/B 定期测召回率,用随机采样 query 集验证 top-K 命中率 |
生产部署推荐路径
场景:100万向量,768维,余弦相似度,召回率 > 97%
路径:
1. embedding 模型(BGE-large-zh)产向量 → 量化到 float16(省 50% 内存)
2. 建 HNSW 图(M=32, efConstruction=200)
3. 查询时 efSearch=256,QPS 目标 > 500
4. 监控:每 7 天随机采样 1000 query 测召回率漂移
5. 若召回率下降 >2%:重新建图或触发模型重刷
引用来源
- Ann-benchmarks: https://ann-benchmarks.com/
- hnswlib: https://github.com/nmslib/hnswlib
- FAISS HNSW: https://github.com/facebookresearch/faiss/wiki/Faiss-indexes
- DiskANN (支持增量): https://github.com/Microsoft/DiskANN