Milvus
Zilliz
  • Home
  • Blog
  • 解锁真正的实体级检索:Milvus 中新的结构阵列和 MAX_SIM 功能

解锁真正的实体级检索:Milvus 中新的结构阵列和 MAX_SIM 功能

  • Engineering
December 05, 2025
Jeremy Zhu, Min Tian

如果您在向量数据库之上构建了人工智能应用程序,您可能会遇到同样的痛点:数据库检索的是单个块的 Embeddings,但您的应用程序关心的是实体这种不匹配使整个检索工作流程变得复杂。

您可能已经看到这种情况一再发生:

  • RAG 知识库:文章被分块成段落嵌入,因此搜索引擎返回的是零散的片段而不是完整的文档。

  • 电子商务推荐:一个产品有多个图片嵌入,而您的系统返回的是同一商品的五个角度,而不是五个独特的产品。

  • 视频平台:视频被分割成片段嵌入,但搜索结果显示的是同一视频的片段,而不是单一的合并条目。

  • ColBERT / ColPali 风格检索:文档扩展为数以百计的标记或片段级嵌入,而搜索结果却显示为仍需合并的小片段。

所有这些问题都源于相同的架构缺陷:大多数向量数据库都将每个嵌入作为一个独立的行来处理,而实际应用操作的是更高层次的实体--文档、产品、视频、项目和场景。因此,工程团队不得不使用重复数据删除、分组、分块和 Rerankers 逻辑手动重构实体。这种方法虽然有效,但却脆弱、缓慢,而且会让应用层臃肿不堪,其中的逻辑本来就不应该存在。

Milvus 2.6.4通过一项新功能弥补了这一缺陷:具有MAX_SIM度量类型的结构数组。这两项功能结合在一起,可将单个实体的所有 Embdings 保存在一条记录中,并使 Milvus 能够对实体进行整体评分和返回。不再有重复填充的结果集。不再需要重排和合并等复杂的后处理工作

在本文中,我们将介绍结构数组(Array of Structs)和 MAX_SIM 的工作原理,并通过两个实际案例进行演示:维基百科文档检索和 ColPali 基于图像的文档搜索。

什么是结构数组?

在 Milvus 中,结构数组字段允许单条记录包含一个有序的Struct 元素列表,每个元素都遵循相同的预定义 Schema。一个 Struct 可以包含多个向量、标量字段、字符串或其他任何支持的类型。换句话说,它可以让你把属于一个实体的所有部分--段落嵌入、图像视图、标记向量、元数据--直接捆绑在一行中。

下面是一个集合实体的示例,其中包含一个 Array of Structs 字段。

{
    'id': 0,
    'title': 'Walden',
    'title_vector': [0.1, 0.2, 0.3, 0.4, 0.5],
    'author': 'Henry David Thoreau',
    'year_of_publication': 1845,
    // highlight-start
    'chunks': [
        {
            'text': 'When I wrote the following pages, or rather the bulk of them...',
            'text_vector': [0.3, 0.2, 0.3, 0.2, 0.5],
            'chapter': 'Economy',
        },
        {
            'text': 'I would fain say something, not so much concerning the Chinese and...',
            'text_vector': [0.7, 0.4, 0.2, 0.7, 0.8],
            'chapter': 'Economy'
        }
    ]
    // hightlight-end
}

在上面的示例中,chunks 字段是一个结构数组字段,每个结构元素都包含自己的字段,即texttext_vectorchapter

这种方法解决了向量数据库中一个长期存在的模型问题。传统上,每个 Embeddings 或属性都必须成为自己的一行,这就迫使多向量实体(文档、产品、视频)被分割成几十、几百甚至几千条记录。有了结构数组,Milvus 可让你在单个字段中存储整个多向量实体,使其自然适用于段落列表、标记嵌入、剪辑序列、多视图图像或一个逻辑项由多个向量组成的任何情况。

结构数组如何与 MAX_SIM 配合使用?

MAX_SIM是一种新的评分策略,它使语义检索具有实体感知能力。当收到查询时,Milvus 会将其与每个结构数组中的每个向量进行比较,并将最大相似度作为实体的最终得分。然后,实体将根据这个单一得分进行排序并返回。这就避免了向量数据库检索分散片段的典型问题,并将分组、去重和重新排序的工作推给了应用层。有了 MAX_SIM,实体级检索变得内置、一致和高效。

为了了解 MAX_SIM 在实践中是如何工作的,让我们举一个具体的例子。

注:本例中的所有向量均由相同的 Embeddings 模型生成,相似度以 [0,1] 范围内的余弦相似度衡量。

假设用户搜索"机器学习初级课程"。

查询被标记为三个标记

  • 机器学习

  • 初级

  • 课程

然后,每个 token 都会被用于文档的相同嵌入模型转换成一个嵌入向量

现在,想象一下向量数据库包含两个文档:

  • doc_1: Python 深度神经网络入门指南

  • doc_2: 高级法学硕士论文阅读指南

这两个文档都已被嵌入向量,并存储在一个结构数组(Array of Structs)内。

步骤 1:计算 doc_1 的 MAX_SIM

对于每个查询向量,Milvus 根据 doc_1 中的每个向量计算其余弦相似度:

简介指南深度神经网络机器学习
机器学习0.00.00.90.3
初学者0.80.10.00.3
课程0.30.70.10.1

对于每个查询向量,MAX_SIM 会从其行中选择相似度最高的一个:

  • 机器学习 → 深度神经网络 (0.9)

  • 初学者 → 入门 (0.8)

  • 课程 → 指南 (0.7)

将最佳匹配值相加,得出 doc_1 的MAX_SIM 分数为 2.4

第 2 步:计算 doc_2 的 MAX_SIM 分数

现在我们对 doc_2 重复上述过程:

高级指导LLM论文阅读
机器学习0.10.20.90.30.1
初学者0.40.60.00.20.5
课程0.50.80.10.40.7

doc_2 的最佳匹配是

  • "machine learning" → "LLM" (0.9)

  • "beginner" → "guide" (0.6)

  • "course" → "guide" (0.8)

将它们相加,doc_2 的MAX_SIM 得分为 2.3

步骤 3:比较得分

因为2.4 > 2.3,所以doc_1 的排名高于 doc_2,这是很直观的,因为 doc_1 更接近机器学习入门指南。

从这个例子中,我们可以突出 MAX_SIM 的三个核心特征:

  • 语义优先,而非基于关键词:MAX_SIM 比较的是 Embeddings,而不是文本字面。尽管"机器学习 ""深度神经网络 "的重叠词为零,但它们的语义相似度为 0.9。这使得 MAX_SIM 对同义词、意译、概念重叠和现代嵌入式丰富的工作负载具有很强的鲁棒性。

  • 对长度和顺序不敏感:MAX_SIM 不要求查询和文档具有相同数量的向量(例如,doc_1 有 4 个向量,而 doc_2 有 5 个,两者都能正常工作)。它也不考虑向量的顺序--"初学者 "出现在查询的前面,而 "介绍 "出现在文档的后面对得分没有影响。

  • 每个查询向量都很重要:MAX_SIM 取每个查询向量的最佳匹配值,并将这些最佳得分相加。这样可以防止不匹配的向量影响结果,并确保每个重要的查询标记都对最终得分有贡献。例如,doc_2 中 "beginner "的低质量匹配会直接降低其总得分。

为什么 MAX_SIM + 结构数组在向量数据库中很重要

Milvus是一个开源的高性能向量数据库,它现在完全支持 MAX_SIM 和结构数组,从而实现了向量原生的实体级多向量检索:

  • 原生存储多向量实体:Array of Structs 可以将相关向量组存储在单个字段中,而无需将它们分割到单独的行或辅助表中。

  • 高效的最佳匹配计算:结合 IVF 和 HNSW 等向量索引,MAX_SIM 可以计算最佳匹配,而无需扫描每个向量,即使是大型文档也能保持较高的性能。

  • 专为语义繁重的工作负载而设计:这种方法在长文本检索、多方面语义匹配、文档摘要对齐、多关键词查询以及其他需要灵活、细粒度语义推理的人工智能场景中表现出色。

何时使用结构数组

了解了结构数组的功能后,它的价值就一目了然了。该功能的核心是提供三种基本功能:

  • 它将向量、标量、字符串、元数据等异构数据捆绑到一个结构化对象中。

  • 它使存储与现实世界的实体保持一致,因此数据库中的每一行都能清晰地映射到文章、产品或视频等实际项目。

  • 当与 MAX_SIM 等聚合函数相结合时,它可以直接从数据库中实现真正的实体级多向量检索,从而消除了应用层中的重复数据删除、分组或重排。

由于具有这些特性,当一个逻辑实体由多个向量表示时,结构数组就自然而然地适用了。常见的例子包括分割成段落的文章、分解成标记嵌入的文档或由多个图像表示的产品。如果您的搜索结果存在重复命中、分散片段或同一实体多次出现在顶部结果中的问题,那么结构体数组就能在存储和检索层解决这些问题,而不是在应用程序代码中进行事后修补。

对于依赖于多向量检索的现代人工智能系统来说,这种模式尤其强大。 例如,ColBERT 将单个文档表示为一个数组

  • ColBERT将单个文档表示为 100-500 个标记嵌入,用于法律文本和学术研究等跨领域的细粒度语义匹配。

  • ColPali每个 PDF 页面转换成 256-1024 个图像补丁,用于财务报表、合同、发票和其他扫描文档的跨模态检索。

通过 Structs 数组,Milvus 可以将所有这些向量存储在一个实体下,并高效、原生地计算集合相似度(例如 MAX_SIM)。为了更清楚地说明这一点,下面是两个具体示例。

以前,具有多张图片的产品存储在一个平面 Schema 中,每行一张图片。一个产品的正面、侧面和斜面照片会产生三行。搜索结果往往会返回同一产品的多张图片,这就需要手动重复删除和重新排序。

使用结构数组后,每个产品都变成了一行。所有图片嵌入和元数据(角度、is_primary 等)都作为结构数组存在于images 字段中。Milvus 会理解它们属于同一个产品,并将产品作为一个整体返回,而不是单个图像。

以前,维基百科的一篇文章被分成N 个段落行。搜索结果会返回分散的段落,迫使系统将它们分组并猜测它们属于哪篇文章。

有了结构数组,整篇文章就变成了一行。所有段落及其 Embeddings 都被归类到一个段落字段下,数据库会返回整篇文章,而不是零散的片段。

实践教程:使用结构数组进行文档级检索

1.维基百科文档检索

在本教程中,我们将介绍如何使用结构数组(Array of Structs)将段落级数据转换为完整的文档记录,从而使 Milvus 能够执行真正的文档级检索,而不是返回孤立的片段。

许多知识库管道都将维基百科文章存储为段落块。这对于嵌入和索引来说效果很好,但却破坏了检索:用户查询通常会返回分散的段落,迫使你手动分组并重建文章。有了结构数组和 MAX_SIM,我们就可以重新设计存储模式,使每篇文章都成为一行,这样 Milvus 就能原生排序并返回整个文档。

在接下来的步骤中,我们将展示如何

  1. 加载和预处理维基百科段落数据

  2. 将属于同一篇文章的所有段落打包成结构数组

  3. 将这些结构化文档插入 Milvus

  4. 运行 MAX_SIM 查询来检索完整的文章--干净利落,无需进行去重或 Rerankers。

本教程结束时,你将拥有一个工作管道,Milvus 可以直接处理实体级检索,完全符合用户的期望。

数据模型:

{
    "wiki_id": int,                  # WIKI ID(primary key) 
    "paragraphs": ARRAY<STRUCT<      # Array of paragraph structs
        text:VARCHAR                 # Paragraph text
        emb: FLOAT_VECTOR(768)       # Embedding for each paragraph
    >>
}

第 1 步:分组和转换数据

在本演示中,我们使用简单维基百科 Embeddings数据集。

import pandas as pd
import pyarrow as pa

# Load the dataset and group by wiki_id df = pd.read_parquet(“train-*.parquet”) grouped = df.groupby(‘wiki_id’)

# Build the paragraph array for each article 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 Collections

from pymilvus import MilvusClient, DataType

client = MilvusClient(uri=“http://localhost:19530”) schema = client.create_schema() schema.add_field(“wiki_id”, DataType.INT64, is_primary=True)

# Define the Struct schema struct_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 步:插入数据并建立索引

# Batch insert documents
client.insert("wiki_docs", wiki_data)

# Create an HNSW index 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 步:搜索文档

# Search query
import cohere
from pymilvus.client.embedding_list import EmbeddingList

# The dataset uses Cohere’s multilingual-22-12 embedding model, so we must embed the query using the same model. co = cohere.Client(f"<>") query = ‘Who founded Youtube’ response = co.embed(texts=[query], model=‘multilingual-22-12’) query_embedding = response.embeddings query_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”] )

# Results: directly return 10 full articles! for hit in results[0]: print(f"Article {hit[‘entity’][‘wiki_id’]}: Score {hit[‘distance’]:.4f}")

比较输出结果:传统检索与结构数组对比

当我们查看数据库实际返回的内容时,结构数组的影响就会很明显:

维度传统方法结构数组
数据库输出返回前 100 个段落(冗余度高)返回前 10 个完整文档- 干净准确
应用逻辑需要分组、重复数据删除和重排(复杂)无需后处理--实体级结果直接来自 Milvus

在维基百科的例子中,我们只演示了最简单的情况:将段落向量组合成统一的文档表示。但 Array of Structs 的真正优势在于,它可以推广到任何多向量数据模型--包括经典检索管道和现代人工智能架构。

传统的多向量检索场景

许多成熟的搜索和推荐系统自然会对具有多个相关向量的实体进行操作符。Array of Structs 可以很方便地映射到这些用例中:

场景数据模型每个实体的向量
🛍️电子商务产品一个产品 → 多张图片5-20
视频搜索一个视频 → 多个片段20-100
📖纸张检索一份论文 → 多个部分5-15

人工智能模型工作量(关键多向量用例)

在现代人工智能模型中,结构体阵列变得更加关键,这些模型有意为每个实体生成大量向量集,以进行细粒度语义推理。

模型数据模型每个实体的向量应用
ColBERT一个文档 → 多个标记嵌入100-500法律文本、学术论文、细粒度文档检索
ColPali一个 PDF 页面 → 许多补丁嵌入256-1024财务报告、合同、发票、多模式文档搜索

这些模型需要多向量存储模式。在使用结构数组(Array of Structs)之前,开发人员不得不跨行拆分向量,并手动将结果拼接在一起。有了 Milvus,现在可以原生存储和检索这些实体,并由 MAX_SIM 自动处理文档级评分。

ColPali是一个强大的跨模式 PDF 检索模型。它不依赖文本,而是将每个 PDF 页面作为图像处理,并将其切成多达 1024 个可视补丁,每个补丁生成一个嵌入。在传统的 Schema 数据库模式下,这需要将一个页面存储为数百或数千个独立的行,使数据库无法理解这些行属于同一个页面。因此,实体级搜索变得支离破碎且不切实际。

Array of Structs 将所有的 Embeddings 都存储在一个字段中,使 Milvus 能够将页面作为一个内聚的多向量实体来处理,从而干净利落地解决了这个问题。

传统的 PDF 搜索通常依赖于OCR,即把页面图像转换成文本。这种方法适用于纯文本,但会丢失图表、表格、布局和其他视觉线索。ColPali 通过直接处理页面图像,保留了所有视觉和文本信息,从而避免了这一限制。这样做的代价是规模问题:现在每个页面都包含数百个向量,这就需要一个能将众多 Embeddings 聚合为一个实体的数据库,而这正是 Array of Structs + MAX_SIM 所能提供的。

最常见的使用案例是Vision RAG,其中每个 PDF 页面都成为一个多向量实体。典型的应用场景包括

  • 财务报告:在成千上万的 PDF 中搜索包含特定图表或表格的页面。

  • 合同:从扫描或拍照的法律文件中检索条款。

  • 发票:按供应商、金额或布局查找发票。

  • 演示文稿:查找包含特定数字或图表的幻灯片。

数据模型:

{
    "page_id": int,                     # Page ID (primary key) 
    "page_number": int,                 # Page number within the document 
    "doc_name": VARCHAR,                # Document name
    "patches": ARRAY<STRUCT<            # Array of patch objects
        patch_embedding: FLOAT_VECTOR(128)  # Embedding for each patch
    >>
}

第1步:准备数据关于ColPali如何将图像或文本转换成多向量表示法,你可以参考文档的详细介绍。

import torch
from PIL import Image

from colpali_engine.models import ColPali, ColPaliProcessor

model_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) # Example: 2 documents, 5 pages each, total 10 images images = [ Image.open(“path/to/your/image1.png”), Image.open(“path/to/your/image2.png”), … Image.open(“path/to/your/image10.png”) ] # Convert each image into multiple patch embeddings batch_images = processor.process_images(images).to(model.device) with torch.no_grad(): image_embeddings = model(**batch_images)

第 2 步:创建 Milvus Collectionions

from pymilvus import MilvusClient, DataType

client = 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 patches struct_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步:插入数据并建立索引

# Prepare data for insertion
page_data=[
    {
        "page_id": 0,
        "page_number": 0,
        "doc_name": "Q1_Financial_Report.pdf",
        "patches": [
            {"patch_embedding": emb} for emb in image_embeddings[0]
        ],
    },
    ...,
    {
        "page_id": 9,
        "page_number": 4,
        "doc_name": "Product_Manual.pdf",
        "patches": [
            {"patch_embedding": emb} for emb in image_embeddings[9]
        ],
    },
]

client.insert(“doc_pages”, page_data)

# Create index 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 步:跨模态搜索:文本查询 → 图像结果

# Run the search
from pymilvus.client.embedding_list import EmbeddingList

queries = [ “quarterly revenue growth chart”
] # Convert the text query into a multi-vector representation 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"Query: '{queries[0]}'") for i, hit in enumerate(results, 1): entity = hit[‘entity’] print(f"{i}. {entity[‘doc_name’]} - Page {entity[‘page_number’]}") print(f" Score: {hit[‘distance’]:.4f}\n")

输出样本:

Query: 'quarterly revenue growth chart'
1. Q1_Financial_Report.pdf - Page 2
   Score: 0.9123

2. Q1_Financial_Report.pdf - Page 1 Score: 0.7654

3. Product_Manual.pdf - Page 1 Score: 0.5231

在这里,结果直接返回完整的 PDF 页面。我们不需要担心底层的 1024 补丁嵌入--Milvus 会自动处理所有的聚合。

结论

大多数向量数据库将每个片段存储为独立的记录,这意味着应用程序在需要完整的文档、产品或页面时必须重新组合这些片段。结构体数组改变了这种情况。通过将标量、向量、文本和其他字段组合到一个结构化对象中,它允许一条数据库记录端到端代表一个完整的实体。

其结果是简单而强大的:过去需要在应用层中进行复杂的分组、删除和重排的工作,现在变成了一种本地数据库功能。这正是向量数据库未来的发展方向--更丰富的结构、更智能的检索和更简单的管道。

有关结构数组和 MAX_SIM 的更多信息,请查看下面的文档:

对最新 Milvus 的任何功能有疑问或想深入了解?加入我们的 Discord 频道或在 GitHub 上提交问题。您还可以通过 Milvus Office Hours 预订 20 分钟的一对一课程,以获得见解、指导和问题解答。

    Try Managed Milvus for Free

    Zilliz Cloud is hassle-free, powered by Milvus and 10x faster.

    Get Started

    Like the article? Spread the word

    扩展阅读