在 Milvus 里,HNSW 通常用于追求较高 Recall 的向量检索。
但它也有一个小缺点:比较吃内存。标准 HNSW 使用 FP32 向量参与建图和距离计算,同时还要维护分层图结构。随着数据规模增加,索引和运行时内存占用也会跟着增长。
在此背景下,Milvus 还提供 HNSW_SQ、HNSW_PQ 和 HNSW_PRQ,四者采用同一套分层图构建与搜索框架,但使用不同精度的向量表示参与建图和距离计算;量化索引还可以使用更高精度的向量表示对候选结果进行重排。
接下来,本文会基于 VectorDBBench Cohere 1M 数据集,基于 Milvus 2.6.17 和 PyMilvus 2.6.15 对 HNSW、HNSW_SQ、HNSW_PQ 和 HNSW_PRQ 进行对比,并进一步测试 SQ8、SQ6、SQ4U,以及 ef、refine_k 和 refine_type 对 Recall、QPS、延迟、运行时内存和索引构建成本的影响。
核心结论:
理解 HNSW 系列索引的关键,是区分图搜索框架、向量表示和候选重排三个概念。
HNSW 负责在分层图上导航;SQ、PQ 和 PRQ 决定向量如何压缩以及如何计算近似距离;refinement 则决定是否使用更高精度的表示对候选结果重新计算距离并排序。

图 1:Milvus 中的 HNSW 系列索引:它们共享 HNSW 的算法框架,但向量压缩表示会参与建图和距离计算,因此实际图拓扑不保证与 FP32 HNSW 完全一致。
标准 HNSW 直接使用 FP32 向量,是本文的全精度参考。HNSW_SQ 采用标量量化;HNSW_PQ 将向量划分为多个子向量,并将每个子向量编码为码本索引;HNSW_PRQ 则在 PQ 粗量化的基础上继续编码残差。三种量化索引均支持 refinement。采用更高精度的refinement通常能提升召回率,但也会增加计算、存储和运行时内存开销。
HNSW 将向量组织成多层小世界图。查询从稀疏的高层入口开始,通过贪心遍历快速接近目标区域,再逐层下降到最底层扩展候选。M、efConstruction 和 ef 分别控制图的连接密度、建图阶段的候选宽度以及查询阶段的搜索宽度:

量化索引沿用相同的 HNSW 分层建图与搜索流程,但向量压缩表示会参与邻居选择和距离计算。因此,“算法框架相同”并不意味着四种索引会生成完全相同的邻接边或搜索路径。量化误差既可能改变建图阶段的邻接关系,也可能影响查询阶段的候选排序。
HNSW_SQ 使用 Scalar Quantization。SQ8 和 SQ6 分别以 8 bit 和 6 bit 表示每个维度,并为各维度独立估计量化范围。SQ4U 从 Milvus 2.6.8 开始提供,它使用全局共享的均匀量化参数,将每个值编码为 4 bit 无符号整数。SQ4U 更适合已经归一化或各维数值分布较一致的数据;其性能收益也更依赖内存带宽、缓存效率和 SIMD 能力。
HNSW_PQ 使用 Product Quantization。PQ 将 D 维向量划分为 m 个子向量,每个子向量使用一个 nbits 位的码本索引表示。本次 Cohere 向量为 768 维,配置为 m=96, nbits=8,即每个子向量包含 8 个维度,理论编码长度为 96 bytes/vector。相比之下,SQ8 的理论编码长度为 768 bytes/vector。
HNSW_PRQ 使用 Product Residual Quantization。它首先使用 PQ 对原始向量进行粗量化,再使用额外码本对 PQ 近似留下的残差进行迭代量化;参数 nrq 控制残差量化的迭代次数。本次实验设置为 m=96, nbits=8, nrq=2。按该配置,理论编码长度约为 192 bytes/vector,为本次 PQ 配置的两倍。残差编码能够保留更多信息,但也会增加码本训练、索引构建和距离计算成本。

图 3:SQ、PQ 与 PRQ 的量化粒度及本次实验的理论编码长度
本文关于 PQ 和 PRQ 的结论仅适用于当前 m=96, nbits=8 和 nrq=2 的配置。增大 m、调整 nbits 或改变 nrq,都可能改变召回率、构建成本和内存占用。上述理论编码长度仅用于说明编码预算,并不等同于最终索引文件大小。本文所说的编码预算,是指每条向量用于保存量化编码的理论字节数,不包括 HNSW 图结构、码本、元数据和可选的 refinement 数据。
量化索引可以通过 refine=true 参数启用候选重排。Milvus 先使用基础量化表示执行 HNSW 搜索,随后使用 refine_type 指定的更高精度向量表示,对扩大后的候选集重新计算距离并排序。refine_type 的精度必须高于基础量化类型,可选值包括 SQ6、SQ8、BF16、FP16 和 FP32;其中只有 FP32 属于全精度重排,其余类型仍会有相应的表示误差。在 Milvus 2.6.x 中,refine_type 必须采用比基础 sq_type 更高精度的表示。常见的精度升级方向如下:


图 4:Refinement 流程:量化 HNSW 负责候选召回,refine_k 控制候选放大倍数,refine_type 决定重排使用的向量精度。
如图4所示,TopK 表示最终返回的结果数量,refine_k 表示重排候选集相对于 TopK 的放大倍数:
进入 refinement 的候选数 = TopK × refine_k
本文固定 TopK=100。当 refine_k=4 时,进入重排阶段的候选数量为 400。系统使用 refine 表示重新计算这些候选结果的距离,最后仍只返回排名最高的 100 个结果。增大 refine_k 有助于将因量化误差而落在 TopK 之外的真实近邻重新排回结果集,但也会增加计算量和延迟。
Refinement 还会改变索引的存储与内存消耗。以 refine_type=FP32 为例,已加载的 segment 需要能够访问 FP32 级别的 refine 表示,因此索引构建产物和运行时内存会显著增加。但是这并不意味着所有 FP32 向量在任何时刻都必须常驻物理内存;实际内存驻留量还会受到 mmap、page cache、加载策略和访问模式的影响。
ef 与 refine_k 面向不同的瓶颈:ef 扩大图搜索阶段的探索范围,主要减少搜索宽度不足造成的候选结果遗漏;refine_k 扩大高精度重排候选集,主要修正量化距离带来的排序偏差。两者不能简单互相替代。
本次实验使用统一的数据集和测试指标,对 HNSW 系列索引进行横向比较。除主测试结果外,还覆盖 SQ 量化位宽、ef、refine_k、refine_type 以及等召回率选点,用于分析不同配置在召回率、吞吐、延迟、运行时内存和索引构建产物大小之间的工程取舍。
实验围绕两个核心问题展开:第一,量化表示能够带来多少内存与吞吐收益;第二,当召回率下降时,应通过增大 ef、启用 refinement,还是更换量化策略来恢复结果质量。第三章按照以下实验矩阵组织结果。


Recall@100 使用与查询相同的 COSINE 相似度度量。对每个查询按下式计算,再对 1,000 个查询取平均值:
Recall@100 = |ANN Top-100 ∩ Ground-truth Top-100| / 100
Recall@100 与 串行查询 P50/P95/P99 来自 1,000 个查询的顺序执行。峰值 QPS 取并发压测中的最高吞吐:关键配置测试并发度 8、16 和 32,参数扫描测试并发度 8 和 16。峰值 QPS 运行点 P95 与 峰值 QPS 来自同一次并发测试,未与串行查询 P95 混用。
加载后容器内存增量通过 docker stats --no-stream 记录 collection 加载前后的容器内存差值,因此它并不是单个 Milvus 进程的严格 RSS。该指标会受到内存分配器、page cache、segment 元数据和 mmap 行为的影响,本文仅将其用于比较同一环境下各配置的相对运行时内存占用。
索引构建产物增量(估算) 通过统计索引构建期间新增的 Milvus 索引文件体积进行估算,不代表实例的总磁盘占用。索引构建时间仅统计从 create_index 到索引构建完成的时间,不包含数据导入时间。
关键配置重复运行 3 次,主表中的 QPS 与延迟优先采用中位数。由于基准测试客户端与 Milvus 部署在同一台虚拟机上,吞吐和延迟包含双方对 CPU 资源的竞争,但不包含跨主机网络延迟。
主表固定查询阶段的搜索宽度为 ef=256;启用 refinement 的配置统一使用 refine_k=4。固定 ef 只保证图搜索宽度一致,并不代表总计算成本相同,因为 refinement 还会扩大候选集并执行更高精度的重排。整体测试结果如下:

在本次 Cohere 1M 的测试中,SQ8(未启用 refinement) 在召回率、吞吐和资源占用之间呈现出最均衡的结果。与HNSW FP32 相比,其 Recall@100 下降约 0.46 个百分点,峰值 QPS 提升至 1.29 倍,加载后容器内存增量 则从3.22 GB 降至约 1.01 GB。
SQ8 + refine_type=FP32 将 Recall@100 提高到 0.9881,但加载后容器内存增量 上升至约 3.97 GB,索引构建产物约为 3.80 GB。该配置的主要价值是恢复召回率,而不是继续降低运行时内存。
SQ4U(未启用 refinement) 的峰值 QPS 达到 1,042.9,加载后容器内存增量降至 608 MB,但 Recall@100 只有 0.8861。
在本次 m=96, nbits=8 配置下,PQ(未启用 refinement) 的 Recall@100 为 0.6083,说明 96-byte PQ 编码带来的量化误差已经成为主要瓶颈。该结果不能代表所有 HNSW_PQ 配置;增大 m、提高 nbits 或采用更长的编码,都可能形成不同的精度与资源取舍。
本次 PRQ 配置使用 nrq=2,理论编码长度约为 PQ 的两倍。与 PQ(未启用 refinement) 相比,PRQ(未启用 refinement) 将 Recall@100 从 0.6083 提高到 0.7869,但索引构建时间达到 5,378 秒,约 89.6 分钟。这说明残差编码可以用更高的编码预算和构建成本换取更多召回率。
PRQ + refine_type=FP32 在 refine_k=4 时达到 0.9869 Recall@100,但峰值 QPS 为 420.4,峰值 QPS 运行点 P95 为 109.5 ms,构建时间约为 88.9 分钟。评估该配置时,需要同时考虑离线构建时间窗口和在线延迟。
该组实验比较 SQ8、SQ6 和 SQ4U 在未启用 refinement 时,形成的压缩梯度,并进一步对比以 FP32 和 SQ8 作为 refine_type 时的结果。

SQ6 将索引构建产物增量从 SQ8 的 874 MB 降至 691 MB,将加载后容器内存增量 从 1,006 MB 降至 828 MB,但 Recall@100 也从 0.9761 降至 0.9563。在 Cohere 1M 上,SQ6(未启用 refinement) 体现的是明确的空间—精度取舍,而不是近乎无损的 SQ8 替代方案。SQ6 的召回率损失能否通过增大 ef 弥补,将在后面的ef参数扫描实验中进一步验证。
SQ4U(未启用 refinement) 的加载后容器内存增量为 608 MB,峰值 QPS 为 1,042.9,但 Recall@100 仅为 0.8861。对于直接依赖向量检索结果质量的应用,需要结合更大的候选集、refinement 或下游 reranker,进一步验证该配置是否满足业务要求。
在固定 ef=256 的条件下,分别测试 refine_k=1/2/4/8的Recall和QPS,结果如下图所示:

图 5:SQ + refine_type=FP32:Recall@100 随 refine_k 的变化。

图 6:SQ + refine_type=FP32:峰值 QPS 随 refine_k 的变化。
SQ4U + refine_type=FP32 在 refine_k=4 时达到 0.9830 Recall@100,在 refine_k=8 时达到 0.9899。这表明,低位宽量化可以通过扩大高精度重排候选集来恢复排序质量;但加载后容器内存增量会回升到约 3.56 GB,SQ4U 原有的运行时内存优势也会大幅减弱。
SQ6 与 SQ8 在采用 refine_type=FP32 后的结果非常接近:refine_k=4 时,Recall@100 分别为 0.9875 和 0.9881;refine_k=8 时,分别为 0.9936 和 0.9940。此时,基础量化位宽对最终召回率的影响已经显著缩小,主要差异转向索引构建产物、查询吞吐和重排开销。
吞吐表现呈现出相反趋势:随着 refine_k 增大,进入高精度重排的候选数量按比例增加,峰值 QPS 整体下降。在单次参数扫描中,SQ8 的峰值 QPS 从 768.6 降至 443.0;SQ4U 则从 827.4 降至 613.5。SQ6 也表现出相同的下降趋势。
因此,refine_k 并不是越大越好。它提高 Recall@100 的同时,会增加候选重排计算和内存访问。在本次测试中,refine_k=4 对三种 SQ 类型都已经把 Recall@100 提升到约 0.983~0.988;继续提高到 refine_k=8 主要用于追求接近 0.99 的召回率,但需要接受更明显的吞吐损失。
在固定 ef=256、refine_k=4 的条件下,对比 refine_type=FP32 与 refine_type=SQ8。

使用 SQ8 作为 refine_type 时,召回率低于 refine_type=FP32,但 refinement 数据的资源占用显著下降。以 SQ6 为例,refine_type=SQ8 的 Recall@100 为 0.9818,加载后容器内存增量 约为 1.55 GB;refine_type=FP32 的 Recall@100 为 0.9875,对应的容器内存增量约为 3.76 GB。
因此,refinement 不能仅作为一个布尔开关看待。refine_type 决定重排表示的精度与存储成本,refine_k 决定进入重排阶段的候选规模,两者需要联合调优。
为比较相近结果质量下的资源成本,选取以下 Recall@100 接近的实测配置:

当目标 Recall@100 约为 0.98 时,SQ6 + refine_type=SQ8 的加载后容器内存增量明显低于采用 refine_type=FP32 的配置。若目标接近 0.99,三种 SQ 位宽都需要 refine_type=FP32 和更大的 refine_k,基础量化带来的内存优势会被 refine_type=FP32 数据显著抵消。

图 7:峰值 QPS 随 ef 的变化。

ef 扫描用于区分瓶颈来自图搜索宽度(ef)不足,还是来自向量压缩表示本身的距离误差。HNSW FP32 和 SQ8(未启用 refinement) 的召回率都会随着 ef 增大而持续上升,说明扩大底层候选搜索范围仍然有效。
在本次 m=96, nbits=8 配置下,PQ(未启用 refinement) 的 Recall@100 从 ef=128 到 ef=512 几乎没有变化;PRQ(未启用 refinement) 的增幅也很有限。这表明主要限制不再是图搜索宽度,而是量化表示中的距离排序误差。
因此,当 PQ/PRQ(未启用 refinement) 的召回率进入平台期后,继续提高 ef 通常只会带来有限的召回率增益,同时降低吞吐。更有效的方向是提高编码预算,例如增大 m,或启用 refinement 并扫描 refine_k。

图 8:Recall@100 随 refine_k 的变化。

图 9:峰值 QPS 随 refine_k 的变化。

固定 ef=256 后,refine_k 的收益与基础量化误差密切相关。SQ8 的量化误差较小,refine_k=1 和 refine_k=2 时 Recall@100 均为 0.9807;继续提高到 4 和 8,召回率仍会上升,但 峰值 QPS 明显下降。
PQ 对 refine_k 的响应最明显:Recall@100 从 0.8629 提高到 0.9848,说明扩大候选集后,FP32 重排能够显著修正量化距离造成的排序偏差。该结论仍仅适用于本次 m=96, nbits=8 配置。
PRQ + refine_type=FP32 在 refine_k=1 时已经达到 0.9704 Recall@100;提高到 4 和 8 后,分别达到 0.9869 和 0.9931。与此同时,峰值 QPS 从 560.1 降至 270.9,串行查询 P95 从 12.3 ms 上升至 24.5 ms。生产评估还应同时观察峰值吞吐对应并发度下的 P95/P99、过滤条件以及真实查询分布。
固定 ef=256,选取各类索引中 Recall@100 最接近 0.98 的实测配置。

如果业务可以接受约 0.976 的 Recall@100,SQ8(未启用 refinement) 在本次测试中同时提供了更高的峰值 QPS 和更低的运行时容器内存。若必须达到 0.98 以上,量化索引通常需要启用 refinement;此时 refine_type=FP32 可能使运行时内存占用接近或超过 HNSW FP32。

以下表格配置仅作为本次 Cohere 1M 测试的评估参考,不代表适用于所有数据集和硬件环境的固定默认值。实际决策仍需结合目标向量分布、TopK、过滤条件、并发模型和硬件重新验证。

当内存允许,并且目标是建立无量化索引参考线,应先使用 HNSW FP32 扫描 ef。这能够给出当前 M、efConstruction 和数据分布下的召回率—延迟基线。
在固定 ef 下,量化索引配合较大的 refine_k 可能得到高于 HNSW 基线点的 Recall@100,因为它扩大了候选集,并使用更高精度进行重排。比较时应同时考虑总计算量、峰值QPS对应并发度下的 P95/P99,以及 refinement 数据带来的资源占用。
在本次 Cohere 1M 测试中,SQ8(未启用 refinement) 的 Recall@100 为 0.9761,峰值 QPS 为 793.1,加载后容器内存增量约为 HNSW FP32 的 31.3%。因此,它可以作为评估量化索引时的首选基线配置。
如果需要进一步降低资源占用,可以测试 SQ6;但本次 SQ6(未启用 refinement) 的 Recall@100 比 SQ8 低约 2.0 个百分点。是否采用 SQ6,应以业务目标 Recall@K 为依据,而不能只比较内存下降比例。
本次 PQ 的理论编码长度为 96 bytes/vector,PRQ 约为 192 bytes/vector,二者的压缩强度都显著高于 SQ8。对应的加载后容器内存增量 分别约为 446 MB 和 516 MB,但 Recall@100 分别为 0.6083 和 0.7869。
在当前参数下,这类未启用 refinement 的配置更适合以下检索场景:
该结论仅适用于本次 m=96, nbits=8, nrq=2 的参数组合。增大 m、调整 nbits 或采用其他编码预算,都可能改变实验结果。
当 PQ/PRQ(未启用 refinement) 的召回率受到量化误差限制时,继续提高 ef 的收益通常较为有限。此时应启用 refinement,并将 refine_type 与 refine_k 作为主要参数联合测试。
本次 PQ + refine_type=FP32 的 Recall@100 从 refine_k=1 时的 0.8629 提高到 refine_k=8 时的 0.9848;PRQ + refine_type=FP32 则从 0.9704 提高到 0.9931。代价是运行时容器内存回升至 3.4 GB 以上,并伴随更高的并发延迟。

refinement 的精度应按业务目标选择:refine_type=FP32 通常能恢复更多召回率,但资源开销更高;refine_type=SQ8 可以降低 refinement 数据占用,但召回率上限也相对较低。
HNSW、HNSW_SQ、HNSW_PQ 和 HNSW_PRQ 共享同一套 HNSW 算法框架,但向量压缩表示会同时影响建图、距离计算和候选排序。选型时,应分别评估图搜索参数、编码预算和 refinement。
本次 Cohere 1M 实验表明,HNSW FP32 适合建立无量化参考基线,SQ8(未启用 refinement) 是均衡的压缩起点;更激进的 SQ、PQ 和 PRQ 配置能够继续降低资源占用,但需要通过更高的编码预算、refinement 或下游 reranker 来恢复结果质量。
最终决策应围绕目标 Recall@K、峰值 QPS、并发延迟、运行时容器内存和索引构建时间窗口展开,并在目标硬件与真实查询分布上重新运行基准测试,而不是寻找脱离具体工作负载的最佳索引。
作者介绍

李成龙
Zilliz 资深开源布道师
文章来自于"Zilliz",作者 "李成龙"。
【开源免费】graphrag是微软推出的RAG项目,与传统的通过 RAG 方法使用向量相似性作为搜索技术不同,GraphRAG是使用知识图谱在推理复杂信息时大幅提高问答性能。
项目地址:https://github.com/microsoft/graphrag
【开源免费】Dify是最早一批实现RAG,Agent,模型管理等一站式AI开发的工具平台,并且项目方一直持续维护。其中在任务编排方面相对领先对手,可以帮助研发实现像字节扣子那样的功能。
项目地址:https://github.com/langgenius/dify
【开源免费】RAGFlow是和Dify类似的开源项目,该项目在大文件解析方面做的更出色,拓展编排方面相对弱一些。
项目地址:https://github.com/infiniflow/ragflow/tree/main
【开源免费】phidata是一个可以实现将数据转化成向量存储,并通过AI实现RAG功能的项目
项目地址:https://github.com/phidatahq/phidata
【开源免费】TaskingAI 是一个提供RAG,Agent,大模型管理等AI项目开发的工具平台,比LangChain更强大的中间件AI平台工具。
项目地址:https://github.com/TaskingAI/TaskingAI