游乐游手机版
首页/AI热点日报/热点详情

Milvus 3.0开源解读:StructArray重构多向量检索解析

类型:热点整理2026-08-20
早期向量数据库通常采用一种简化建模方式:一个实体(Entity)只对应一条向量(Embedding)。无论文档、商品还是多媒体内容,都会先被编码成单个向量,再通过 query vector 与这些 entity vectors 执行近邻检索。第一代语义搜索、推荐召回以及 RAG 检索增强生成,大多建

早期向量数据库通常采用一种简化建模方式:一个实体(Entity)只对应一条向量(Embedding)。无论文档、商品还是多媒体内容,都会先被编码成单个向量,再通过 query vector 与这些 entity vectors 执行近邻检索。第一代语义搜索、推荐召回以及 RAG 检索增强生成,大多建立在这一模型之上。但这个模式有一个前提:实体本身足够简单,一条 embedding 就能较完整地表达原始内容。对于几百字的短文本,这种假设尚且可用;可一旦面对视频、长文档、多图商品等复杂数据,把全部信息压缩进一个向量,局部细节就容易被平均和稀释,而真正决定检索相关性的,往往恰恰是这些局部内容。一个常见思路是把局部内容拆开处理,例如将视频切分为多个 clips,并为每个 clip 单独保存 embedding、start_time、end_time、caption、scene_type 和 confidence 等信息。但这种“暴力拆分”也会引出新的问题:

  • 同一条视频的多个 clips 可能会同时进入 Top-K 结果;
  • 父级 metadata 需要在多条 records 中重复存储;
  • 业务最终展示的是“视频”而不是“clip”,应用层还要自行完成 grouping、dedup 和 rerank;
  • 数据库本身并不知道这些 clips 原本共同属于同一个逻辑对象。
这里的本质矛盾在于粒度不一致:业务消费的是整体实体,而搜索命中的往往是局部片段。那么,如何在向量检索中既保留局部信息的精度,又让数据库理解整体与局部之间的关系?基于这一需求,Milvus 3.0 引入了 StructArray。它允许在一个 Entity 内保存一组彼此对齐的 Elements。每个 Element 都可以包含受支持的 scalar metadata 和 vector sub-fields,同时仍然归属于同一个 Parent Entity。这样一来,数据库既能明确知道哪些数据共同描述同一个对象,也能灵活决定搜索发生在 entity 层、element 层,或在两种粒度之间完成过滤、排序与结果融合。下面将详细介绍 StructArray 如何重构多向量检索。

从一条记录到一组有身份的Elements

仍以前面提到的视频检索为例,一个 video entity 往往会被切分成多个 clips。虽然 JSON 或多列 Array 也能存储这些数据,但它们无法把同一个 clip 的多个字段作为一组强关联的数据单元来处理。JSON 目前不支持向量子字段,因此无法在其中定义并索引 clip-level vector。多列 Array 虽然能并排保存数据,但各列彼此独立,数据库无法保证相同 offset 的元素一定属于同一个 clip,例如 scene_type[3] 与 label_confidence[3] 的对应关系只能依赖应用层维护,也不利于后续 match_family 这类跨字段匹配。相比之下,StructArray 直接把 Element 的结构定义进 Schema 中,让数据库清楚地知道:同一个 offset 上的多个 sub-fields 属于同一个 Element。

    scene_type         → VARCHAR
    label_confidence → FLOAT
    emb              → FLOAT_VECTOR

数据库也因此能够识别:同一 offset 下的这些 subfields 共同描述的是同一个 Element。这样,它们就可以分别参与索引、向量搜索、条件过滤和结果输出。在视频建模场景中,一个 video entity 可以包含多个 clips:

    clips: ARRAY
      clip_embedding_list: FLOAT_VECTOR,
      clip_embedding: FLOAT_VECTOR,
      start_sec: DOUBLE,
      end_sec: DOUBLE,
      caption: VARCHAR,
      scene_type: VARCHAR,
      label_confidence: FLOAT
    >>
在后文示例中,clip_embedding_list 和 clip_embedding 保存的是同一个 clip 的向量,但分别服务于两种不同的搜索模式:前者用于 EmbeddingList search,后者用于 element-level search。在这里:
  • clips 是 parent field;
  • clip_embedding、start_sec、caption 等都是 sub-fields;
  • clips[0] 表示第一个 clip;
  • clips[0][clip_embedding] 与 clips[0][caption] 属于同一个 clip;
  • clips[3][scene_type] 与 clips[3][label_confidence] 则属于另一个 clip。

建模:一个Video Entity如何保存多个Clips

下面通过简化版 PyMilvus 示例来创建一个视频 Collection。该 Collection 包含一个顶层向量字段,以及一个用于保存视频片段的 StructArray。为了同时演示两种检索方式,这里为 clip 定义了两个独立的向量子字段。

    from pymilvus import DataType, MilvusClient
    client = MilvusClient(uri="http://localhost:19530")
    schema = client.create_schema(auto_id=False, enable_dynamic_field=False)
    schema.add_field("id", DataType.INT64, is_primary=True)
    schema.add_field("title", DataType.VARCHAR, max_length=512)
    schema.add_field("video_embedding", DataType.FLOAT_VECTOR, dim=768)
    # struct需要显式定义schema
    clip_schema = client.create_struct_field_schema()
    clip_schema.add_field("clip_embedding_list", DataType.FLOAT_VECTOR, dim=768)
    clip_schema.add_field("clip_embedding", DataType.FLOAT_VECTOR, dim=768)
    clip_schema.add_field("start_sec", DataType.DOUBLE)
    clip_schema.add_field("end_sec", DataType.DOUBLE)
    clip_schema.add_field("caption", DataType.VARCHAR, max_length=2048)
    clip_schema.add_field("scene_type", DataType.VARCHAR, max_length=128)
    clip_schema.add_field("label_confidence", DataType.FLOAT)
    schema.add_field(
         "clips",
         datatype=DataType.ARRAY,
         element_type=DataType.STRUCT,
         struct_schema=clip_schema,
         max_capacity=1024,
    )
    client.create_collection("videos", schema=schema)

如果后续需要进行向量搜索或高效过滤,还要显式创建索引。为了与后文的 embedding-list search 示例保持一致,这里为 clips[clip_embedding_list] 创建 MAX_SIM_COSINE 索引:

    index_params = client.prepare_index_params()
    # EmbeddingList search
    index_params.add_index(
         field_name="clips[clip_embedding_list]",
         index_type="HNSW",
         metric_type="MAX_SIM_COSINE",
         index_name="clips_clip_embedding_list_maxsim_idx",
         params={"M": 16, "efConstruction": 200},
    )
    # Element-level search
    index_params.add_index(
         field_name="clips[clip_embedding]",
         index_type="HNSW",
         metric_type="COSINE",
         index_name="clips_clip_embedding_cosine_idx",
         params={"M": 16, "efConstruction": 200},
    )
    client.create_index("videos", index_params=index_params)

这里需要分别为两个 vector sub-fields 建立索引。clips[clip_embedding_list] 使用 MAX_SIM_COSINE,服务于 EmbeddingList search;clips[clip_embedding] 使用常规的 COSINE metric,服务于 element-level search。之所以拆分为两个字段,是因为一个 vector sub-field 只能绑定一个索引,而这两种搜索模式使用的是不同的 metric family。插入数据时,用户可以按照最自然的 entity 结构直接写入:

    rows = [
         {
             "id": 1,
             "title": "cooking tutorial",
             "video_embedding": video_vec,
             "clips": [
                 {
                     "clip_embedding_list": clip_vec_1,
                     "clip_embedding": clip_vec_1,
                     "start_sec": 0.0,
                     "end_sec": 8.0,
                     "caption": "A person washes vegetables.",
                     "scene_type": "kitchen",
                     "label_confidence": 0.92,
                 },
                 {
                     "clip_embedding_list": clip_vec_2,
                     "clip_embedding": clip_vec_2,
                     "start_sec": 8.0,
                     "end_sec": 16.0,
                     "caption": "A person cuts carrots on a board.",
                     "scene_type": "kitchen",
                     "label_confidence": 0.96,
                 },
             ],
         }
    ]
    client.insert("videos", rows)
    client.flush("videos")
    client.load_collection("videos")

基于StructArray的三种过滤与检索方式

从用户视角来看,clips 就是一组结构化对象。而在有了这层结构之后,Milvus 便可以围绕同一组 Elements 提供三种不同的执行语义:

  1. 先在 Element 上判断条件,再过滤 Parent Entity;
  2. 将一组 Element embeddings 作为整体参与 Entity-level search;
  3. 让每个 Element embedding 独立参与 ANN 检索,并直接返回局部命中结果。

其中,第一类 MATCH_* 系列操作符可用于判断父实体是否应该被过滤。MATCH_ANY、MATCH_ALL、MATCH_LEAST、MATCH_MOST 和 MATCH_EXACT 会先在 Struct 元素上执行 predicate,再统计有多少元素满足条件,最后据此决定整个父实体是否通过过滤。例如

    MATCH_ANY(clips, $[scene_type] == "kitchen" && $[label_confidence] > 0.8)

这个表达式要求同一个 offset 上的 scene_type 和 label_confidence 同时满足条件,然后再把 element-level predicate 聚合成 entity-level predicate。它不是跨多个 clips 拼接条件,也不是几列普通 arrays 各自独立过滤。第二类是 embedding-list search,它可以直接返回父实体。clips[clip_embedding_list] 中的多个向量共同组成这个视频的一个 EmbeddingList。查询本身同样基于 EmbeddingList 进行。Milvus 会使用 MAX_SIM* metric 对查询 EmbeddingList 与实体中存储的 EmbeddingList 进行比较,并最终返回 entity 级搜索结果。

    clips[clip_embedding_list] = [
      embedding_0,
      embedding_1,
      embedding_2,
      ...
    ]

第三类是 element-level search;通过 element_filter 还可以进一步限制哪些 Elements 参与搜索。也就是说,即使 StructArray 已经把多个 embeddings 组织成 embedding list,Milvus 仍然保留了单个 embedding 粒度的向量检索能力:让每个 element embedding 独立参与 ANN,并在返回结果中携带 offset,明确告诉应用命中的是 parent entity 中的哪一个 element。与此同时,element_filter 还能把标量过滤条件约束在同一个 element 上,例如只让 scene_type == "kitchen" 且 label_confidence > 0.8 的 clips 参与 element-level search。下面分别来看。

MATCH:先在同一个Element上判断,再决定Entity是否命中

StructArray 在过滤场景中的核心价值,在于让数据库明确知道:当一个 filter 同时涉及多个 scalar sub-fields 时,这些条件必须在同一个 element 上一起判断,而不是分散到同一个 parent entity 的不同 elements 上。比如在视频检索中,用户可能想找到“厨房场景,并且标签置信度足够高”的视频。这里真正要判断的,不是 entity 中是否出现过 kitchen,也不是 entity 中是否存在某个高置信度值,而是是否存在同一个 clip 同时满足这两个条件。为此,Milvus 提供了多种聚合语义的 MATCH 表达式:

  • 唐晨杰

    Senior Software Engineer at Zilliz

    阅读推荐官宣开源|Milvus 3.0 正式发布Milvus 3.0 开源解读之backfill|亿级 AI 数据,如何做高效特征回填Milvus 3.0 开源解读|从Kafka、Pulsar到Woodpecker,数据库如何低延迟、低成本的写入Milvus 3.0 开源解读之Manifest|AI 数据管理,应该彻底放弃文件中心架构Milvus 3.0 开源解读之,数据库原生聚合排序如何取代应用侧Pandas胶水代码Milvus 3.0开源解读之Regex |从=~到NGRAM,如何选择最优性价比的正则过滤Milvus 3.0 开源解读之Snapshot|无需复制embedding数据的Milvus Collection视图Milvus 3.0 开源解读|自定义词典如何优化BM25 与Text Match 的专业词理解能力Milvus 3.0 开源解读|如何借助原生 TEXT 类型和 LOB 高效管理原始文本
    Milvus 3.0 开源解读|词法+语义高亮,如何解决Agent的搜索噪音?

登录查看剩余 70% 内容

来源:https://www.53ai.com/news/RAG/2026081921340.html

相关热点

继续查看同栏目近期热点。

延伸阅读

补充最近整理过的热点入口。