从检索到结构化结果:Milvus 3.0 中的聚合和 ORDER BY

  • Engineering
August 07, 2026
Chun Han

考虑一个熟悉的产品搜索流程。购物者上传一张连衣裙照片,向量搜索会从包含数千万件商品的目录中检索出一组相关候选结果。

然而,页面需要的不仅仅是一个排序列表。它需要品牌分面。它需要按价格排序。商品运营团队想知道哪些品牌在这组结果中占主导地位、每个品牌内的价格范围,以及每个分组中的几个代表性商品。

在 Milvus 3.0 之前,应用程序通常自行处理第二步:从 Milvus 获取行,在 pandas 或服务层中对其分组和排序,然后组装响应。一些团队维护了单独的分析流水线,只为计算已存在于向量数据库中的数据的计数和分布。

向量数据库找到了候选项;应用程序则必须把它们转换成结构化结果。

Milvus 3.0 将更多这类工作移入检索引擎。它新增了三项相关但不同的能力:

  • 查询聚合在经过过滤且可见的行上计算 countsumavgminmax,并可选使用 GROUP BY 字段。
  • Search Aggregation 将保留下来的近似最近邻(ANN)候选项组织到桶中,计算每个桶的指标,构建嵌套桶,并返回代表性命中结果。
  • 服务端 **ORDER BY** 在应用程序接收结果之前,按一个或多个标量字段对查询结果或 ANN 候选项进行排序。

查询和搜索之间的区别很重要:

能力被汇总或排序的数据主要结果形态精确性边界
查询聚合匹配过滤条件的所有可见行每个分组一行,包含聚合值在查询的可见行集合上精确
Search Aggregation由 ANN 搜索和分组阶段保留的候选项桶、指标、代表性命中结果,以及可选的子桶设计上就是近似的
查询 ORDER BY匹配过滤条件的可见行排序后的行在过滤后的查询结果上精确
搜索 ORDER BYANN 候选项排序后的搜索命中结果或分组不会扩大 ANN 召回边界

本文将解释为什么这些操作应属于数据库内部,分布式聚合如何工作,Search Aggregation 与 Grouping Search 有何不同,以及新语义的边界在哪里。

为什么应用侧后处理会失效

将聚合和排序移到应用程序中,看起来可能只是一个小的实现选择。在规模化场景下,它会造成三个更大的问题。

应用程序移动的数据远多于答案本身包含的数据

假设一个运营仪表板需要在两百万条有库存行中,统计每个类别的产品数量和平均价格。即使每行仅按 100 字节的粗略负载来计算,用于类别、价格、主键和序列化开销,应用程序也必须先接收约 200 MB 数据,才能计算结果。

如果目录有 200 个类别,答案只是几百个键和数字——大约只有 KB 级。应用程序移动的数据比它返回的数据多出几个数量级,每次刷新都要支付同样的成本,并且需要足够的客户端内存来保存或流式处理这些中间行。

引擎内聚合改变了数据移动的单位。原始行留在原处。跨节点传输并最终离开 Milvus 的,是小得多的部分分组状态和最终分组状态集合。

页面内排序不是全局排序

分页之后再排序是正确性缺陷,而不仅仅是低效实现。

如果应用程序获取第 11 到第 20 行,并仅按价格对这些行排序,它得到的是该页面内部的价格顺序,而不是全局按价格排序结果中的第 11 到第 20 行。后续页面可能包含比第一页中所有产品都更便宜的商品。

同样的边界也适用于向量搜索。获取一个较小的 Top-K 集合并在应用程序中排序,只能重新排序这些候选项。它无法找回 ANN 阶段未返回的相关候选项,并且常常导致应用程序为了让客户端排序有意义而过度获取。

服务端排序让 Milvus 控制排序和分页顺序。对于查询工作负载,引擎在应用页面窗口之前先对过滤后的行集合排序。对于搜索工作负载,它在 ANN 候选边界内排序,并明确保留这一限制。

客户端无法复现数据库可见性

聚合还取决于查询时间戳下哪些行是可见的。删除、过期实体和并发写入都由 Milvus 的多版本并发控制(MVCC)和一致性语义管理。

一旦原始行离开数据库,应用程序通常会假设收到的批次代表正确快照。在客户端重建相同的可见性规则并不现实,尤其是在 collection 正在接收写入和删除时。

常见的变通方案是使用由导出和 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 细节,但它反映的是一种不同的结果模型:输出不再是一页实体。它是一个行代表分组的关系。

Search Aggregation:ANN 候选项的桶视图

查询聚合回答的是:“匹配此过滤条件的可见行是什么样的?”Search Aggregation 问的是另一个问题:“为这个向量检索到的候选集是什么样的?”

这个操作没有精确的 SQL 等价物。ANN 搜索首先建立一个由相似度驱动的候选边界。然后,Milvus 按标量键组织保留下来的候选项,并返回一个桶树,而不是普通的扁平命中列表。

一个桶可以包含:

  • 一个键,例如 brand,或一个复合键,例如 (brand, color)
  • 保留候选项计数;
  • 包括 countsumavgminmax 在内的指标;
  • 通过 top_hits 选择的代表性实体;以及
  • 用于创建子桶的嵌套 sub_aggregation

对于产品搜索页面,一个请求即可返回品牌桶、每个桶内的平均价格,以及每个品牌的三个代表性商品:

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 读取桶响应。

聚合规范设置了两个不同的边界

Search Aggregation 不会对 collection 中的每个实体运行 GROUP BY,也不是简单地拿一个普通 Top-K 响应并聚合那个扁平列表。

它的执行有三个阶段:

  1. Milvus 运行 ANN 搜索,以检索靠近查询向量的候选项。
  2. 分组阶段为每个完整桶键保留有界数量的候选项。
  3. Milvus 构建桶,基于保留候选项计算指标,对桶排序,并附加代表性命中结果或子桶。

两个参数控制结果的不同部分:

  • SearchAggregation.size 限制该聚合层级返回多少个桶。
  • 聚合树中任意位置最大的 TopHits.size 会设置每个完整复合键的保留候选预算。如果请求不包含 top_hits,则每个键的预算默认为一。

顶层搜索 limit 不控制此模式,并且在存在 search_aggregation 时会被忽略。

在阅读桶的 count 或指标时,这一区别至关重要。使用 TopHits(size=3) 时,一个品牌桶最多只能汇总其完整键下三个保留候选项,即使 collection 中包含数千个来自该品牌的相关产品。增大 TopHits.size 会扩大每个键的指标窗口,但它不会把 ANN 搜索变成精确扫描。

如果应用程序需要对匹配过滤条件的每个可见行进行精确统计,应使用查询聚合。Search Aggregation 用于描述和比较由相似度检索产生的候选项。

Search Aggregation 和 Grouping Search 解决的是不同问题

Milvus 自 Milvus 2.4 起就支持 Grouping Search(group_by)。很容易因为两个功能中都有“grouping”这个词,就认为它们是同一操作的两个接口。它们的输出契约不同。

Grouping Search 改变的是哪些实体出现在排序结果列表中。一个常见的 RAG 模式是将 chunk 存储为单独实体,按 doc_id 对它们分组,并从每个文档返回一个或几个 chunk。主要输出仍然是普通搜索命中结果,只是来自分组字段的重复值更少。

Search Aggregation 返回的是统计视图。主要输出是一个包含键、计数、指标、代表性命中结果和可选子桶的桶树。

应用需求优先选择消费对象
一个在某个字段上具有更大多样性的排序实体列表Grouping Search普通搜索命中结果
分面计数、每组指标、代表性命中结果或嵌套分布Search Aggregationresult.agg_buckets 中的 AggregationBucket 对象

一个实用规则是从 UI 或 API 响应形态出发。如果应用程序渲染的是列表,Grouping Search 通常是正确的原语。如果它渲染的是分面、分布卡片或分组层级结构,则使用 Search Aggregation。

这两种模式在一个请求中互斥,因为它们定义了不同的主要结果形态。

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 放在最前。排序字段不必出现在 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 会保留它们原始的相似度分数顺序。

排序也可以与 Grouping Search 组合。Milvus 会按每个分组顶部实体的配置标量值对分组排序,同时保留分组结果形态。当应用程序既希望在某个字段上具有多样性,又希望分组按业务相关顺序排列时,这很有用。

这些能力带来了什么可能

这些 API 是通用数据库原语,但有几类检索工作负载会立即受益。

RAG 和 agents:检查检索集中度

RAG 或 agentic 系统可以按源文档、产品线、租户或内容类型对检索到的 chunk 分桶。集中在两个文档中的结果,与分散在数十个来源中的结果相比,传递的是不同的覆盖率信号。

这种分布并不是答案质量的保证。不过,它是一个有用的检索诊断信号,应用程序或 agent 可以在决定是否扩展查询、再次检索或要求澄清时,将其与分数、引用和其他检查结合起来。

当目标只是让返回的 chunk 更加多样化时,Grouping Search 仍然是正确选择。当系统需要分布本身时,Search Aggregation 很有用。

开头的产品搜索页面可以从 Milvus 接收品牌桶、价格指标、代表性商品,以及按标量排序的候选列表。应用程序仍然控制展示和业务逻辑,但不再需要从导出的命中结果中重建基础桶语义。

日志与安全:将相似度与事件分布结合

相似度搜索可以找到与可疑日志行相关的事件。Search Aggregation 随后可以显示哪些主机在这些候选项中占主导地位、每个主机桶中的最小和最大时间戳,或候选项如何按严重级别和服务划分。

结果仍然是检索候选项的视图,而不是精确的全局事件计数。当调查需要对匹配过滤条件的每个事件进行精确计数时,查询聚合提供了第二条路径。

操作与数据探索:计算而不是导出

仪表板和管理工具可以对过滤后的行运行精确计数和平均值,然后按定义好的标量顺序浏览底层实体。这消除了许多一次性的“导出、计算和排序”工具,同时也不会假装 Milvus 已经变成完整的分析数据库。

边界:聚合和 ORDER BY 不能替代什么

这些功能扩展了检索引擎;它们并不会把 Milvus 变成在线分析处理(OLAP)系统。

  • 查询聚合支持分组以及 countsumavgminmax。它不会新增 join、窗口函数或复杂子查询。大型离线分析任务仍然属于 Spark 等系统,这些系统可以使用 Milvus 3.0 快照和共享存储路径。
  • 查询分组键支持整数、VARCHARTIMESTAMPTZ 字段。Search Aggregation 桶键还额外支持布尔字段。浮点、向量、JSON 和数组值不能作为桶键。
  • 对于 Search Aggregation,count 接受 "*" 或非 JSON、非 dynamic 来源;sumavg 需要数值来源;minmax 也支持字符串和 TIMESTAMPTZ 来源。查询聚合遵循相同的算术类型边界。在对复杂字段类型应用聚合之前,请查阅 API 指南。
  • 查询聚合可以按分组键对分组输出排序,而按计算聚合值(例如 count(*))排序仍然是当前边界。没有显式排序时,分组顺序不保证。
  • Search Aggregation 目前不能在同一个请求中与 Hybrid Search、Grouping Search、Search Iterators、非零 offset 或高亮组合使用。
  • Search Aggregation 的计数和指标描述的是保留下来的 ANN 候选项,而不是完整 collection,也不是每个可能语义相关的实体。
  • 搜索 ORDER BY 改变的是候选项呈现方式。它不会修复遗漏的 ANN 候选项,也不会将相似度检索转换成精确的标量 Top-N 查询。

在这些新原语之间做选择,最清晰的方法是从问题本身出发:

  • 对于过滤后的可见行上的精确统计,使用查询聚合。
  • 对于相似度检索候选项上的分布,使用 Search Aggregation。
  • 对于多样化的排序列表,使用 Grouping Search。
  • 对于定义好的标量顺序,根据哪条路径建立了结果集,使用查询或搜索 ORDER BY

从候选列表到结构化结果

向量数据库传统上优化的是一个问题:哪 K 个实体离这个向量最近?

生产检索系统会立即提出后续问题。哪些分组主导了结果?它们的计数和范围是多少?哪些示例能代表每个分组?应用程序应以什么业务顺序呈现这些行或候选项?

Milvus 3.0 将这些操作带入同一个拥有数据、ANN 候选边界和可见性语义的引擎中。查询聚合对可见行执行精确的分布式规约。Search Aggregation 在保留的 ANN 候选项上构建桶视图。ORDER BY 为查询和搜索路径提供服务端标量顺序,而无需应用程序逐页重建它。

结果并不是隐藏在向量数据库中的 OLAP 引擎。它是一个能够返回应用程序实际需要的更多结构的检索引擎。

在 Milvus 3.0 中试用聚合和 ORDER BY

Milvus 3.0 现已可用。使用 Query 指南了解精确聚合和查询排序,使用 Search Aggregation 指南了解桶语义和限制,使用 基础向量搜索指南了解搜索排序,当你的主要目标是结果多样性时,使用 Grouping Search 指南

如需了解更广泛的版本发布内容,请参阅 Milvus 3.0 发布博客Milvus 3.0 发布说明,以及 milvus-io/milvus 仓库

如果你想在不自行运维集群的情况下评估同样的 API,可以在 Zilliz Cloud 上试用它们。当前的 Zilliz Cloud 查询参考搜索参考介绍了托管集群类型的可用性和参数。

如需与团队讨论工作负载或边界案例,请加入 Milvus Discord 社区,或预约 Milvus Office Hours 场次

    Try Managed Milvus for Free

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

    Get Started

    Like the article? Spread the word

    扩展阅读