Chapter 03

索引内核:在取舍三角里选一个点

02 章拆了流式分布式架构——写入怎么落 WAL、segment 怎么在节点间流转。这一章钻进 sealed segment 内部那份索引:每一种索引(FLAT / IVF / HNSW / DiskANN / GPU / 稀疏)都是召回、延迟、内存这个三角里的一个选点,选索引就是选放弃哪个角。读完,「该用哪个索引、参数调在哪一侧」会从背口诀变成能推导。

本章你将建立的 schema

  • 取舍三角:召回 vs 延迟·QPS vs 内存——三者不可兼得,索引类型 + 参数就是在这三角里定位。
  • 索引家族谱:FLAT(精确基线)/ IVF(聚类倒排)/ HNSW(分层图)/ DiskANN(磁盘图)/ GPU·CAGRA / 稀疏倒排。
  • 两类旋钮的硬区分:建索引旋钮(nlist、M、efConstruction,改了要重建)vs 查询旋钮(nprobe、ef,每次可调、无需重建)。
  • 相容矩阵:向量类型 × 索引类型 × 度量类型,三者必须对齐(GPU 无 COSINE 是高频陷阱)。

这一章承接 01 §1.6 留下的「索引内部怎么工作」。每种索引按同一套结构讲:一句话本质 → 为什么要它 → 比文档深一层的机制 → 必要处配图或 callout。代码片段只用来让旋钮落地,未在本机运行,按概念读。读者已懂 ANN 的召回↔速度权衡,3.1 只快速把第三个轴(内存)补进来,不重述基础。

3.1取舍三角:召回 / 延迟·QPS / 内存

向量索引在三个互相拉扯的维度间取舍——高召回、低延迟·高 QPS、低内存——没有任何一种索引能同时占满三个角。

为什么需要它

ANN 的经典叙事是召回↔速度二维权衡。但在工程落地里,内存是同等重要的第三条轴:一份 768 维 float32 向量约 3 KB,十亿条就是 ~3 TB 纯向量,再叠加图结构或聚类元数据——内存(或显存)往往才是先撞上的墙。把内存补进来,权衡就从一条线变成一个三角。

比文档深一层的机制:三个角彼此牵制,背后是同一组物理约束。要高召回就得检查更多候选点,候选越多越慢——召回顶着延迟。要低延迟·高 QPS就得把数据结构和向量留在内存、用更精巧的图减少跳数——速度顶着内存。要低内存就得量化压缩(float32 → uint8 甚至 1-bit)或把数据下沉到 SSD——省内存又顶着召回(量化有损)和延迟(SSD 慢于 RAM)。索引类型决定你坐在三角的哪一区,参数(nprobe、ef)再在小范围里微调。

高召回 低延迟 · 高 QPS 低内存 FLAT 召回100% · 内存=原始 HNSW 高召回+高QPS · 内存重 IVF_FLAT 平衡点 IVF_SQ8 / PQ 量化↓内存↓召回 DiskANN ↓RAM · 延迟受SSD CAGRA(GPU) 顶QPS · ↑内存↑硬件
图 3.1每种索引落在三角的不同位置——越靠近某个角,越偏向那个目标、越牺牲对面。注意:没有任何索引同时占满三个角。选索引的本质不是「选最好的」,而是选愿意放弃哪个角——业务能容忍 95% 召回就能换走数倍内存或延迟。

与索引粒度的关系:回指 01 §1.6——索引不是整张 collection 一份,而是每个 sealed segment 各建一份。所以下文每一种索引,描述的都是「一个 segment 内部那份结构」长什么样;一次检索是各 segment 各跑一遍再合并 top-k(读路径在 04 章展开)。这也意味着索引选型的内存账要乘以 segment 数。

本章读法

下面 3.2–3.7 逐个索引走一遍,每个都标出它在三角里的位置 + 暴露哪些旋钮。3.8 单独把「建索引旋钮 vs 查询旋钮」这条面试高频线拎出来。3.9 给决策树和相容矩阵收口。不必记全部参数默认值——记住每个索引放弃了哪个角就够推导。

3.2FLAT:暴力精确,召回的基准线

FLAT 不建任何近似结构,把查询向量和每一条向量逐一算距离——召回率恒为 100%,但每次检索是一次全量扫描。

为什么需要它

近似索引的召回率是相对「真实最近邻」的比值,那这个「真实」从哪来?FLAT 给出。它是ground truth 基线:评测任何 ANN 索引的召回率,都拿它的结果当分母。同时在小数据量(几万级、低维)下,全扫的绝对耗时本就够低,没必要引入近似误差。

比文档深一层的机制:FLAT 在三角里坐死在「高召回」顶点——召回拉满,代价是延迟随数据量线性增长(O(N) 距离计算),且内存等于原始向量全量(不压缩、不加结构)。它没有任何建索引旋钮,也没有召回↔速度的拨盘可调——召回永远是 1,唯一能动的是 metric。这正是它和后面所有索引的分界:其他索引都在用召回换速度或内存,FLAT 一样都不换。Milvus 还有个 GPU_BRUTE_FORCE,是 FLAT 的 GPU 版,靠显存带宽把全扫做快,召回同样是 1。

什么时候真用它

三种场景:① 给新索引测召回率时当 ground truth;② 数据量小到全扫也够快(经验上几万条、能整个塞进内存);③ 业务对漏检零容忍(合规、去重)且数据不大。一旦数据涨到百万级以上,FLAT 的线性延迟就会先撞墙——该换图或聚类索引。

3.3IVF 家族:聚类倒排,召回↔速度有个拨盘

IVF(inverted file,倒排文件)先用 k-means 把所有向量聚成 nlist 个簇,查询时只扫离查询向量最近的 nprobe 个簇——nprobe 就是召回换速度的连续拨盘。

为什么需要它

全扫慢在「扫了全部」。IVF 的洞察是:最近邻多半落在与查询向量同簇或邻簇里。先聚类、查询时只看少数几个最近簇,就把候选集从「全部」砍到「几个簇」——用一点漏检风险(最近邻偶尔落在没扫的簇)换数量级提速。

比文档深一层的机制:建索引时跑一次 k-means 得到 nlist 个簇心,每条向量按最近簇心归入对应倒排桶——nlist 是建索引旋钮,定了簇的数量(粒度),改它要重建。查询时算查询向量到各簇心的距离,取最近 nprobe 个簇、只在这几个桶里做精算——nprobe 是查询旋钮,每次检索可调:nprobe 调大 → 扫更多簇 → 召回升、变慢;调小 → 反之。同一份建好的 IVF,靠 nprobe 就能在召回↔速度间滑动,无需重建——这是 3.8 那条线的第一个实例。

三个变体:在「内存 / 召回」上再分叉

IVF 的桶里存什么向量,决定了它在三角里往「低内存」角滑多远:

表 3.1 · IVF 三变体(桶内向量表示不同 → 内存/召回不同)
变体桶里存什么内存召回 / 速度
IVF_FLAT完整 float32 向量≈ 原始(仅多簇元数据)无精度损失,召回最高;速度中等
IVF_SQ8标量量化:每维 float32→uint8约省 70–75%(~4× 小)召回通常仅降 2–3%;比 PQ 快
IVF_PQ乘积量化:切子向量各自码本编码最小(可压到原始几分之一甚至更低)体积最小、最快;召回最低

SQ 与 PQ 的取舍方向相反:SQ8(scalar quantization,标量量化)把每一维独立从 4 字节压到 1 字节,简单、解码快,所以更快;PQ(product quantization,乘积量化)把向量切成若干子向量、每段用一个小码本(codebook)的索引来代替,压缩率更狠,所以在同等压缩比下召回优于 SQ,但码本查表使它在解码上让出一点速度。一句话:要省内存优先 PQ(同压缩召回更高),要快优先 SQ。

版本差异 · nlist 默认值

官方文档里 nlist 默认值出现过两个数:较新的 per-index 页写 128,较旧的 in-memory index 页写 16384。这是文档版本差异,不是你记错。以你所用 Milvus 版本的对应 per-index 文档页为准,别照搬教程里的硬编码值。经验法则(非默认):nlist 常取 ~4·√N 量级,N 为该 segment 内向量数。

想一想

一份 IVF_FLAT 索引建好后,线上发现召回率偏低、但延迟还有富余。要提升召回,必须重建索引吗?换成另一个问法:调 nlist 和调 nprobe,哪个要重建、哪个不用?

展开答案(先停 10 秒)

不必重建。召回偏低、延迟有余,正是调大 nprobe 的标准场景——扫更多簇、召回上去、把富余的延迟预算花掉。nprobe 是查询旋钮,每次检索现传现改。

而 nlist 是建索引旋钮——它定义了簇的划分本身,写进了索引结构。改 nlist 等于重新跑 k-means、重新分桶,必须重建索引。所以线上调召回的第一反应永远是先动查询旋钮(nprobe),而不是重建。

3.4HNSW:分层导航小世界图,高召回区的 QPS 王者

HNSW(hierarchical navigable small world,分层可导航小世界)建一个多层图:顶层稀疏、底层稠密,查询从顶层入口贪心下降,逐层精化到底层目标——结构上几乎就是向量版的跳表(skip list)。

为什么需要它

IVF 靠「只扫几个簇」剪枝,但簇边界附近的最近邻容易漏。图索引换一种思路:把向量连成一张「谁离谁近」的邻居图,查询沿着边一步步走向更近的点。HNSW 再叠一层分层结构解决「图上从哪开始走、怎么快速跨越远距离」——让它在高召回区的 QPS 显著高于 IVF,是目前最常用的默认稠密索引。

比文档深一层的机制:图分多层。顶层只有极少数节点、连接稀疏,边「跨度大」,用来快速粗定位到目标区域;越往下层节点越多、连接越密,用来精细逼近。查询从顶层一个入口点出发,在当前层贪心地走向离查询向量更近的邻居,走不动了就下沉一层、继续走,直到底层(L0,含全部节点)锁定 top-k。三个旋钮:M(每个节点的最大连接数)和 efConstruction(建图时每步维护的候选集广度)是建索引旋钮——M 越大图越密、召回越高但内存和建图耗时越涨;ef(查询时候选集广度)是查询旋钮,ef 越大召回越高、越慢。和 IVF 同构:建好的图靠 ef 滑动召回↔速度,无需重建。

L2 顶层 稀疏 · 跨度大 L1 中层 L0 底层 稠密 · 精确 入口 目标 粗定位 逐层精化
图 3.2查询从稀疏顶层的入口贪心下降,逐层精化到底层目标——和跳表一个套路。注意:高层连接少、跨度大,负责快速跨越大距离;底层连接密,负责精确逼近。这套分层正是 HNSW 在高召回区还能保持高 QPS 的原因,代价是内存里同时压着完整向量和整张图。
在三角里的位置 · 以及 2.6 的新变体

HNSW 坐在「高召回 + 高 QPS」那条边上,但离「低内存」角最远——它要把完整向量 + 整张多层图都留在内存。为了把它往低内存角拉,2.6 提供了加量化的变体:HNSW_SQ(标量量化)、HNSW_PQ(乘积量化)、HNSW_PRQ(product residual 量化)——以一点召回换显著的内存下降,思路和 IVF 的 SQ/PQ 同源,只是套在图索引上。

3.5DiskANN:把图放到 SSD,数据超过内存时的选择

DiskANN 把图(Vamana 图)和完整向量放在 SSD,只在 RAM 里留一份 PQ 压缩向量做近似距离筛选,用 beam search(束搜索)分批从磁盘取数——用于单机内存装不下全部向量的规模。

为什么需要它

HNSW 快,但前提是全部向量 + 图都进得了内存。当一个 segment 的数据量超过 RAM 预算,要么加机器、要么把数据下沉到磁盘。DiskANN 走后者:让容量受限于 SSD(便宜、大)而非 RAM(贵、小),以可控的延迟代价换「单机扛更大数据」。

比文档深一层的机制:分两层存。RAM 里只放每条向量的 PQ 压缩版(很小),用来快速算近似距离、决定图上往哪走;SSD 里放完整精确向量 + 图的邻接表。查询用 beam search:根据 RAM 里的近似距离选一批候选节点,批量从 SSD 读它们的完整向量和邻居(批量是为了摊薄 SSD 的随机读延迟),算精确距离、再扩展——如此迭代。所以「on-disk」是个半真半假的说法:它仍占 RAM(那份 PQ 不小),只是把大头(完整向量 + 图)挪到了 SSD。

三个上手即撞的约束

① 必须 NVMe SSD——beam search 的随机读全压在盘上,普通 SATA SSD 或机械盘延迟会爆。② 仅支持 float 向量(FloatVector 系),二值/稀疏不行。③ 2.4+ 默认关闭,要在 query node 配置里打开 queryNode.enableDisk 才能建。这三条任何一条没满足,建索引就会失败或退化——比起算法本身,这些部署前提才是真正卡住人的地方。

3.6GPU / CAGRA:显存换数十倍吞吐

Milvus 的 GPU 索引族(GPU_BRUTE_FORCE / GPU_IVF_FLAT / GPU_IVF_PQ / GPU_CAGRA)把检索搬到显卡;CAGRA 是 NVIDIA RAFT/cuVS 做的 GPU 原生邻近图,在大批量、高并发查询下吞吐可达 CPU 的数十倍。

为什么需要它

GPU 的强项是大规模并行。当查询批量大、并发高(一次几百上千条查询向量、QPS 极高),把距离计算和图遍历铺到几千个 CUDA 核心上,单位时间能处理的查询数远超 CPU。代价是显存贵、要专门硬件,且小批量、低并发时 GPU 的启动开销反而不划算——它是吞吐导向而非低延迟导向。

比文档深一层的机制:前三个 GPU_* 是 CPU 同名索引的显卡移植(暴力 / IVF_FLAT / IVF_PQ),把扫描和距离计算放到 GPU。GPU_CAGRA 不一样——它是专为 GPU 设计的近邻图(来自 NVIDIA 的 RAFT/cuVS 库),图的构造和遍历都按 GPU 的并行访存模式优化,所以在高并发批量查询下最能榨干显卡。内存账:CAGRA 约占原始数据的 1.8 倍显存(图结构的额外开销),规划显存时要按这个系数估。

高频陷阱 · GPU 不支持 COSINE

所有 GPU 索引只支持 L2 和 IP 两种度量,不支持 COSINE。而很多文本嵌入模型按 cosine 相似度训练。解法是标准做法:先把向量 L2 归一化,再用 IP——归一化向量上的内积(IP)在排序意义上等价于 cosine。忘了归一化直接用 IP,召回会莫名其妙地差,且不报错。这是 GPU 索引上线最常见的静默故障。

3.7稀疏索引:给稀疏向量与全文检索的倒排

SPARSE_INVERTED_INDEX 是为稀疏向量(每条只有少量非零维)和 BM25 全文检索准备的倒排索引,度量只能是 IP 或 BM25。

为什么需要它

稀疏嵌入(如 SPLADE)和全文检索的向量动辄几万维、但绝大多数维是 0。稠密索引(IVF/HNSW)按「每条都是稠密 d 维」假设设计,套在这种数据上极其浪费。倒排索引天生适配稀疏:按「哪些向量在某一维非零」组织,查询只触碰查询向量非零的那几维对应的倒排链。

比文档深一层的机制:检索算法用 DAAT(document-at-a-time,逐文档推进)家族。当前默认是 DAAT_MAXSCORE,可选 DAAT_WAND——两者都靠上界剪枝(提前跳过进不了 top-k 的文档)加速,MAXSCORE 在多数分布下剪枝更稳。旧的 SPARSE_WAND 索引类型自 2.5.4 起已弃用,新代码统一用 SPARSE_INVERTED_INDEX 并通过参数选 DAAT 算法。度量上它只认 IP(稀疏点积)和 BM25(全文打分),稠密那套 L2/COSINE 在这里没有意义。

2.6 的两个相关新进展

2.6 在量化与精度上推进了两点,和压内存这条主线呼应:RaBitQ——把向量压到 1-bit 的量化方案,压缩率极致、配合重排可控召回;以及把 INT8 向量提为一等公民(01 §1.6 已提)。两者都是「用更激进的量化换内存/带宽」的延续,标志 Milvus 在量化方向持续下注。

3.8建索引旋钮 vs 查询旋钮(这一节单独记)

参数分两类:建索引时写进结构、改了必须重建的(nlist、M、efConstruction),和查询时每次现传、随时可调的(nprobe、ef、search_list)——这条区分是面试高频考点。

为什么这条值得单独成节

前面每个索引都顺带提了它的旋钮,但「哪些要重建、哪些不用」这条规律横跨所有索引、且最容易被问到也最容易答错。把它抽出来:建索引旋钮定义了数据结构本身(簇怎么分、图怎么连),改它等于重新组织数据;查询旋钮只控制这次检索看多宽,不动已建好的结构。

比文档深一层的机制:核心一句话——同一份建好的索引,靠查询旋钮就能在「低延迟 ↔ 高召回」之间连续滑动,完全不用重建。IVF 调 nprobe(扫几个簇)、HNSW 调 ef(候选集多宽)、DiskANN 调 search_list(beam 宽度)——调大都是「多看一些 → 召回升、变慢」,调小反之。这就是为什么线上 A/B 召回、按时段切换召回/延迟侧重,都不需要重建索引,只改检索请求里的参数。反过来,nlist/M/efConstruction 任何一个想改,都得推倒重建——它们决定了簇划分或图拓扑,是「冷」参数。

表 3.2 · 两类旋钮对照(按索引)
索引建索引旋钮(改 → 重建)查询旋钮(每次可调)查询旋钮调大的效果
IVF_*nlist(簇数量)nprobe(扫几个最近簇)召回↑ · 延迟↑
HNSWM、efConstructionef(候选集广度)召回↑ · 延迟↑
DiskANN建图参数search_list(beam 宽度)召回↑ · 延迟↑(更多 SSD 读)
FLAT无无召回恒为 1,无拨盘
一句话记法

建索引旋钮 = 「数据怎么组织」(冷、重建才生效);查询旋钮 = 「这次看多宽」(热、现传现改)。被问「线上召回不够怎么办、要不要重建」,标准答案是:先把查询旋钮(nprobe/ef)调大试,把延迟预算花掉;调到顶还不够,才考虑动建索引旋钮重建。这两个查询旋钮(nprobe、ef)会在 04 章的检索路径里再次出现。

3.9选型决策树 + 度量相容矩阵

选索引先按「数据是否超内存 / 要不要 GPU / 要最高召回还是省内存」走一棵决策树定家族,再用相容矩阵确认向量类型与度量对齐。

决策的主干是几个互斥的硬约束,按顺序问下来基本就定了家族(图 3.3)。注意这棵树只解决稠密、常规规模的主路径,两个旁支单列:稀疏向量 / 全文 → SPARSE_INVERTED_INDEX;要 100% 精确且数据小 → FLAT。

数据量超过 单机内存? 是 DiskANN 否 要极致吞吐 且有 GPU? 是 GPU_CAGRA 否 要最高召回 且内存充足? 是 HNSW 否 · 要平衡/省内存 IVF_SQ8 旁支:稀疏 / 全文 → SPARSE_INVERTED_INDEX · 要 100% 精确且数据小 → FLAT
图 3.3稠密索引选型主路径:三个互斥硬约束顺序问下来定家族,两个旁支处理稀疏与精确场景。注意:这棵树给的是起点默认,不是终判——定了家族后仍要按召回/延迟实测,再用查询旋钮(3.8)微调。过滤比也会影响选择(见下方),细节留到 04 章。

度量相容矩阵:选嵌入模型训练用的那个

定了索引家族,还要确认向量类型 × 度量对齐——更关键的是,度量必须选嵌入模型训练时用的那个,否则距离的语义都不对,召回无从谈起。

表 3.3 · 向量类型 → 相容度量(呼应 01 §1.6 表 1.1)
向量 / 场景相容度量备注
稠密 float(含 fp16/bf16/int8)L2 / IP / COSINE最常见;按模型训练目标选
二值(BinaryVector)HAMMING / JACCARD哈希指纹类
稀疏(SparseFloatVector)IP / BM25稀疏嵌入 / 全文,仅 3.7 的稀疏索引
GPU 索引(任意稠密)L2 / IP(无 COSINE)cosine 场景:先归一化再用 IP
还有一条选型维度:过滤比(04 章细讲)

带标量过滤的检索里,过滤比(filtered ratio,被过滤条件排除的比例)也左右索引选择:过滤比 < 85% 时,图索引(HNSW)通常优于 IVF;而在过滤比 85–95% 且 topK 很大(> 2000)时,IVF 反而更优。这条涉及「带过滤的检索路径怎么走」,是 04 章的内容——这里先埋一个前向引用,知道「选型不只看数据规模,还看查询形态」即可。

一份把旋钮写明白的建索引示例

build_index.py · 示例,未在本机验证 Python
# HNSW:高召回 + 高 QPS 的默认稠密索引;M / efConstruction 是建索引旋钮
from pymilvus import MilvusClient

client = MilvusClient(uri="http://localhost:19530")

index_params = client.prepare_index_params()
index_params.add_index(
    field_name="vector",
    index_type="HNSW",                 # 换 "IVF_SQ8" 即走省内存路径;"DiskANN" 走超内存路径
    metric_type="COSINE",              # GPU 索引时这里不能填 COSINE:改 IP + 先归一化
    params={"M": 16, "efConstruction": 200},   # 建索引旋钮:改了要重建
)
client.create_index("docs", index_params=index_params)   # 每个 sealed segment 各建一份(01 §1.6)

# 检索时才传查询旋钮 ef —— 同一份索引靠它在 召回↔延迟 间滑动,无需重建
res = client.search(
    collection_name="docs", data=[query_vector], limit=10,
    search_params={"metric_type": "COSINE", "params": {"ef": 64}},  # 查询旋钮(04 章再现)
)

M / efConstruction写进图结构,改了必须重建。ef每次检索现传——调大召回↑延迟↑,无需重建。create_index不是整表一份,而是每个 sealed segment 各一份。

§本章 self-check

先合上教程,把答案写在纸上或编辑器里。写完再点开对照——直接点开等于把这一节当再读一遍。

  1. 取舍三角的三个角分别是什么?说出一个坐死在某个角、放弃另两个角的索引,并指出它放弃了哪两个。
  2. nlist 和 nprobe 各属于哪类旋钮?线上召回偏低、延迟有富余,应该先动哪个、要不要重建?同样地 HNSW 的 M 和 ef 呢?
  3. DiskANN 号称「on-disk」,但它仍然占用 RAM——RAM 里放的是什么、SSD 里放的是什么?为什么它要求 NVMe?
  4. (选型题)一个 collection:单机内存装得下全部向量,没有 GPU,业务要求尽量高召回。该选哪个索引家族?如果改成「内存吃紧、能接受召回降 2–3%」,又该换成哪个?
答案(先做完再展开)
  1. 三个角:高召回 / 低延迟·高 QPS / 低内存。FLAT 坐死在「高召回」角(召回恒 100%),放弃了「低延迟」(O(N) 全扫)和「低内存」(存原始全量、不压缩)。
  2. nlist 是建索引旋钮、nprobe 是查询旋钮。召回低、延迟有余时先调大 nprobe,不必重建;调到顶仍不够才考虑改 nlist 重建。HNSW 同理:M 是建索引旋钮(改了重建),ef 是查询旋钮(现传现改、调大召回↑)。
  3. RAM 里放PQ 压缩向量(做近似距离、决定图上往哪走);SSD 里放完整精确向量 + 图邻接表。要 NVMe 是因为 beam search 是大量随机读压在盘上,慢盘延迟会爆。
  4. 内存够 + 无 GPU + 要高召回 → HNSW。改成内存吃紧、可接受召回降 2–3% → IVF_SQ8(标量量化省约 70% 内存、召回仅降 2–3%);要更省可上 IVF_PQ,但召回更低。
进阶挑战 · 刚好够不着

给一个真实约束组合定索引 + 旋钮

一个 RAG 检索库:稠密 768 维 float32 向量、约 2 亿条、单机内存 64 GB(装不下全部原始向量 + 图)、有一块 NVMe SSD、无 GPU;嵌入模型按 cosine 训练;线上 QPS 中等但要求 95%+ 召回。

(a) 按图 3.3 的决策树,第一个硬约束把它导向哪个家族?(b) 这个家族支持 cosine 吗、度量该怎么配?(c) 上线后实测召回只有 90%,延迟还有富余——在不重建的前提下,调哪个旋钮、往哪个方向?(d) 如果之后加了 GPU 想换 CAGRA 提吞吐,度量配置要做什么改动?

提示(卡住再展开)

(a) 第一个问题是「数据超过单机内存?」——2 亿 × 768 × 4B ≈ 600 GB 远超 64 GB,是 → DiskANN。(b) DiskANN 仅支持 float、度量用 L2/IP/COSINE 均可,cosine 直接配 COSINE 即可(它不是 GPU 索引,没有 COSINE 限制)。(c) 召回不够、延迟有余 = 调大查询旋钮,DiskANN 是 search_list(beam 宽度)调大 → 召回↑、更多 SSD 读、延迟↑,无需重建。(d) 换 GPU_CAGRA 后不能用 COSINE——要把向量先 L2 归一化、度量改成 IP(3.6 的陷阱)。