第 7 篇 把查询落到段级执行。段内真正跑 向量索引构建与搜索 的,是 Knowhere——Milvus 文档称之为 core vector execution engine:集成 Faiss、Hnswlib、Annoy 等库,并决定索引构建与搜索在 CPU 还是 GPU 上执行(名字含义:know where to execute)。
一个常见的简化是把 Knowhere 想成「Faiss 换了个 C++ 命名空间」:会用就行,不用管它多做了什么。这个简化在两个地方会出错——软删怎么进查询、新增一种索引要改哪几个文件——本文就是为了把这两处钉住。读完应能回答:软删如何进到索引查询?IDMAP 为何也算「索引」?新增一种索引要挂在哪一层工厂?CPU 与 GPU 路径在类层次上怎么分叉?
本文建立 Knowhere 的 工程接口与类型层次,不重讲 HNSW/DiskANN 的图论与证明(见 db-frontier/08)。
本文是「向量检索引擎」系列第 8 篇(共 19 篇)。→ 系列目录
版本锚定:Milvus v2.6.21;Knowhere v2.6.18(
zilliztech/knowhere,由 Milvusinternal/core/thirdparty/knowhere/CMakeLists.txt的KNOWHERE_VERSION钉死)。算法参数语义外链 frontier/08。
一、Knowhere 在架构中的位置
官方分层(自下而上):
- 系统硬件(CPU/GPU;文档称未来可扩展 DPU/TPU);
- 第三方索引库;
- Knowhere;
- 通过 CGO 与 index / query 相关节点交互(Go 调用 C/C++)。
Architecture Overview 写明 Milvus 构建在 Faiss、HNSW、DiskANN、SCANN 等流行向量检索库之上。Knowhere 的工作是把这些实现收成 统一执行与硬件选择层,而不是让每个 Worker 直接散落调用互不兼容的 API。
与第 7 篇的边界再强调一次:Knowhere 只处理向量索引运算;标量索引、段编排、跨段归并不在 Knowhere 职责内(官方 Knowhere 文档)。
二、相对 Faiss 的明确扩展
官方列出 Knowhere 相对 Faiss 的优势点(A 级文档陈述):
2.1 BitsetView 与软删
Milvus 用 bitset 实现软删:向量仍在库中,但相似度搜索/查询时不参与计算。bitset 中每一位对应一个索引中的向量;标记为删除的向量在搜索中跳过。该参数应用到 Knowhere 暴露的 Faiss 索引查询 API(含 CPU 与 GPU 索引)。
这把「删除」从「立刻改图」变成「查询时掩码」——与 Sealed 不可变、后台 compaction 回收的模型一致(第 3、13 篇)。掩码发生的位置很关键:不是在 Segcore 拿到候选之后再过滤,而是在 Knowhere 内部扫描/遍历索引结构的过程中跳过——对暴力搜索这只是少算几次距离,但对 HNSW 这类图索引,意味着图遍历本身要在每一步判断邻居是否可见,否则候选数量不够会导致召回下降(这也是「先搜后过滤」在高删除率场景失效的根源之一,第 11 篇展开)。
sequenceDiagram
participant Seg as Segment(Segcore)
participant Del as 删除 bitset
participant KW as Knowhere VecIndex
participant Idx as Faiss / HNSW 索引结构
Seg->>Del: 标记已删除行的 offset
Seg->>KW: Query(vector, topk, BitsetView)
KW->>Idx: 距离扫描 / 图遍历
Idx-->>KW: 候选 id(含已删除)
KW->>KW: 按 bitset 跳过已删除 id
KW-->>Seg: 过滤后的 top-k
2.2 二进制向量上的多种相似度
Knowhere 支持 Hamming、Jaccard、Tanimoto、Superstructure、Substructure 等,覆盖集合相似与化学结构等场景(官方列举)。这超出「只会 L2/IP 的浮点向量」的窄模型。
2.3 AVX512 与自动 SIMD 选择
在 Faiss 已支持的 AArch64、SSE4.2、AVX2 之外,Knowhere 支持 AVX512;文档称相对 AVX2,索引构建与查询可提升约 20%–30%(官方陈述,非本机实测)。
自动 SIMD 选择:把依赖 SIMD
的相似计算抽成多版本(SSE/AVX/AVX2/AVX512),分别编译,运行时按
CPU flag hook 到合适函数指针——用户无需在编译期手动指定
-msse4 一类标志。这与 CPU/GPU
两条后端路径共同构成 Knowhere 「同一个 VecIndex
接口、运行时才决定具体执行体」的分发结构:
flowchart TB
vecindex["VecIndex(虚基类接口)"]
cpu["FaissBaseIndex<br/>CPU 系索引"]
gpu["GPUIndex<br/>CUDA 系索引"]
hybrid["IVFSQHybrid<br/>粗量化 GPU + 桶内 CPU"]
simd["自动 SIMD 选择<br/>按 cpuid 挂 SSE/AVX2/AVX512 函数指针"]
vecindex --> cpu
vecindex --> gpu
vecindex --> hybrid
cpu --> simd
图中三条分支不是互斥的部署选项,而是同一进程里可以并存的执行体:一个 Query Node 上可能同时有走 CPU 路径的 HNSW 段索引和走 GPU 路径的 IVF 段索引,取决于每段建索时选择的索引类型与硬件可用性。
2.4 其它优化
文档指向系统论文 Milvus: A Purpose-Built Vector Data Management System(SIGMOD 2021)讨论更多性能优化。论文数字与硬件绑定,引用时须回到原文 figure/table,本篇不转述未核对实验表。
三、代码结构:从 DataObj 到 IndexNode
官方描述的类层次(概念层)与 Knowhere
v2.6.18
头文件可对上号。当前公共检索虚接口在
knowhere::IndexNode(不再用旧文档里的
VecIndex::Query
字面名;工程语义仍是「统一索引对象 + Train/Search +
bitset」):
| 类型 | 角色 |
|---|---|
Object /
DataObj(文档名) |
数据结构基类;体量统计 |
Index / 序列化层 |
Serialize() / Load() /
Deserialize |
IndexNode |
向量索引虚基类;Build/Add/Search/RangeSearch
等 |
| Faiss / HNSW / 自研实现 | 具体 IndexNode 子类 |
3.0
源码钉点:Search 带 BitsetView(knowhere
v2.6.18)
// zilliztech/knowhere v2.6.18
// include/knowhere/index/index_node.h
virtual expected<DataSetPtr>
Search(const DataSetPtr dataset, std::unique_ptr<Config> cfg,
const BitsetView& bitset,
milvus::OpContext* op_context = nullptr) const = 0;注释写明 bitset 用于过滤结果;Milvus Segcore
在段执行层物化可见性掩码后,经此参数进入索引遍历(第 7、11
篇)。Milvus
侧建索引工厂入口之一:internal/core/src/indexbuilder/VecIndexCreator.cpp(v2.6.21)。
3.1 训练与搜索数据同一集合
经典 ANN 流水线常把 训练集 与 检索集 分开(例如 SIFT1M 的 train/test)。Knowhere 文档明确:在 Knowhere 中,训练与搜索使用同一段数据——对 segment 内全部数据 train,再插入并 build。这与「每 Sealed segment 一份索引」(第 3、10 篇)一致:索引生命周期贴着 segment,而不是贴着全局离线训练集。
这个假设本身是有代价的,见第六节的工程间隙:它简化了「不需要单独维护训练集」的运维复杂度,但意味着 segment 划分策略直接决定了每份索引训练时能看到的数据分布——如果 compaction 把分布差异很大的旧数据合并进同一个新段,训练出的量化码本(例如 IVF 的聚类中心)可能不再适配段内的局部分布。
3.2 IDMAP:暴力搜索也走同一接口
IDMAP
技术上不是索引,而是暴力搜索:插入时无需
train/build,查询直接打在原始向量上。为了代码一致性,它仍继承
VecIndex 并实现相同虚接口。工程含义:
- Flat / 暴力是 召回金标准 与小数据路径;
- 与 HNSW/IVF 共用同一套 Build/Search 调用约定,便于工厂与测试。
这也是为什么它不能被当成「性能分析时可以忽略的特例」:任何依赖
IndexNode::Search(...)(含
BitsetView)签名的调用路径(bitset
过滤、统计埋点、GPU/CPU 分发判断)在 IDMAP
上同样要走一遍,只是内部实现是线性扫描而不是图/桶查找。
本机用 hnswlib 对照 Flat 与 HNSW 的墙钟与 Recall@10(非 Knowhere
计时)见第 7 篇 4.1 节与 reproduce/run_exp_07_08.sh:在
\(N=10^4\)、\(d=128\)、ef_search=200
下 Flat 中位 251 ms、HNSW 8.84 ms、Recall@10=0.922。Knowhere 的 IDMAP
在工程角色上对应这里的 Flat 金标准。
3.3 IVF 家族
IVF 派生自 VecIndex 与
FaissBaseIndex,再扩展
IVFSQ、IVFPQ;GPU 侧有
GPUIVF → GPUIVFSQ /
GPUIVFPQ。IVFSQHybrid
为自研混合:粗量化在 GPU、桶内搜索在 CPU,文档称与
GPUIVFSQ 召回相同但减少 CPU–GPU
拷贝。二进制侧有
BinaryIDMAP、BinaryIVF。
算法层的 nlist /
nprobe / PQ 码本含义见 db-frontier/08
与 llm-infra/18;本篇只钉
类型挂载点。
3.4 第三方图/树索引
除 Faiss
外,文档写明当前常见第三方:Annoy(树)、HNSW(图),均派生自
VecIndex。Architecture 还提到 DiskANN、SCANN
等作为 Milvus 整体能力版图——具体版本启用哪些类型以当时
Release Note / Index
文档为准,升级时核对,勿假设全文永久静态。
四、插件契约:如何理解「新增一种索引」
官方给出向 Knowhere 加索引的步骤(工程 checklist):
- 在
IndexEnum增加名称(字符串); - 在
ConfAdapter.cpp增加训练/查询参数校验; - 新文件实现,基类含
VecIndex及必要虚接口; - 在
VecIndexFactory::CreateVecIndex()注册构建逻辑; - 在
unittest下加单测。
量化系参考 IVF_FLAT,图系参考
HNSW,树系参考
Annoy。这五步画成流水线,就是第 2 篇「ANN
工程接口」要抽象的契约——枚举 → 配置校验 → VecIndex
实现 → 工厂注册 → 测试,而不是在 Query Node
里开特例分支:
flowchart LR
enum["1. IndexEnum<br/>新增名称字符串"]
conf["2. ConfAdapter.cpp<br/>训练/查询参数校验"]
impl["3. 新文件实现<br/>继承 VecIndex"]
factory["4. VecIndexFactory::<br/>CreateVecIndex() 注册"]
test["5. unittest"]
enum --> conf --> impl --> factory --> test
这条流水线暴露了一个容易被忽视的约束:新索引类型不是「实现一个
search() 函数」就算完成——它必须能在
ConfAdapter
里描述自己合法的参数范围,否则用户传一个该索引不支持的参数(例如给
Flat 传
nprobe)不会在建索时报出清晰错误,而可能在运行期表现成沉默的默认值或崩溃。
五、与 Data Node 建索的衔接
Data Processing · Index building:建索由 Data Node 执行;为避免频繁重建,Collection 再分为 segment,每段自有索引。Data Node 从对象存储加载该段日志快照,反序列化后建索,再把索引写回对象存储。向量索引构建是计算与内存密集型,依赖 SIMD;标量侧另有 Bloom、哈希、树、倒排等(官方列举),计划中的 bitmap 等不在本篇展开。
Knowhere 出现在这条路径的 「真正跑 Train/Build/序列化」 处;调度与对象路径是 Data Node + Coordinator(第 10 篇)。
六、最小故事:一次带软删的
Query() 调用
设想一个 128 维向量的 Sealed segment,段内 10
万条向量,建的是 HNSW 索引;其中 2000 条已被标记软删(bitset
对应位为 1)。客户端请求 top_k=10:
- Segcore 把这次请求要用的
BitsetView(10 万位,2000 位为 1)与查询向量一并传给 Knowhere 的VecIndex::Query(); - Knowhere 内部走 HNSW 的图遍历:从入口点开始按贪心策略扩展邻居候选;
- 每扩展到一个候选节点,Knowhere 先查该节点在
BitsetView上的位——若为 1(已删除),该候选不计入候选堆,但仍可以作为图遍历的中继节点继续往外扩展(否则删除率高时图会被「削断」,召回骤降); - 遍历结束后,候选堆里剩下的可见候选按距离排序,截断到 10 个返回。
这个故事说明两件事:软删不是「在最终列表里划掉几行」,而是嵌在索引遍历过程中的可见性判断;而且 可见性判断与图连通性判断是分离的——被删的节点仍在图里占位置、仍参与导航,只是不进最终结果。理解这一点,才能看懂为什么高删除率会拖慢查询(图变大但有效候选变少)而不只是「结果集需要多算一步过滤」。
七、从现象定位到 Knowhere 内的哪一层
Knowhere 内部也分层——Train/Build、Query、SIMD 分发、bitset 掩码不是同一段代码。排障时先按现象归到下表的行,再决定该看哪部分:
| 现象 | 更可能对应 | 先检查什么 |
|---|---|---|
| 建索耗时突然变长,其它不变 | Train() / Build() 路径 |
数据量、维度、量化参数(nlist/PQ
码本大小)是否变化 |
| 查询延迟稳定,但召回率下降 | bitset 掩码 / 图遍历有效候选不足 | 软删比例是否升高(第六节故事)、ef/nprobe
是否偏小 |
| 同类型索引,CPU 版比 GPU 版慢很多或反过来 | CPU/GPU 分发(第二节 2.3 图) | 数据是否命中 GPU 显存、是否触发了 CPU 兜底路径 |
| 换了一台新机器后 QPS 明显下降 | 自动 SIMD 选择 | 目标机器 CPU flag(是否有 AVX512/AVX2)、容器是否暴露了正确 cpuid |
| 新增索引类型后线上出现参数相关的诡异行为 | 插件契约(第四节流水线) | ConfAdapter
参数校验是否覆盖了新参数、工厂注册是否正确 |
这张表和第 7 篇的延迟归因表衔接:如果第 7 篇的表格已经把症状定位到「Knowhere 参数」这一层,再用这里的表格进一步定位到 Train/Build、Query 掩码、CPU/GPU 分发还是插件契约中的哪一个具体环节。
八、常见误解
误解一:IDMAP
不算真正的索引,性能分析时可以先跳过它。 IDMAP
同样实现 VecIndex 全部虚接口,也走同一套
bitset、统计埋点与工厂分发;它是暴力搜索不代表它在代码路径里是特例。反而因为它是召回金标准,性能与召回对比分析里经常需要用它作为基线(第三节
3.2)。
误解二:Knowhere 只是把 Faiss 重新包了一层命名空间,性能上没有本质差异。 官方明确列出的扩展——bitset 软删掩码嵌入查询过程、二进制相似度族、AVX512 与自动 SIMD 选择、CPU/GPU 统一调度——都是 Faiss 原生 API 之上的工程加法,不是简单转发。跳过这些扩展直接假设「行为等价于裸 Faiss」,会在软删和多硬件混部场景下得出错误结论。
误解三:给 Knowhere
新增一种索引,只要把算法实现塞进一个新文件就完成了。
第四节的五步契约(IndexEnum →
ConfAdapter → VecIndex 实现 →
工厂注册 →
单测)说明真正的接入点不止「算法代码」;缺任何一步,新索引可能在工厂查不到、在参数校验时被静默接受不合法配置,或者根本没有回归测试兜底。
误解四:GPU 索引和 CPU
索引在同一套参数下应该有相近的召回表现,差异只是速度。
IVFSQHybrid 之所以存在,恰恰是因为纯 GPU 与纯
CPU
路径在同一算法家族里也有实现细节差异(例如精度、内存布局);官方只声明
IVFSQHybrid 与 GPUIVFSQ
召回相同,并未对所有 CPU/GPU
索引对做这类保证。切换硬件后端时召回也需要重新核对,不能只盯
QPS。
九、学术谱系、争论、开放问题
9.1 谱系
| 层 | 代表 | 本篇角色 |
|---|---|---|
| 算法 | HNSW TPAMI 2020;IVF-PQ;DiskANN NeurIPS 2019 | 外链 frontier/08 |
| 库 | Faiss、Hnswlib、Annoy | Knowhere 之下 |
| 执行引擎 | Knowhere | 硬件选择 + 统一 VecIndex + bitset |
| 系统 | Milvus SIGMOD 2021 → 2.6 Workers | 段生命周期调用 Knowhere |
9.2 争论:库内嵌 vs 独立执行引擎
| 立场 | 主张 |
|---|---|
| 直接链 Faiss | 少一层抽象,调试短 |
| Knowhere 式引擎 | 统一 bitset/SIMD/GPU、多库并存、与软删语义对齐 |
Milvus 选择后者;代价是多一层版本与 CGO 边界。Qdrant 走的是另一条路:自研 HNSW 实现直接嵌入服务进程,不做「多库统一执行引擎」这一层抽象(第 15 篇对照)——两边都能达到生产级召回与延迟,说明这不是「谁的算法更好」的争论,而是「团队愿意在哪一层付出维护成本」的工程取舍:Milvus 用一层引擎换来跨库切换与硬件后端的灵活性;Qdrant 用自研单一实现换来更短的调用链和更少的类型分发开销。
9.3 工程间隙
- 文档中的 AVX512 百分比、IVFSQHybrid「更好性能」均依赖硬件与数据分布;容量规划须自测。
- 「同一 segment 数据既 train 又 search」简化了离线 ANN 竞赛设定,但意味着 segment 重划 / compaction 会触发重建成本,且训练出的量化码本只适配当前段内分布(第三节 3.1)。
- GPU 索引在共享集群上的隔离与碎片,官方架构层未给出完备多租户答案(开放问题)。
- DiskANN 的核心假设是「图结构常驻
SSD、按扇区对齐读取」(Subramanya et al., NeurIPS
2019);Knowhere「同段数据同时 train/search
且默认驻内存」的假设与之并不同构——如果某个 release 把
DiskANN 接入 Knowhere 的
VecIndex体系,磁盘驻留与内存驻留索引在同一套接口下的资源计费与加载调度会是新的复杂度来源,需要以当时源码为准,本篇不代入未核实的实现细节。
9.4 开放问题
- DiskANN / 磁盘索引与对象存储冷热分层如何统一计费与缓存(Architecture 提及未来冷热池)?
- 多向量字段时,单段多
VecIndex实例的内存与加载顺序? - 自动 SIMD hook 与发行版 glibc/CPU 基线在容器里的最低能力集如何声明?
- 软删比例上升到什么阈值后,图索引的「候选变少但图变大」代价会超过重建成本——这是一个可以用 compaction 频率反推的调度问题,官方文档未给出明确公式。
IVFSQHybrid之类的自研混合执行体在算法族持续演进(新增图索引、新增量化方案)时,是否需要为每一种新算法都手写一个「粗量化异构、细搜索同构」的混合版本,还是应该抽象出通用的异构执行框架?
十、小结
三句话小结
- Knowhere 是 Milvus 的向量索引执行引擎:统一
Faiss/Hnswlib/Annoy 等实现,处理软删 bitset、SIMD/GPU
选择,并用
VecIndex+ 工厂形成可扩展插件契约。 - bitset 软删是嵌在索引遍历/扫描过程中的可见性掩码,不是查询完成后的结果过滤;这一点决定了高删除率会拖慢查询而非只多一步过滤。
- 新增一种索引类型要走枚举、参数校验、
VecIndex实现、工厂注册、单测五步契约,不是随手加一个类就能接入。
算法怎么走贪心、怎么量化,去 db-frontier/08;段何时建索、如何 handoff,去第 10 篇;过滤与 bitset 如何夹击召回,去第 11、13 篇。
参考资料
核心论文
- Wang et al., Milvus: A Purpose-Built Vector Data Management System, SIGMOD 2021。
- Malkov & Yashunin, HNSW, IEEE TPAMI 2020(算法外链 frontier/08)。
- Subramanya, S. J., Devvrit, Kadekodi, R., Krishaswamy, R., Simhadri, H. V., DiskANN: Fast Accurate Billion-point Nearest Neighbor Search on a Single Node, NeurIPS 2019(磁盘驻留假设,与 Knowhere 默认内存假设对照)。
- Johnson, Douze & Jégou, Faiss 相关工作(库层;具体论文以 Faiss 文档引用为准)。
文档与源码
- Milvus Documentation v2.6.x, Knowhere(分层、相对 Faiss 扩展、类层次、加索引步骤)。
- Milvus Documentation v2.6.x, Data Processing(index building)。
- Milvus Documentation v2.6.x, Architecture Overview、Bitset。
- zilliztech/knowhere
v2.6.18:
include/knowhere/index/index_node.h(IndexNode::Search(..., const BitsetView&))。 - milvus-io/milvus
v2.6.21:
internal/core/thirdparty/knowhere/CMakeLists.txt(KNOWHERE_VERSION v2.6.18);internal/core/src/indexbuilder/VecIndexCreator.cpp。 - 本机微基准:reproduce/run_exp_07_08.sh(与第 7 篇共用)。
- db-frontier/08、第 7 篇、系列 index。
返回 系列目录 | 上一篇:Query Node 与 Segcore | 下一篇:分布式 search 归并
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【向量检索引擎】ANN 算法工程接口:从 HNSW/IVF 到 Knowhere 契约
把 HNSW、IVF、DiskANN、Flat 收成引擎侧 Train/Build/Load/Search 契约与构建期/查询期参数面;用生命周期图与召回–QPS–内存三角说明索引如何贴着 Segment,并与 db-frontier/08、第 8 篇 Knowhere 分工。
【向量检索引擎】向量引擎全景:算法、RAG 与专用引擎之间的一层
定位专用向量检索引擎相对 ANN 算法、RAG 应用与湖仓格式的分工;以 Milvus 2.6.x 四层架构与 insert/search 最小故事建立坐标系,并交代从 SIGMOD 2021 到 Streaming 演进的谱系与常见误解。
【向量检索引擎】混合检索与标量过滤:表达式、bitset 与选择度打穿归并
按 Milvus 2.6.x Bitset、Filter Templating 文档拆解表达式如何变成 bitset 再喂给 Knowhere;用最小故事、bitset 取反细节与选择度打穿 Top-k 归并的示意图,说明为何过滤会改变延迟而不只是改变结果数量;对读 db-frontier/09 的 ACORN、Filtered-DiskANN 争论。
【向量检索引擎】Delete · Upsert · TTL:软删生命周期与覆盖写的两条路径
按 2.6.x Delete/Upsert/TTL 文档拆解软删 bitset 从逻辑不可见到 compaction 物理回收的完整生命周期,用官方 override/merge 内部步骤的时序图区分两种 upsert,并与 FreshDiskANN 的图索引删除模型对照,说明 Milvus 用「整段重建」而非「增量合并」处理删除。