资讯详情

资讯详情

Milvus 统一成员过滤表达式 `membership_match`:从 Bloom/Roaring 双实现到单一参数化链路的设计与实践

Milvus 统一成员过滤表达式membership_match从 Bloom/Roaring 双实现到单一参数化链路的设计与实践【免费下载链接】milvusMilvus is a high-performance, cloud-native vector database built for scalable vector ANN search项目地址: https://gitcode.com/GitHub_Trending/mi/milvus导读本文围绕 Milvus 设计文档 20260822-membership-match-expression.md 展开系统讲解统一成员过滤表达式membership_match(field, {blob}, typebloom|roaring)的语法、类型判定、解析器与 Proxy 侧实现、segcore C 执行链路、资源配额与滚动升级策略。读完本文你将掌握如何用一条表达式同时承载近似Bloom与精确Roaring两种大集合成员过滤MBF1/MRB1两种自描述信封如何通过魔数完成运行时分派proxy.maxMembershipFilterSize与proxy.maxMembershipFilterPlanSize两个维度的预算如何防止资源滥用以及为何统一控制流能把新增第三种成员结构的成本从复制三份代码降为一次注册。背景为什么需要统一bloom_match近似成员过滤设计见 20260707-bloom-filter-expression.md与roaring_match精确成员过滤设计见 20260714-roaring-exact-membership-expression.md是先后独立落地的两条成员过滤表达式。它们共享约 90% 的机制——表层解析形态、延迟调用填充deferred-call fill、按请求的尺寸预算、删除/元素级守卫、日志脱敏以及几乎逐行相同的 segcore 执行器——但每一份副本都存在于各自文件中带着各自的命名。这种复制带来三个现实后果每项跨切面修复walkExpr中的新容器表达式、新守卫点、脱敏覆盖都要做两次且可能静默漏掉其中一种两份副本在声称镜像同一参考实现的地方已经出现分歧下文已解决的分歧详述增加第三种成员结构如 cuckoo/xor会使表面积翻三倍。统一控制流、同时保持每种 kind 的数据平面信封格式、哈希/探测算法、错误模型相互独立可以在不触碰真正不同的部分的前提下消除重复。这正是membership_match的设计目标。表层语法与类型判定统一后的表层语法如下membership_match(field, {blob-bytes-template}) membership_match(field, {blob-bytes-template}, typebloom|roaring) not membership_match(field, {blob-bytes-template})其过滤器种类由 blob 自描述的魔数头推导MBF1→ 降级为已有的BloomFilterExpr计划节点近似MRB1→ 降级为已有的RoaringFilterExpr计划节点精确。旧名称bloom_match与roaring_match刻意不再保留因为二者均未在任何发布版本中交付过两份旧文档顶部均已标注 Superseded因此无需别名或弃用期。线协议不变携带成员过滤器的计划与今日字节兼容。维度membership_match种类Kind动态blob 魔数可选type钉死Blob 格式嗅探MBF1 → bloomMRB1 → roaring字段Fields按解析后的 kind 强制删除Delete按解析后的 kind 决定可选的 type 参数可选参数type用于提升表达式与日志的可读性遵循表达式语法中已有的运算符参数约定如text_match(field, query, minimum_should_match2)。它必须为bloom或roaring且必须与 blob 魔数一致不一致属于请求错误。省略时两种信封都保持自描述在填充fill时嗅探。未知或过短的头会失败关闭fail closed。在源码层面这一约定由 membership_filter.go 中的fillMembershipMatchExpressionValue强制执行先通过sniffMembershipKind从 blob 前 4 字节解析出 kind若存在第三个参数则将其小写化后与解析出的 kind 比对type %q does not match filter blob format即为不一致时的报错文案。sniffMembershipKind的判定逻辑非常直接——长度不足 4 字节报too short to carry a format headerMBF1映射到 bloom、roaringfilter.Magic映射到 roaring其余一律返回unknown format magic参数错误。注意该错误只报告协议期望不回显调用方控制的字节以避免把 blob 内容泄露到日志。兼容性要点membership_match是唯一受支持的表层名称。由于前身表达式未发布过不保留别名与弃用期。线格式不变统一语法降级为既有计划节点BloomFilterExproneof field 22RoaringFilterExprfield 23。滚动升级行为与两份前身 MEP 描述一致旧 QueryNode 通过其默认分支拒绝包含这些字段的计划Proxy 必须如前所述先升级。Go 侧解析器与 Proxy 改动所有改动集中在internal/parser/planparserv2/共七项核心变更单一规格表membership_filter.go统一函数 → kind 属性allowInDelete。行为开关读表格式校验留在 bloom_match.goMBF1 信封 值域检查和 roaring_match.goMRB1 主体遍历 解码尺寸估算。源码中的membershipFilterSpec结构体只有两个字段kind与allowInDelete。统一入口membership_match的规格为kind: membershipUnknownkind 在填充时由 blob 魔数决定allowInDelete: false——这是任何必须在 blob 被检查前做出决定的代码路径上的保守答案。软关键字选项解析普通两参数调用与typebloom|roaring形式都发出同一个延迟CallExpr。type与membership_match在该调用形态之外仍是合法的字段标识符。visitMembershipArgs要求恰好两个位置参数第一参必须是标量字段名第二参必须是{template}占位符isTemplateExpr并在错误文案中刻意不回显字面量列表。单一填充路径fillMembershipMatchExpressionValue严格参数形状校验采纳 roaring_match 的受守卫形式——旧的 bloom 填充对调用参数无守卫索引、模板解析、由魔数加可选 type 一致性检查得到 kind、按 kind 的准入门、物化为BloomFilterExpr/RoaringFilterExpr。填充路径会在已解析 kind 后重新校验探测列因为统一名称只在这里解析 kind且填充也服务于手工拼装的计划——c-shared 解析器边界。树工具合并hasMembershipFilterExpr、hasDeleteUnsafeMembershipFilterExpr、collectMembershipFilterExprs、PlanContainsMembershipFilter、PlanContainsMembershipFilterUnsafeForDelete共用同一个walkExpr遍历器。walkExpr递归覆盖 CallExpr、UnaryExpr、BinaryExpr、BinaryArithExpr、RandomSampleExpr、ElementFilterExpr、MatchExpr 等全部容器节点——把递归放在一处正是防止 blob 记账、删除安全、元素级守卫、计划尺寸记账与日志脱敏随新容器节点漂移的关键。脱敏移出bloom_match.go至 plan_redact.go由 kind 无关的 blob 槽驱动roaring 特性不再依赖 bloom 文件中声明的标识符。collectMembershipFilterExprs通过membershipBlobSlotget/set 闭包统一收集BloomFilterExpr.FilterBlob与RoaringFilterExpr.BitmapBlob两种槽位。Preflight 按 body 计费而非整 blobfill_expression_value.go。这修复了中间状态的一个真实 bug聚合 preflight 按完整 blob 长度计费而单 blob 门允许在其上加固定 32 字节头导致最大尺寸的 SBBF body64 MiB 32 B在物化前就被拒绝——可用层级被砍半正是原始设计警告过的 bug。preflight 现在先嗅探 kind 再减去信封头与单 blob 门保持一致。MembershipPreflightBudget携带共享的 roaring 校验缓存同一模板名多次出现时复用结构校验与聚合计划尺寸预算。Proxy 配置收敛旧 kind 专属名称收敛同时保留两个相互独立的资源限制详见下文资源配额。守卫统一删除当且仅当计划包含无法证明为精确的 kind 时拒绝——BloomFilterExpr、以及 blob 尚未嗅探的延迟membership_match。精确 kind 放行。这在源码中即hasDeleteUnsafeMembershipFilterExpr物化的RoaringFilterExpr返回 false安全BloomFilterExpr与 kind 未知的延迟调用返回 true不安全Proxy删除路径通过PlanContainsMembershipFilterUnsafeForDelete使用该判定。element_filter所有成员过滤 kind 都被拒绝出现在元素表达式内行偏移执行器 vs 全局元素 ID 的不匹配。MATCH_*从仅 bloom 收紧为全部 kind。此前roaring_match出现在 MATCH_* 元素谓词中不会在语法上被拒绝而每个 MATCH_* 谓词字段都是元素级的其执行器提供元素 ID而行偏移探测者期望段偏移——这与roaring_match在element_filter中被拒绝是同一个原因。该缺口实践中不可达整数元素字段会因嵌套路径而无法通过 roaring 字段检查但守卫现在明确声明并强制了这一不变量。C segcore 执行改动新增 exec/expression/MembershipFilterExpr.{h,cpp}template typename LogicalExpr, typename ProbePolicy class PhyMembershipFilterExpr : public SegmentExpr { ... }; struct BloomMembershipProbe { /* SplitBlockBloomFilterView */ }; struct RoaringMembershipProbe { /* shared RoaringMembership* */ }; using PhyBloomFilterExpr PhyMembershipFilterExprexpr::BloomFilterExpr, BloomMembershipProbe; using PhyRoaringFilterExpr PhyMembershipFilterExprexpr::RoaringFilterExpr, RoaringMembershipProbe;所有控制流只存在于模板中一次执行路径选择优先 raw-dataindex-only 反向查找为回退、批处理执行、可缓存性IsCacheable() false、reorder-tier 行为、JSON 探测仅 bloomroaring 通过if constexpr在实例化时丢弃。每种 kind 的数据平面留在各自的探测策略中MBF1 零拷贝视图 域门控 vs 解码一次的 portable Roaring64。C 类型别名保留了历史类名因此工厂构造不变。已解决的分歧两份旧实现曾在三点上不一致每一点都对照已验证的事实而非凭偏好解决NULL 与候选掩码的顺序。bloom_match在其原始 raw 路径上先查 NULL 再查候选掩码roaring_match先查掩码由要求 raw≡index 位恒等性的测试钉死。本工作推进期间上游标准化了未触碰的候选契约并为 bloom 也加了显式钉ScalarBitmapInputLeavesExcludedNullCandidatesUntouched、JsonBitmapInputLeavesExcludedNullCandidatesUntouched、IndexOnlyBitmapInputPrunesReverseLookupsByCandidatePosition。统一链路因此处处采用掩码优先被排除的候选保持初始(false, valid)状态与 WithMask 索引路径辅助函数一致——同一查询无论段以何种方式加载都返回相同列。被探测的 NULL 行在任何极性下都不匹配res valid false这正是三值逻辑承诺所在。索引回退管道。bloom_match用未掩码的ProcessDataByOffsets/ProcessIndexLookupByOffsetsroaring_match用...WithMask变体。统一采用 WithMask 变体其空掩码退化情形与未掩码辅助函数相同Expr.h中验证has_candidate_mask mask ! nullptr !mask-empty()因此一条代码路径同时服务偏移输入与普通批次。bitmap_input 尺寸断言。roaring_match断言bitmap_input.size real_batch_sizebloom_match没有。两者统一保留断言尺寸不一致会静默越界读取位图。两种信封MBF1 与 MRB1 的数据平面统一控制流之下两种 kind 的数据平面保持独立且格式完全向后兼容——昨天构建的 blob 今天交给membership_match依然有效。MBF1Bloom近似MBF1 信封常量定义于 bloom_match.go与客户端构建器client/membership/sbbf及 C 探测者SplitBlockBloomFilterView通过共享 golden vectors 保持一致offset size field 0 4 magic MBF1 4 2 version (uint16 LE, 1) 6 2 algo (uint16 LE, 1 parquet_sbbf_xxh64) 8 8 n_declared (uint64 LE, informational) 16 8 fpr_declared (float64 LE, informational) 24 4 num_blocks (uint32 LE; body length must equal num_blocks * 32) 28 1 domains (bitmask: 0x01 int64, 0x02 utf8) 29 3 reserved (must be 0) 32 ... body: SBBF blocks, bit-identical to the parquet-format spec layout服务端独立校验信封该包编译进 plan-parser c-shared 库不得依赖独立 client 模块magic/version/algo/domains/num_blocks/body 长度全部校验num_blocks必须为 2 的幂且在[1, 128 MiB/32]内body 长度必须等于num_blocks * 32未知 domains 位与保留位非零均拒绝。值得展开的是checkBloomFilterValueDomain两个哈希域共享同一个 XXH64 输出空间因此blob 声明了目标字段永远无法探测的域若不拦截查询会静默执行并少返回行且无任何报错——这种静默召回损失正是该检查要转化为请求边界输入错误的原因。对于类型化标量字段域不匹配直接拒绝典型触发场景是 JSON/JS 层把 ID 字符串化[]string过滤器对准 INT64 字段JSON 路径按行分型故不要求域domains 0空成员集合法且匹配任何行。字段域校验同样严格checkBloomMatchField仅接受 INT8/16/32/64 与 VARCHAR 标量字段以及 JSON 路径含动态字段对非 JSON 字段上的嵌套路径、ARRAY 等一律拒绝。注意它使用精确的 VARCHAR 检查而非typeutil.IsStringType使 Proxy 接受集合与 segcore 执行器完全一致避免 STRING/TEXT 字段在 Proxy 构建成功、扇出到 QueryNode 后才被拒。MRB1Roaring精确MRB1 为固定 32 字节小端头 一个 portable Roaring64 bodyoffset size field 0 4 magic MRB1 4 2 version (uint16 LE, 1) 6 2 format (uint16 LE, 1 portable_roaring64) 8 8 cardinality (uint64 LE, 精确去重键数) 16 8 body_length (uint64 LE) 24 8 reserved ( 0) 32 ... body portable Roaring64 serialization完整的 MRB1 长度即32 body_lengthbody 不得超过 128 MiB。与 SBBF 的不透明位数组不同Roaring body 是容器描述符、偏移与 run 区间的嵌套结构畸形 body 可能驱动越界读或解码器中的平方级工作因此 roaring_match.go 中的validateRoaringBitmapBlob调用roaringfilter.Validate完整走查body——不物化位图、时间与输入字节线性相关——在 Proxy 处就拒绝而不是让每个 QueryNode 在扇出后才发现问题。MRB1 强制校验项包括完整 32 字节头、magic/version/format 正确、reserved 为零、body_length 等于剩余全部字节且不超过 128 MiB截断与尾部多余字节均拒绝、portable body 结构规则、结构扫描所得基数等于头中 cardinality、高 32 容器数不超过 262,144、按body_length high_container_count * 128 low_container_count * 64估算的解码尺寸不超过 64 MiB。Proxy 校验必须线性于输入字节、成功路径零堆分配、绝不构造roaring64.Bitmap来校验请求QueryNode 独立完成同样的无分配扫描与解码内存准入后才解码。字段域方面checkRoaringMatchField仅接受顶层有符号整数字段roaring 索引的是整数若把字符串哈希进整数键空间就会重新引入 roaring_match 存在目的就是要避免的假阳性因此 VARCHAR、JSON 路径、动态字段一律拒绝。有符号整数映射规范性所有生产者与消费者必须应用同一映射源值符号扩展到 int64保留其补码位模式为 uint64。等价操作包括 Go 的uint64(int64(value))与 C 的static_castuint64_t(static_castint64_t(value))。禁止 ZigZag 编码、禁止加2^63偏移、禁止零扩展窄有符号值如把 INT8(-1) 映射为0x00000000000000ff。源值符号扩展 int64Roaring64 无符号键INT8(-1)-10xffffffffffffffffINT8(-128)-1280xffffffffffffff80INT32_MIN-21474836480xffffffff80000000INT64_MIN-92233720368547758080x800000000000000042420x000000000000002aINT64_MAX92233720368547758070x7fffffffffffffff资源配额两个独立维度两个可配置预算共享于两种 kind因为它们消耗相同的线资源——客户端构建的 blob 嵌入序列化计划并扇出到每个 QueryNode——因此携带每样一个的请求绝不允许花掉单独任一种的两倍。配置定义于 pkg/util/paramtable/component_param.goMaxMembershipFilterSize/MaxMembershipFilterPlanSize均refreshable:true。限制默认边界proxy.maxMembershipFilterSize64 MiB单个 MBF1 或 MRB1body。固定 32 字节头允许叠加。对 Roaring解码内存准入通常先于线预算成为更紧的约束proxy.maxMembershipFilterPlanSize128 MiB一次 Search/HybridSearch/Query/复杂 Delete 中所有携带成员过滤器的计划的聚合序列化尺寸MRB1 高容器准入262,144每个表达式独立分配的 Roaring32 子容器数SDK/Proxy/QueryNode 三方强制MRB1 估算解码准入64 MiBbody 128 字节/高容器 64 字节/低容器SDK/Proxy/QueryNode 三方强制逐 blob 门必须在 body 解码前检查因此超大 blob 无需遍历恶意结构即被拒绝。按 body 计费而非整 blob的细节值得强调proxy.maxMembershipFilterSize预算 SBBFbody默认 64 MiB固定 32 字节 MBF1 头允许叠加len(blob) maxSize kHeaderSize才拒绝。由于 SBBF body 恒为 2 的幂完整 64 MiB body 就是 64 MiB 32 B——若整 blob 上限恰为 64 MiB 会拒绝它静默把可用上限砍到下一个 2 的幂32 MiB body约一半成员容量。validateBloomFilterBlob的注释精确记录了这段推导。键回退与防扩权新键proxy.maxMembershipFilterSize依次回退到已发布的proxy.maxBloomFilterSize与仅开发期存在的proxy.maxRoaringFilterSizeproxy.maxMembershipFilterPlanSize回退到proxy.maxBloomFilterPlanSize。保持两个维度相互独立防止旧计划键静默放宽逐 blob 准入上限——component_param_test.go 中包含显式回归钉当新键未设置而旧键设置了较小值时采用旧值但当新键显式设置后旧键不得影响结果legacy plan key cannot widen the per-blob limit。报错语义超出聚合上限在 Proxy 处、QueryNode 路由之前返回ErrParameterTooLarge非可重试的输入错误使超预算请求不会演变为 gRPCResourceExhausted或拉黑健康 QueryNode。非法、零值、负数、整数溢出配置值一律回退到 128 MiB。体量提示oversizedBlobHint把过滤器太大转化为可执行建议。SBBF body 是 2 的幂成员数刚过边界会翻倍 blob该函数从 blob 头防御性读取 n_declared按fpr (1 - e^(-n/usable))^8反推建议的 fpr上取整到四位小数保证建议值永不会因舍入而低于实际可容纳值并给出重建过滤器或减少成员数/调高上限两类提示。有意不改的部分MBF1 与 MRB1 格式、其 golden vectors、以及全部四个 SDK 构建器Go、pymilvus、C、Java——昨天构建的 blob 交给membership_match永远有效。计划 proto字段 22/23 冻结无新消息。每种 kind 的语义假阳性模型、NULL/三值逻辑、JSON 严格分型、有符号整数键映射、空集边界情形。固定的 MRB1 准入值与 MBF1 128 MiB 格式上限。测试策略测试覆盖贯穿解析器、Proxy 与 segcore 三层按设计文档列出的计划全部对应到仓库实际测试文件新增 membership_match_test.goMBF1→bloom / MRB1→roaring 降级、显式 type/magic 一致性、软关键字字段兼容性、隐私安全的未知魔数失败、填充时按 kind 的字段域强制、删除安全分类含失败关闭的延迟情形、element_filter/MATCH_* 拒绝、统一名称下的共享 preflight 预算。既有 bloom_match_test.go 与 roaring_match_test.go 迁移到统一谓词与 body 基预算所有既有断言保留。internal/proxy/membership_filter_plan_size_test.go精确序列化计划门与解析器 preflight 预算在 HybridSearch 子请求间共享。pkg/util/paramtable/component_param_test.go回退键优先级测试含旧计划键不能放宽逐 blob 上限的回归钉。segcore既有 bloom/roaring 表达式单元测试编译到统一物理类上golden-vector 一致性测试原样保留。未来工作第三种成员结构cuckoo/xor增加一个信封 一个探测策略 规格表一行。统一链路的全部价值在此兑现——membershipFilterSpec表、walkExpr、脱敏槽、preflight 计费、删除安全守卫全部复用无需 fork。SDK 侧糖构建器可提供membership_match模板并非必需因为服务端嗅探使任何既有构建器的 blob 都可直接用。设计启示小结membership_match的收敛本质上是把机制与语义彻底分层控制流解析、填充、记账、守卫、脱敏、执行框架全局只有一份而每种结构只在两个地方声明自己的存在——格式校验器信封 域与探测策略数据平面。由此跨切面修复从改两遍、可能漏一遍变成改一遍历史分歧被以掩码优先 WithMask 统一 尺寸断言三条已验证的规则钉死资源保护从按整 blob 计费修复为按 body 计费消除了 64 MiB 层级被静默腰斩的 bug。对使用者而言这意味着一条表达式、两种精度选择、四个 SDK 通用构建、与既有线协议完全兼容的升级路径——这正是大型集合成员过滤场景权限白名单、已见去重、精确受众圈选在 Milvus 中可落地的工程基础。【免费下载链接】milvusMilvus is a high-performance, cloud-native vector database built for scalable vector ANN search项目地址: https://gitcode.com/GitHub_Trending/mi/milvus创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →