土法炼钢兴趣小组的算法知识备份

【向量检索引擎】Knowhere:向量索引执行引擎与插件契约

文章导航

分类入口
databasestorage
标签入口
#knowhere#milvus#faiss#hnsw#ivf#bitset#simd#vector-index#vector-engine

源码下载

本文相关源码已整理,共 1 个文件。

打开下载目录 →

目录

第 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.18zilliztech/knowhere,由 Milvus internal/core/thirdparty/knowhere/CMakeLists.txtKNOWHERE_VERSION 钉死)。算法参数语义外链 frontier/08。


一、Knowhere 在架构中的位置

官方分层(自下而上):

  1. 系统硬件(CPU/GPU;文档称未来可扩展 DPU/TPU);
  2. 第三方索引库;
  3. Knowhere
  4. 通过 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.cppv2.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 并实现相同虚接口。工程含义:

这也是为什么它不能被当成「性能分析时可以忽略的特例」:任何依赖 IndexNode::Search(...)(含 BitsetView)签名的调用路径(bitset 过滤、统计埋点、GPU/CPU 分发判断)在 IDMAP 上同样要走一遍,只是内部实现是线性扫描而不是图/桶查找。

本机用 hnswlib 对照 Flat 与 HNSW 的墙钟与 非 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、=0.922。Knowhere 的 IDMAP 在工程角色上对应这里的 Flat 金标准。

3.3 IVF 家族

IVF 派生自 VecIndexFaissBaseIndex,再扩展 IVFSQIVFPQ;GPU 侧有 GPUIVFGPUIVFSQ / GPUIVFPQIVFSQHybrid 为自研混合:粗量化在 GPU、桶内搜索在 CPU,文档称与 GPUIVFSQ 召回相同但减少 CPU–GPU 拷贝。二进制侧有 BinaryIDMAPBinaryIVF

算法层nlist / nprobe / PQ 码本含义见 db-frontier/08llm-infra/18;本篇只钉 类型挂载点

3.4 第三方图/树索引

除 Faiss 外,文档写明当前常见第三方:Annoy(树)、HNSW(图),均派生自 VecIndex。Architecture 还提到 DiskANN、SCANN 等作为 Milvus 整体能力版图——具体版本启用哪些类型以当时 Release Note / Index 文档为准,升级时核对,勿假设全文永久静态。


四、插件契约:如何理解「新增一种索引」

官方给出向 Knowhere 加索引的步骤(工程 checklist):

  1. IndexEnum 增加名称(字符串);
  2. ConfAdapter.cpp 增加训练/查询参数校验;
  3. 新文件实现,基类含 VecIndex 及必要虚接口;
  4. VecIndexFactory::CreateVecIndex() 注册构建逻辑;
  5. 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

  1. Segcore 把这次请求要用的 BitsetView(10 万位,2000 位为 1)与查询向量一并传给 Knowhere 的 VecIndex::Query()
  2. Knowhere 内部走 HNSW 的图遍历:从入口点开始按贪心策略扩展邻居候选;
  3. 每扩展到一个候选节点,Knowhere 先查该节点在 BitsetView 上的位——若为 1(已删除),该候选不计入候选堆,但仍可以作为图遍历的中继节点继续往外扩展(否则删除率高时图会被「削断」,召回骤降);
  4. 遍历结束后,候选堆里剩下的可见候选按距离排序,截断到 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 新增一种索引,只要把算法实现塞进一个新文件就完成了。 第四节的五步契约(IndexEnumConfAdapterVecIndex 实现 → 工厂注册 → 单测)说明真正的接入点不止「算法代码」;缺任何一步,新索引可能在工厂查不到、在参数校验时被静默接受不合法配置,或者根本没有回归测试兜底。

误解四:GPU 索引和 CPU 索引在同一套参数下应该有相近的召回表现,差异只是速度。 IVFSQHybrid 之所以存在,恰恰是因为纯 GPU 与纯 CPU 路径在同一算法家族里也有实现细节差异(例如精度、内存布局);官方只声明 IVFSQHybridGPUIVFSQ 召回相同,并未对所有 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 工程间隙

9.4 开放问题

  1. DiskANN / 磁盘索引与对象存储冷热分层如何统一计费与缓存(Architecture 提及未来冷热池)?
  2. 多向量字段时,单段多 VecIndex 实例的内存与加载顺序?
  3. 自动 SIMD hook 与发行版 glibc/CPU 基线在容器里的最低能力集如何声明?
  4. 软删比例上升到什么阈值后,图索引的「候选变少但图变大」代价会超过重建成本——这是一个可以用 compaction 频率反推的调度问题,官方文档未给出明确公式。
  5. IVFSQHybrid 之类的自研混合执行体在算法族持续演进(新增图索引、新增量化方案)时,是否需要为每一种新算法都手写一个「粗量化异构、细搜索同构」的混合版本,还是应该抽象出通用的异构执行框架?

十、小结

三句话小结

  1. Knowhere 是 Milvus 的向量索引执行引擎:统一 Faiss/Hnswlib/Annoy 等实现,处理软删 bitset、SIMD/GPU 选择,并用 VecIndex + 工厂形成可扩展插件契约。
  2. bitset 软删是嵌在索引遍历/扫描过程中的可见性掩码,不是查询完成后的结果过滤;这一点决定了高删除率会拖慢查询而非只多一步过滤。
  3. 新增一种索引类型要走枚举、参数校验、VecIndex 实现、工厂注册、单测五步契约,不是随手加一个类就能接入。

算法怎么走贪心、怎么量化,去 db-frontier/08;段何时建索、如何 handoff,去第 10 篇;过滤与 bitset 如何夹击召回,去第 11、13 篇。


参考资料

核心论文

  1. Wang et al., Milvus: A Purpose-Built Vector Data Management System, SIGMOD 2021。
  2. Malkov & Yashunin, HNSW, IEEE TPAMI 2020(算法外链 frontier/08)。
  3. 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 默认内存假设对照)。
  4. Johnson, Douze & Jégou, Faiss 相关工作(库层;具体论文以 Faiss 文档引用为准)。

文档与源码

  1. Milvus Documentation v2.6.x, Knowhere(分层、相对 Faiss 扩展、类层次、加索引步骤)。
  2. Milvus Documentation v2.6.x, Data Processing(index building)。
  3. Milvus Documentation v2.6.x, Architecture OverviewBitset
  4. zilliztech/knowhere v2.6.18include/knowhere/index/index_node.hIndexNode::Search(..., const BitsetView&))。
  5. milvus-io/milvus v2.6.21internal/core/thirdparty/knowhere/CMakeLists.txtKNOWHERE_VERSION v2.6.18);internal/core/src/indexbuilder/VecIndexCreator.cpp
  6. 本机微基准:reproduce/run_exp_07_08.sh(与第 7 篇共用)。
  7. db-frontier/08第 7 篇系列 index

返回 系列目录 | 上一篇:Query Node 与 Segcore | 下一篇:分布式 search 归并

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。


By .