向量检索做到亿级规模之后,内存成了第一个瓶颈。我最早接触 DiskANN 是因为一个图像检索项目:底库大概 1.2 亿条 128 维向量,用 HNSW 全量驻留内存,光索引就吃掉 30 多 GB,而且机器加内存的成本远高于换一块 NVMe SSD。后来调研了一圈,发现 DiskANN 这种“索引放 SSD、查询也跑得快”的方案,正好卡在这个痛点上。它既不是常驻内存的 HNSW,也不是传统意义的暴力扫码,而是把图索引和压缩向量结合起来,用磁盘换内存,再用算法换回大部分性能。这篇文章我会把 DiskANN 的原理、适用场景、和常见工具(HNSW、FAISS、SPANN 这些)的差异,以及从安装到调参的完整路径梳理一遍,帮你在选型和落地时少走弯路。适合正在做向量检索选型、处理十亿以内量级检索需求的开发者,也适合想了解“外存索引”这条路子到底怎么走的人。
1. DiskANN 整体设计拆解:为什么要把它做成 SSD 上的索引
1.1 一个关键矛盾:内存装不下,磁盘又太慢
先回头看所有向量检索工具面临的基本矛盾。HNSW 这类图索引在内存里表现非常好,单机千万级向量、QPS 做到几千上万都常见。但是当底库上亿,内存占用直线上升。常规做法是压缩向量,比如用乘积量化(PQ)把一条 128 维 float 向量从 512 字节压到 16 字节甚至 8 字节,内存确实降下来了,代价是召回率下降、构建时间变长、细节丢失。另一个做法是上分布式,用多台机器分片,但工程复杂度和成本都上去了。
DiskANN 的思路是把注意力从内存挪到 SSD。SSD 的顺序读带宽非常高,一块普通 NVMe 盘就能跑到 3GB/s 以上,随机读虽然慢一些,但通过图结构和预取策略也能压住延迟。DiskANN 用两个办法同时缓解问题:第一,把图索引(节点和边)放在 SSD 上,而不是内存里;第二,把原始向量做 PQ 压缩后还是放 SSD,查询时只读出图结构上走过的那些节点对应的压缩向量。这样内存占用就非常低,一条 1 亿级索引可能只占几百 MB 内存做缓存和重排序,剩下的都交给磁盘。
1.2 三个核心设计动作:Vamana 图、PQ 压缩、外存放置
DiskANN 不是简单地把 HNSW 搬到磁盘上,它有三个配套的核心设计:
第一是图结构用了自己提出的 Vamana 图,而不是直接的 HNSW。Vamana 和 HNSW 类似,都是基于邻近图做贪婪搜索的索引结构,但 Vamana 构造过程更简单,没有 HNSW 那种层级结构,而是一条单层的、边数可配置的图。它通过一个参数 R 控制每个节点的出度,用另一个参数 L 控制构建时的搜索宽度。单层结构的好处是:搜索时不需要跨层,每一步都直接从当前节点跳到邻居,适合外存布局,SSD 上按顺序读邻居列表也更友好。
第二是 PQ 压缩。原始向量存在磁盘上,读取成本高,所以 DiskANN 在构建过程中会对向量做乘积量化,把每条向量压缩成短码。查询时,先用压缩向量算出大致的距离序,再做重排序(reranking),从原始向量里精算前若干候选的距离。这个“先粗排再精排”的思路在很多检索系统里都有,但 DiskANN 把它和图的遍历过程绑定在一起,边遍历边用压缩码算距离,大幅减少 IO。
第三是外存放置。图的邻接表、PQ 码、原始向量全部以文件形式放在 SSD 上,查询时通过 mmap 或者预取的方式读入。Search 时需要并行预取多个节点,用异步 IO 掩盖磁盘延迟。实际效果是,单机百万级到十亿级的检索场景下,DiskANN 能做到几十到几百毫秒的 p95 延迟,而且内存占用控制在极低水平。
1.3 为什么单层图反而更适合磁盘
HNSW 的多层结构有一个隐含假设:所有层都可以快速随机访问,这在内存里不是问题。但放到磁盘上就不一样了,每跳一层都可能触发一次磁盘寻址,多层图在随机 IO 场景下会显得非常吃亏。Vamana 选择单层宽图,搜索时的路径基本是在同一层内游走,邻居节点集中在附近,SSD 顺序读的效率更高。这个取舍值得我们在选型时记住:一个算法在内存里最优,不等于在外存场景最优;DiskANN 的“慢”是有意设计的快,它的慢在 IO 模式上换回了更多的顺序读。
2. 核心原理细节拆解:构建、搜索、压缩到底怎么配合
2.1 Vamana 图构建过程:边怎么加,参数怎么定
Vamana 的构建流程可以简单理解为:先给每个点找一批近邻,再通过一种“距离约束”逐步把图变密。它有一个关键操作用到一个参数 R(最大出度)。构建时,每个节点最多保留 R 条出边,这些边不是随便选的,而是通过一个裁剪过程,保证图里每个节点的邻居在空间上尽量分散,避免出现“所有边都指向同一个方向”这种局部退化。
实际操作中,R 一般取 32 到 64。R 越大,图越密,搜索时召回越高,但构建时间、索引大小和查询 IO 都会增大。R 太小,图太稀,搜索容易走丢,召回率断崖式下降。另一个参数 L 是构建时用的搜索列表宽度,取值通常比查询时的宽度大不少,常见的构建 L 在 100 到 200 之间,查询时用更小的值如 20 到 50。这样构建阶段能更充分地探索候选集,让图的结构更合理。
还有一点很容易被忽略:Vamana 构建是支持并行的。DiskANN 的实现里会用多线程并行处理节点,因为图的构建过程可以分批进行,每批内部独立搜索、独立裁剪,最后合并。实测下来,10 亿级数据在几十台机器的分布式构建下可以小时级完成,单机处理千万级数据也能控制在几十分钟内。
2.2 PQ 压缩的细节:为什么用乘积量化而不是普通降维
普通降维(如 PCA)会把高维向量压到低维,但丢信息太多,而且非线性近似能力弱。乘积量化不一样,它把向量切成若干段,每段用独立的码本量化。比如一条 128 维向量切成 8 段,每段 16 维,每段用一个 256 条码字的码本,每条向量最终用 8 个 8 位码字表示,也就是 64 bit,压缩到原来的 1/64。查询时,只要查表就能得到分段距离,相加得到近似距离。
DiskANN 的 PQ 表是放在内存里的,因为码本本身很小。压缩后的向量(PQ 码)放磁盘,查询时读到某个节点的 PQ 码,直接在内存码本里查距离,避免读原始 float 向量。只有做重排序时,才从磁盘上读原始向量。这个设计非常巧妙:大部分距离计算走 PQ 码,少量重排序走原始数据,IO 和计算的平衡点找得很稳。
PQ 的码本数和每段维度是需要调试的。码本数越大,量化失真越小,但码本训练时间和内存开销也越大。一般 256 或 65536 的码本较常见。段数越多,距离分辨力越高,但查询时查表次数也更多。DiskANN 的默认设置在大多数场景下够用,我自己调参时会先用小数据集扫一遍 PQ 段数对召回率的影响,再决定线上配置。
2.3 Beam Search 在外存上是怎么跑的
查询阶段,DiskANN 用的是一种 beam search:维护一个候选列表,每次从列表里取最近的节点,读取它的邻居,计算邻居节点的压缩距离,再把更近的节点放回候选列表。和内存里 HNSW 的搜索比,DiskANN 更依赖“预取”:把当前候选节点的所有邻居一次性从磁盘读出来,而不是等距离算完再逐个读。
实现上,这通常靠异步 IO 或者 SSD 的并行读能力。某些实现里还加了一个“搜索缓存”,把高频查询路径上的节点放在内存里,避免每次都走磁盘。这也是为什么 DiskANN 在实际查询中经常能做到低于 10 毫秒:热点路径被缓存了,冷路径靠顺序预取,整体延迟可控。
beam search 的宽度参数 L(查询时的候选列表大小)直接决定召回率和延迟的平衡。L 越大,候选探索越充分,但 IO 和计算量也变大。通常可以从 L = 20 开始,逐步加大,观察召回率曲线,直到增长趋缓就停。
2.4 重排序为什么能救回召回率
PQ 距离是近似距离,用它排出来的 top-k 会有误差。DiskANN 的标准做法是:先用 PQ 粗排,取比 top-k 大几倍的候选(比如 k=10 时取 100 个候选),再从磁盘上读取这些候选的原始向量做精确距离计算,最后排序输出真正的 top-k。
这个“重排序”阶段是影响端到端延迟的重要环节。候选集太大,重排序耗时增加;候选集太小,召回率又会掉。实际工程中,候选集一般取最终返回数量的 10 到 20 倍。有一次我把候选集从 10 倍调到 30 倍,召回率涨了 1.2%,但 p95 延迟从 9ms 涨到 16ms,明显不划算。用更细的 PQ 段数也能提升召回率,但磁盘读和查表的开销都会增加,我建议优先调整候选集倍率,再看 PQ 配置。
3. 应用场景:什么样的项目适合用 DiskANN
3.1 核心场景:亿级底库、可接受毫秒级延迟的中大规模检索
DiskANN 最舒服的位置是处理“内存装不下,但延迟要求又没那么苛刻”的检索需求。典型的例子:
- 图像检索:底库几千万到几亿条特征向量,单机内存有限,希望用一台普通的 64GB 内存服务器跑全量检索。
- 推荐系统语义召回:用户 embedding 和物品 embedding 做语义匹配,数量级动辄上亿,离线构建索引,线上查询,QPS 要求几百到几千。
- 跨模态检索:文本、图像、音频混合检索,向量维度高,底库规模大,DiskANN 可以把索引文件放在 SSD 上,内存留给模型推理。
这些场景的共同特点是:底库数量大,索引体积超过内存舒适区,但又不希望引入多机分布式带来的运维复杂度。DiskANN 用一台机器加一块好 SSD,就能把活干完,成本优势非常明显。
3.2 边缘场景:小规模但索引文件要离线归档
还有一种常见需求是“索引工具当时能用,但历史数据要长期保存”。DiskANN 的索引是文件形式,可以随时加载和卸载,不像某些内存索引需要整库 dump 到序列化文件再加载。你完全可以把历史底库的索引文件归档到冷存储,等需要时再挂到一台机器上查询,内存占用量很小,启动和卸载都很轻量。
这个特性对 ToB 项目很友好。比如做私有化部署,客户只有一台普通服务器,没有 GPU,也没有大内存,但底库确实有几千万条向量。DiskANN 能在这类低配环境下提供还不错的检索性能,演示和交付都更轻松。
3.3 不适合的场景:超低延迟高并发、小底库频繁更新
DiskANN 不适合的场景分两类。第一类是底库很小(百万级以内)又想压到微秒级延迟,内存里直接放 HNSW 或者 IVF 更合适,毕竟磁盘访问再怎么优化也比不过内存。第二类是写入和更新非常频繁,比如实时插入、实时删除、索引实时生效。DiskANN 是为“批量构建 + 长期查询”设计的,动态更新不是它的强项,虽然有些版本支持在线更新,但代价比较大。如果业务是流式写入,建议考虑混合方案:热数据放内存索引,冷数据放 DiskANN,中间做合并。
我自己踩过一个坑:当时在某个场景里用户持续上传新图片,底库每天加几十万条,我用 DiskANN 后每次都得重建索引,构建时间虽然不算长,但频繁重建明显浪费 IO。后来改成每日凌晨做增量合并,白天用旧索引查询,问题才缓解。
4. 工具选型对比:DiskANN、HNSW、FAISS、SPANN 怎么选
4.1 与 HNSW 的对比:内存和磁盘的分水岭
HNSW 是图索引的“内存王者”,它把多层图全部放在内存里,查询时纯计算,不涉及磁盘 IO,所以延迟非常低。但内存容量就是天花板,单机几十 GB 内存最多也就能支撑几千万到一亿条 float 向量,压缩后能多点,但压缩后召回率又会下降。
DiskANN 的优势在于内存占用低几个数量级。同一份 1 亿向量数据,HNSW 可能要吃 30GB 内存,DiskANN 可能只吃几百 MB 到 1GB,剩下的全部放 SSD。代价是查询延迟从一两毫秒涨到十毫秒级。如果你的延迟预算很紧(比如在线广告实时召回要求 5ms 内),HNSW 依然是首选;如果底库规模大且内存预算有限,DiskANN 就是那个替代方案。
还有一点是构建和加载时间。HNSW 构建过程非常吃内存和 CPU,构建期间可能 OOM;DiskANN 把数据和索引都放磁盘,构建时的内存压力小得多,构建速度也被外存 IO 限制,但总体上更容易在低配机器上跑起来。
4.2 与 FAISS 的对比:一个是检索框架,一个是完整方案
FAISS 是一个功能丰富的向量检索库,它提供了 Flat(暴力)、IVF、HNSW、PQ、SQ 等多种索引类型。你可以用 FAISS 搭一个“IVF + PQ + GPU”的组合,在 GPU 上跑大规模检索,效果也不错。但 FAISS 默认更多是围绕内存和 GPU 设计,虽然也支持 mmap 到磁盘的索引,但它的外存方案不如 DiskANN 专门优化过,随机访问和预取策略相对粗糙。
选型建议:如果你需要 GPU 加速、又想在同一个框架里做聚类、向量变换、批量搜索等丰富实验,FAISS 是更灵活的选择。如果你的核心诉求就是“SSD 上也能跑出不错的延迟和召回率”,DiskANN 是更聚焦的方案。实际项目里两者不冲突,可以用 FAISS 做离线分析和实验,用 DiskANN 做线上服务。
4.3 与 SPANN 类外存索引的对比:都是外存,侧重点不同
SPANN 也是近年出现的基于 SSD 的近似最近邻索引,它走的是“聚类 + 倒排 + 图搜索”的路线,先把数据分成很多个簇,每个簇对应一个段,查询时只加载最相关的几个段,再做段内图搜索。DiskANN 走的是“全局图 + 外存”的路线,一次搜索会遍历到图中多个区域。
两者的差异主要体现在数据分布上。如果数据分布非常不均匀,有些簇特别大,SPANN 的段大小平衡会是个问题;DiskANN 的全局图结构对这种数据集的适应性更强,因为它没有“簇”这个固定分块概念。但如果数据本身聚类特性明显,SPANN 的剪枝更激进,查询时可以跳过大量无关区域,延迟可能更有优势。
实际选型时,我会先用小数据在两种方案上各跑一轮,对比召回率和 p95 延迟,再做决定。不要看 benchmark 文档里的官方数据,因为数据分布不同,结果差异很大。
4.4 一张表看懂选型参考
| 对比维度 | DiskANN | HNSW | FAISS (IVF-PQ/GPU) | SPANN |
|---|---|---|---|---|
| 索引位置 | SSD + 少量内存缓存 | 全内存 | 主要是内存/GPU显存 | SSD + 内存分段缓存 |
| 内存占用 | 低 | 高 | 中到高 | 低 |
| 查询延迟 | 毫秒级(10~50ms) | 亚毫秒到毫秒级 | 取决于索引类型和硬件 | 毫秒级 |
| 构建速度 | 中等,受磁盘IO限制 | 快,受内存限制 | 快(GPU更快) | 中等 |
| 动态更新 | 不灵活 | 支持但代价高 | 部分支持 | 不灵活 |
| 硬件要求 | 需要 SSD,内存需求低 | 需要大内存 | 推荐 GPU/大内存 | 需要 SSD |
| 适合规模 | 亿级 | 千万到亿级(内存允许内) | 百万到十亿级(受资源限制) | 亿级 |
这张表是我根据自己的实测和踩坑整理的,不是通用结论。不同版本、不同数据形态下数据会有波动,但它能帮你快速判断方向。
5. 基本使用方式:从安装到调参的一步步实操记录
5.1 环境准备:代码、依赖和数据集
DiskANN 官方实现是基于 C++ 的,需要在 Linux 环境下编译。编译依赖包括 CMake、OpenMP、BLAS/LAPACK 等。我一般用 Ubuntu 20.04 或更新的系统,先装基础依赖:
sudo apt-get update sudo apt-get install -y build-essential cmake libopenblas-dev libomp-dev然后拉取代码并编译:
git clone <某个DiskANN相关仓库地址> cd <仓库目录> mkdir build && cd build cmake .. make -j这里我不写具体仓库地址,因为不同维护组织有不同 fork,建议你在 GitHub 上搜 DiskANN,选择 stars 较多、最近有更新的仓库。编译过程中最容易踩的坑是 BLAS 库链接失败,通常可以通过安装 libopenblas-dev 解决。
数据准备上,我用的是一个模拟的 1000 万条 128 维 float 向量数据集。把向量文件转成 DiskANN 内部格式时,每条向量用二进制 float 数组存储,另外准备一个文本文件存 ID 和路径映射,方便验证召回率。
5.2 构建索引:参数怎么设,构建时间怎么估
DiskANN 构建索引的命令大致是这样的:
./build_index --data_type float --data_path <向量文件> --index_path <索引输出路径> \ --graph_degree 32 --Lbuild 100 --alpha 1.2graph_degree对应 Vamana 的 R,控制每个节点的出度。Lbuild是构建时的搜索宽度,alpha是裁剪参数,值大于 1 时表示允许保留稍微远一点的邻居,能提升图连通性。我对 1000 万数据构建时,R 取 32、Lbuild 取 100、alpha 取 1.2,构建耗时大约在 15 分钟左右,索引文件大小 1.5GB 左右,内存占用峰值约 3GB。
构建过程中建议监控内存和磁盘 IO。如果内存吃紧,可以调低并发线程数;如果 IO 打满,可以尝试把索引输出路径放到另一块盘上,减少读写竞争。
构建完成后,索引文件夹里会有三个主要文件:图文件、PQ 压缩向量文件、原始向量文件。这三个文件缺一不可,删除任何一个都会导致加载失败。
5.3 执行查询:命令行测延迟和召回
查询命令大致是:
./search_index --index_path <索引路径> --query_path <查询向量文件> \ --result_path <结果输出路径> -L 30 -K 10-K是最终返回的 top-k 数量,-L是搜索宽度。这条命令会输出查询吞吐率、平均延迟、p95 延迟以及召回率(如果提供了 groundtruth 文件)。我在 1000 万条数据上,K=10、L=30 时,召回率大概 0.95 左右,p95 延迟 8ms。L 调到 50 后召回率到 0.98,延迟涨到 15ms。
实际工程里,建议先用官方工具跑几轮小规模实验,确认召回率和延迟都在预算内,再写服务代码。命令行工具跑通之后,还可以通过自带的 C++ 接口直接把索引加载到进程里做在线查询,接口风格和调用方式都比较直观,照着示例改业务逻辑就行。
5.4 调参思路:从默认参数开始小步快跑
我的调参路径是固定的:先固定 R=32、Lbuild=100、alpha=1.2 构建一个基线索引,然后用不同 L 值跑查询,统计召回率和延迟曲线。如果召回不达标,优先增大 L,L 到 100 还不够就同时增大 R 和 alpha 重新构建。如果延迟超预算,尝试减小 L,再看召回率损失能不能接受。
另外要注意 PQ 参数的调整。DiskANN 支持配置 PQ 的段数(一般用 8 或 16),段数越大,PQ 编码越精细,召回越好,但构建时间和查询时间也会增加。我试过一个 128 维数据集,PQ 段数从 8 提到 16,召回率提升 1%,但查询变慢 20%,性价比不高。所以不是 PQ 越精细越好,要看业务对召回和延迟的敏感度。
6. 常见问题与避坑记录
6.1 常见问题速查
| 现象 | 可能原因 | 解决思路 |
|---|---|---|
| 构建时内存溢出 | 并发线程过多、数据集过大 | 减小线程数、分批构建、换更大内存机器 |
| 构建后查询召回率极低 | R 或 Lbuild 太小 | 增大 R、Lbuild、alpha 后重建 |
| 查询延迟突然变高 | SSD 写入竞争、缓存未命中 | 检查 IO 负载、调整预取策略、扩大缓存 |
| PQ 粗排效果差 | PQ 段数太粗、码本数量少 | 增加段数或码本数,做小规模验证 |
| 索引加载失败 | 文件缺失或路径错误 | 检查文件完整性、重新构建索引 |
| 在线更新不及时 | 磁盘索引不支持实时写 | 改用批处理合并、热冷混合方案 |
6.2 实际踩坑实录
第一个坑是构建时没注意线程数。我在一台 16 核机器上直接用了默认线程数,结果 IO 被打满,构建时间比预期多了一倍不止。后来把线程数限制到 8,构建时间反而更快。原因很简单:过量线程导致随机 IO 竞争,SSD 带宽被无谓地消耗在寻址上。
第二个坑是查询时忘了做缓存预热。刚启动服务时,第一次查询会特别慢,因为缓存是空的,所有节点都要从磁盘读。正确做法是服务启动后先跑一批预热查询,把热点路径上的节点加载到缓存里,再开始接线上流量。我试过预热和没预热,p95 延迟差了近 3 倍。
第三个坑是关于重排序候选集大小的。之前为了追求召回率,我把候选集设得特别大,结果延迟增长远超预期。后来用 profiling 工具查了一下,发现瓶颈不在图搜索,而在重排序阶段频繁读原始向量文件。解决方法是把候选集从 50 倍降到 20 倍,同时增大 PQ 段数来弥补召回损失,效果好了很多。
6.3 一些别的实现细节
DiskANN 对 SSD 的要求其实不低。我用过普通 SATA SSD 和 NVMe SSD 对比,NVMe 在随机读上的优势非常明显,查询延迟能差一倍以上。如果你的检索服务延迟敏感,一定优先上 NVMe。另外,预留至少 20% 的 SSD 空闲空间也很有必要,因为索引文件会随构建过程产生临时文件,容量不够容易构建中断。
还有一个小细节:查询线程数。DiskANN 的搜索是并行的,线程数一般设为物理核心数即可。线程太多会增加上下文切换开销,反而降低吞吐率。我在 32 核机器上测试,24 线程时的 QPS 最高,再往上就躺平了。
结尾
我个人在实际操作中的体会是,DiskANN 并不适合所有检索场景,但在“亿级底库 + 中低延迟 + 低内存”这个三角约束里,它几乎是性价比最高的选择之一。它的学习曲线不陡,真正难的是理解 IO 和算法如何协同,以及愿意花时间在你的真实数据集上打磨参数。用之前先问自己一句:你的索引真的需要塞进内存吗?如果答案是不需要,那 DiskANN 就值得你花一个下午跑通 Demo。最后再分享一个小技巧:做选型对比时,别只用公开的 benchmark 数据集,拿你自己的业务采样数据跑一轮,才最可靠。