设想一个真实场景:一个多租户 RAG 系统里,用户发起一次
search,条件是
tenant_id = 42 AND status = 'published',向量距离要求
Top-5。这条查询在功能测试里跑得又快又准——测试租户的数据只有几百条,status = 'published'
几乎全选。上线后同样的查询模板套到另一个租户身上,那个租户有八十万条历史记录,published
只占 0.3%,同一套 ef_search 参数下响应时间从
20ms 涨到 800ms,个别请求甚至返回不满 5
条。运维第一反应是”向量索引变慢了”,但 Knowhere
的向量距离计算量根本没变——变的是满足过滤条件的候选点在哪、有多少。db-frontier/09
已经从算法侧拆开 pre-filter / post-filter / in-filter 与
ACORN 的争论;本文只回答一个更窄的问题:在 Milvus
的段执行与 Knowhere
路径上,一条布尔表达式如何变成参与向量搜索的
bitset,选择度又是怎样打穿第 9 篇里”局部 Top-k 合并成全局
Top-k”的正确性假设。
本文是「向量检索引擎」系列第 11 篇(共 19 篇)。→ 系列目录
版本锚定:Milvus v2.6.21 Bitset、Knowhere、Filter Templating、Data Processing;Knowhere v2.6.18。
一、形式化问题(与 frontier/09 对齐)
在满足谓词 \(\varphi\) 的子集上求近邻:
\[ \mathrm{Topk\_filter}(q,k,\varphi)=\arg\min_{\substack{x\in X\\ \varphi(x)=\mathrm{true}}}\mathrm{dist}(q,x) \]
选择度 \(s=|\{x:\varphi(x)\}|/|X|\)
跨多个数量级时,同一套索引参数可以从”很快”变成”召回崩或延迟崩”——开篇的故事就是同一个
ef_search、同一份 HNSW 参数,在 \(s\) 从”接近 1”跌到 \(s \approx 0.003\)
时发生的事。
三种朴素求解策略,本质是在下面三个操作的顺序与位置上做不同选择(第三节会展开对应的引擎直觉与陷阱):
\[ \text{Post-filter:}\quad \{x \in \mathrm{ANN}_{k'}(q) : \varphi(x)\} \quad\text{(先用 ANN 求近似 Top-}k'\text{,再筛 }\varphi\text{)} \]
\[ \text{Pre-filter:}\quad \mathrm{ANN}_{k}\big(q,\ X_\varphi\big),\quad X_\varphi=\{x\in X:\varphi(x)\} \quad\text{(先缩小候选全集,再在子集上求近邻)} \]
\[ \text{In-filter:}\quad \mathrm{ANN}_k\big(q,\ X,\ \text{可见性谓词}=\varphi\big) \quad\text{(检索本身感知 }\varphi\text{,遍历时只接受满足条件的节点进候选堆)} \]
三者在 \(s\to 1\) 时退化为同一个操作(全集近邻),差异只在 \(s\) 较小时才显现——这也是开篇故事里”测试环境测不出问题”的数学原因:测试租户的 \(s\) 接近 1,三种策略的结果几乎无差别。
二、引擎落点:表达式如何变成一个 bitset
2.1 官方 bitset 语义:不是”谁满足条件标 1”这么简单
Bitset 文档用一个 8 个实体的段举例:4 个实体在
ts=100 插入,4 个在 ts=200
插入,其中 2 个在 ts=300 被删除;假设只有主键
[1,3,5,7]
满足某个属性过滤条件。文档给出的组合步骤是:
- 先算出
filter_bitset:满足条件的位置标1,得到[1,0,1,0,1,0,1,0]。 - 把
filter_bitset取反,得到[0,1,0,1,0,1,0,1]——因为最终喂给搜索阶段的 bitset,语义是”这一位为 1 就跳过”,而filter_bitset原始语义是”这一位为 1 就是满足条件、要保留”,两者刚好相反。 - 与删除位图
del_bitset做OR:只要某一位在”不满足过滤”或”已删除”任一 bitset 里为 1,最终result_bitset该位就是 1(跳过)。 - 把
result_bitset交给 Knowhere 的索引查询接口。
这个”先取反再
OR”的顺序不是实现细节的随手一提——它直接决定了过滤条件与删除标记能不能用同一套位运算叠加。跳过第
2 步、直接把 filter_bitset 和
del_bitset 做
OR,会把”满足过滤”的行也标记成”跳过”,结果是过滤条件被读反。
2.1.1
源码钉点:ConvertPredicateToFilteredBitset(milvus
v2.6.21)
Segcore 执行算子 PhyFilterBitsNode
在拿到谓词位图后,调用同文件内的转换函数,把「1=满足谓词」翻成「1=排除」——与文档步骤
2 同构,并额外把 NULL/UNKNOWN 并入排除集:
// milvus-io/milvus v2.6.21
// internal/core/src/exec/operator/FilterBitsNode.cpp
ConvertPredicateToFilteredBitset(TargetBitmapView data,
TargetBitmapView valid,
const size_t size) {
// FilterBitsNode outputs a filtered-row bitset: 1 means excluded.
// UNKNOWN/NULL must be excluded together with FALSE.
if (valid.all()) {
data.flip();
return true;
}
data.flip();
TargetBitmap invalid(valid);
invalid.flip();
data.inplace_or(invalid, size);
valid.set();
return false;
}删除侧与 Growing 的叠加入口见第 7
篇:SegmentGrowingImpl::mask_with_delete。Knowhere
侧接收掩码的虚接口是
IndexNode::Search(..., const BitsetView&)(第
8 篇)。
2.2 把例子摆成一张表:删除恰好命中了一条满足过滤的行
光看步骤描述容易漏掉一个真实存在的交互:如果被删除的行本身满足过滤条件,会发生什么?用官方场景的设定(4
个实体 ts=100 插入、4 个 ts=200
插入,主键 [1,3,5,7]
满足过滤条件)补一组具体删除位置,把整条链路的每一步都摆出来(下表的删除位置是教学取值,用来让例子覆盖”删除行与过滤行重叠”这种更容易出错的情况,不是官方文档的原始数值):
| pk | 插入时刻 | 是否满足 φ | filter_bitset |
取反后 | 是否已删除 | del_bitset |
result_bitset(1=跳过) |
|---|---|---|---|---|---|---|---|
| 1 | ts=100 | 是 | 1 | 0 | 是(ts=300 删除) | 1 | 1 |
| 2 | ts=100 | 否 | 0 | 1 | 否 | 0 | 1 |
| 3 | ts=100 | 是 | 1 | 0 | 否 | 0 | 0 |
| 4 | ts=100 | 否 | 0 | 1 | 否 | 0 | 1 |
| 5 | ts=200 | 是 | 1 | 0 | 否 | 0 | 0 |
| 6 | ts=200 | 否 | 0 | 1 | 是(ts=300 删除) | 1 | 1 |
| 7 | ts=200 | 是 | 1 | 0 | 否 | 0 | 0 |
| 8 | ts=200 | 否 | 0 | 1 | 否 | 0 | 1 |
最后一列只剩 pk 3、5、7 对应的位是
0(参与向量计算),pk 1
虽然满足过滤条件,但因为已被删除,result_bitset
该位仍是 1(跳过)。这正是”先取反、再与删除位图
OR“这一步顺序存在的意义:过滤条件和删除标记各自独立产生一份
1,只要任一份为
1,这一行就必须在向量计算里被跳过——两个互不相关的”跳过理由”共享同一份最终位图,检索侧不需要分别处理”因为不满足过滤跳过”和”因为已删除跳过”两套逻辑。
2.3 引擎骨架
综合 Bitset 与 Knowhere 文档(第 8 篇已引 Knowhere 的软删细节),混合检索在引擎里的骨架是:
flowchart LR
expr["Boolean expression<br/>tenant_id = 42 AND status = 'published'"]
ast["Parse to AST<br/>(Filter Templating for huge IN-lists)"]
scan["Scalar index / row scan per segment"]
fbit["filter_bitset<br/>1 = satisfies φ"]
flip["Invert bits<br/>1 = does NOT satisfy φ"]
delbit["del_bitset<br/>1 = soft-deleted"]
orop["OR"]
result["result_bitset<br/>1 = skip during ANN traversal"]
kw["Knowhere IndexNode::Search(..., BitsetView)"]
merge["Segment / shard / Proxy merge"]
expr --> ast --> scan --> fbit --> flip --> orop
delbit --> orop
orop --> result --> kw --> merge
Filter Templating 文档补充了 AST
之前的一层成本:超大 IN 列表、非 ASCII(如大量
CJK)字面量会显著增加解析开销,官方提供占位符
+ filter_params
的模板化机制缓解。这是解析层的字符串与 AST 成本,与图中
result_bitset 之后的 ANN
侧选择度问题正交——排障时要先区分”卡在解析”还是”卡在过滤后候选不够”,两者的修复手段完全不同(模板化
vs 调整策略)。
三、选择度如何打穿 Top-k 归并
3.1 第 9 篇的假设在哪里成立
第 9 篇证明过:若各分区两两不交且覆盖全集,各分区局部 Top-\(k\) 的并集再取 Top-\(k\),等于直接对全集取 Top-\(k\)。这个证明依赖一个隐含前提——参与排序的候选集合,就是最终要在其上求 Top-k 的那个集合,中间不能插入一次”先排序、后过滤”的操作。
过滤恰恰是插在排序之后的那个操作。ANN 索引(HNSW 的图游走、IVF 的 posting list 扫描)在检索时不知道 \(\varphi\),它按距离给出的局部候选顺序,与”该候选是否满足 \(\varphi\)“完全无关。如果某个 segment 里满足 \(\varphi\) 的实体恰好都排在按距离算的第 50 名之后,而这个 segment 只取了局部 Top-\(k'=10\) 的候选去做过滤,那么这些满足 \(\varphi\) 的真实近邻根本没有出现在候选列表里,之后再怎么合并也补不回来。
下面是一个教学示意(数值为构造出的最小反例,非实测):设 \(k=2\),两个 segment 各自按距离由近到远给出前若干候选,括号标注是否满足 \(\varphi\):
flowchart TB
subgraph SegA["Segment A:ANN 局部候选(按距离升序)"]
a1["c1 dist=0.10 φ=false"]
a2["c2 dist=0.12 φ=false"]
a3["c3 dist=0.30 φ=true"]
end
subgraph SegB["Segment B:ANN 局部候选(按距离升序)"]
b1["c4 dist=0.20 φ=true"]
b2["c5 dist=0.22 φ=false"]
end
a3 --> keepA["Segment A 过滤后只剩 c3(dist=0.30)"]
b1 --> keepB["Segment B 过滤后只剩 c4(dist=0.20)"]
keepA --> merge["Proxy 合并 k=2:{c4, c3}"]
keepB --> merge
merge --> caveat["若局部 top-k' 只取了 2 个候选(c1、c2),\nc3 根本不会出现在候选列表里——\n合并结果会缺 c3,而不是排序错误"]
这张图想说明的是两种不同的失败模式,容易被混为一谈:
- 候选量不足:局部 \(k'\) 太小,满足 \(\varphi\) 的候选没被检索到,合并结果条数不足或用不满足 \(\varphi\) 的邻居顶替。
- 候选顺序失真:即便扩大 \(k'\) 补齐了候选,各 segment “过滤后剩余候选数”不均衡(A 剩 1 个、B 剩 3 个),会让 Proxy 侧的合并结果系统性偏向候选剩余多的 segment,即便那个 segment 里的候选实际距离更远。
3.2 局部候选数 \(k'\) 该取多大:一个工程下界估计
开篇故事里”同一套 ef_search,延迟从 20ms
涨到
800ms”不是凭空发生的,可以用一个粗略但可核对的模型估计量级。做一个简化假设:候选是否满足
\(\varphi\) 与它在 ANN
检索中的距离排名互相独立(即过滤字段与向量语义无关)。在这个假设下,“从局部候选流中找到
\(k\) 个满足 \(\varphi\)
的候选”近似一个负二项过程——每看一个候选,它满足 \(\varphi\) 的概率是 \(s\),要连续凑够 \(k\)
个”成功”,期望需要看的候选数是:
\[ E[k'] \approx \frac{k}{s} \]
代入开篇故事的数字:\(k=5\),测试租户 \(s\approx 1\)(几乎全选)时 \(E[k']\approx 5\),局部候选和最终结果几乎重合,索引正常工作;生产租户 \(s\approx 0.003\) 时 \(E[k']\approx 1667\)——如果引擎仍按测试环境调好的 \(k'\)(例如几十)去检索,大概率filter 之后剩不到 \(k\) 个,这正是”个别请求返回不满 5 条”的来源。把 \(k'\) 调到 1667 量级能修复”凑不够”的问题,但图游走或 posting list 扫描的候选数从几十涨到近两千,检索本身的开销随之上涨——这就是延迟从 20ms 涨到 800ms 的量级来源之一(这是一个工程下界估计,不是本文的实测数字,也不是 Milvus 官方给出的容量公式)。
这个模型的局限必须显式指出:真实业务里过滤字段常常和向量语义相关(例如”未下架商品”的向量分布本身就和”已下架商品”不同),独立性假设不成立时,\(E[k']\approx k/s\) 可能严重低估或高估所需候选数——这也是”选择度”这个单一标量,永远不能完全刻画混合检索难度的原因:同样的 \(s=0.003\),如果满足 \(\varphi\) 的点在向量空间里恰好聚成一簇,图游走可能很快找到入口后就稳定命中;如果这些点在空间里随机散布,则大概率要遍历远超 \(k/s\) 的候选。
3.3 三种朴素策略在引擎里的直觉
| 策略 | 引擎直觉 | 选择度陷阱 |
|---|---|---|
| Post-filter | 先 ANN 取较大 \(k'\),再套 bitset/表达式 | \(s\) 很小时局部 \(\mathrm{Top}\text{-}k'\) 被滤空,全局归并凑不满 \(k\)(3.1 节的图) |
| Pre-filter | 先得满足 \(\varphi\) 的行集,再在子集上搜或扫 | \(s\) 很大时子集仍巨大,暴力不可行 |
| In-filter / 图感知 | 检索游走时感知谓词(ACORN 等) | 实现与索引类型相关;不是所有 index_type
等价 |
db-frontier/09 给出的选择度分段(\(s<10^{-4}\) pre-filter、\(10^{-2}\le s<0.3\) in-filter、\(s\ge 0.3\) post-filter)是教学示意的经验分段,不是 Milvus 的实测阈值——本篇不重复该分段的数值,只强调结论:Milvus 段路径上,Knowhere 带 bitset 的查询更接近”在索引遍历中跳过无效行”,具体是”跳过后继续游走”还是”先物化候选再排序”随索引类型变化(第 8 篇已说明 HNSW 走的是前者)。不要假设”加了 filter 只是结果少一点、延迟不变”。
3.4 本机微基准:post-filter 放大 \(k'\)(非 Milvus bitset 计时)
用 hnswlib 模拟「先取 \(k'=\min(N,\max(\lceil
k/s\rceil,k))\),再按随机谓词留下命中」的 post-filter
路径。脚本:reproduce/run_exp_11.sh。环境:Linux
6.8、2 核、Python 3.12.3、numpy 1.26.4、hnswlib;\(N=10^4\),\(d=128\),\(n_q=50\),\(k=10\),\(M=16\),ef_search=200,5
轮中位。
| \(s\) | \(k'\) | 中位延迟 (ms) | 平均命中数(≤\(k\)) | 凑不满 \(k\) 的查询比例 |
|---|---|---|---|---|
| 1.0 | 10 | 7.92 | 10.00 | 0.00 |
| 0.1 | 100 | 8.43 | 8.10 | 0.60 |
| 0.01 | 1000 | 33.67 | 8.40 | 0.58 |
| 0.001 | 10000 | 209.25 | 10.00 | 0.00 |
读法:\(s\) 下降时,为凑候选必须放大 \(k'\),延迟随之上升;在 \(s=0.1/0.01\) 时即使放大后仍有大量查询凑不满 \(k\)(随机谓词 + 有限图召回)。\(s=0.001\) 时 \(k'=N\),退化为近似全量扫描,延迟跳到约 209 ms,但命中数被拉满——对应「用扫描换凑齐 Top-\(k\)」的代价。这不是 FilterBitsNode/Knowhere 的墙钟,只验证第三节的选择度直觉可在本机复现。
四、工程实践:没有官方选择度估计器时怎么自测
官方文档没有公开”引擎自动估计 \(s\) 并切换 pre/post/in-filter”这类机制(第三节已明确 Milvus 段路径的行为更接近固定策略的 post-filter 式跳过,不是自适应切换)。这意味着,当开篇故事发生时,排查者需要自己动手确认问题出在哪一层,而不是靠猜。一个可复现、不依赖大规模集群的最小验证流程:
- 单独测选择度:对同一个过滤表达式,先用一次纯标量
query(不带向量距离)统计满足条件的行数,除以 collection 总行数得到 \(s\) 的实测值——这一步不涉及 ANN,纯粹是标量过滤链路(第二节的filter_bitset生成),可以确认”选择度是不是真的很低”,排除”过滤表达式写错、误伤了大量行”这类更简单的问题。 - 对照 Flat 基线:用同样的过滤条件在小样本上跑一次 Flat(暴力)搜索,记录延迟与召回,作为”不依赖图结构剪枝效果”的参照——如果 Flat 也慢,说明瓶颈是标量扫描或候选数量本身很大,不是 HNSW 图遍历的问题(对照第 2、8 篇 Flat 作金标准的角色)。
- 单变量扫描 \(k'\)(如果引擎暴露该参数):固定
\(\varphi\)、固定 \(k\),只改变局部候选数或等价的
ef_search,观察延迟与”合并结果是否凑够 \(k\) 条”两个指标的变化曲线——曲线开始”凑够 \(k\) 条”的那个 \(k'\) 附近的值,可以和 3.2 节的 \(E[k']\approx k/s\) 下界互相印证:如果实测所需 \(k'\) 远高于 \(k/s\),说明过滤字段与向量语义相关(3.2 节末尾指出的独立性假设失效场景),需要重新审视过滤字段的分布,不只是调大参数。 - 对比不同选择度租户:如果条件允许,选一个高 \(s\) 租户和一个低 \(s\) 租户跑同一套参数,横向对比延迟——这一步能直接确认”延迟差异是否随 \(s\) 变化”,而不是其它无关因素(如索引是否已建好,第 3、10 篇)造成的巧合。
这四步的共同原则是:先用最简单的手段(纯标量统计、Flat
基线)把选择度问题和其它问题(索引未就位、表达式写错、一致性水位)分开,再决定要不要动
\(k'\) /
ef_search
这类参数。跳过前两步直接调参数,容易把”过滤字段选得不好”误判成”索引参数不对”。
五、与 ACORN、Filtered-DiskANN 的衔接(不重写算法)
第 8 篇已经指出 Knowhere 的软删掩码是”嵌在图遍历过程中判断可见性,被删节点仍可作为中继继续游走”——这正是过滤感知图索引要解决的核心难题:只允许遍历满足条件的节点,会不会把图切成互不连通的碎片。
- ACORN(Patel et al., SIGMOD 2024)的解法是把 HNSW 底层度数稠密化到 \(\gamma \cdot M_0\)(\(\gamma\) 常取 10–40),再配合两跳剪枝,让任意谓词下的诱导子图大概率仍连通;论文报告在 \(s\in[0.001,0.1]\) 区间相对 pre-filter + FAISS 基线有 2–1000\(\times\) 的延迟加速(论文实验数字,与本站实测口径不可比,仅作方向性引用)。
- Filtered-DiskANN(Gollapudi et al., WWW
2023)走另一条路:构建时给每个节点带 label
集合,
RobustPrune优先保留跨 label 的边,对预先枚举的谓词分组效果好,但假设谓词集合可枚举。
把两条路径的假设与代价并排摆开,能看出它们各自把复杂度转移到了哪一步:
| 维度 | ACORN | Filtered-DiskANN |
|---|---|---|
| 优化目标 | 任意谓词下都不严重掉召回,牺牲一些索引密度 | 已知谓词分组下延迟/召回最优,节省索引密度 |
| 构建期假设 | 不需要预先知道谓词分布,稠密化对所有谓词一视同仁 | 需要在构建时枚举可能的 label 组合,用于
RobustPrune 选边 |
| 依赖的传统组件 | HNSW 图结构 + 两跳剪枝,不依赖额外倒排 | HNSW 图结构 + label-aware 边选择,隐含一份 label→节点的倒排 |
| 已知局限 | \(\gamma\) 越大索引膨胀越明显,论文未覆盖谓词分布随时间漂移的场景 | 谓词集合在线新增(如新租户)时,旧图的边选择未必适配新分组,可能需要重建 |
| 与 Partition 的相似度 | 无直接对应;不依赖预声明分区 | 假设可枚举,思路上接近”给每个分区单独优化边”,但作用在图边而非物理切分 |
两者的分叉点,本质是”谓词感知这件事应该发生在构建期还是运行期、应该覆盖已知谓词还是任意谓词”这个设计选择的两种答案——ACORN 选择”运行期任意谓词都尽量兼顾”,代价是索引密度;Filtered-DiskANN 选择”构建期只优化已知谓词”,代价是新谓词出现时的适配成本。
Milvus 官方 Knowhere 文档并未描述”删除掩码之外,还对过滤谓词做图稠密化或 label-aware 剪枝”这类机制——第 8 篇确认的只是”bitset 掩码嵌在遍历过程中,被跳过的节点仍可作为中继”。这意味着:Knowhere 的 HNSW 在高选择度低通过率场景下,是否会遇到 ACORN 论文里描述的”图碎片化、召回骤降”问题,取决于默认构建参数下图的连通冗余度,官方文档没有给出等价于 ACORN 稠密化的保证。本系列不假设 Milvus 已经内建了这层保护——这是第七节的开放问题之一,也是”读文档不能替代读源码”的一个具体例子。
分区(Partition)能缓解一部分问题,但只对预先声明、按业务字段切分的等值类过滤有效(如
tenant_id):它让查询少扫一些
segment,相当于粗粒度的 pre-filter,不能替代任意布尔表达式的
in-filter 能力。
六、常见误解
误解一:过滤只会让结果变少,不会影响延迟。 第二节的 bitset 掩码是在索引遍历过程中逐候选判断的,不是查询完成后再筛一遍列表;对图索引而言,被跳过的节点仍要占用一次遍历/距离计算。第三节进一步说明:选择度低时,延迟涨在”要遍历多少节点才能凑够满足条件的候选”,不是”多算一步过滤”这么轻量。
误解二:filter_bitset 里标
1 的行,就是最终会参与向量计算的行。
第 2.1 节的官方例子说明恰好相反:filter_bitset
的 1 表示”满足过滤条件”,但喂给 Knowhere 的
result_bitset 语义是”1
就跳过”,中间必须经过一次取反再与删除位图做
OR。读 Bitset
文档示例时如果跳过这步取反,很容易把过滤条件理解反。
误解三:只要把局部候选数 \(k'\) 调得足够大,选择度问题就解决了。 3.1 节的图已经说明,扩大 \(k'\) 只解决”候选量不足”,不解决”各 segment 过滤后剩余候选数不均衡导致合并偏向某个 segment”的顺序失真问题;而且 \(k'\) 越大,每层归并的序列化与内存开销越大(第 9 篇的归并放大链),不是没有代价的旋钮。
误解四:选择度 \(s\) 是一个只取决于过滤条件本身的固定数字,同一个 \(s\) 在不同场景下的检索代价应该差不多。 3.2 节的 \(E[k']\approx k/s\) 模型依赖”候选是否满足 \(\varphi\) 与距离排名独立”这一假设,而这个假设在真实业务里经常不成立。同样 \(s=0.003\),如果满足条件的点在向量空间里聚簇,图游走可能很快稳定命中;如果这些点随机散布在空间各处,代价会远超按独立假设估出的下界。把选择度当成唯一的难度指标、忽略过滤字段与向量语义的相关性,是过滤性能预测里最容易踩的第二个坑。
七、学术谱系、工程间隙与开放问题
7.1 谱系
| 主题 | 代表 work | 与 Milvus 的关系 |
|---|---|---|
| Predicate-agnostic 图索引 | Patel et al., ACORN, SIGMOD 2024 | 图稠密化 + 两跳剪枝;Knowhere 文档未描述等价机制 |
| 谓词分组图索引 | Gollapudi et al., Filtered-DiskANN, WWW 2023 | label-aware RobustPrune;假设谓词可枚举,与
Partition 的适用边界类似 |
| 系统落地 | Wang et al., Milvus, SIGMOD 2021 | bitset + 段级路由是本篇讨论的工程基座 |
三者的分叉点在于”谁负责让过滤后的图仍然连通”:ACORN 在构建时对任意谓词都稠密化;Filtered-DiskANN 在构建时按已知谓词分组重排边;Milvus 官方文档目前只公开了”运行时掩码 + 遍历时跳过”这一层,没有公开构建时的谓词感知优化。
7.2 工程间隙
- Bitset 文档的示例含 Time Travel
语义(
ts=150/250/350三个案例),产品默认路径以一致性级别 + 当前可见性为主(第 12 篇),阅读官方示例时注意历史 API 名与当前查询路径的差异。 - 标量倒排/位图索引(Data Processing
提及)改变的是生成
filter_bitset的成本,不改变 ANN 侧的选择度问题——降低了图 2 中 “scan” 这一步的耗时,但不影响图遍历本身要跳过多少节点。 - 模板化(Filter Templating)降低解析成本,不提高低选择度下的召回;两者是完全独立的优化维度,运维排障时容易混为一谈。
7.3 开放问题
- Knowhere 默认构建参数下的 HNSW,在高选择度、低通过率谓词下是否会出现 ACORN 论文描述的图碎片化——这是一个可以通过构造对抗性谓词实测召回来验证的问题,官方文档未给出定量保证。
- 段级自适应:何时该从 post-filter 自动切到 pre-filter 或扩大 \(k'\),切换阈值是否应该按 segment 实时估计的选择度动态调整,而不是全局静态配置?
- 多过滤器 AND/OR 组合下,
filter_bitset的物化顺序与 SIMD 友好布局如何优化——这属于第 2.1 节位运算链的性能细节,公开资料未展开。 - 与 RAG 权限过滤叠加时:强过滤(如按用户 ACL 过滤文档)是否应该默认绑定 Strong 一致性,以避免第 3.1 节候选顺序失真与一致性陈旧同时发生、故障定位更困难?
- 3.2 节的 \(E[k']\approx k/s\) 只是独立性假设下的下界,能否找到一种运行期低成本的信号(例如统计过滤字段与已知簇标签的相关性)来判断”这次查询的独立性假设有多不成立”,从而给出比固定下界更贴近真实代价的 \(k'\) 建议——这类”选择度感知的候选数自适应”目前没有公开的 Milvus 实现可以引用,只能作为工程方向记录。
八、小结
三句话小结:
- 表达式过滤在引擎里落成一个
result_bitset——它是filter_bitset取反后与删除位图OR的结果,语义是”这一位为 1 就在向量遍历时跳过”,方向弄反是最容易踩的坑。 - 选择度打穿的不是”结果变少”这么简单,而是”先排序、后过滤”的假设在过滤介入时失效:局部 Top-\(k'\) 可能根本没检索到满足条件的候选,合并结果会系统性缺失或偏向候选剩余多的 segment。
- ACORN、Filtered-DiskANN 分别用构建期稠密化与谓词分组回答”过滤后图还连通吗”,Milvus 公开文档目前只描述了运行时掩码这一层,两者之间的差距是本篇留下的开放问题,不是已解决的既有事实。
下一篇专门收束可见性本身:一致性模型——过滤解决”哪些行参与计算”,一致性解决”这次计算能看到哪个时间点之前的数据”,两者经常在 RAG 权限场景里同时踩坑。
参考资料
核心论文
- Patel, L., Kraft, P., Guestrin, C., Zaharia, M., ACORN: Performant and Predicate-Agnostic Search Over Vector Embeddings and Structured Data, SIGMOD 2024(图稠密化 + 两跳剪枝;本篇仅引用其问题定义与量级方向,不代入 Milvus 具体实现)。
- Gollapudi, S. et al., Filtered-DiskANN: Graph
Algorithms for Approximate Nearest Neighbor Search with
Filters, WWW 2023(label-aware
RobustPrune,与 Partition 的适用边界对照)。 - Wang, J., Yi, X., Guo, R., et al., Milvus: A Purpose-Built Vector Data Management System, SIGMOD 2021。
文档与源码
- Milvus Documentation v2.6.x, Bitset(filter_bitset 取反、del_bitset OR 组合的完整示例)。
- Milvus Documentation v2.6.x, Knowhere、Filter Templating、Data Processing。
- milvus-io/milvus
v2.6.21:
internal/core/src/exec/operator/FilterBitsNode.cpp(ConvertPredicateToFilteredBitset、PhyFilterBitsNode);SegmentGrowingImpl::mask_with_delete。 - zilliztech/knowhere
v2.6.18:
IndexNode::Search(..., BitsetView)。 - 本机微基准:reproduce/run_exp_11.sh(post-filter 选择度模型;非 Milvus 计时)。
- db-frontier/09 混合过滤检索(pre/post/in-filter 选择度分段、ACORN 技术细节)。
- 第 7、8、9 篇、系列 index。
返回 系列目录 | 上一篇:Data Node | 下一篇:一致性模型
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【向量检索引擎】Knowhere:向量索引执行引擎与插件契约
按官方 Knowhere 文档说明其在 Milvus 中的位置、相对 Faiss 的扩展(bitset、SIMD 选择、二进制度量)、VecIndex 类层次与 IDMAP/IVF/HNSW 等类型,用插件注册、CPU/GPU 分发与 bitset 进查询三张图钉住工程契约,并与 db-frontier/08 的算法细节分工。
【向量检索引擎】Delete · Upsert · TTL:软删生命周期与覆盖写的两条路径
按 2.6.x Delete/Upsert/TTL 文档拆解软删 bitset 从逻辑不可见到 compaction 物理回收的完整生命周期,用官方 override/merge 内部步骤的时序图区分两种 upsert,并与 FreshDiskANN 的图索引删除模型对照,说明 Milvus 用「整段重建」而非「增量合并」处理删除。
【向量检索引擎】向量引擎全景:算法、RAG 与专用引擎之间的一层
定位专用向量检索引擎相对 ANN 算法、RAG 应用与湖仓格式的分工;以 Milvus 2.6.x 四层架构与 insert/search 最小故事建立坐标系,并交代从 SIGMOD 2021 到 Streaming 演进的谱系与常见误解。
【向量检索引擎】ANN 算法工程接口:从 HNSW/IVF 到 Knowhere 契约
把 HNSW、IVF、DiskANN、Flat 收成引擎侧 Train/Build/Load/Search 契约与构建期/查询期参数面;用生命周期图与召回–QPS–内存三角说明索引如何贴着 Segment,并与 db-frontier/08、第 8 篇 Knowhere 分工。