文章摘要
针对传统向量检索要么需拷贝数据湖数据,带来额外存储成本与同步维护复杂度,要么直接查询数据湖难以满足生产低延迟要求的痛点,Milvus 3.0推出External Collection外部集合功能,无需复制源数据即可基于数据湖原生数据提供低延迟多负载向量检索,可消除数据副本、保障数据一致性,优化存储与内存占用,支持多种主流存储格式,适配多业务负载共享同一份数据的场景。

在AI应用的日常运维中,不少团队都会面临一个棘手的问题:数据湖已经妥善存储了商品属性、多模态embedding和训练语料等核心数据,但传统向量数据库往往需要将数据复制到本地构建专属副本,才能实现高效的在线检索。这种模式不仅会带来额外的存储成本,更会因为多份数据同步的问题,增加工程维护的复杂度。Milvus 3.0推出的External Collection功能,为这类场景提供了第三种解决方案。

如果直接对数据湖中的文件进行高性能向量检索,目前常见的实现路径有两种:

  • 搭建ETL管道将数据复制进向量数据库。这种方式可以灵活配置向量索引,保证在线检索的低延迟,但需要维护多份数据副本,每次商品目录更新、embedding模型升级或是字段补全,都需要触发完整的数据同步流程。

  • 直接查询数据湖。这种方式无需复制数据,但无法直接利用向量数据库为在线检索构建的HNSW等ANN索引,通常会退化为大范围扫描,难以满足生产环境的低延迟要求。

Milvus 3.0的External Collection打破了这种两难局面。借助这一能力,源数据可以继续保留在Parquet、Iceberg、Lance、Vortex等受支持的外部存储格式中,Milvus仅负责管理指向外部数据的引用、字段映射关系,以及为检索构建的索引、manifest等服务状态,具体包括:

  • external_source:用于标识外部文件或数据表的路径
  • external_spec:描述源数据格式和存储访问方式的配置
  • external_field映射:将Milvus Schema中的字段对应到外部数据集的列
  • 为检索构建的索引、manifest和服务运行状态

完成配置后,只需要将外部字段映射到Milvus Schema、定义所需索引并执行Refresh操作,加载集合后即可直接使用Milvus的search和query API。需要注意的是,并非每次查询都需要从对象存储重新读取数据,索引和高频访问的热数据可以缓存在本地,有效降低远程读取带来的延迟开销。

这样一来,在线检索无需再创建独立的源数据副本,同时仍能通过Milvus的索引和查询引擎提供低延迟检索服务;Spark训练流程、离线评测任务和数据治理工具则可以继续在原有位置工作,不同业务负载可以基于同一份数据湖数据构建。

核心价值:消除数据副本与保障数据一致性

对于数据团队来说,External Collection的第一层价值在于存储优化,避免将数TB的数据在不同Pipeline中重复搬运与存储。而更深层次的价值,则是以极低的成本保证不同业务链路中的数据始终保持一致。

以商品目录场景为例,传统模式下数据平台会先产出Parquet格式的原始数据,再通过ETL作业将数据灌入向量库形成第一份副本;推荐团队为了做离线召回分析,又会导出一份数据到自己的Spark集群形成第二份副本;当模型迭代需要更新embedding时,每份副本都需要重新同步更新。每新增一个数据消费方,就会多一份存储拷贝、多一条同步链路,也多了一份数据不一致的风险,团队需要投入大量工程资源来协调和维护数据同步。

而在External Collection模式下,原始数据始终只保留一份。Milvus可以通过External Collection为商品搜索、推荐或智能检索提供在线服务,同时其他系统可以直接处理数据湖中的同一份数据:

  • Spark可以快速识别重复商品
  • 模型训练流程可以使用最新模型生成embedding
  • 数据质量任务可以及时发现格式错误或异常记录
  • 评测流程可以对比不同模型版本的检索效果
  • 批处理任务可以自动生成商品摘要、标签或新的元数据

离线处理产生的改进数据或新字段,可以直接写回数据湖,当下一次Refresh操作完成后,更新后的数据集版本就会对Milvus查询可见,无需再维护专门的export/import流程来为在线服务重建数据副本。同时治理边界也十分清晰:Milvus负责集合级的访问控制,以及访问外部存储所需的身份与凭证;而源数据本身的版本、血缘和所有权,则仍由数据湖平台统一管理。

适配多样存储:数据源支持与安全机制

目前External Collection已支持AWS S3、阿里云OSS、Google Cloud Storage等主流云厂商存储服务,以及MinIO等S3兼容的自建存储系统。在数据格式方面,首批支持Parquet、Iceberg、Lance、Vortex等主流开放格式。

实际应用中,选择的数据湖存储格式会直接影响线上检索的延迟和成本。通常来说,Lance、Vortex这类现代列存格式的随机读写性能更优,成本也更低。

考虑到不同业务Pipeline中数据管理方式的差异,External Collection还支持字段映射功能。例如,可以将源数据中的product_id字段映射为Milvus中的id字段,将image_vec字段映射为embedding字段。如果源表包含大量字段,也不需要将所有列都暴露给集合,数据平台无需为了适配在线服务数据库,就重命名或重写自己的源数据。

针对Iceberg这类带版本的数据源,External Collection还支持指定某个历史快照来建表或刷新数据,用户可以检索「上周二的数据集版本」来完成可复现的评测、回归对比或审计工作,无需担心评测过程中源数据更新影响最终结果,同时底层文件仍可以被数据技术栈中的其他系统正常使用。

在数据访问安全方面,External Collection支持基于IAM Role的访问方式以及跨账号STS AssumeRole,可以将身份认证交由云厂商的身份体系管理,避免在配置中长期保存静态Access Key。需要注意的是,这里的存储身份仅决定Milvus如何访问源数据,Milvus内部的授权体系属于另一个独立的安全边界。

快速上手:创建与使用External Collection

External Collection的完整生命周期主要包含四个步骤:

  1. 定义外部数据源,并将其中的列映射到Milvus Schema
  2. 定义当前工作负载所需的索引类型
  3. 执行Refresh操作,让Milvus发现源数据并准备可查询的数据集版本
  4. 加载集合,之后即可照常使用Milvus的search和query API

以下是商品目录场景下创建External Collection的示例代码:

import json
import time
from pymilvus import DataType, MilvusClient

client = MilvusClient(
uri=“http://localhost:19530”,
token=“root:Milvus”,
)

schema = client.create_schema(
external_source=“s3://my-lake/datasets/products/”,
external_spec=json.dumps(
{
“format”: “parquet”,
“extfs”: {
“cloud_provider”: “aws”,
“region”: “us-east-1”,
“use_iam”: “true”,
“iam_endpoint”: “https://sts.us-east-1.amazonaws.com”,
},
}
),
)

schema.add_field(
field_name=“id”,
datatype=DataType.INT64,
external_field=“product_id”,
)

schema.add_field(
field_name=“embedding”,
datatype=DataType.FLOAT_VECTOR,
dim=768,
external_field=“image_vec”,
)

schema.add_field(
field_name=“title”,
datatype=DataType.VARCHAR,
max_length=256,
external_field=“product_name”,
)

schema.add_field(
field_name=“stock”,
datatype=DataType.INT64,
external_field=“stock”,
)

schema.add_field(
field_name=“rating”,
datatype=DataType.FLOAT,
external_field=“rating”,
)

client.create_collection(
collection_name=“products_ext”,
schema=schema,
)

索引的创建可以使用普通的Milvus接口:

index_params = client.prepare_index_params()
index_params.add_index(
    field_name="embedding",
    index_type="HNSW",
    metric_type="COSINE",
)
index_params.add_index(field_name="stock", index_type="AUTOINDEX")
index_params.add_index(field_name="rating", index_type="AUTOINDEX")
client.create_index(
    collection_name="products_ext",
    index_params=index_params,
)

完成索引配置后,即可对外部数据源执行Refresh操作:

job_id = client.refresh_external_collection(
    collection_name="products_ext",
)
while True:
    progress = client.get_refresh_external_collection_progress(job_id=job_id)
    if progress.state == "RefreshCompleted":
        break
    if progress.state == "RefreshFailed":
        raise RuntimeError(progress.reason)
    time.sleep(2)

Refresh操作完成后,就可以像使用普通Milvus Collection一样加载集合并进行检索:

client.load_collection("products_ext")
results = client.search(
    collection_name="products_ext",
    data=[query_vec],
    anns_field="embedding",
    filter="stock > 0 and rating >= 4.0",
    limit=10,
    output_fields=["id", "title", "stock", "rating"],
)

与普通Milvus Collection相比,search调用本身的变化不大,核心区别在于数据生命周期的起点不同:普通Collection的数据从写入或导入Milvus开始,而External Collection则从引用一份已存在于外部的数据集开始。

数据更新可见:Refresh的工作机制

从Milvus的视角来看,External Collection是只读的,但底层的数据湖数据集并不需要始终保持不变。当商品数据处理流程新增了一批数据、更新了元数据,或是写入了由新模型生成的embedding时,这些变化并不会自动被Milvus感知,需要通过Refresh操作才能同步到服务中。

Refresh操作会读取外部元数据,解析源数据片段,更新将这些片段关联到Milvus Collection的manifest,并准备相应的索引状态。整个过程支持增量更新:Milvus会识别未发生变化的源数据片段,复用已有的segment和索引结果,只有新增或发生变化的片段才需要重新处理。因此即使是数TB的大型数据集,只修改其中一小部分时,也不需要重新执行完整的数据导入和索引重建。

此外,Refresh还为在线服务提供了明确的数据版本边界。在新版本准备期间,查询仍然使用上一个已发布的版本;等Refresh操作完成后,新版本才会作为完整版本对外可见,不会出现查询混合了旧数据和部分准备完成的新数据的情况。这种模式非常适合按小时更新的商品目录、每日夜间更新的知识库、周期性的embedding更新、模型生成特征的处理流程等批处理工作负载。

优化内存占用:懒加载与按需读取

如果在线服务在处理查询前需要将所有数据加载到本地,那么将源数据保留在对象存储中就无法发挥任何优势。启用Milvus Tiered Storage后,Collection加载时,QueryNode初始只会保留轻量级元数据,例如Schema信息、索引定义、chunk map以及指向远程对象的引用。字段数据只有在查询需要时,才会按chunk从远端读取;索引也可以一直保留在远端,第一次使用时再加载并缓存到本地。经常访问的热数据会保持缓存状态,不常使用的冷数据则会被自动淘汰。

这一特性对于字段众多的AI数据集尤为重要。一条商品记录可能包含多个embedding、长文本描述、原始JSON、图片元数据、自动生成的摘要、库存、价格、评分等大量属性,而一次典型的相似度搜索可能只需要用到一个向量字段,再加上库存、价格和评分几个标量字段,没有理由让其他不相关的字段长期占用在线服务的内存资源。

总的来说,External Collection可以从两个层面缩小在线服务侧的数据占用:

  • Schema级投影:通过external_field映射,External Collection只暴露业务需要的源数据列,其他列继续保留在数据湖中,不会进入当前的服务Schema。
  • 运行时投影:启用Tiered Storage后,QueryNode只会读取并缓存实际工作负载需要的字段和索引,而非在加载阶段一次性加载所有已映射的数据。

当然这里存在明确的取舍:如果查询访问了尚未缓存的冷字段或冷索引,第一次访问时可能会产生远程读取的额外开销。可以通过warm-up策略预先加载对延迟敏感的字段或索引,同时结合缓存和淘汰策略,避免低频使用的数据长期占用本地资源。我们的核心目标并不是让对象存储表现得像内存一样,而是让内存和本地磁盘围绕检索工作负载真正访问的工作集来配置,而非按照源数据集的总规模和总宽度来规划资源。针对不同的源数据格式,按需访问时会呈现不同的I/O特征,External Collection并不会消除这些存储层面的差异,只是让Milvus可以在这些数据格式之上构建统一的检索层。

完整检索能力:支持的索引与查询类型

External Collection并非简单地让Milvus指向一个embedding文件目录然后扫描其中的数据。在生产环境中,搜索结果很少只依赖向量相似度,还需要结合精确关键词、访问权限、库存、时间戳、类别、价格、来源质量或业务排序信号等多维度条件。因此,Milvus会在外部数据上构建完整的检索结构,并通过标准的检索引擎执行查询。根据字段类型和工作负载需求,Milvus可以构建以下类型的索引:

  • 用于近似最近邻搜索的向量索引
  • 用于元数据过滤的标量索引
  • 用于半结构化属性的JSON索引
  • 用于词法检索的BM25和全文索引
  • Milvus数据模型支持的Function生成字段

最终,External Collection可以支持包括向量检索、关键词检索、全文检索、标量过滤、混合检索与排序在内的完整生产级检索能力,为存放在数据湖中的数据提供了数据库级的检索路径,而非仅仅提供一种从文件中读取向量的方法。

场景适配:何时选择External Collection

External Collection并不能替代流式写入路径,它更适合以下场景:

  • 权威数据已经存放在Parquet、Vortex、Lance、Iceberg或其他受支持的外部数据源中
  • 数据集主要通过批处理流程生成,而非高频事务写入
  • 维护第二份服务副本会带来明显的ETL、数据新鲜度或治理成本
  • 多个系统需要使用同一份开放数据
  • 可以接受通过明确的Refresh边界来控制在线数据的新鲜度
  • 希望使用Milvus的生产级检索能力,但不希望让Milvus接管源数据记录

而以下场景则更适合使用普通的Milvus Collection:

  • 应用需要持续执行insert或upsert操作
  • delete操作需要通过在线写入路径及时生效
  • 工作负载依赖External Schema尚不支持的Collection功能
  • 在线服务设计希望将所有需要的数据常驻内存,以避免远程缓存未命中的情况

此外,还有几个关键边界需要注意:

  • External Collection是只读的,源数据的修改只能在Milvus之外进行
  • 所谓的零复制指的是源数据记录,索引、manifest、缓存和计算资源仍然需要付出成本
  • Refresh操作需要显式执行,并非流式同步机制
  • 外部数据源必须始终保持可访问,搜索、索引构建和Refresh操作仍然依赖存储访问权限和凭证
  • 在开源Milvus 3.0中,使用External Collection前必须先启用Storage V3
  • External Collection不会替代上游的数据处理流程,Embedding生成、聚类、去重和数据清洗等工作仍然由相应的上游系统完成

总的来说,一个系统可以针对快速变化的在线数据使用普通Milvus Collection,同时针对规模大、批量生成、天然存放在数据湖中的数据使用External Collection,从而兼顾检索效率与数据治理的灵活性。

以上内容不代表本平台立场,仅供读者参考