從檢索到結構化結果:Milvus 3.0 中的聚合與 ORDER BY

  • Engineering
August 07, 2026
Chun Han

試想一個熟悉的產品搜尋流程。購物者上傳一張洋裝照片,向量搜尋會從包含數千萬件商品的型錄中擷取出相關的候選集合。

然而,頁面需要的不只是一個排序清單。它需要品牌篩選面向。它需要價格排序。商品營運團隊想知道哪些品牌主導了這個結果集、每個品牌內的價格範圍,以及每個群組中的幾個代表性商品。

在 Milvus 3.0 之前,應用程式通常自行處理第二個步驟:從 Milvus 擷取資料列,在 pandas 或服務層中分組並排序,然後組裝回應。有些團隊維護一條獨立的分析管線,只為了計算已經存在於向量資料庫中的資料的計數與分布。

向量資料庫找到了候選項;應用程式必須將它們轉換成結構化結果。

Milvus 3.0 將更多這類工作移入檢索引擎。它新增了三項相關但不同的能力:

  • 查詢聚合可在經篩選、可見的資料列上計算 countsumavgminmax,並可選擇使用 GROUP BY 欄位。
  • 搜尋聚合會將保留下來的近似最近鄰(ANN)候選項組織成 bucket,計算每個 bucket 的指標,建立巢狀 bucket,並回傳代表性命中結果。
  • 伺服器端 **ORDER BY** 會在應用程式收到之前,依據一個或多個純量欄位排序查詢結果或 ANN 候選項。

查詢與搜尋之間的區別很重要:

能力被彙總或排序的資料主要結果形狀精確性邊界
查詢聚合所有符合篩選條件的可見資料列每個群組一列,並包含聚合值對查詢的可見資料列集合而言是精確的
搜尋聚合由 ANN 搜尋與分組階段保留下來的候選項Bucket、指標、代表性命中結果,以及選用的子 bucket設計上即為近似
查詢 ORDER BY符合篩選條件的可見資料列已排序資料列對經篩選的查詢結果而言是精確的
搜尋 ORDER BYANN 候選項已排序的搜尋命中結果或群組不會擴大 ANN 的召回邊界

本文說明為什麼這些操作應該屬於資料庫內部、分散式聚合如何運作、搜尋聚合與分組搜尋有何不同,以及新語義的邊界在哪裡。

為什麼應用程式端後處理會失效

將聚合與排序移到應用程式端,看起來可能只是個小小的實作選擇。但在大規模情境下,它會造成三個更大的問題。

應用程式搬移的資料遠多於答案本身

假設一個營運儀表板需要針對兩百萬筆有庫存資料列中的每個類別,取得產品數量與平均價格。即使每列只粗略估算 100 位元組的負載,用於類別、價格、主鍵與序列化開銷,應用程式仍必須在能計算結果之前接收約 200 MB 的資料。

如果型錄有 200 個類別,答案只會是幾百個鍵與數字——大約是 KB 等級。應用程式搬移的資料比它回傳的結果多出好幾個數量級,每次刷新都支付同樣成本,並且需要足夠的用戶端記憶體來保存或串流中間資料列。

引擎內聚合改變了資料移動的單位。原始資料列留在原處。跨節點傳輸並最終離開 Milvus 的,是小得多的部分與最終群組狀態集合。

頁面內排序不是全域排序

分頁之後再排序是正確性錯誤,而不只是效率不佳的實作。

如果應用程式擷取第 11 到第 20 筆資料列,並只對這些資料列按價格排序,它產生的是該頁面內的價格順序,而不是全域按價格排序結果中的第 11 到第 20 筆。後續頁面可能包含比第一頁每件商品都更便宜的商品。

同樣的邊界也適用於向量搜尋。擷取一個較小的 Top-K 集合並在應用程式中排序,只能重新排序那些候選項。它無法找回 ANN 階段沒有回傳的相關候選項,而且往往導致應用程式過度擷取,只為了讓用戶端排序有用。

伺服器端排序讓 Milvus 控制排序與分頁序列。對查詢工作負載而言,引擎會先排序經篩選的資料列集合,再套用頁面視窗。對搜尋工作負載而言,它會在 ANN 候選邊界內排序,並明確保留該限制。

用戶端無法重現資料庫可見性

聚合也取決於哪些資料列在查詢時間戳下可見。刪除、過期實體與並行寫入,都由 Milvus 的多版本並行控制(MVCC)與一致性語義管理。

一旦原始資料列離開資料庫,應用程式通常會假設收到的批次代表正確快照。在用戶端重建相同的可見性規則並不切實際,尤其是在集合仍持續接收寫入與刪除時。

常見的權宜作法——透過匯出與 ETL 餵給第二個分析引擎——會增加另一份資料副本、另一個一致性邊界,以及另一條需要維運的管線。計數、指標與排序應該在資料與其可見性規則已經存在的地方執行。

現在,讓我們看看 Milvus 3.0 提供了什麼。

查詢聚合:針對可見資料列的精確統計

查詢聚合可回答下列問題:

  • 每個類別中有多少有庫存商品?
  • 每個品牌的平均價格是多少?
  • 每台主機的事件時間戳最小值與最大值是多少?
  • 套用篩選條件與 TTL 可見性後,還剩下多少筆紀錄?

這個 API 對任何使用過 SQL 的人來說都很熟悉:在 group_by_fields 中傳入一個或多個欄位,然後在 output_fields 中放入聚合運算式。

res = client.query(
    collection_name="products",
    filter='status == "on_sale"',
    group_by_fields=["category"],
    output_fields=["category", "count(*)", "avg(price)"],
)

# [ # {"category": "books", "count(*)": 18734, "avg(price)": 45.3}, # … # ]

語法是簡單的部分。真正讓結果有用的是執行模型,尤其是在分散式向量資料庫中。

Segment 本地狀態取代原始資料列移動

一個 Milvus collection 可以跨越分布在多個查詢節點上的數百或數千個 segment,最近寫入的資料仍在串流路徑上。沒有任何單一執行節點一開始就擁有所有可見資料列。

因此,Milvus 會將聚合下推到 segment:

  1. 每個 segment 在本地套用篩選條件與 MVCC 可見性規則。
  2. Segment 會為每個群組發出一個部分狀態,而不是發出符合條件的資料列。
  3. 部分狀態會在查詢節點內合併。
  4. Proxy 會執行最終的跨節點合併,並回傳完成的群組。

現在,中間資料量會隨群組數量與聚合狀態數量擴展,而不是直接隨符合條件的資料列數量擴展。

合併操作取決於聚合函式:

聚合部分狀態合併規則
count部分計數加總計數
sum部分總和加總總和
min部分最小值取最小值
max部分最大值取最大值
avg部分總和與計數加總兩種狀態,然後只在最終階段除一次

avg 是最具啟發性的案例。當分割區包含不同數量的資料列時,將兩個部分平均值再平均是錯誤的。Milvus 會分別攜帶 sumcount,並且只在兩者都已全域合併後才計算最終平均值。

這也是聚合應該屬於資料庫內部的原因之一:這個操作不只是「在幾個批次上執行同一個函式」。引擎必須在 segment 與節點邊界之間保留每種聚合的代數性質。

可見性會在聚合之前套用

已刪除與過期的資料列會根據查詢的可見性邊界,在 segment 層級從部分狀態中移除。它們不會向上傳遞後再由應用程式修正。

因此,結果描述的是 Milvus 認為對該請求可見的資料列,而不是在略有不同時間拉取的任意批次集合。

limit 現在計算的是群組數

在一般查詢中,limit 控制回傳多少實體資料列。在分組查詢中,它控制回傳多少群組。由於結果基數由群組而不是符合條件的資料列決定,因此查詢聚合在需要每個群組時,也可以省略 limit

這聽起來像是一個小小的 API 細節,但它反映了不同的結果模型:輸出不再是一頁實體。它是一個其資料列代表群組的關聯。

搜尋聚合:ANN 候選項的 bucket 化視圖

查詢聚合回答的是:「符合這個篩選條件的可見資料列長什麼樣?」搜尋聚合問的是另一個問題:「為這個向量擷取出的候選集合長什麼樣?」

這個操作沒有精確的 SQL 對等物。ANN 搜尋會先建立由相似度驅動的候選邊界。接著 Milvus 會依純量鍵組織保留下來的候選項,並回傳 bucket 樹,而不是一般的扁平命中清單。

一個 bucket 可以包含:

  • brand 這樣的鍵,或像 (brand, color) 這樣的複合鍵;
  • 保留候選項計數;
  • 包含 countsumavgminmax 的指標;
  • top_hits 選出的代表性實體;以及
  • 建立子 bucket 的巢狀 sub_aggregation

對於產品搜尋頁面,一個請求就能回傳品牌 bucket、每個 bucket 內的平均價格,以及每個品牌的三個代表性商品:

from pymilvus import SearchAggregation, TopHits

aggregation = SearchAggregation( fields=[“brand”], size=10, # Return up to 10 brand buckets metrics={“avg_price”: {“avg”: “price”}}, order=[{“_count”: “desc”}], # Order by retained-candidate count top_hits=TopHits( size=3, sort=[{“_score”: “desc”}], # Use “asc” for L2 distance ), )

res = client.search( collection_name=“products”, data=[query_vector], anns_field=“embedding”, search_aggregation=aggregation, output_fields=[“title”, “brand”, “price”], )

buckets = res.agg_buckets[0]

設定 search_aggregation 時,一般命中清單會是空的。應用程式會從 result.agg_buckets 讀取 bucket 回應。

聚合規格設定了兩種不同的邊界

搜尋聚合不會在 collection 中每個實體上執行 GROUP BY,也不是單純拿一般 Top-K 回應的扁平清單來聚合。

它的執行包含三個階段:

  1. Milvus 執行 ANN 搜尋,以擷取接近查詢向量的候選項。
  2. 分組階段會為每個完整 bucket key 保留有界數量的候選項。
  3. Milvus 建立 bucket、針對保留候選項計算指標、排序 bucket,並附加代表性命中結果或子 bucket。

兩個參數控制結果的不同部分:

  • SearchAggregation.size 限制該聚合層級回傳多少個 bucket。
  • 聚合樹中任何位置最大的 TopHits.size,會設定每個完整複合鍵的保留候選項預算。如果請求不包含 top_hits,則每個鍵的預設預算為一。

頂層搜尋 limit 不控制此模式,並且在存在 search_aggregation 時會被忽略。

在解讀 bucket 的 count 或指標時,這個區別至關重要。使用 TopHits(size=3) 時,即使 collection 中包含數千個來自該品牌的相關商品,一個品牌 bucket 最多也只能彙總其完整鍵的三個保留候選項。增加 TopHits.size 會擴大每個鍵的指標視窗,但不會把 ANN 搜尋變成精確掃描。

如果應用程式需要對符合篩選條件的每個可見資料列取得精確統計,應使用查詢聚合。搜尋聚合用於描述與比較由相似度檢索產生的候選項。

搜尋聚合與分組搜尋解決不同問題

Milvus 自 Milvus 2.4 起支援分組搜尋(group_by)。看到兩個功能都有「grouping」這個詞,很容易以為它們是同一個操作的兩種介面。它們的輸出契約不同。

分組搜尋會改變哪些實體出現在排序結果清單中。常見的 RAG 模式會將 chunk 儲存為個別實體,依 doc_id 分組,並從每份文件回傳一個或幾個 chunk。主要輸出仍是一般搜尋命中結果,但分組欄位的重複值較少。

搜尋聚合會回傳統計視圖。主要輸出是一棵 bucket 樹,包含鍵、計數、指標、代表性命中結果,以及選用的子 bucket。

應用程式需求建議使用消費內容
在某個欄位上具有更高多樣性的排序實體清單分組搜尋一般搜尋命中結果
面向計數、每群組指標、代表性命中結果,或巢狀分布搜尋聚合result.agg_buckets 中的 AggregationBucket 物件

一個實用規則是從 UI 或 API 回應形狀開始思考。如果應用程式呈現的是清單,分組搜尋通常是正確的原語。如果它呈現的是篩選面向、分布卡片或群組階層,則使用搜尋聚合。

這兩種模式在同一個請求中互斥,因為它們定義了不同的主要結果形狀。

ORDER BY:將排序移到應用程式邊界之前

排序是此版本中最不新奇的功能,卻也是在引擎外最容易實作錯誤的功能之一。

Milvus 3.0 在查詢與搜尋上都暴露排序能力,但兩條路徑使用不同的 SDK 參數,並作用於不同的輸入集合。

查詢排序會排序經篩選的資料列集合

PyMilvus 查詢使用 order_by,以 "field:direction" 字串清單表示。引擎會套用篩選條件、排序可見資料列,然後再套用 limitoffset

res = client.query(
    collection_name="products",
    filter='category == "books"',
    output_fields=["title", "price"],
    order_by=["price:desc", "title:asc"],
    limit=10,
    offset=10,  # Rows 11-20 in the filtered, price-sorted result
)

這讓查詢可用於依業務順序瀏覽:最新匯入的紀錄、某個篩選條件內最高價的商品、最低庫存,或用於資料檢查的極值。若沒有伺服器端排序,應用程式必須先擷取資料列,且無法跨頁定義可靠的業務順序。

對於可為 null 的查詢欄位,升冪會將 null 放在最後,降冪會將 null 放在最前。排序欄位不一定要出現在 output_fields 中;只有當應用程式需要在回應中取得該值時才包含它。

搜尋排序會重新排序 ANN 候選集合

PyMilvus 搜尋使用 order_by_fields,其中每個項目會指定一個純量欄位與方向:

res = client.search(
    collection_name="products",
    data=[query_vector],
    anns_field="embedding",
    limit=50,
    output_fields=["title", "price"],
    order_by_fields=[
        {"field": "price", "order": "asc"},
    ],
)

ANN 仍然決定哪些實體會成為候選項。order_by_fields 會改變這些候選項的回傳方式;它不會讓搜尋全域掃描 collection 以尋找最便宜的商品。

這個邊界賦予兩個 API 不同的工作:

  • 當純量順序本身定義結果時,例如十個最便宜的有庫存商品,請使用查詢加上 order_by
  • 當語意或向量相關性定義候選集合,而純量欄位決定如何呈現這些候選項時,請使用搜尋加上 order_by_fields

多欄位排序會依清單順序套用鍵。當搜尋候選項在每個指定純量鍵上的值都相同時,Milvus 會保留它們原本的相似度分數順序。

排序也可與分組搜尋組合使用。Milvus 會依每個群組頂端實體的已設定純量值排序群組,同時保留分組結果形狀。當應用程式同時需要跨欄位的多樣性與符合業務需求的群組順序時,這很有用。

這些能力帶來的可能性

這些 API 是通用的資料庫原語,但有幾種檢索工作負載會立即受益。

RAG 與代理:檢視檢索集中度

RAG 或代理式系統可以依來源文件、產品線、租戶或內容類型,將擷取到的 chunk 分桶。結果集中在兩份文件中,與分散在數十個來源中的結果相比,代表不同的覆蓋訊號。

這種分布並不保證答案品質。不過,它是一個有用的檢索診斷訊號,應用程式或代理在決定是否擴展查詢、再次檢索或要求澄清時,可以將它與分數、引用和其他檢查結合使用。

當目標只是讓回傳的 chunk 更具多樣性時,分組搜尋仍是正確選擇。當系統需要分布本身時,搜尋聚合很有用。

開頭的產品搜尋頁面可以從 Milvus 收到品牌 bucket、價格指標、代表性商品,以及按純量排序的候選清單。應用程式仍控制呈現與業務邏輯,但不再需要從匯出的命中結果中重建基本 bucket 語義。

日誌與安全:結合相似度與事件分布

相似度搜尋可以找出與可疑日誌行相關的事件。搜尋聚合接著可以顯示哪些主機主導這些候選項、每個主機 bucket 中的最小與最大時間戳,或候選項如何依嚴重性與服務分布。

結果仍然是擷取候選項的視圖,而不是精確的全域事件計數。當調查需要對符合篩選條件的每個事件取得精確計數時,查詢聚合提供了第二條路徑。

營運與資料探索:計算而非匯出

儀表板與管理工具可以針對經篩選的資料列執行精確計數與平均值,然後以明確的純量順序瀏覽底層實體。這移除了許多一次性的「匯出、計算、排序」工具,同時不假裝 Milvus 已成為完整的分析資料庫。

邊界:聚合與 ORDER BY 不會取代什麼

這些功能擴展了檢索引擎;它們不會把 Milvus 變成線上分析處理(OLAP)系統。

  • 查詢聚合支援分組以及 countsumavgminmax。它不會新增 join、window function 或複雜子查詢。大型離線分析工作仍適合放在 Spark 等系統中,這些系統可以搭配 Milvus 3.0 快照與共享儲存路徑運作。
  • 查詢群組鍵支援整數、VARCHARTIMESTAMPTZ 欄位。搜尋聚合 bucket key 額外支援布林欄位。浮點、向量、JSON 和陣列值不能作為 bucket key。
  • 對於搜尋聚合,count 接受 "*" 或非 JSON、非動態來源;sumavg 需要數值來源;minmax 也支援字串與 TIMESTAMPTZ 來源。查詢聚合遵循相同的算術型別邊界。在將聚合套用到複雜欄位型別之前,請查閱 API 指南。
  • 查詢聚合可以依群組鍵排序分組輸出,而依計算出的聚合值(例如 count(*))排序仍是目前的邊界。若未明確指定排序,群組順序不保證。
  • 搜尋聚合目前不能在同一個請求中與 Hybrid Search、Grouping Search、Search Iterators、非零 offset 或 highlighting 組合使用。
  • 搜尋聚合的計數與指標描述的是保留下來的 ANN 候選項,而不是完整 collection,也不是每個可能在語意上相關的實體。
  • 搜尋 ORDER BY 會改變候選項呈現方式。它不會修復 ANN 遺漏的候選項,也不會將相似度檢索轉換成精確的純量 Top-N 查詢。

在新的原語之間做選擇,最清楚的方法是從問題本身開始:

  • 若要對經篩選的可見資料列取得精確統計,使用查詢聚合。
  • 若要取得相似度檢索候選項的分布,使用搜尋聚合。
  • 若要取得多樣化的排序清單,使用分組搜尋。
  • 若要取得明確的純量順序,依據是哪條路徑建立結果集,使用查詢或搜尋 ORDER BY

從候選清單到結構化結果

向量資料庫傳統上一直優化一個問題:哪 K 個實體最接近這個向量?

生產環境中的檢索系統會立即提出後續問題。哪些群組主導了結果?它們的計數與範圍是多少?哪些範例代表每個群組?應用程式應以什麼業務順序呈現資料列或候選項?

Milvus 3.0 將這些操作帶入同一個擁有資料、ANN 候選邊界與可見性語義的引擎。查詢聚合會對可見資料列執行精確的分散式歸約。搜尋聚合會在保留下來的 ANN 候選項上建立 bucket 化視圖。ORDER BY 讓查詢與搜尋路徑具備伺服器端純量順序,不需要應用程式逐頁重建。

這個結果不是藏在向量資料庫裡的 OLAP 引擎。它是一個能回傳更多應用程式實際需要之結構的檢索引擎。

在 Milvus 3.0 中試用聚合與 ORDER BY

Milvus 3.0 現已推出。請使用 Query guide 了解精確聚合與查詢排序,使用 Search Aggregation guide 了解 bucket 語義與限制,使用 Basic Vector Search guide 了解搜尋排序;當你的主要目標是結果多樣性時,請使用 Grouping Search guide

若要了解更廣泛的版本發布內容,請參閱 Milvus 3.0 launch blogMilvus 3.0 release notes,以及 milvus-io/milvus repository

如果你想在不自行維運叢集的情況下評估相同 API,可以在 Zilliz Cloud 上試用。最新的 Zilliz Cloud query referencesearch reference 說明了受管叢集類型的可用性與參數。

若要與團隊討論工作負載或邊界案例,請加入 Milvus Discord community,或預約 Milvus Office Hours session

    Try Managed Milvus for Free

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

    Get Started

    Like the article? Spread the word

    繼續閱讀