HNSW 索引深度剖析:为什么它是向量检索的"王者算法"?👑
🔥 本文是《向量数据库实战:选型、调优与落地》专栏第 04 篇
⏱️ 阅读时间:约 14 分钟
🎯 开篇:为什么你需要理解索引算法?
一个真实的故事 📖
某团队花了一周搭建向量数据库,导入了 500 万条数据。结果查询延迟高达 3 秒!
排查了一圈,发现是索引参数没调好。改了 HNSW 的
ef参数后,延迟直接降到 8 毫秒。300 倍的性能提升,只改了两个数字。
这就是理解索引算法的价值 💎
🧠 暴力搜索的问题
在讲 HNSW 之前,先理解"暴力搜索"(Flat/Brute-Force)为什么不行。
暴力搜索 :查询时,把目标向量和数据库里每一个向量都算一遍距离。
数据量 = 100 万,向量维度 = 1024
暴力搜索的计算量:
= 100万 × 1024 次乘法
= 10.24 亿次运算 / 每次查询
→ 延迟:约 500ms~2s(取决于硬件)
这完全不可接受! 我们需要一种"聪明"的方法来减少计算量。
👑 HNSW:分层可导航的小世界
核心思想
HNSW (Hierarchical Navigable Small World)= 分层 + 小世界图 + 可导航
名字很长,但核心思想很简单 👇
想象你在一个陌生的城市找一家餐厅:
第 1 步(高层):看全国地图
→ "餐厅在华东地区" ← 快速定位到大范围
第 2 步(中层):看城市地图
→ "餐厅在浦东新区" ← 缩小范围
第 3 步(底层):看街道地图
→ "餐厅在陆家嘴环路 88 号" ← 精确定位
HNSW 就是这个思路!
用多层图结构,从粗到细快速定位目标向量
结构图解
┌─────────────────────────────────────────────────────────────┐
│ HNSW 多层图结构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Layer 2(最稀疏): │
│ A ─────────────────── D │
│ │ │ │
│ Layer 1(中等密度): │
│ A ────── B ────── D ────── F │
│ │ │ │ │ │
│ Layer 0(最密集,包含所有节点): │
│ A ─── B ─── C ─── D ─── E ─── F ─── G ─── H │
│ │ │ │ │ │ │ │ │ │
│ └─────┴─────┴─────┴─────┴─────┴─────┴─────┘ │
│ │
│ 搜索过程(查询 = ★): │
│ │
│ Layer 2: 从入口节点 A 出发,发现 D 更近 → 跳到 D │
│ Layer 1: 从 D 出发,发现 F 更近 → 跳到 F │
│ Layer 0: 从 F 出发,逐步搜索附近节点 → 找到最近邻 │
│ │
│ 总计算量:远小于暴力搜索! │
│ │
└─────────────────────────────────────────────────────────────┘
为什么叫"小世界"?
小世界网络 的特点是:任意两个节点之间,只需要很少的跳数就能到达。
就像"六度分隔理论"------世界上任意两个人之间,最多通过 6 个人就能联系起来。
HNSW 利用这个特性:
- 高层的"长边"连接远距离节点 → 快速跳跃
- 底层的"短边"连接近距离节点 → 精细搜索
两者结合,既快又准! ⚡
⚙️ 关键参数详解
HNSW 有几个核心参数,直接决定性能和精度的平衡:
参数一览表
| 参数 | 含义 | 典型值 | 影响 |
|---|---|---|---|
| M | 每个节点的最大连接数 | 16~64 | 越大 → 精度↑ 内存↑ 构建速度↓ |
| efConstruction | 构建索引时的搜索范围 | 128~512 | 越大 → 索引质量↑ 构建速度↓ |
| ef (efSearch) | 查询时的搜索范围 | 64~256 | 越大 → 召回率↑ 查询速度↓ |
| num_candidates | 搜索候选数量 | 与 ef 相关 | 越大 → 结果越准但越慢 |
参数调优指南
┌─────────────────────────────────────────────────────────┐
│ HNSW 参数调优决策树 │
├─────────────────────────────────────────────────────────┤
│ │
│ 你的首要目标是什么? │
│ │ │
│ ├── 🚀 查询速度优先(实时搜索) │
│ │ M=16, ef=64~128 │
│ │ → 牺牲一点精度,换取极致速度 │
│ │ │
│ ├── 🎯 召回率优先(不能漏结果) │
│ │ M=32~64, ef=256~512 │
│ │ → 牺牲速度,确保不遗漏 │
│ │ │
│ ├── ⚖️ 均衡(大多数场景) │
│ │ M=16~32, ef=128~256 │
│ │ → 精度和速度的最佳平衡点 │
│ │ │
│ └── 💾 内存敏感 │
│ M=8~16, efConstruction=64~128 │
│ → 减少连接数,降低内存占用 │
│ │
└─────────────────────────────────────────────────────────┘
📊 不同参数下的性能实测
以下是我在 100 万条 1024 维向量 上的实测数据 👇
| M | efConstruction | ef | 召回率@10 | 查询延迟 | 内存占用 | 构建时间 |
|---|---|---|---|---|---|---|
| 8 | 64 | 32 | 85.2% | 2.1ms ⚡ | 1.2 GB | 45s |
| 16 | 128 | 64 | 92.5% | 3.8ms | 2.1 GB | 90s |
| 16 | 256 | 128 | 96.8% | 6.2ms | 2.1 GB | 180s |
| 32 | 256 | 128 | 97.5% | 8.5ms | 3.8 GB | 320s |
| 32 | 512 | 256 | 98.9% | 14.2ms | 3.8 GB | 580s |
| 64 | 512 | 256 | 99.5% | 22.1ms | 6.5 GB | 1050s |
📊 召回率 vs 查询延迟(气泡大小 = 内存占用)
召回率
100% ┤ ●(64,512,256)
99% ┤ ●(32,512,256)
98% ┤
97% ┤ ●(32,256,128)
96% ┤ ●(16,256,128)
95% ┤
93% ┤ ●(16,128,64)
90% ┤
85% ┤●(8,64,32)
└────┬────┬────┬────┬────┬────┬────┬──→ 延迟(ms)
0 5 10 15 20 25 30
关键发现:
- 🟢 M=16, ef=128 是性价比最高的配置(96.8% 召回率,6.2ms 延迟)
- 🟡 M=32 以上 收益递减,内存翻倍但精度提升有限
- 🔴 M=64 在大多数场景下没必要,内存和构建时间都太高
💻 代码实战:Milvus 中的 HNSW 配置
python
from pymilvus import connections, Collection, FieldSchema, DataType
# 连接 Milvus
connections.connect("default", host="localhost", port="19530")
# 定义字段
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True),
FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=1024),
]
# 创建集合
collection = Collection("demo", fields)
# 配置 HNSW 索引
index_params = {
"index_type": "HNSW",
"metric_type": "COSINE",
"params": {
"M": 16, # 每个节点最大连接数
"efConstruction": 256 # 构建时的搜索宽度
}
}
collection.create_index("embedding", index_params)
# 查询时设置 ef(搜索时的搜索宽度)
search_params = {
"metric_type": "COSINE",
"params": {"ef": 128} # 查询时的搜索宽度
}
results = collection.search(
data=[query_vector],
anns_field="embedding",
param=search_params,
limit=10,
output_fields=["id"]
)
📐 HNSW 的内存估算
这是很多人忽略的关键问题! HNSW 是内存索引,所有数据都在内存中。
内存估算公式(简化版):
单节点内存 ≈ 向量大小 + 连接指针
= (维度 × 4 bytes) + (M × 2 × 8 bytes)
总内存 ≈ 数据量 × 单节点内存 × 1.2(额外开销)
示例:100万条 1024维向量,M=16
= 1,000,000 × (1024×4 + 16×2×8) × 1.2
= 1,000,000 × (4096 + 256) × 1.2
= 1,000,000 × 4352 × 1.2
≈ 5.2 GB
| 数据量 | 维度 | M | 预估内存 |
|---|---|---|---|
| 10 万 | 1024 | 16 | ~0.5 GB |
| 100 万 | 1024 | 16 | ~5.2 GB |
| 1000 万 | 1024 | 16 | ~52 GB |
| 100 万 | 1536 | 32 | ~8.5 GB |
| 1 亿 | 1024 | 16 | ~520 GB |
⚠️ 注意 :超过 1000 万条数据时,单机内存可能不够,需要考虑分布式部署(后面第 16 篇详细讲)。
⚖️ HNSW vs 其他索引算法
| 对比维度 | HNSW | IVF | PQ | ScaNN |
|---|---|---|---|---|
| 查询速度 | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 召回率 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| 内存占用 | ⭐⭐(高) | ⭐⭐⭐ | ⭐⭐⭐⭐⭐(低) | ⭐⭐⭐ |
| 构建速度 | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| 适合数据量 | < 1000万 | > 100万 | > 1000万 | > 100万 |
| 是否支持动态插入 | ✅ | ❌(需重训) | ❌ | ❌ |
结论 :HNSW 在召回率 和动态插入方面完胜,是大多数场景的首选。
🔑 本篇核心要点回顾
| 要点 | 说明 |
|---|---|
| HNSW 原理 | 多层图结构,从粗到细快速定位 |
| M 参数 | 控制连接数,影响精度和内存 |
| ef 参数 | 控制查询搜索范围,影响速度和召回率 |
| 推荐配置 | M=16, ef=128 是大多数场景的最佳平衡点 |
| 内存估算 | 每百万条 1024 维向量约需 5GB 内存 |
| 何时不用 HNSW | 超大规模(>1亿)或内存极度受限时考虑 IVF/PQ |
✍️ 写在最后
HNSW 之所以被称为"王者算法",是因为它在精度、速度、灵活性三者之间取得了最好的平衡。
记住这个经验公式:
M=16, ef=128 → 先跑起来,再根据业务需求微调
不要一上来就追求 99.9% 的召回率------96% 的召回率 + 5ms 的延迟 ,往往比 99.5% 的召回率 + 50ms 的延迟 用户体验更好 🎯
📌 下篇预告:《IVF、PQ、ScaNN 索引对比:大规模数据下的性能与内存博弈 ⚖️》
💬 有问题欢迎评论区讨论,觉得有用请点赞收藏 👍
作者:高炉炼铁智能化技术研究者,专注钢铁冶金与人工智能 交叉领域。
👍 如果觉得有帮助,请点赞、收藏、转发!
版权归作者所有,未经许可请勿抄袭,套用,商用(或其它具有利益性行为) 。
🔔 关注专栏,不错过后续精彩内容