
Milvus 外部表新格式milvus-table把 Milvus 快照当作可查询外部表源的完整设计解读【免费下载链接】milvusMilvus is a high-performance, cloud-native vector database built for scalable vector ANN search项目地址: https://gitcode.com/GitHub_Trending/mi/milvusMilvus 推出了面向外部表external table的新格式milvus-table它允许把一份由 Milvus 自身生成的集合快照snapshot当作外部表的数据源来创建与刷新外部集合从而以零拷贝方式直接读取快照中 StorageV3 列组文件。本文基于仓库内的设计文档 20260526-milvus-table-external-source.md 展开结合rootcoord、datacoord、datanode、querynodev2、storagev2/packed等模块的实现文件系统讲解该格式的公共契约、Schema 对齐、刷新流程、物理列解析、主键模式、删除语义与失败行为。读完本文你将理解milvus-table与 Parquet 等常规外部表格式的本质差异掌握它的创建约束、配置格式与内部数据通路并能在自己的部署中判断快照外部表化这一方案的适用边界。一、背景为什么快照需要一种新的外部表格式Milvus 已有的外部表体系支持把 Parquet、Lance、Vortex、Iceberg 等格式的外部数据文件当作只读集合查询。这些格式的数据是相互独立的数据文件集合外部表把用户路径下的文件范围映射成外部片段external fragment即可。但 Milvus 快照snapshot并不是一堆独立数据文件。一份 Milvus 集合快照里包含集合的 Schema字段定义与字段 ID段清单segment manifests删除日志delta logs含 L0 覆盖层主键布隆过滤器统计primary-key bloom-filter statistics。因此若要把快照作为外部表源目标外部集合必须在数据字段上保留源字段的身份field ID同时仍然以普通的 Milvus 字段名和external_field映射暴露给用户。这正是milvus-table格式要解决的核心问题也是它与通用文件格式外部表在实现上分道扬镳的起点。该设计对应 issue #45881当前仓库中的状态为Implemented涉及 External Table、Snapshot、StorageV3、QueryNode、DataNode 等组件设计文档位于 20260526-milvus-table-external-source.md。二、公共契约external_spec、external_source与字段映射2.1 最小化的公开接口milvus-table格式不新增任何 Milvus-table 专有的source_collection_id、snapshot_idAPI而是复用外部表既有契约仅通过两个字段选择格式与数据源external_spec.format取值milvus-tableexternal_source指向具体的快照元数据 JSON 文件路径。external_spec示例来自设计文档 Public Contract 小节{ format: milvus-table, extfs: { cloud_provider: aws, region: us-west-2, access_key_id: ..., access_key_value: ... } }其中extfs描述访问快照所在对象存储所需的云厂商与凭据信息。external_source则必须是快照元数据 JSON 的完整对象存储路径s3://bucket/snapshots/{source_collection_id}/metadata/{snapshot_id}.json在代码层面外部格式常量集中定义于 pkg/util/externalspec/external_spec.goFormatMilvusTable见 L36-L40其底层解析逻辑位于 pkg/util/externalspec/specutil/spec.go该文件维护了受支持格式白名单Parquet、lance-table、vortex、iceberg-table、milvus-table与ParseExternalSpec等入口spec_test.go 中对{format:milvus-table}的解析用例可以直接验证这一点。设计上刻意不把快照路径布局暴露进外部表 API——快照元数据 JSON 是唯一权威产物canonical artifact。这样外部表 API 就无需耦合 Milvus 内部快照目录结构若将来内部布局调整只需保证 metadata JSON 路径不变即可。2.2 禁止链式chaining快照源必须是普通 Milvus 集合生成的快照。用另一个外部集合的快照再去创建或刷新milvus-table外部集合会被拒绝。原因很直接若允许链式刷新与读取路径就需要追着前一个集合的外部源和存储契约继续解析这已超出milvus-table快照契约的职责边界详见设计文档 Public Contract 的说明。2.3 目标集合的字段映射链目标集合 Schema 使用普通字段名。对每个用户数据字段external_field必须填源字段名目标字段名 - external_field源字段名 - 源字段 ID创建时 RootCoord 读取源快照元数据把源字段 ID 复制进目标字段持久化的目标 Schema 里external_field仍然保存源字段名字符串而不是数字形式的字段 ID。三、端到端架构六大组件各司其职设计把 Milvus 特有行为尽量压进既有外部表生命周期而不是扩大公共 API。设计文档 Implementation Overview 给出的组件分工如下组件职责RootCoord创建外部集合时读取快照元数据校验 Schema 身份将目标数据字段 ID 对齐到源字段 ID拒绝外部表链式DataCoord构建刷新任务refresh job预分配 ID 区间把 DataNode 结果归类为 kept / new / manifest-only update 三类段更新DataNode读取 Milvus 快照探索清单explore manifest生成目标 StorageV3 manifest执行目标函数复制或翻译删除日志并抽样估算 fake-binlog 内存大小storagev2/packed解析快照元数据、解析源相对路径通过 Loon FFI 导入源 manifest对外暴露 Milvus-table manifest 辅助函数QueryNode加载外部 StorageV3 manifest拆分源侧与目标侧 deltalog按需 eager 加载真实 PK 与时间戳列Segcore解析物理存储列合成虚拟 PK 系统字段按需加载真实 PK 与源时间戳输出字段使用take()其中主数据路径是零拷贝的目标 manifest 在根据external_source解析出真实路径后直接指向源 StorageV3 列组文件仅当需要写入生成式函数的输出、或删除日志必须落在目标 PK 空间时才会在目标侧写入新文件。四、Schema 对齐与 field ID 保留RootCoord 阶段4.1 为什么必须对齐字段 IDStorageV3 的段清单使用字段 ID 字符串作为物理列名例如源字段 ID101其物理列就叫101。如果目标集合被分配了不同的字段 ID那么读取时就会指向错误或不存在的列。因此对齐发生在最早期——集合创建时。4.2 RootCoord 的创建流程设计文档给出 RootCoord 在创建时的完整步骤校验external_source与external_spec解析快照元数据 JSON校验目标 Schema 与源快照 Schema 的身份一致性构建源字段名 → 源字段 Schema映射对每个目标用户数据字段通过external_field找到匹配的源字段把源字段 ID 写入target.field_id给创建请求打上preserve_field_idstrue标记。字段对齐逻辑在 internal/rootcoord/create_collection_task.go 中实现配套测试见 internal/rootcoord/create_collection_task_test.go。4.3 哪些字段属于目标自有字段以下字段不映射源数据列由目标侧自持系统字段如 RowID 等目标虚拟主键字段当目标没有用户主键时由系统注入目标函数输出字段function output fields。值得注意的一个细节如果源普通快照里已经存有某个函数输出字段的列目标的一个普通字段仍可通过external_field映射到这份已存储的源列而目标集合自己的函数输出在每次刷新时重新计算不读取外部源数据。这一边界也在设计的 Non-Goals 中明确列出目标函数输出字段是 target-owned刷新时重新生成绝不从外部源数据反读。4.4 DDL 重放不依赖源桶在 DDL 重放replay时只要preserve_field_ids已置位RootCoord会跳过重新读取外部快照。这保证了 Schema 一旦持久化故障恢复就与源桶及其凭据解耦不再需要回源校验。五、刷新流程DataCoord 编排与 DataNode 落盘5.1 探索阶段DataCoord 的刷新任务会重新校验外部源与 spec读取快照元数据然后在目标存储下写出探索清单explore manifestDataNode 按自己分配到的文件区间消费这份清单并生成目标段清单。对milvus-table而言DataCoord 探索时会读取快照元数据 JSON且强制要求存在storagev2_manifest_list字段。若缺失则快速失败并给出明确错误——因为这通常意味着源快照并非来自 StorageV3 集合。这一点有直接源码佐证在 internal/storagev2/packed/milvus_table.go 中错误errMilvusTableStorageV2ManifestListMissing的文案明确写着milvus-table requires storagev2_manifest_list in snapshot metadata; create the source snapshot from a StorageV3 collection (enable common.storage.useLoonFFItrue before writing source data)。也就是说要产生可用于milvus-table的源快照源集合在写数据阶段就必须启用 StorageV3 / Loon FFI 存储。5.2 片段身份fragment identity探索会把源 StorageV3 段清单转换成目标侧的外部片段片段身份由三元组唯一标识source_manifest_path:start_row:end_row关键设计点是删除日志不参与片段身份。这带来一种很有用的刷新行为——如果 L1 数据片段没变、只是快照的 L0 覆盖层变了DataNode 会重写目标段清单并返回既有 segment ID 作为更新段DataCoord 就地更新该段的ManifestPath而不是先删后建只有当源 L1 manifest 路径或行区间变化时旧目标段才被作废并新建目标段。三种情形的刷新行为归纳如下L1 片段L0 覆盖刷新行为变化任意状态丢弃旧目标段并创建新目标段未变化未变化目标段保持不变未变化新增 / 删除 / 变化保留目标段 ID仅重写其 manifest5.3 一个片段一个目标段目前milvus-table下 DataNode 对每个刷新片段创建一个目标段而不会把多个源片段 bin-pack 进一个目标段。这样做的理由是把基于行偏移的虚拟 PK 与删除日志翻译限制在单个目标 manifest 内、保证局部性代价是源快照中 StorageV3 段很多时会产生较多目标外部段并消耗更多预分配 ID。设计文档也指出未来的 bin-packing 需要一个显式的源行偏移 → 目标行偏移映射层才能安全换算虚拟 PK 删除。5.4 目标段清单的构造差异目标段清单的构造方式与通用外部文件不同Parquet 等格式是从文件区间创建列组column groupsmilvus-table则是从源段清单导入源 StorageV3 列组进目标段清单。这保证了列数据主体零拷贝仅当源删除需要转换或复制时才可能写出目标自有的 deltalog。DataNode 会把 manifest 直接写到最终的 StorageV3 insert-log 路径下使用 DataCoord 预分配的 ID 区间。该区间被三处消耗目标段 ID、fake-binlog 的 log ID以及当源 deltalog 没有携带显式 LogID 时的回退 deltalog ID。相关实现集中在 internal/datanode/external/ 目录核心文件包括milvus_table_refresh.go、milvus_table_deltalog.go、task_update.go、function_executor.go及其同名_test.goDataCoord 侧的任务编排在 internal/datacoord/external_collection_refresh_manager.go配套测试external_collection_refresh_manager_test.go。刷新任务的对外状态机Pending / InProgress / Completed / Failed与进度信息可参考 client/entity/external_table.go 中的RefreshExternalCollectionState与RefreshExternalCollectionJobInfo。六、物理列解析规则一套规则双端实现持久化 Schema 只存external_field源字段名那么物理列到底叫什么就需要统一裁决。设计把物理列选择集中到两处Go 侧StorageColumnResolverC 侧Schema::GetPhysicalColumnName/Schema::IsExternalManifestStoredField。规则本身取决于读的是源数据还是目标段清单格式物理列名Parquet、Lance、Vortex、Icebergexternal_field即源字段名Milvus table目标字段 ID 字符串已对齐到源字段 ID为什么milvus-table要用数字字段 ID 字符串因为源 StorageV3 manifest 本就是 Milvus 写的物理列按字段 ID 存储。而目标自有字段虚拟 PK、目标函数输出不属于源数据其中函数输出在刷新后以目标数字字段 ID 落盘。该规则在实际物理访问点统一生效包括DataNode 刷新时的字段大小抽样存储 manifest 读取器QueryNode 段加载Segcore 的 search / query / retrieve / takeDataNode 索引构建外部字段大小抽样。设计文档特别说明早期把external_field改写为数字字符串的方案被否弃因为那样会把用户意图藏进持久化 Schema后续维护极易出错。七、两种主键模式根据目标集合是否声明用户主键milvus-table分为真实主键与虚拟主键两条数据通路。7.1 真实主键Real Primary Key若目标 Schema 含用户主键则它必须映射到源快照的主键。真实主键列在 QueryNode 段加载时被 eager 加载以便 retrieve / take / search 输出真实 ID。真实 PK 段的要点导入源 PK 布隆过滤器统计或以外部统计方式读取QueryNode 可复用常规 PK 剪枝源段 deltalog 作为外部 StorageV3 delta 路径导入目标 manifest快照 L0 覆盖层则被复制为目标自有 StorageV3 deltalog落在目标段 base path 之下加载时 QueryNode 把源外部 deltalog与目标自有 deltalog分开前者用外部 StorageV3 读取器解码后者用常规目标存储配置解码Segcore eager 加载源插入时间戳列——这样即使出现先删后插delete-before-reinsert快照作为外部表后仍能保持原有的可见性语义。7.2 虚拟主键Virtual Primary Key若目标集合没有用户主键Milvus 会注入外部虚拟主键其取值为virtual_pk (target_segment_id 0xffffffff) 32 | row_offset即段 ID 的低 32 位左移 32 位 段内行偏移。虚拟 PK 段的要点源 PK 布隆过滤器不能复用因为它以源 PK 为键而非目标虚拟 PKQueryNode 路由使用保守的 PK 候选刷新时DataNode 把源段 deltalog 与快照 L0 覆盖层里的源 PK 删除记录全部转换成目标虚拟 PK 删除记录。转换策略是先读删除键再只扫描源主键列去匹配这些键从而让内存占用正比于删除键数 命中行数而不是源总行数。八、删除语义贯穿Delete HandlingMilvus 快照可能包含两类删除来源挂在源段 manifest 上的 deltalog快照段清单引用的 L0 delta 覆盖层。8.1 真实 PK 目标删除语义留在源 PK 空间Loon FFI 的 manifest 构造器在目标有外部主键时从源 manifest 导入源段 deltalogDataNode 把快照 L0 覆盖层复制为目标自有 StorageV3 deltalogQueryNode 通过外部 StorageV3 deltalog 读取器加载源侧删除、通过常规读取器加载目标侧删除Segcore 使用源插入时间戳比较删除时间戳使删除只对插入时间早于删除时间的行生效。8.2 虚拟 PK 目标删除语义必须转换合并快照 L0 覆盖层与源段 deltalog读取源 deltalog收集被删除的源 PK仅对受影响片段扫描源 PK 列与时间戳列把每个匹配的源行偏移映射到目标虚拟 PK写出目标自有虚拟 PK deltalog将这些 deltalog 加入目标 StorageV3 manifest。当源 PK 存在重复时一个删除键会映射到所有匹配的目标行偏移时间戳排序遵循 Milvus 既有删除语义删除仅移除插入时间戳早于删除时间戳的行。deltalog 路径判断相关辅助函数可在 internal/storagev2/packed/milvus_table.go 找到例如用/_delta/识别 StorageV3 deltalog、用/delta_log/识别流式删除冲刷产生的旧版 L0 deltalog。九、QueryNode 加载与读写路径QueryNode 把milvus-table外部段当作带 StorageV3 manifest 的常规 sealed 外部段加载。加载阶段的行为物理列名一律经 Schema 解析见第六节规则真实 PK 字段 eager 加载真实 PK 的milvus-table段额外 eager 加载源时间戳列虚拟 PK 使用既有的合成虚拟 PK 列机制deltalog 对外部段不再被跳过真实 PK 段在解码前会先把源外部 delta 路径与目标自有 delta 路径分离真实 PK 的布隆过滤器统计从外部感知external-aware的存储中读取。相关代码集中在 internal/querynodev2/segments/segment_loader.go 与 internal/querynodev2/segments/utils.go并有对应_test.go覆盖外部真实 PK 布隆过滤器与 deltalog 的加载行为。search / query / retrieve / take 阶段的行为Segcore 用Schema::GetPhysicalColumnName请求物理列对milvus-table返回字段 ID 字符串对其他外部格式返回external_field输出字段 ID 与结果 ID 仍是目标的 Milvus 字段 ID 与目标 PK——对外用户看到的是普通外部表语义。十、索引构建保持一致索引构建没有单独维护一套Schema 变更路径。DataNode 把external_source与external_spec原样透传进BuildIndexInfoC 侧索引构建读取external_spec并采用与查询路径完全相同的物理列解析规则milvus-table字段 ID 字符串其他外部格式external_field。由此保证索引构建与 load / search / query / take 完全同构避免索引数据与查询数据指向不同物理列。十一、兼容性与失败行为设计明确列出以下失败语义全部为 fail-fast 参数/校验错误milvus-table的external_source不是 JSON 路径创建或刷新直接以参数错误失败快照元数据缺少storagev2_manifest_list刷新快速失败参考 internal/storagev2/packed/milvus_table.go 的报错定义其中明确提示源须为启用 Loon FFI / StorageV3 的集合源 deltalog 不是 StorageV3 的_delta路径刷新快速失败源 deltalog 需要物化进目标 manifest但 DataNode 无法推导或分配目标 deltalog ID刷新失败而非写出不稳定路径目标 Schema 与源快照 Schema 不匹配创建或刷新失败而不是以错误的字段 ID 读数据后续刷新指向了不同 Schema 的快照刷新在 Schema 身份校验处失败既有 Parquet、Lance、Vortex、Iceberg 外部表的external_field物理列行为完全不变。这些约束共同构成了一个强校验边界milvus-table宁可快速失败也绝不在 schema / 路径 / 删除语义不确定时静默写错数据。十二、设计边界Non-Goals为了保证主题聚焦这里汇总该设计的明确边界——它不做以下事情不支持非 StorageV3 源集合产生的快照不根据source_collection_id与snapshot_id反推快照路径路径布局不是 API 契约的一部分外部表不可写刷新之间不支持 Schema 演进虚拟 PK 目标不复用源 PK 布隆过滤器不支持 dynamic fields、struct 字段、分区键、聚簇键、text match、auto ID 等特性用于该外部集合不从外部源数据读取目标函数输出字段目标函数输出为 target-owned刷新时重算。十三、测试覆盖矩阵设计文档的 Validation 小节声明了完整测试覆盖仓库中大部分对应文件也已就位可作为阅读源码的入口RootCoord Schema 对齐测试internal/rootcoord/create_collection_task_test.goDataCoord 刷新 Schema 校验测试internal/datacoord/external_collection_refresh_manager_test.goDataNode manifest 创建、deltalog 复制与虚拟 PK deltalog 翻译测试internal/datanode/external/milvus_table_refresh_test.go、internal/datanode/external/milvus_table_deltalog_test.go、internal/datanode/external/task_update_test.goQueryNode 外部真实 PK 布隆过滤器与 deltalog 加载测试internal/querynodev2/segments/segment_loader_test.go、internal/querynodev2/segments/utils_test.goStorageV3 packed 读取器与 Milvus-table 快照元数据测试internal/storagev2/packed/exttable_test.go、[internal/storagev2/packed/transaction_test.go] 等Segcore 字段 ID 物理列解析测试C 侧Go 客户端 E2E 测试覆盖milvus-table快照的刷新、query、take、真实 PK、虚拟 PK 与删除行为。结语milvus-table是 Milvus 外部表家族中一个内部格式外部化的特例它没有引入全新的公开 API而是把 Milvus 集合快照这一复杂产物Schema StorageV3 manifest 各类删除日志 PK 统计塞进了既有的external_spec/external_source/external_field契约里靠 RootCoord 创建期字段 ID 对齐、DataCoord 刷新编排、DataNode 清单翻译与删除转换、QueryNode/Segcore 双端一致的物理列解析来支撑整条零拷贝只读链路。理解它的关键是记住三组对立源身份 vs 目标身份字段 ID 必须对齐、Schema 身份必须一致、源数据 vs 目标自有数据虚拟 PK、函数输出、目标 deltalog 均为 target-owned、真实 PK vs 虚拟 PK决定布隆过滤器能否复用、删除日志是导入还是翻译。如果你的场景需要把 Milvus 集合的不可变快照快速交付给另一套查询视图milvus-table正是为这条快照即数据源的路径而设计的。【免费下载链接】milvusMilvus is a high-performance, cloud-native vector database built for scalable vector ANN search项目地址: https://gitcode.com/GitHub_Trending/mi/milvus创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考