Milvus 3.0开源解读:StructArray重构多向量检索机制
时间:2026-08-21 | 作者:半糖攻略君 | 阅读:0向量数据库早期将实体简化为单向量,致使复杂数据的局部信息丢失。而Milvus 3.0通过StructArray重构多向量检索,实现了整体与局部粒度的平衡。
核心内容如下:
- 向量数据库Entity设计的局限:复杂数据的局部细节丢失。
- 暴力拆分方案的不足:存在数据关联与索引问题。
- StructArray的核心设计:支持Entity内对齐Elements及多粒度检索。
向量数据库早期有一个默认抽象:一个实体(Entity)=一条向量(Embedding)。
文档、商品等都可编码成向量。搜索的基本动作,是将query vector与这些entity vectors做近邻搜索。第一代语义搜索、推荐召回和RAG,基本都建立在这一模型上。
但这个模型有一个隐含前提:实体足够简单,一条embedding就足以表示完整原始数据信息。对几百字文章,这个假设勉强成立。可对视频、长文档、多图商品等复杂数据而言,把所有内容压缩成一条向量,细节会被平均掉。
而检索中真正决定相关性的,往往正是局部内容。
单向量Entity模型的局限
一个直接的办法,是把局部内容拆开。比如将视频拆成多个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。
StructArray解决什么问题
StructArray允许一个Entity内保存一组彼此对齐的Elements。每个Element可包含受支持的scalar metadata和vector sub-fields,同时仍属于同一个Parent Entity。
这意味着,数据库能够明确知道哪些数据是在共同描述同一个对象。也可以决定搜索发生在entity层、element层,或在两种粒度之间完成过滤、排序和结果融合。
从一条记录到一组有身份的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)
如果后面要做vector search或高效过滤,还需显式创建索引。为和后文的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提供三种不同的执行语义:
- 在Element上判断条件,再过滤Parent Entity;
- 把一组Element embeddings作为整体参与Entity-level search;
- 让每个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,并最终返回实体级结果。
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,同时满足这两个条件。
我们提供了多种聚合语义的MATCH表达式:
- MATCH_ANY:只要存在任意一个Element满足条件;
- MATCH_ALL:要求所有Element都满足条件;
- MATCH_LEAST:至少有指定数量的Element满足条件;
- MATCH_MOST:至多有指定数量的Element满足条件;
- MATCH_EXACT:恰好有指定数量的Element满足条件。
免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。
相关文章
更多-
- 字节跳动发布豆包工作 与飞书深度打通构建企业级Agent
- 时间:2026-08-25
-
- AI通话录音手机续航实测能用多长时间
- 时间:2026-08-23
-
- Adobe Photoshop 27.7桌面版发布 移除工具支持端侧AI运行 苹果Mac需24GB内存
- 时间:2026-08-23
-
- 育碧远哭7测试AI生成画面效果 知情者评价其表现不佳
- 时间:2026-08-23
-
- AMD锐龙9 PRO 9965X3D及锐龙AI PRO 400商用台式机2026Q3上市
- 时间:2026-08-22
-
- 思必驰冲刺上市,AI语音行业护城河为何消失
- 时间:2026-08-21
-
- 道题看懂AI商业趋势与未来发展方向
- 时间:2026-08-21
-
- AI清理500GB空间实测:Agent暂时还替代不了电脑管家
- 时间:2026-08-21
精选合集
更多大家都在玩
大家都在看
更多-
- 为什么湿头发更容易断裂 蚂蚁庄园今日答案9.15
- 时间:2026-09-14
-
- 蚂蚁庄园今天答题答案2026年9月15日
- 时间:2026-09-14
-
- 蚂蚁庄园答题今日答案2026年9月15日
- 时间:2026-09-14
-
- 蚂蚁庄园小课堂2026年9月15日最新题目答案
- 时间:2026-09-14
-
- 小鸡答题今天的答案是什么2026年9月15日
- 时间:2026-09-14
-
- 蚂蚁庄园每日答题答案2026年9月15日
- 时间:2026-09-14
-
- 糖尿病患者禁食所有含糖食物吗 蚂蚁庄园今日答案9月15日
- 时间:2026-09-14
-
- 2026年9月14日蚂蚁新村答案
- 时间:2026-09-14
