从零构建现代搜索引擎(E05):磁盘 ANN 与引擎工程对照
主线第 19 篇把向量全部加载到内存中的 HNSW 图里。100 篇文档、512 维 float32,总共 200 KB——任何机器都装得下。但如果语料从 100 篇增长到 100 万篇,200 KB 变成 2 GB;到 1 亿篇,就是 200 GB。常规服务器的内存通常在 64-256 GB 之间,向量数据已经不够放了。
这是向量检索的内存墙问题。解法有三个方向:量化压缩(第 22 篇)、分片分散(第 26 篇)、把向量放到磁盘上。本篇讨论第三个方向——磁盘 ANN,以及在什么规模下应该考虑从 Lucene 切换到专用向量引擎。
内存墙
向量数据的内存占用计算:
1 | |
int8 量化(第 22 篇)把每维从 4 字节降到 1 字节,内存降到 1/4。但 1 亿篇文档量化后仍需 50 GB,加上 HNSW 图结构的额外开销(每个节点的邻居列表),实际内存可能达到 80-100 GB。
分片(第 26 篇)把数据分散到多台机器。但每台机器仍需要装下自己那份向量。4 片 × 25 GB/片 仍需要每台 32 GB 以上内存,机器成本线性增长。
磁盘 ANN 提供了另一条路径:图结构留在内存(占用小),向量数据放在 SSD 上(容量大、成本低)。
DiskANN 的核心思路
DiskANN(Subramanya et al., 2019)是微软研究院提出的 SSD 友好的 ANN 算法。
Vamana 图
DiskANN 用 Vamana 图替代 HNSW。Vamana 是一个度数有界的导航图:
1 | |
与 HNSW 的区别:
- HNSW 有多层(skip-list 结构),Vamana 是单层图
- HNSW 的邻居数随层变化,Vamana 每个节点固定 R 个邻居
- Vamana 的图结构更紧凑,占用内存更少
内存-磁盘分离
1 | |
搜索过程:
1 | |
关键:SSD 只在最后一步介入,且只读 K 个向量(通常 10-100 个)。每次搜索的 SSD 随机读次数有限,延迟可控。
性能数据
DiskANN 原论文在 10 亿向量(SIFT-1B)上的测试结果:
1 | |
对比纯内存 HNSW 在相同数据集上:
- 需要 ~500 GB 内存(float32)或 ~125 GB(int8)
- 延迟 < 1ms
- Recall@10 略高(97%+)
DiskANN 用 2ms 额外延迟换来了 97% 的内存节省。
Lucene 的磁盘向量方案
Lucene 的 HNSW 实现本身支持 mmap(内存映射文件):
1 | |
mmap 把文件映射到虚拟地址空间,操作系统通过页缓存自动管理热数据在内存、冷数据在磁盘。但 mmap 不等于 DiskANN:
| 维度 | Lucene mmap | DiskANN |
|---|---|---|
| 图结构 | 跟向量一起在文件中 | 图结构常驻内存 |
| 访问模式 | 依赖 OS 页缓存 | 显式控制加载 |
| 冷启动 | 大量缺页中断 | 图结构预加载 |
| 稳态性能 | 取决于可用内存 | 稳定(图在内存) |
| 大数据集 | 性能下降快 | 优雅降级 |
Lucene mmap 的问题:当向量数据超过物理内存时,HNSW 图遍历会频繁触发缺页中断(page fault)。每次 page fault 意味着一次 SSD 随机读(~100μs),图遍历一次查询可能触发几十次 page fault,延迟从 1ms 飙升到几十毫秒。
DiskANN 的优势在于:图遍历完全在内存中完成(0 次 page fault),只在最后的精排阶段做少量 SSD 读取。
专用向量引擎对照
当数据规模超过 Lucene 的舒适区,该考虑专用向量引擎:
| 引擎 | 向量索引 | 磁盘支持 | 混合检索 | 适用规模 |
|---|---|---|---|---|
| Lucene | HNSW + mmap | 通过 OS 页缓存 | 原生 BM25 + 向量 | < 10M 向量 |
| Elasticsearch/OpenSearch | Lucene HNSW | 同上 | 原生 | < 10M 向量/分片 |
| Vespa | HNSW | 内存/磁盘分层 | 原生 BM25 + 向量 | 10M-1B |
| Milvus | DiskANN / IVF_PQ | 原生磁盘索引 | 需外部组合 | 10M-10B |
| Qdrant | HNSW + mmap | 通过 mmap | 有限 | < 100M |
| Weaviate | HNSW | 支持磁盘 | BM25 + 向量 | < 100M |
选择标准
继续用 Lucene 的条件:
- 向量数据量 < 服务器内存的 50%(量化后)
- 混合检索(BM25 + 向量)是核心需求
- 不想引入新的运维组件
- 延迟要求 < 10ms
考虑专用引擎 的条件:
- 向量数据量超过单机内存
- 纯向量搜索(不需要 BM25)
- 需要 10 亿级向量支持
- 可以接受额外的运维复杂度
迁移成本
从 Lucene 迁移到专用引擎的成本:
1 | |
最大的隐性成本是第 5 项。本系列的搜索管线依赖 BM25 + 向量融合——如果向量引擎不支持 BM25,就需要维护两套索引(Lucene 做 BM25,向量引擎做 ANN),协调两者的数据一致性。
SSD 硬件选择
磁盘 ANN 对 SSD 的性能敏感:
| SSD 类型 | 随机读延迟 | 随机读 IOPS | 适合 DiskANN |
|---|---|---|---|
| SATA SSD | ~100μs | ~90K | 勉强可用 |
| NVMe SSD | ~20μs | ~500K | 推荐 |
| Intel Optane | ~10μs | ~500K | 最佳 |
| HDD | ~5ms | ~200 | 不可用 |
DiskANN 每次查询需要 10-100 次随机读。NVMe SSD 上总延迟 0.2-2ms,SATA SSD 上 1-10ms,HDD 上 50-500ms——HDD 完全不可行。
预取策略
图遍历时可以预测下一步可能访问的向量,提前发起 SSD 预读:
1 | |
预取可以隐藏 SSD 延迟,但增加了 I/O 带宽消耗。在高并发场景下,预取过多反而会导致 SSD 带宽饱和。
缓存分层
热门文档的向量可以常驻内存缓存:
1 | |
缓存策略:
- LRU:最近最少使用,适合查询模式变化快的场景
- LFU:最不常用,适合有热门文档的场景
- 混合:热门文档 LFU 保底,长尾 LRU 周转
在搜索场景下,20% 的文档通常承担 80% 的查询命中。把这 20% 的向量常驻内存,可以大幅减少 SSD 访问。
练习
- 计算 1000 万篇文档、512 维 float32 向量的内存需求,分别算 HNSW 和 DiskANN 的内存占用
- 用 Lucene MMapDirectory 打开一个超过物理内存的索引,测量查询延迟的 P50/P99
- 对比 Lucene + mmap 与一个专用向量引擎在 100 万向量上的查询延迟和召回率
- 设计缓存策略:给定 4GB 内存预算和 1000 万向量,计算最大可缓存的向量比例
延伸阅读
- Subramanya et al., “DiskANN: Fast Accurate Billion-point Nearest Neighbor Search on a Single Node”, NeurIPS 2019
- Jayaram Subramanya et al., “Rand-NSG: Fast Accurate Billion-point Nearest Neighbor Search on a Single Node”, NeurIPS 2019(Vamana 图的前身)
- Lucene MMapDirectory Javadoc
- Vespa 官方文档:向量搜索架构






