☰
Milvus向量索引选型与调优实战:从IVF到HNSW
2026/10/21 6:13:40 网站建设 项目流程

1. 先搞清楚:向量索引到底在优化什么

Milvus 现在已经是做向量检索绕不开的一个名字了,很多人拿它搭推荐系统、做图搜、处理非结构化数据的语义召回。但我见过不少团队,数据量才几百万条,查询却慢得离谱,或者干脆内存爆掉,最后排查下来,问题都出在索引上——要么选错了类型,要么参数完全是凭感觉拍的。

先说个基础概念,向量索引本质上就是在干一件事:用空间换时间,或者用精度换速度。不建索引的时候,Milvus 做查询就是暴力扫描,每条向量都跟查询向量算一遍距离,数据量上万之后这种搞法基本没法用。建索引的本质,是提前把向量空间的结构组织起来,让查询时能跳过大量不相关的向量,直奔可能近似的那些区域。

Milvus 根据不同的数据类型、数据规模、性能诉求,提供了多条索引路线。我自己的体会是,没有任何一种索引能通吃所有场景。选错了索引,后果比不建索引还严重——不光查询慢,索引构建本身就可能卡上几个小时,甚至把内存撑爆。所以这篇文章会把常见的索引类型、它们的原理差异、以及实际调优的套路从头到尾过一遍,重点是把我踩过的坑和验证过有效的配置直接给你。

先给个总览,Milvus 主流的向量索引可以分成这几个流派:

索引类型核心思路适合规模内存占用查询精度
Flat暴力全扫描万级以内低100%
IVF_FLAT聚类分桶 + 桶内暴力百万级中高
IVF_SQ8聚类 + 标量量化压缩千万级低中高
IVF_PQ聚类 + 乘积量化亿级极低中
HNSW多层图结构百万到千万级高高
DiskANN图结构 + 磁盘存储十亿级低(磁盘)高

别急着对号入座,这几个路线背后的原理、参数、适用边界差异非常大,下面逐一说清楚。

2. 主流索引类型逐个拆解:原理、代价、适用场景

2.1 Flat:最朴素,但有时候它就是最优解

Flat 索引说白了就是不做任何加速结构,查询时逐条计算查询向量和目标向量的距离。这在工程上听起来很“笨”,但我必须强调一个观点:数据量小的时候,Flat 反而是最靠谱的选择。

为什么?因为它不需要构建时间,不占额外内存,更关键的是它给出的是精确结果,召回率是100%。很多业务场景其实数据量就几万条,比如一个小规模的人脸库、几百个用户的个性化推荐冷启动,这种情况下建 IVF 或者 HNSW 完全是杀鸡用牛刀,而且还得花时间调参。

我见过有人非要在十万条不到的集上建 HNSW,结果构建参数没调好,查询延迟一点没降,反而多占了几个 GB 的内存。其实 Milvus 有个自动判定的机制,当你插入的数据量低于index_build_threshold(默认1024条)时,即使你指定了其他索引类型,系统也会自动走 Flat 的路径。所以小数据量阶段,别折腾,直接 Flat,稳得很。

2.2 IVF 家族:聚类分桶是核心思想

IVF(Inverted File)系列是 Milvus 支持最久、用的最多的索引路线。它的核心思想其实很朴素:先对整个向量空间做聚类,把向量分成若干个桶(bucket),每个桶对应一个聚类中心。查询的时候,先找到离查询向量最近的几个桶(具体几个由nprobe参数控制),然后只在这几个桶内部做暴力搜索。

这个思路很像图书馆找书:先根据索书号定位到某个区域,再在这个区域内一本一本翻。相比全馆挨个找,效率是数量级的提升。

IVF 家族里有三个最主要的变体,它们的差异主要体现在“桶内部怎么存”和“向量怎么压缩”上。

IVF_FLAT:向量不压缩,桶内部存的是完整向量。这是精度最高的 IVF 方案,召回率几乎是100%。代价是内存占用偏高——每条向量128维的 float32 需要512字节,再加聚类中心的额外开销。百万级数据量,float32 维度128,光原始向量就要 100万×128×4 = 512MB,加上索引结构,实际占用会比原始数据多出10%到20%。

IVF_SQ8:这里用到了标量量化(Scalar Quantization)。简单理解就是,把原本 float32 的每个维度用 int8 来存,也就是把每个维度的数值范围映射到 [-128, 127] 的整数区间。这样一来,每条向量的内存占用直接从4字节/维降到1字节/维,整体压缩了75%。代价是精度有所下降,因为量化本身是有损的,浮点数的尾数信息被丢掉了。实测下来,在 Recall@10 这种指标上,SQ8 相比 Flat 通常只掉1-2个百分点,但对内存紧张的场景来说,节约的这几百 GB 完全是值得的。

IVF_PQ:PQ(Product Quantization)是更激进的压缩方案。它先把向量拆成若干个子向量(比如把128维拆成8组,每组16维),然后对每个子向量分别做聚类,用聚类中心的 ID 来近似替代原始向量。这样做的好处是压缩比极高,128维向量可以被压到几十个字节,内存占用能降到原始数据的1/16甚至更低。代价也很明显:精度损失比 SQ8 更大,而且 PQ 的聚类存在信息瓶颈,如果你把向量切得太碎,或者每个子空间的聚类中心数太少,召回率会肉眼可见地下降。

我自己的建议是:只在数据量达到千万级以上,且内存预算确实紧张的时候才考虑 IVF_PQ。在几百万条的场景里硬上 PQ,纯粹是自己给自己找麻烦。

2.3 HNSW:图结构的暴力美学

HNSW(Hierarchical Navigable Small World)是目前ANN(近似最近邻)领域公认的“性能天花板”之一。它构建的是一种多层图结构:底层包含所有数据点,上层逐层稀疏。查询的时候,从顶层开始,沿着图的边不断跳转到离目标更近的节点,然后在稀疏层快速定位到一个区域,再到底层做精细搜索。

这个过程很像你在一个陌生城市找某家店:先开车上高速(顶层)快速跨越长距离,下高速进入城市主干道(中层),最后钻小巷子(底层)精确到达目的地。

HNSW 最大的优势是查询速度快、召回率高,在同等的召回率要求下,HNSW 的查询延迟通常比 IVF 低一个数量级。但它有两个硬伤:

第一是内存占用高。HNSW 不只是存向量本身,还要维护节点之间的连接关系,每个节点的出边都要记录邻居的 ID。这个开销大约是原始向量的1.3到2倍。百万级128维向量,HNSW 索引光内存就能吃掉接近2GB。

第二是构建时间长。图结构的插入是逐条进行的,而且要反复评估“这个节点该连哪些邻居”,构建速度比 IVF 的批量聚类慢不少。在千万级数据上,HNSW 的构建可能要跑几个小时甚至十几个小时。

2.4 DiskANN:把图搬到磁盘上

DiskANN 是近几年的新方案,它把 HNSW 的图结构从内存搬到了 SSD 上。这听起来很反直觉——磁盘访问不是比内存慢几个数量级吗?但 DiskANN 做了两件事来弥补:一是用 Vamana 图结构控制每个节点的出度,让单次查询只需要访问有限几个节点和它们的数据;二是向量本身做了压缩,读取时只需要把相关的边和经过压缩的向量读进内存,而不是把整张图全加载进来。

DiskANN 最大的价值在于支持数据量远超内存的场景。比如你只有64GB内存,但向量数据有200GB,HNSW 和 IVF 都装不下,DiskANN 就是为数不多还能正常跑的方案。在十亿级、百亿级的数据量上,DiskANN 几乎是唯一选择。

不过 DiskANN 的查询延迟会比 HNSW 高一截,毕竟有磁盘 I/O 的开销。实测下来,在 SSD 上 DiskANN 的 p99 延迟大概在几十毫秒级别,HNSW 能做到几毫秒。所以 DiskANN 不是用来替代 HNSW 的,而是用来解决“内存放不下”这个极端问题的。

3. 索引选型:照着场景抄作业即可

3.1 从数据量和内存预算反推

索引选型不需要那么纠结,大多数场景按数据量分档就能定下来:

数据规模推荐索引理由
0 到 10万条Flat 或 IVF_FLAT暴力扫也够快,别折腾
10万到500万条HNSW 或 IVF_FLATHNSW 快,IVF_FLAT 省内存,看预算
500万到5000万条IVF_SQ8 或 HNSW内存紧了用 SQ8,性能优先用 HNSW
5000万到数亿条IVF_PQ 或 DiskANN内存吃紧用 PQ,超大场景直接 DiskANN
十亿级DiskANN内存装不下,必须磁盘卸载

这个表不是拍脑袋写的,它背后是两个影响因素:索引的构建时间、查询时的内存占用模型。比如 IVF 的查询复杂度是 O(nprobe × nlist 桶内的向量数),当你把数据量翻倍时,只要把聚类数 nlist 也翻倍、同时保持每个桶内向量数不变,查询延迟是可以基本保持稳定的。而 HNSW 的查询延迟增长是亚线性的,所以数据量大了之后 HNSW 的速度优势会更加突出。

3.2 看业务对精度的容忍度

除了数据量,业务本身的精度要求也是选型的关键。

这里要先澄清一个高频误解:很多人以为“索引是近似搜索,召回率一定低”,其实不是。召回率是可以调的,代价是查询延迟。IVF 系列的 nprobe 越大,扫描的桶越多,召回率越高,延迟也越高。HNSW 的 ef 参数同理。所以索引选型时要回答的问题是:你到底能接受多少延迟?

我个人有一个经验公式:对于要求高召回(Recall > 0.99)的场景,比如人脸识别、指纹匹配这类安全敏感型业务,尽量用 IVF_FLAT 或 HNSW,并且把查询参数调到接近暴力搜索的程度。对于做语义召回、粗排的场景,比如向量召回作为推荐系统的一环,召回率在0.90到0.95之间往往就够用了,这时候可以用 IVF_SQ8 或 IVF_PQ,把内存成本打下来。

我在实际项目中做过一个对比测试,同样100万条128维数据,IVF_SQ8 的索引比 IVF_FLAT 省了接近80%的常驻内存。因为数据量级过了千万之后,内存成本就成了头号敌人——8GB内存条在云上的价格,比单次查询多出几毫秒更让老板心疼。

3.3 度量方式对索引选型的影响

一个很多人忽视但非常关键的点是:度量方式是先于索引类型定下来的。

Milvus 支持的度量方式主要有三种:欧氏距离(L2)、内积(IP)、余弦相似度(COSINE)。其中 COSINE 本质上是对向量做归一化后在原点附近度量,跟 IP 在数据归一化后是等价的。

为什么这个很重要?因为metric_type一旦定了,它不仅影响距离计算,还直接决定了向量归一化的方向。在真实业务里,很多团队在建库的时候没想清楚,后来发现需要切换度量方式,结果只能全量重新构建索引。这个时间成本在千万级数据上是按小时计的。

我踩过这个坑。有一次在某个项目里,先用的是 L2,后来产品想改成按语义相似度排序,我意识到要重建索引的时候,构建任务已经在集群里跑了大半天。从那之后,我的习惯是:建库前先问清楚业务到底要度量什么,宁可多花半天做验证,也不要事后返工。

4. 核心参数配置:从公式到最佳实践

4.1 IVF 系参数:nlist 与 nprobe

IVF 系列的核心参数是nlist和nprobe。

nlist是聚类中心的数量,也就是把向量空间分成多少个桶。nprobe是查询时搜多少个桶。这两个参数一个管构建粒度,一个管查询精度,它们共同决定了查询的代价。

nlist大小的经验公式是:nlist取 4×sqrt(N) 到 16×sqrt(N),其中 N 是向量总数。比如有100万条向量,sqrt(100万) = 1000,那么 nlist 在4000到16000之间都合理。太小则桶内向量太多,搜索效率低;太大则桶太碎,可能很多桶是空的,而且构建时聚类开销变大。

nprobe的调整逻辑是:越大召回率越高,延迟也越高。官方建议的取值范围是 1 到 nlist。实际操作中,我一般从1开始线性往上调,每调整一次看 Recall@10 和 p99 延迟,在召回率不再快速增长的位置停下。对于百万级数据,nprobe = 8到16通常就能跑到 Recall@10 > 0.95。

这里提醒一下:nprobe 是可以放到查询请求里动态调整的,不用重建索引。所以调参的正确姿势是——先固定 nlist,把索引构建出来,然后用不同的 nprobe 分别跑同一个测试集,测出“召回率—延迟”曲线,根据业务需求选一个落点。不要上来就拍脑袋设一个大的 nprobe,那会让每次查询的延迟直接翻好几倍。

4.2 HNSW 参数:M、efConstruction、ef

HNSW 的三个参数公认为:M、efConstruction、ef。

M是每个节点的最大边数,对应图的连通度,影响内存占用和检索跳转速度,官方建议范围是4到64。M越大,图越密集,检索的路径越短,但构建时间和内存占用越高。

efConstruction是控制构建时动态候选列表的大小。这个参数决定了图的质量,值越大,构建出的图越接近理想结构,但构建时间线性增长。官方范围是1到65536,实战中在128到512之间比较常见。

ef是查询时动态候选列表的大小,类似 IVF 的 nprobe。ef越大召回率越高、延迟越高。它同样可以在查询请求里动态设置,不用重建索引。官方建议初始值设成M的两倍,然后按需往上调。

我自己测试的经验是:M=32、efConstruction=200、ef=64在多数场景下是个不错的起点。如果你追求极致的召回率,可以把ef调到512以上,延迟会增加,但 HNSW 的速度底子在,一般还能扛得住。

需要注意一个细节:HNSW 构建是逐条插入的,所以构建时间对参数很敏感。我见过有人在千万级数据上用M=64+efConstruction=1000来构建,结果构建任务跑了将近一天才完成。如果你的场景需要频繁构建索引,建议压制一下这两个参数,构建时间能缩短几倍,查询性能的损失其实很小。

4.3 PQ 相关参数:M(子空间数)与 nbits

PQ 除了 nlist/nprobe,还有两个特有参数:M(子空间数量,避免和 HNSW 的 M 混淆,有些资料里写作pq_m)和nbits(每个子空间聚类中心数的对数值)。

以128维向量为例,如果 PQ 的 M 设置为16,则每个子空间包含 128/16 = 8 个维度。nbits 为8意味着每个子空间有 2^8 = 256 个聚类中心。那么每个向量实际存储的就是16个字节的聚类中心编号(每个编号占1字节),相比原始512字节,压缩率是32倍。

但注意:PQ 的M不是越大越好。子空间切得越细,每个子空间的维度越少,聚类效果越差,召回率掉的越凶。nbits越小,压缩越狠,但量化误差越大。实际操作中,我的建议是:先保持 nbits=8 不动,调节 M 观察召回率和内存变化;如果数据维度本身不高(比如只有64维或128维),那么 PQ 的 M 设置到维度的一半左右就要非常谨慎了。我个人的底线是不管怎么压,每个子空间至少要有8个维度,否则 PQ 的聚类失真会严重到没法用。

4.4 其他场景参数:index_build_threshold 与 metric_type

index_build_threshold之前提过,是触发索引构建的向量条数阈值。取值越小,越早开始建索引,查询快,但建索引的CPU和时间成本占用越早出现。置于1024这个默认值,我个人觉得挺好用的,不用动。

metric_type我这里再强调一次:它对索引的查询结果影响非常直接。L2 度量的是欧氏距离,IP 度量的是内积值,COSINE 其实可以理解为归一化后的 IP。同类索引在不同度量下,召回率的标定方式也不同。所以调参之前,一定要先确认自己的 metric_type 是哪个,别拿 IP 下测出的参数直接套到 L2 场景里。

5. 实操:从建集合到查询,完整走一遍调优流程

5.1 场景假设与预估

假设我们要做一个千万级的图片向量检索系统,向量的维度是128维,数据量在500万左右,业务要求是单次查询 p99 延迟不超过20ms,召回率 R@10 尽量在0.95以上。

先按内存粗算一下:如果采用 HNSW,数据本身是 500万×128×4 = 2.56GB,加上图结构开销大约1.5倍,总计约6.4GB。如果采用 IVF_SQ8,压缩后向量是 500万×128×1 = 0.64GB,加上聚类中心和索引结构约1GB出头,差距很大。

所以这台机器如果内存只有8GB,HNSW 很勉强,IVF_SQ8 轻松跑;如果内存有32GB,那 HNSW 的查询性能优势就可以充分发挥。我们这次按内存16GB的常见配置来选型,最终锁定 HNSW。

5.2 创建索引的完整步骤

在 Milvus 里创建索引的流程,用 PyMilvus 客户端来演示比较直观:

from pymilvus import ( connections, CollectionSchema, FieldSchema, DataType, Collection, utility ) # 1. 连接 Milvus,指定本机的服务地址 connections.connect(host="127.0.0.1", port="19530") # 2. 定义 schema,字段包括主键、向量字段和业务标签字段 pk_field = FieldSchema( name="id", dtype=DataType.INT64, is_primary=True ) vector_field = FieldSchema( name="embedding", dtype=DataType.FLOAT_VECTOR, dim=128 ) tag_field = FieldSchema( name="category", dtype=DataType.VARCHAR, max_length=64 ) schema = CollectionSchema( fields=[pk_field, vector_field, tag_field], description="demo image vector collection" ) # 3. 创建集合 collection = Collection(name="image_vectors", schema=schema) # 4. 创建 HNSW 索引(注意 metric_type 这里用 COSINE) index_params = { "index_type": "HNSW", "metric_type": "COSINE", "params": {"M": 32, "efConstruction": 200} } collection.create_index( field_name="embedding", index_params=index_params, index_name="idx_hnsw" )

这里有几个细节值得展开讲。

创建索引前,metric_type一定要想清楚。代码里用的是 COSINE,如果你的业务更关心原始距离,就用 L2。IP 和 COSINE 的差别在于是否自动归一化,Milvus 在 COSINE 下会自动把向量归一化,后端计算时相当于做了归一化的 IP。如果你的向量本身已经归一化了,那 IP 和 COSINE 在数学上完全等价。

创建集合之后,插入数据到一定量级会自动触发索引构建。你也可以手动触发,用collection.create_index()其实就已经完成了定义,真正的后台构建会在索引load之后开始。这里有一个常见的坑:如果你不调用load_collection(),虽然索引创建了,但查询时系统可能不会走这个索引,而会退化成全量搜索。所以建索引之后,务必要把集合加载到内存:

collection.load()

加载过后,你可以通过utility.index_building_progress()查看索引构建的状态,确认构建完成再开始压测。

5.3 查询参数动态调优

查询参数(HNSW 的 ef、IVF 的 nprobe)可以在每次 search 时动态指定,不需要重建索引。这是调参最舒服的一点。代码大致长这样:

search_params = { "metric_type": "COSINE", "params": {"ef": 64} } results = collection.search( data=query_vector, anns_field="embedding", param=search_params, limit=10, output_fields=["id", "category"] )

压测时我通常会写一个循环,让 ef 从32扫到512,每次跑相同的测试集(比如1000条真实查询向量),记录平均延迟、p99延迟和 R@10。然后把结果画一条曲线,找一个“召回率增长趋缓、延迟还能接受”的拐点定下来。

有人图省事,直接抄网上推荐的一大套参数,但不同机器不同数据分布的最佳参数就是不一样。集群的CPU主频、内存带宽、核数,甚至你有没有开 SIMD 指令集,都会影响最终延迟。抄来的参数顶多算个起点,自己测试出来的参数才是能拿去上生产的参数。

5.4 构建时间估算与优化

关于构建时间,我提供一个经验参考:在8核CPU、32GB内存、SSD存储的机器上,500万条128维向量构建 HNSW(M=32, efConstruction=200)大约需要1到2小时。IVF_SQ8(nlist=16384)的构建时间会短一些,通常在20到40分钟左右。

如果你觉得构建时间不可接受,可以从几个方向下手优化:一是降低efConstruction到128甚至64,构建时间能省将近一半,查询召回率损失通常很小;二是控制M不要超过48,M=64虽然查询更快,但图构建和内存占用的代价都很高;三是分批插入数据,让 Milvus 在数据量达到阈值后边插边构建,而不是一次性灌入大批数据后再触发全量构建。

5.5 压测验证:用实跑数据说话

压测这个环节,我强烈建议大家自己写一个简单的评测脚本,不要只看 Milvus 官方给的 benchmark 数据。因为官方测试用的机器配置、数据分布、查询模式跟你的业务很可能差非常多,直接套用没什么参考价值。而且官方基准测试测的是纯查询性能,真实业务必然有索引构建、数据更新、内存容不下这些场景。

一个可用的评测脚本按这个思路写:

import random import time import numpy as np from pymilvus import Collection collection = Collection("image_vectors") collection.load() # 构造测试向量,可以是从真实库里抽的,也可以是随机生成的 query_vectors = [np.random.rand(128).tolist() for _ in range(1000)] recall_results = [] latencies = [] for qvec in query_vectors: # 用 superset 方式的暴力搜索作为“标准答案”,只在小集上做 start = time.time() res = collection.search( data=[qvec], anns_field="embedding", param={"metric_type": "COSINE", "params": {"ef": 64}}, limit=10 ) end = time.time() latencies.append((end - start) * 1000) avg_lat = sum(latencies) / len(latencies) p99_lat = sorted(latencies)[int(len(latencies) * 0.99) - 1] print(f"avg: {avg_lat:.2f} ms, p99: {p99_lat:.2f} ms, qps: {1000 / avg_lat:.1f}")

这里要注意,如果你的集合很大(比如千万级),用暴力搜索求标准答案本身就非常贵,不现实。替代方案是从真实业务里挑一批你已经标注过的标准答案,或者用小规模 subset 来标定召回率,再外推到全量数据集。这种方式虽然不完美,但能相对准确地评估“索引参数变了,召回率变化了多少”这个相对趋势。

6. 踩坑实录:那些文档里不会写的问题

6.1 构建成功但查询慢:八成是没真的走到索引

这是我在排查别人集群时遇到最多的一类问题。表象是索引明明建好了,查询却还是秒级延迟,一看就是在做暴力搜索。最常发生的原因有这几种:

一是集合压根没有 load。索引在后台构建完成后,如果没有load_collection(),查询可能不会实际使用索引结构。这种情况多见于只写了创建索引的代码,忘了 load。

二是 segment 没有合并。Milvus 是日志结构的存储模型,新插入的数据以小 segment 的形式存在磁盘上。小 segment 往往还没达到索引构建的阈值,或者索引构建还在排队,查询时系统只能对这部分小 segment 走暴力扫描。所以插入完数据后,记得调用collection.compact()做一次手动合并,让 segment 变成规整的大段,索引构建也会更高效。

三是查询指定的字段不对,查了一个没有建索引的向量字段。Milvus 允许一个集合存在多个向量字段,每个字段的索引是独立的,如果anns_field指定的是没建过索引的字段,那自然会走暴力搜索。

6.2 内存爆掉:索引不是只有向量本身

还有一个高频问题:任务跑着跑着内存就爆了,而且清理不掉。这个问题的根源在于,很多人只算了向量本身占多少内存,忘了算索引结构本身的开销。

拿 HNSW 来说,除了原始向量,每个节点还需要存邻居 ID 列表,邻居列表一般是定长的数组。M=32 时,每个节点需要 32×4 字节 = 128字节来存邻居ID,再加一些指针、层级标记之类的开销,每个节点总开销至少200字节以上。500万条数据,这个附加开销就是1GB以上。

讲一个我实际踩过的坑。当时一个场景要用 HNSW 建千万级数据的索引,选了 M=64。算完之后发现光附加开销就超过了3GB,加上原始向量 1.28GB,再加 Milvus 自己管理 segment 的开销,整个进程直接吃掉了12GB内存。后来把 M 降回 32,内存立刻回到了7GB以内,查询延迟的差别完全能接受。

所以在选 HNSW 相关参数之前,我建议先执行一下内存预算公式:

HNSW 索引预估内存 ≈ 1.3 × 原始向量内存 + N × M × 4 字节

其中 N 是向量条数,M 是图的最大度数。如果你按这个公式算出来超预算,要么降 M,要么换成 IVF_SQ8 压缩方案。

6.3 召回率假高:评估方法本身有毛病

很多团队报上来的召回率很好看,但上线之后召回效果完全不达预期,往往不是索引的问题,而是评估方法有问题。

最常见的问题出在评测集的构造上。有些人用随机向量作为 query 去测召回率,这在绝大多数业务场景里是没有意义的——真实业务里 query 的分布跟入库数据的分布高度相关,随机向量根本不在数据分布的密集区域,测出来的召回率参考价值极低。正确的做法是用一部分真实 query(比如从线上日志里扒下来的)放进测试集,先把它们对应的标准答案算出来,再作为压测基准。

另一个容易犯的错是:评估时只跑一次就下结论。索引查询的延迟本身有波动,特别是有 merge 任务在跑的时候;不同时间点跑出的 p99 会差很多。如果你真要拿数据做决策,至少要跑三轮,每轮1000个 query,取中位数或者平均,这样才能压掉噪音。

6.4 参数调整很勤快,但忽略了数据更新频率

还有一个容易被忽视的问题:你的数据不是静态的,索引参数要配着更新频率来调整。

如果你的业务每天都有大量增量数据入库,那么索引即便构建完成了,新进来的数据也会以 segment 的形式常驻内存,和已有索引并存。这些 segment 如果长期不合并,查询时的“索引加速”效果会被多个小 segment 的暴力扫描稀释。因此,高更新频率场景下,除了调查询参数,还要定一个频率来做 compact 合并。我见过有人是每天凌晨跑一次 compact,效果就挺不错。

反过来,如果业务数据本身就是只读的,那索引构建完就长期不动,你可以放心地把参数往高配,追求极致查询速度。

6.5 CPU 与内存分配:别忽略了机器的实际能力

很多人忽略了 Milvus 的底层向量计算高度依赖 CPU 指令集。如果机器CPU不支持 AVX512,甚至不支持 AVX2(比如一些老旧的云实例或者便宜的 ARM 机器),那么向量距离计算的性能会大幅下降,同样的索引参数在不同机器上跑出来的延迟能差出3倍以上。

所以在压测之前,先确认机器的 CPU 型号和向量指令集是否给力。在云上开机器的时候,不要贪便宜选那种“共享核”的实例,向量检索是计算密集型任务,独享核和共享核的差距非常明显。

7. 一条经验:从需求出发,反推所有参数

最后分享一个我个人的经验:做向量检索的性能优化,不要从“哪个索引最快”出发,要从“业务到底要什么”出发。

先回答四个问题:数据量有多大?内存预算多少?能接受的查询延迟是多少?召回率的最低红线是多少?这四个答案定下来之后,索引类型和参数基本就自动浮出水面了。比如你的答案是“500万条、8GB内存、p99 20ms、R@10 0.93以上”,那 HNSW 就基本出局了(6.4GB内存太紧张),IVF_SQ8 是主选,nlist=12000左右,nprobe 从8开始扫描,大概率能满足要求,然后在这个基础上微调一下就行。

另一个建议是,先把整套流程在小数据集上跑通,比如先抽10万条数据,建索引、压测、调参,流程跑顺了再上全量。不要一上来就在500万条上反复改参数重建索引,改一次构建要等几个小时,试错成本太高。小数据量上的相对变化趋势,在大量级上基本是同样成立的。

索引调优这事,说到底是围绕一个三角在做平衡:内存、速度、召回率。主动放弃一头,才能把另外两头做扎实。和做产品是一个道理。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询