微信扫码
添加专属顾问
我要投稿
Milvus 2.6.4的Struct Array + MAX_SIM功能,彻底解决多向量检索返回完整实体的行业痛点。核心内容: 1. 多向量检索场景中的常见问题与业务需求 2. Struct Array技术原理与MAX_SIM算法实现 3. 知识库、电商、视频三大场景的实践案例解析
本文为Milvus Week系列第二篇,该系列旨在分享Zilliz、Milvus在系统性能、索引算法和云原生架构上的创新与实践,以下是DAY2内容划重点:
Struct Array + MAX_SIM ,能够让数据库看懂 “多向量组成一个实体” 的逻辑,进而原生返回业务要的完整结果
用向量数据库的人大概率都碰过这类问题:数据库里存的是被拆成片段的向量(比如一篇文档的段落向量、商品的单张图片向量),但业务要的是完整的实体(整个文档、整个商品)。
举几个真实场景中的案例:
知识库检索:存的是段落向量,然而用户想搜的是最相关的几篇文档,却因为搜到的多个段落匹配到的同一篇文档,导致去重后文档数量不足;
电商搜索:存的是商品图向量,结果召回的结果是同一商品的不同角度图占满检索结果,无法返回足量商品;
视频平台:存的是片段向量,导致最后搜到的都是同一部视频的不同切片;
这些问题本质上都是一回事:视角错位。数据库认为“一个向量 = 一条数据”,但业务看来 “多个向量 = 一个实体”。结果就是应用层需要额外加去重、分组、rerank,既麻烦又容易出 bug。
好在 Milvus 2.6.4 出了 Struct Array + MAX_SIM 功能,能够让数据库看懂 “多向量组成一个实体” 的逻辑,进而原生返回业务要的完整结果。
下面用Wikipedia 文档检索、ColPali 文档图像检索两个真实案例,做详细解读。(在本文场景中:我们用它来存储一个实体的多个向量,但它的能力远不止于此,你还能用它聚合任何类型的结构化数据。)
Struct Array 的本质,就是允许在一个字段里存储多个结构化对象(可以包含标量、向量、字符串等任意类型),然后把它们组织成一个整体。
Struct Array的核心价值在于打破传统数据结构的限制:允许在单个字段中存储多个结构化对象(可包含标量、向量、字符串等任意数据类型),并将这些对象组织为一个逻辑整体。这种结构特别适用于处理 “多向量组合” 场景(如文本分词后的 embedding list)。
而 MAX_SIM(最大相似度求和)算法则是基于 Struct Array 实现语义级检索的核心实现路径 。 它解决了传统检索依赖词形完美匹配的痛点,通过向量语义相似度实现更灵活的匹配逻辑。
接下来我们通过一个案例,来详细拆解 MAX_SIM 的计算逻辑(所有向量均通过相同的 embedding 模型生成,相似度采用余弦相似度计算,取值范围 [0,1])。
假设用户输入的query是“机器学习入门课程”,由4个向量组成,"机器", "学习", "入门", "课程"。数据库中有两篇doc,[1]新手深度神经网络python实战; [2]理论进阶之大模型paper详解; 也分别tokenization后按向量储存。
我们先来计算query和doc_1的相似度。首先,我们计算query中的每个向量和doc内的每个向量之间的cosine相似度,如下表所示。
对于query中的每个向量,我们都会从doc中找到最为匹配的向量。例如query中的“机器学习”将匹配doc_1中的“深度神经网络”,“入门”将匹配“新手”,“课程”将匹配“实战”,最终query和doc_1的相似度为以上最佳匹配的相似度之和,0.9 + 0.8 + 0.7 = 2.4.
同理,我们计算query和doc_2的相似度,“机器学习”将匹配“大模型”,“入门”和“课程”都会匹配“详解”,但是我们注意到,“入门”和最佳匹配“详解”的相似度只有0.6,所以最终相似度得分只有0.9 + 0.6 + 0.8 = 2.3,低于doc_1,这符合我们的预期。
基于上述案例,可总结 MAX_SIM 的三大关键特性:
语义优先,不依赖词形匹配:核心依赖向量 embedding 的语义相似度,而非关键词的字面重合(如 “机器学习” 与 “深度神经网络” 无相同字符,但语义匹配度高达 0.9),更适合处理同义词、相关概念的检索场景。
长度与顺序无关:不限制 query 和文档的向量列表长度(如 doc_1 含 4 个向量、doc_2 含 5 个向量,均能正常计算),且无需考虑向量的顺序(如 query 的 “入门” 在前、文档的 “新手” 在后,不影响匹配结果)。
平等关注每个 query 向量:对 query 中的每个核心向量,均取其最佳匹配值参与求和,避免因部分向量未匹配导致的检索偏差(如 “入门” 向量的匹配质量直接影响最终得分)。
目前,Milvus 作为开源向量数据库,依托其高效的向量检索引擎,已扩展支持基于 Struct Array 的 MAX_SIM 算法:
可直接存储多向量组合的 Struct Array 数据,无需额外拆分字段;
结合向量索引(如 IVF、HNSW)优化最佳匹配的计算效率,避免全量遍历;
适用于长文本检索、多维度语义匹配等场景(如文档摘要匹配、多关键词语义检索),为 AI 应用提供更灵活的检索方案。
Struct Array的核心能力概括来说,有三点:
能把不同类型的数据(标量、向量、字符串)凑成一个结构化对象;
让数据库里的 “一行” 对应业务里的 “一个东西”(文章 / 商品 / 视频);
配合 MAX_SIM 这类聚合函数,数据库直接返回实体级结果,不用应用层做额外工作。
因此,如果你的数据存在 “整体 - 部分” 结构(如一篇文章包含多个段落、一个商品对应多张图片),业务需要返回完整实体而非碎片化向量(如用户需获取文章列表而非零散段落),且正面临应用层需手动实现复杂去重、分组与重排逻辑,或是向量检索结果中同一实体反复占据 Top 位导致冗余的问题时,Struct Array 正是适配这类需求的解决方案。
在需要多向量检索的AI应用场景中尤其适合:ColBERT 模型将一个文档拆分为 100-500 个 Token 向量,适用于法律文档、学术论文的细粒度检索;ColPali 模型把一个 PDF 页转化为 256-1024 个 Patch 向量,可满足财报、合同、发票等跨模态检索场景的需求。
拿电商商品举例子就懂了:
以前存商品图:是扁平化存储思路,一张图一行数据,同一个商品的正面、侧面图得拆成 3 行,搜的时候还得自己去重;
用 Struct Array:一个商品占一行,所有图片的角度、是否主图、向量信息,都塞在images这个字段里 —— 数据库可以直接认出这是一个商品的所有图。
再看知识库的场景:
以前存维基百科:一篇文章拆成 N 个段落,每个段落一行,搜出来全是零散段落;
用 Struct Array:一篇文章一行,所有段落的文本和向量都包在 “paragraphs” 字段里,数据库返回的直接是整篇文章。
目标:将段落数据转换为文档数据,实现文档级检索。
核心流程:数据分组 → 创建 Schema → 插入数据 → 创建索引 → 搜索
-
-
-
-
-
-
-
{ "wiki_id": int, "paragraphs": ARRAY<STRUCT< text:VARCHAR emb: FLOAT_VECTOR(768) >>}
1. 数据分组转换
数据集来源: https://huggingface.co/datasets/Cohere/wikipedia-22-12-simple-embeddings
-
-
-
-
-
-
-
-
-
-
-
-
-
import pandas as pdimport pyarrow as padf = pd.read_parquet("train-*.parquet")grouped = df.groupby('wiki_id')wiki_data = []for wiki_id, group in grouped: wiki_data.append({ 'wiki_id': wiki_id, 'paragraphs': [{'text': row['text'], 'emb': row['emb']} for _, row in group.iterrows()] })
2. 创建 Milvus Collection
-
-
-
-
-
-
-
-
-
-
-
-
from pymilvus import MilvusClient, DataTypeclient = MilvusClient(uri="http://localhost:19530")schema = client.create_schema()schema.add_field("wiki_id", DataType.INT64, is_primary=True)# 定义 Struct Arraystruct_schema = client.create_struct_field_schema()struct_schema.add_field("text", DataType.VARCHAR, max_length=65535)struct_schema.add_field("emb", DataType.FLOAT_VECTOR, dim=768)schema.add_field("paragraphs", DataType.ARRAY, element_type=DataType.STRUCT, struct_schema=struct_schema, max_capacity=200)client.create_collection("wiki_docs", schema=schema)
3. 插入数据并创建索引
-
-
-
-
-
-
-
-
-
-
-
-
client.insert("wiki_docs", wiki_data)index_params = client.prepare_index_params()index_params.add_index( field_name="paragraphs[emb]", index_type="HNSW", metric_type="MAX_SIM_COSINE", params={"M": 16, "efConstruction": 200})client.create_index("wiki_docs", index_params)client.load_collection("wiki_docs")
4. 搜索文档
# 搜索查询import coherefrom pymilvus.client.embedding_list import EmbeddingList# 数据集的向量是通过 cohere的 embedding 模型multilingual-22-12,query文本也需要使用相同的模型生成co = cohere.Client(f"<<COHERE_API_KEY>>")query = 'Who founded Youtube'response = co.embed(texts=[query], model='multilingual-22-12')query_embedding = response.embeddingsquery_emb_list = EmbeddingList()for vec in query_embedding[0]:query_emb_list.add(vec)results = client.search(collection_name="wiki_docs",data=[query_emb_list],anns_field="paragraphs[emb]",search_params={"metric_type": "MAX_SIM_COSINE","params": {"ef": 200, "retrieval_ann_ratio": 3}},limit=10,output_fields=["wiki_id"])# 结果:直接返回 10 篇不同的文章!for hit in results[0]:print(f"文章 {hit['entity']['wiki_id']}: 相似度 {hit['distance']:.4f}")
效果对比:
当然,以上Wikipedia 案例展示了基础的段落检索场景。Struct Array 的真正威力在于支持各种多向量场景:
传统检索场景
AI模型场景(重点)
ColPali 是现在做 PDF 跨模态检索的热门模型,它会把一页 PDF 切成 1024 个 Patch,每个 Patch 一个向量。要是用传统方式存,一页 PDF 得拆成 1024 行,搜的时候根本没法聚合 ——Struct Array 刚好能解决这个问题。
此外,传统 PDF 检索靠 OCR 转文本,会丢图表、布局信息;ColPali 直接从图像切 Patch,保留所有视觉和文本信息,但需要数据库能处理 “一页 = 1024 个向量” 的聚合需求。
Struct Array 在ColPali文档图像检索领域的典型场景是Vision RAG。比如:财报检索(在数千份PDF中找到包含特定图表的页面)、合同审查(从扫描的合同中检索特定条款)、发票处理(检索特定供应商或金额的发票)、 演示文稿(找到包含特定图示的幻灯片)。
-
-
-
-
-
-
-
-
{ "page_id": int, "page_number": int, "doc_name": VARCHAR, "patches": ARRAY<STRUCT< patch_embedding: FLOAT_VECTOR(128) >>}
1. 数据准备
https://huggingface.co/vidore/colpali-v1.3
可以参考这个文档获取colpali如何将图片/文本转成多向量
import torchfrom PIL import Imagefrom colpali_engine.models import ColPali, ColPaliProcessormodel_name = "vidore/colpali-v1.3"model = ColPali.from_pretrained(model_name,torch_dtype=torch.bfloat16,device_map="cuda:0", # or "mps" if on Apple Silicon).eval()processor = ColPaliProcessor.from_pretrained(model_name)# 假设有2个文档,每个文档5页,共10张图片images = [Image.open("path/to/your/image1.png"),Image.open("path/to/your/image2.png"),....Image.open("path/to/your/image10.png")]# 将图片转换成多向量batch_images = processor.process_images(images).to(model.device)with torch.no_grad():image_embeddings = model(**batch_images)
2. 创建Collection:
-
-
-
-
-
-
-
-
-
-
-
-
-
from pymilvus import MilvusClient, DataTypeclient = MilvusClient(uri="http://localhost:19530")schema = client.create_schema()schema.add_field("page_id", DataType.INT64, is_primary=True)schema.add_field("page_number", DataType.INT64)schema.add_field("doc_name", DataType.VARCHAR, max_length=500)# Struct Array for patchesstruct_schema = client.create_struct_field_schema()struct_schema.add_field("patch_embedding", DataType.FLOAT_VECTOR, dim=128)schema.add_field("patches", DataType.ARRAY, element_type=DataType.STRUCT, struct_schema=struct_schema, max_capacity=2048)client.create_collection("doc_pages", schema=schema)
3. 插入并索引
# 插入数据page_data=[{"page_id": 0,"page_number": 0,"doc_name": "Q1财报.pdf","patches": [{"patch_embedding": emb} for emb in image_embeddings[0]],},...,{"page_id": 9,"page_number": 4,"doc_name": "产品手册.pdf","patches": [{"patch_embedding": emb} for emb in image_embeddings[9]],},]client.insert("doc_pages", page_data)# 创建索引index_params = client.prepare_index_params()index_params.add_index(field_name="patches[patch_embedding]",index_type="HNSW",metric_type="MAX_SIM_IP",params={"M": 32, "efConstruction": 200})client.create_index("doc_pages", index_params)client.load_collection("doc_pages")
4. 跨模态搜索:文本查询→图像结果
# 搜索from pymilvus.client.embedding_list import EmbeddingListqueries = ["quarterly revenue growth chart"]# 将查询文本转换成多向量batch_queries = processor.process_queries(queries).to(model.device)with torch.no_grad():query_embeddings = model(**batch_queries)query_emb_list = EmbeddingList()for vec in query_embeddings[0]:query_emb_list.add(vec)results = client.search(collection_name="doc_pages",data=[query_emb_list],anns_field="patches[patch_embedding]",search_params={"metric_type": "MAX_SIM_IP","params": {"ef": 100, "retrieval_ann_ratio": 3}},limit=3,output_fields=["page_id", "doc_name", "page_number"])print(f"查询: '{queries[0]}'")for i, hit in enumerate(results, 1):entity = hit['entity']print(f"{i}. {entity['doc_name']} - 第{entity['page_number']}页")print(f" 相似度: {hit['distance']:.4f}\n")
输出示例:
-
-
-
-
-
-
-
查询: 'quarterly revenue growth chart'1. Q1财报.pdf - 第2页 相似度: 0.91232. Q1财报.pdf - 第1页 相似度: 0.76543. 产品手册.pdf - 第1页 相似度: 0.5231
这里的输出结果直接是 PDF 页面,我们不用管背后 1024 个 Patch 的细节,数据库已经自动搞定了聚合。
传统数据库将数据打散成一行行记录,而 Struct Array 让数据库真正支持结构化聚合:通过灵活组合标量、向量、字符串等多种类型,让一行数据真正对应一个业务实体。
这意味着,复杂的数据聚合直接应用层的工程问题变成了数据库的原生能力,而这也是数据库的长期进化方向。
作者介绍
朱文星
Zilliz Senior Software Engineer in Quality Assurance
田敏
Senior Software Engineer at Zilliz
53AI,企业落地大模型首选服务商
产品:场景落地咨询+大模型应用平台+行业解决方案
承诺:免费POC验证,效果达标后再合作。零风险落地应用大模型,已交付160+中大型企业
2025-12-02
企业级 AI Agent规模化落地的避坑指南,就藏在这四大趋势里
2025-12-01
MCP与数据库的完美结合
2025-11-30
KnowEval:RAG 工程化的最后一公里,让问答质量有据可依
2025-11-30
大模型文本分类:从原理到工程落地(含代码)
2025-11-29
RAG 只是 AI 的上半场,OmniThink 才是类人的真思考(深度)
2025-11-28
详解用Palantir AIP几分钟搭建一个文档智能搜索应用
2025-11-27
从检索增强到自主检索:构建可行动的 Agentic RAG 系统
2025-11-27
RAG被判死刑:Google用一行API架空工程师!
2025-09-15
2025-09-08
2025-09-10
2025-09-10
2025-10-04
2025-09-30
2025-10-11
2025-10-12
2025-09-08
2025-11-04
2025-11-23
2025-11-20
2025-11-19
2025-11-04
2025-10-04
2025-09-30
2025-09-10
2025-09-10