Milvus
Zilliz
  • Home
  • Blog
  • 開啟真正的實體層級擷取:Milvus 中新的結構陣列與 MAX_SIM 功能

開啟真正的實體層級擷取:Milvus 中新的結構陣列與 MAX_SIM 功能

  • Engineering
December 05, 2025
Jeremy Zhu, Min Tian

如果您在向量資料庫之上建立了人工智慧應用程式,您可能會遇到相同的痛點:資料庫擷取的是個別區塊的嵌入,但您的應用程式關心的實體這種錯配讓整個擷取工作流程變得複雜。

您可能已經看到這種情況一再發生:

  • RAG 知識庫:文章被分成段落嵌入,因此搜尋引擎會返回分散的片段,而不是完整的文件。

  • 電子商務推薦:一個產品有多個圖片嵌入,而您的系統返回的是同一個項目的五個角度,而不是五個獨特的產品。

  • 視訊平台:影片被分割成片段嵌入,但搜尋結果顯示的是同一影片的片段,而不是單一的整合項目。

  • ColBERT / ColPali 式檢索:文件會擴展成數百個標記或片段層級的嵌入,而您的結果則是仍需合併的小片段。

所有這些問題都源自於相同的架構差異:大多數向量資料庫都將每個嵌入視為獨立的一行,而實際應用程式是在更高層級的實體上運作 - 文件、產品、視訊、項目、場景。因此,工程團隊不得不使用重複資料刪除、群組、分類和重排邏輯來手動重建實體。這種方法雖然有效,但卻脆弱、緩慢,而且會為應用程式層增添原本就不該存在的邏輯。

Milvus 2.6.4 藉由一項新功能縮小了這個差距:具有MAX_SIM公制類型的結構陣列。兩者結合起來,可讓單一實體的所有嵌入都儲存在單一記錄中,並使 Milvus 能夠整體地評分和回傳實體。不再有重複的結果集。不再有複雜的後期處理,例如重排和合併。

在這篇文章中,我們將介紹 Array of Structs 和 MAX_SIM 如何運作,並透過兩個實例進行示範:維基百科文件檢索和 ColPali 圖像文件搜尋。

什麼是結構陣列?

在 Milvus 中,Structs 陣列欄位允許單一記錄包含 Struct 元素的有序清單,每個元素都遵循相同的預定義模式。一個 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 欄位是一個 Array of Structs 欄位,而每個 Struct 元素都包含自己的欄位,即text,text_vector, 和chapter

這種方法解決了向量資料庫中一個存在已久的建模問題。傳統上,每個嵌入或屬性都必須成為自己的一行,這就迫使多向量實體 (文件、產品、影片)被分割成數十條、數百條,甚至數千條記錄。有了 Array of Structs,Milvus 讓您可以在單一欄位中儲存整個多向量實體,使其自然地適用於段落列表、符號嵌入、剪輯序列、多視圖或任何一個邏輯項目由許多向量組成的情況。

結構體陣列如何使用 MAX_SIM?

在這個新的結構陣列結構之上是MAX_SIM,這是一個新的評分策略,讓語意檢索具有實體感知能力。當查詢來到時,Milvus 會將其與每個結構陣列中的每個向量進行比較,並將最大相似度作為該實體的最終得分。然後根據單一的分數對實體進行排序和傳回。這避免了傳統向量資料庫擷取零散片段的問題,並將群組、刪除和重新排序的負擔推到應用程式層。有了 MAX_SIM,實體層級的擷取變得內建、一致且有效率。

為了瞭解 MAX_SIM 如何實際運作,讓我們來看一個具體的範例。

注意:本範例中的所有向量都是由相同的嵌入模型產生,相似度是以 [0,1] 範圍內的余弦相似度來衡量。

假設使用者搜尋「機器學習初級課程」。

該查詢被標記化成三個標記

  • 機器學習

  • 初學者

  • 課程

然後,每個標記都會被用來處理文件的相同嵌入模型轉換成嵌入向量

現在,想像向量資料庫包含兩個文件:

  • doc_1: 使用 Python 的深度神經網路入門指南

  • doc_2: An Advanced Guide to LLM Paper Reading

兩個文件都已經內嵌成向量,並儲存在一個 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.3doc_1 的排名比 doc_2 高,這是直覺的道理,因為 doc_1 比較接近機器學習入門指南。

從這個例子,我們可以突顯 MAX_SIM 的三個核心特徵:

  • 語義第一,而非基於關鍵字:MAX_SIM 比較的是嵌入,而不是文字。即使「機器學習」「深度神經網路」的重疊字彙為零,它們的語意相似度仍高達 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,實現向量原生、實體層級的多向量檢索:

  • 原生儲存多向量實體:Array of Structs 可讓您在單一欄位儲存相關向量群組,而無須將其分割成獨立的行或輔助表。

  • 高效的最佳匹配計算:結合向量索引 (例如 IVF 與 HNSW),MAX_SIM 可在不掃描每個向量的情況下計算最佳匹配,即使是大型文件也能保持高效能。

  • 專為語意繁重的工作負載而設計:此方法適用於長文檢索、多面向語意比對、文件摘要對齊、多關鍵字查詢,以及其他需要彈性、細粒度語意推理的人工智慧情境。

何時使用結構陣列

當您檢視Array of Structs 的功能時,它的價值就一目了然了。這項功能的核心在於提供三種基本能力:

  • 它可以將異質資料(向量、標量、字串、元資料)捆綁成單一結構物件。

  • 它將儲存與現實世界的實體對齊,因此資料庫的每一行都能清楚地對應到文章、產品或視訊等實際項目。

  • 當與 MAX_SIM 等聚合功能結合時,它可以直接從資料庫中進行真正的實體層級多向量擷取,而不需要在應用程式層中進行重複資料刪除、分組或重新排序。

由於這些特性,當單一邏輯實體由多向量來表示時,Array of Structs 就是一個天然的選擇。常見的例子包括分割成段落的文章、分解成標記嵌入的文件,或是由多張圖片代表的產品。如果您的搜尋結果出現重複點擊、分散的片段,或是同一個實體多次出現在頂端結果中,Array of Structs 可在儲存和檢索層解決這些問題,而不是在應用程式碼中進行事後修補。

對於依賴多向量擷取的現代 AI 系統來說,這種模式尤其強大。 舉例來說,ColBERT 將單一文件表示為一個矢量

  • ColBERT將單一文件表示為 100-500 個標記嵌入,用於跨領域(如法律文本和學術研究)的精細語義匹配。

  • ColPali可將 每個 PDF 頁面轉換成 256-1024 個影像修補碼,以進行財務報表、合約、發票和其他掃描文件的跨模式檢索。

Structs 陣列可讓 Milvus 將所有這些向量儲存在單一實體之下,並有效且原生地計算集合相似度 (例如 MAX_SIM)。為了更清楚說明這一點,這裡有兩個具體的範例。

以前,具有多張圖片的產品會儲存在平面模式中,每行只有一張圖片。一個產品有正面、側面和角度拍攝,會產生三行。搜尋結果通常會傳回同一產品的多張圖像,需要手動重複刪除和重新排序。

有了 Structs 陣列,每個產品都變成一列。所有的圖片嵌入與元資料 (角度、is_primary 等) 都以結構陣列的形式存在於images 欄位中。Milvus 瞭解它們屬於同一個產品,並返回產品整體,而非其個別圖片。

以前,一篇 Wikipedia 文章會分成N 個段落行。搜尋結果會傳回分散的段落,迫使系統將它們分組,並猜測它們屬於哪篇文章。

有了 Structs 陣列,整篇文章就變成了一行。所有段落及其嵌入都會歸類到段落欄位,資料庫會傳回完整的文章,而不是零碎的片段。

實作教學:使用結構陣列進行文件層級檢索

1.維基百科文件擷取

在本教程中,我們將介紹如何使用結構陣列(Array of Structs)將段落層級的資料轉換為完整的文件記錄-允許 Milvus 執行真正的文件層級檢索,而不是返回孤立的片段。

許多知識庫管道會將 Wikipedia 文章儲存為段落區塊。這對於嵌入和索引非常有效,但卻會破壞擷取:使用者查詢通常會返回分散的段落,迫使您手動將文章分類並重組。有了 Structs 陣列和 MAX_SIM,我們就可以重新設計儲存模式,讓每篇文章成為一列,Milvus 就可以原生排序並傳回整份文件。

在接下來的步驟中,我們將展示如何

  1. 載入並預先處理維基百科的段落資料

  2. 將屬於同一篇文章的所有段落綑綁成結構陣列

  3. 將這些結構化文件插入 Milvus

  4. 執行 MAX_SIM 查詢以擷取完整的文章--乾淨、無需重整或重新排序

本教學結束時,您將擁有一個工作管道,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:組合和轉換資料

在這個示範中,我們使用Simple Wikipedia 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 資料集

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}")

比較輸出:傳統檢索 vs. 結構陣列

當我們檢視資料庫實際回傳的內容時,結構陣列的影響就很明顯了:

維度傳統方法結構陣列
資料庫輸出回傳前 100 個段落(冗餘度高)回傳前 10 個完整的文件- 乾淨且精確
應用程式邏輯需要分組、重複資料刪除和重新排序(複雜)不需要後處理 - 實體層級的結果直接來自 Milvus

在維基百科的範例中,我們只展示了最簡單的情況:將段落向量結合為統一的文件表示。但 Array of Structs 的真正優勢在於它可以通用於任何多向量資料模型 - 包括傳統的檢索管道和現代的人工智能架構。

傳統多向量檢索情境

許多成熟的搜尋和推薦系統自然會在具有多個相關向量的實體上運作。Array of Structs 可輕鬆對應這些使用個案:

使用情境資料模型每個實體的向量
🛍️電子商務產品一個產品 → 多個影像5-20
🎬影片搜尋一個視訊 → 多個片段20-100
📖紙張檢索一份論文 → 多個部分5-15

AI 模型工作量(關鍵多向量使用個案)

在現代的 AI 模型中,Structs 的陣列變得更加重要,這些模型會刻意為每個實體產生大量向量集,以進行細粒度的語意推理。

模型資料模型每個實體的向量應用程式
ColBERT一個文件 → 許多標記嵌入100-500法律文本、學術論文、細粒度文件檢索
詞庫一個 PDF 頁面 → 許多修補嵌入256-1024財務報告、合約、發票、多模式文件搜尋

這些模式需要多向量的儲存模式。在使用 Array of Structs 之前,開發人員必須跨行分割向量,並將結果手動拼接回來。有了 Milvus,這些實體現在可以原生儲存與擷取,並由 MAX_SIM 自動處理文件層級的評分。

ColPali是一個強大的跨模式 PDF 檢索模型。它不依賴文字,而是將每個 PDF 頁面視為一張圖片來處理,並將其切割成多達 1024 個可視化斑塊,每個斑塊產生一個嵌入。在傳統的資料庫模式下,這需要將單一頁面儲存為數百或數千個獨立的行,使得資料庫無法理解這些行屬於同一頁面。因此,實體層級的搜尋變得支離破碎且不切實際。

Array of Structs 將所有的 patch embeddings 儲存在單一欄位中,讓 Milvus 可以將頁面視為一個有凝聚力的多向量實體,乾淨地解決了這個問題。

傳統的 PDF 搜尋通常依賴OCR,將頁面影像轉換成文字。這種方式適用於純文字,但會遺失圖表、表格、排版和其他視覺提示。ColPali 可直接處理頁面影像,保留所有視覺與文字資訊,從而避免此限制。取捨是規模:現在每一頁面都包含數百個向量,這就需要一個資料庫能夠將許多嵌入聚合為一個實體 - 這正是 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 Collection

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 會自動處理所有的聚合。

結論

大多數向量資料庫將每個片段儲存為獨立的記錄,這表示應用程式在需要完整的文件、產品或頁面時,必須重新組合這些片段。Structs 陣列改變了這種情況。透過將標量、向量、文字和其他欄位結合為單一結構化物件,它允許一條資料庫行代表一個完整的端對端實體。

其結果是簡單但強大的:以往需要在應用程式層中進行複雜的群組、遞減和重新排序的工作,現在都變成了本機資料庫功能。這正是向量資料庫的未來發展方向 - 更豐富的結構、更聰明的擷取和更簡單的管道。

如需更多關於 Array of Structs 和 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

    繼續閱讀