主线第 19 篇把向量全部加载到内存中的 HNSW 图里。100 篇文档、512 维 float32,总共 200 KB——任何机器都装得下。但如果语料从 100 篇增长到 100 万篇,200 KB 变成 2 GB;到 1 亿篇,就是 200 GB。常规服务器的内存通常在 64-256 GB 之间,向量数据已经不够放了。

这是向量检索的内存墙问题。解法有三个方向:量化压缩(第 22 篇)、分片分散(第 26 篇)、把向量放到磁盘上。本篇讨论第三个方向——磁盘 ANN,以及在什么规模下应该考虑从 Lucene 切换到专用向量引擎。

内存墙

向量数据的内存占用计算:

1
2
3
4
5
6
7
8
9
内存 = 文档数 × 向量维度 × 每维字节数

512d float32:
1K 文档: 2 MB
100K 文档: 200 MB
1M 文档: 2 GB
10M 文档: 20 GB
100M 文档: 200 GB
1B 文档: 2 TB

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
2
3
4
5
6
构建过程:
1. 随机初始化每个节点的邻居列表(R 个邻居,如 R=64)
2. 对每个节点 p,从图的中心点出发,贪心搜索到 p 的近邻
3. 用搜索路径上的节点更新 p 的邻居列表
4. 剪枝:限制邻居数不超过 R,优先保留方向多样性
5. 重复多轮直到收敛

与 HNSW 的区别:

  • HNSW 有多层(skip-list 结构),Vamana 是单层图
  • HNSW 的邻居数随层变化,Vamana 每个节点固定 R 个邻居
  • Vamana 的图结构更紧凑,占用内存更少

内存-磁盘分离

1
2
3
4
5
6
内存:
- Vamana 图结构(节点 ID + 邻居列表)
- PQ 压缩后的向量(用于粗排)

SSD:
- 原始精度的向量(用于精排)

搜索过程:

1
2
3
4
5
1. 从起始节点出发,在内存中遍历 Vamana 图
2. 每到一个新节点,用内存中的 PQ 向量做粗略距离计算
3. 维护一个候选集(大小为 L,如 L=100)
4. 对候选集中分数最高的 K 个节点,从 SSD 加载原始向量
5. 用原始向量重新计算精确距离,得到最终 Top-K

关键:SSD 只在最后一步介入,且只读 K 个向量(通常 10-100 个)。每次搜索的 SSD 随机读次数有限,延迟可控。

性能数据

DiskANN 原论文在 10 亿向量(SIFT-1B)上的测试结果:

1
2
3
4
Recall@10: 95%+
延迟: < 5ms(NVMe SSD)
内存: ~3.5 GB(PQ 压缩向量 + 图结构)
磁盘: ~120 GB(原始向量)

对比纯内存 HNSW 在相同数据集上:

  • 需要 ~500 GB 内存(float32)或 ~125 GB(int8)
  • 延迟 < 1ms
  • Recall@10 略高(97%+)

DiskANN 用 2ms 额外延迟换来了 97% 的内存节省。

Lucene 的磁盘向量方案

Lucene 的 HNSW 实现本身支持 mmap(内存映射文件):

1
2
// Lucene 默认用 mmap 读取索引
Directory dir = MMapDirectory.open(indexPath);

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
2
3
4
5
6
1. 数据导出:从 Lucene 索引导出文档和向量
2. Schema 映射:Lucene field → 引擎 collection schema
3. 索引导入:批量写入新引擎
4. 查询适配:重写查询逻辑(API 不同)
5. 混合检索:如果需要 BM25,要么引擎支持,要么两套系统
6. 运维:新增监控、备份、扩缩容流程

最大的隐性成本是第 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
2
3
4
5
6
7
8
// 伪代码:遍历图时预取邻居的向量
for (int neighbor : graph.getNeighbors(currentNode)) {
if (!visited.contains(neighbor)) {
// 异步预读邻居的向量数据
ssdReader.prefetch(vectorOffset(neighbor));
}
}
// 处理当前节点时,邻居数据可能已经在缓存中

预取可以隐藏 SSD 延迟,但增加了 I/O 带宽消耗。在高并发场景下,预取过多反而会导致 SSD 带宽饱和。

缓存分层

热门文档的向量可以常驻内存缓存:

1
2
3
4
5
查询向量

内存缓存命中? → 是 → 直接计算距离
↓ 否
SSD 读取 → 计算距离 → 放入缓存

缓存策略:

  • LRU:最近最少使用,适合查询模式变化快的场景
  • LFU:最不常用,适合有热门文档的场景
  • 混合:热门文档 LFU 保底,长尾 LRU 周转

在搜索场景下,20% 的文档通常承担 80% 的查询命中。把这 20% 的向量常驻内存,可以大幅减少 SSD 访问。

练习

  1. 计算 1000 万篇文档、512 维 float32 向量的内存需求,分别算 HNSW 和 DiskANN 的内存占用
  2. 用 Lucene MMapDirectory 打开一个超过物理内存的索引,测量查询延迟的 P50/P99
  3. 对比 Lucene + mmap 与一个专用向量引擎在 100 万向量上的查询延迟和召回率
  4. 设计缓存策略:给定 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 官方文档:向量搜索架构