← 回總覽

Milvus 3.0 开源解读之,数据库原生聚合排序如何取代应用侧 Pandas 胶水代码

📅 2026-08-10 18:30 Zilliz 软件编程 7 分鐘 8551 字 評分: 86
向量数据库 Milvus 聚合查询 分布式系统 排序
📌 一句话摘要 Milvus 3.0 将聚合、排序等计算下推至引擎内部,避免数据搬运和语义错误,提升向量检索后处理效率。 📝 详细摘要 文章指出,传统向量检索后常需在应用层用 Pandas 进行分组、统计、排序,导致大量数据搬运、语义错误和架构复杂度。Milvus 3.0 将这些能力下推至引擎:query 支持 GROUP BY 与聚合函数,search 支持分层桶聚合和 top_hits,query 与 search 均支持标量字段排序并正确与分页组合。在分布式环境中,Milvus 采用局部聚合+归并的方式,确保 avg 等不可直接归并的函数通过拆解为 sum/count 实现正确结果,

原创 韩淳 2026-08-10 18:30 上海

!Image 1

!Image 2: 图片

想象一个很普通的电商搜索页:用户上传一张连衣裙的照片,以图搜款。关于检索本身,向量数据库做得很好——五千万商品里取回最相似的一千条,典型配置下只需几十毫秒。

但产品真正要交付的,从来不只是这一千条结果。左侧有品牌筛选器;顶部可以让用户按价格、按销量、按评分排序;运营还想知道这次召回集中在哪些品牌、各自的价格水平和代表款。

于是系统里通常会出现第二段逻辑:工程师需要把检索结果拉回应用层,用 pandas 或业务代码做分组、计数、排序,再拼成最终响应。

久而久之,我们把这种架构当成了理所当然:向量数据库负责把东西找到,至于怎么把这些结果通过分组、统计、排序、分页变成业务真正需要的「答案」,由应用自己解决。

过去,在传统 SQL 数据库里,这些本来就是查询能力的一部分。到了向量数据库时代,却长期需要在应用侧二次开发。

Milvus 3.0 把这段逻辑收进了引擎:让query 和 search 都支持按标量字段排序,query 支持 GROUP BY 加聚合函数,search 的结果集可以直接做分层聚合分析。

接下来,这篇文章会重点讲清楚三件事:第一,为什么这部分逻辑应该进入数据库,而不是留在应用层;第二,Milvus 在分布式场景下,是如何让聚合和排序保持正确的语义;第三,这些能力的边界在哪里?

01

为什么后处理不应该长期留在应用层

在应用层做后处理,表面上只是「多写几行代码」,实际上藏着三个代价:数据搬运、语义正确性,以及额外的系统复杂度。

第一个代价是搬运,为了几 KB 的答案,搬几百MB的数据

还是那个商品库:运营想看「在售商品里各类目的数量和均价」,过滤条件命中两百万行。

如果应用层自己聚合,就必须先把这些数据取出来。哪怕每行只返回类目、价格和主键,加上序列化开销,一行也可能超过上百字节。两百万行,就是几百 MB 的网络传输和客户端内存。

但最后的答案有多大?

假设只有两百个类目,每个类目返回一个数量和一个均价,最终结果不过几 KB。

为了几 KB 的答案搬运几百 MB 的数据,中间差着五个数量级;而且报表每刷新一次,这笔成本就再付一次。

第二个代价是应用层很容易得到一个“看起来正确”的答案。

相比网络成本,语义错误更危险,因为它通常不会报错。最典型的例子就是分页加排序。

用户希望「按价格从低到高浏览检索结果」。如果应用每次只拿一页结果回来,然后对当前页排序,那么得到的只是这一页内部的价格顺序,而不是整个结果集的全局顺序。

于是翻到第二页,发现价格可能突然比第一页还低。

过去,不少系统的排序功能一直是有问题的,只是很少有人翻到第二页仔细验证。

类似的问题还包括可见性。

一条刚刚被删除的数据是否还应该参与统计?TTL 已经过期但物理数据尚未清理的记录要不要算?某个查询时刻,究竟哪些版本的数据对当前请求可见?

这些可见性语义只有数据库自己知道。在客户端算,等于默认「我拉到的就是对的」。而这个假设在一个持续写入、持续删除的系统里并不成立。

第三个代价是架构,一次 count,最后可能新增一套分析链路。

当应用层后处理越来越复杂,很多团队最后会走向同一种架构:向量数据库负责在线检索;数据定期导出,经 ETL 进入另一套分析系统;统计和报表在那边完成。

技术上当然可行,但多一份数据,就会多一条同步链路,多一种一致性问题,也多一个故障面。

这三个代价指向同一个结论:分组、统计、排序,如果计算可以在数据所在的位置完成,就不要先移动数据再移动计算。

02

Milvus 3.0如何在分布式系统中做聚合?

从 3.0 开始,这些能力成为 Milvus 的原生查询语义(接口上,pymilvus、RESTful v2、Go SDK 陆续覆盖):

Query聚合:query 支持 GROUP BY 与 count / sum / avg / min / max,像 SQL 一样直接返回统计结果;

Search 聚合:向量检索的结果可以直接做分桶统计、逐桶取样;

Order By:search 和 query 都支持按标量字段排序——多字段、升降序、明确的 null 语义,并且和分页正确组合。

当然,这些都是SQL 数据库几十年前就有的概念,但在一个分布式向量数据库里完成这些能力,其实并不容易。

Milvus 作为一个典型的分布式数据库,其一个 collection 的数据,会分散在成百上千个 segment 中,这些 Segment 又分布在不同的查询节点上。同时,一部分刚写入的数据可能仍然位于流式执行路径中。

这意味着 一个简单的GROUP BY category背后,并不存在一个把所有数据读出来然后算一下的单机执行环境。

但如果把所有数据汇到一个节点再算,那就回到了把数据搬出来的老路,只是搬运发生在数据库内部,但网络和内存成本依旧存在。

Milvus 采取的是典型的分布式聚合路径:尽可能把计算推到数据所在的位置。每个 Segment 先对自己的数据执行局部聚合,为每个分组生成部分聚合状态;同一个节点上的结果先做一次归并;最后由接入层 Proxy 汇总不同节点的部分结果,得到全局结果。

过程中,跨节点传输的不再是海量原始行,而是已经“压缩”过的聚合状态。

数据量由多少行数据,变成:多少个分组 × 每个分组的中间状态。

当然,这里还有一个容易被忽略的问题:这个方案成立的前提,是每个聚合函数的中间状态都可以正确归并。并不是所有聚合函数都可以直接“局部算一次,再把局部结果算一次”。

count 可以相加,sum 可以相加,min 和 max 也可以继续取最小值和最大值。但 avg 不行。两个部分平均值合并,不等于全局平均值。所以 avg 在引擎内部被拆成 sum 和 count 两个可归并的状态,一路归并到最后一层才相除。

这个细节看起来很底层,在数据库内也不是什么很高难度的操作,但它恰好解释了为什么这类逻辑应该由数据库来做:在应用层手写聚合的用户,大概率不会意识到「分批拉数据、分批算平均、最后合并」是错的。

同样的原则也适用于数据可见性。

Milvus 在 Segment 本地执行聚合时,就会同时应用 MVCC(多版本并发控制)的可见性规则。已经删除或过期、当前查询不应该看到的数据,会在最底层就被排除。

这种情况下,我们最终得到的不是客户端恰好拉到的那些数据的统计,而是:这个查询时刻,按照数据库一致性语义真正可见的数据的统计。

这层语义在客户端无法复现。

顺带一提,limit 在 query 聚合里的含义也变了:它限制的不再是返回多少行数据,而是返回多少个分组。聚合的结果规模由组数决定,通常远小于行数——这也是为什么普通 query 不带过滤条件时必须给 limit,而聚合查询可以豁免。

03

Search 聚合:给相似性检索一个分析视角

Query 聚合和 Search 聚合解决的是两类不同的问题。Query 聚合回答的是满足某个结构化条件的数据,整体是什么分布?。Search 聚合回答的是另一个问题:和这个查询最相似的这批数据,呈现出什么结构?

这不是传统 SQL 里最典型的问题。实际上,它是把语义检索与结构化分析两个步骤合到了一次执行里。

Milvus 3.0 的 search 聚合是分层的桶聚合:按字段分桶;桶内可以算指标(count / sum / avg / min / max);桶可以按数量、按键、按指标排序;桶内还可以嵌套下一层聚合,或者用 top_hits 取出每个桶里最有代表性的几条文档。这套表达能力 对标的是 Elasticsearch 这类成熟全文检索引擎的 aggregation 语义,在向量数据库产品里并不多见。

开头那个以图搜款页面就是一个典型例子。

一次向量检索之后,应用真正想知道的可能是:召回主要集中在哪些品牌?每个品牌有多少候选?这些品牌的平均价格是多少?每个品牌最相似的三件商品是什么?

过去,这意味着先把大量结果拉出来,再自行统计。现在,这些信息可以作为一次 Search 请求的组成部分直接返回。

RAG 也是一样。假设一次检索返回几十个 Chunk。单纯看 Top-K,我们只知道“有哪些片段相似”。但如果再按 doc_id 分桶,就会得到另一个维度的信息:结果到底高度集中在两三份文档里,还是分散在几十份来源中?

这里需要特别把统计口径说清楚:search 聚合不是「先取 top-N,再对它做全量统计」。在聚合规格中,用户会声明需要多少个桶(size)、每个桶保留多少条样本(top_hits.size)。引擎根据这套规格反推需要检索和保留多少候选,桶的计数和指标在这些样本上计算。

也因此,请求里的 limit 在聚合模式下不生效,检索规模完全由聚合规格决定;想让统计更接近全集,把桶数和每桶样本数调大即可。

04

它和 grouping search 是什么关系

熟悉 Milvus 的用户很容易产生另一个疑问:从 2.4 开始,Search 已经支持 group_by,也就是 Grouping Search。现在又增加 Search 聚合,两者是不是同一种能力?

它们都出现了“分组”,但作用在完全不同的层面。

Grouping search 改变的是「返回哪些结果」:按字段分组,每组只出最相似的一条或几条,返回的仍然是文档列表——经典用法是 RAG 里按 doc_id 分组,chunk 粒度检索、文档粒度返回,保证结果多样性。

Search 聚合返回的则是「结果的统计视图」:桶、计数、指标,文档本身退到 top_hits 里,作为每个桶的样本。

一个简单的判断标准:页面上要渲染的是列表,用 grouping search;要渲染的是筛选器计数、分布、统计卡片,用 search 聚合。

05

排序:看起来简单,做对不容易

这几个能力里,order by 看起来最不起眼,也正因为如此,它过去最容易被放到客户端自行解决,然后掉进上面说的分页陷阱。

但在分布式检索系统里,会排序和能得到正确的全局顺序是两件完全不同的事。

向量 Search 的原始结果天然按照相似度组织,而且候选本身来自多个节点。当用户要求按价格、时间或其他业务字段排序时,排序只有在全局结果完成归并之后才有意义。

当用户要求按价格排时,引擎在全局结果确定之后、分页窗口切出之前完成重排——offset 应用在排序之后,「第二页」才是全局意义上的第 11 到 20 名,而不是「相似度第二页内部再按价格排一遍」。这个顺序被固化在执行流程里,用户不再有机会做错。

排序一旦成为数据库能力,一些过去可以含糊处理的细节就必须定义清楚。

比如 null。Milvus 的默认规则与 PostgreSQL 一致:升序时 null 排在最后,降序时 null 排在最前。同时,也可以针对每个字段显式指定 nulls_first 或 nulls_last。

二是排序字段不需要出现在 output_fields 里:引擎会在内部自动补齐排序所需的列,排完序再按用户要求的字段返回——你不需要为了排序而改变返回结构。

排序也和 grouping search 正确组合:按字段分组的 search 里,以每组的最佳命中(组内相似度最高的那条)为键对组排序,组内顺序保持不变,返回结构仍然是分组的。排序键可以是多字段组合(价格降序、同价再按标题升序),支持数值、字符串、布尔等标量类型。

到这里说的都是 search。3.0 把同样的排序语义也给了 query——这件事的意义容易被低估。query 是没有向量参与的那半个世界:管理后台要「按创建时间看最新入库的一千条」,运营要「把满足条件的商品按价格从高到低翻上几页」,数据探查要「先看看极端值长什么样」。在此之前,query 的翻页顺序实际上锁死在主键上,「按业务字段浏览」只能把数据拉出来自己排——又回到了开头那笔账。现在 filter、排序、分页在引擎里按正确的顺序组合,query 第一次成为可以按业务语义浏览的接口。而且 search 和 query 的 order by 是同一套表达——同样的字段、方向和 null 规则,pymilvus、RESTful v2、Go SDK 正在按这套语义陆续补齐入口。

06

代码实操示例

回到开头的搜款页面,那段 pandas 代码换成下面这些请求。示例基于 pymilvus(以 3.0 正式 SDK 文档为准)。

query 聚合,像 SQL 一样: res = client.query( collection_name="products", filter='status == "on_sale"', output_fields=["category", "count(*)", "avg(price)"], group_by_fields=["category"], ) # [{'category': 'books', 'count(*)': 18734, 'avg(price)': 45.3}, ...]

query / search 排序,分页与排序正确组合: res = client.query( collection_name="products", filter="category == 'books'", output_fields=["title", "price"], order_by_fields=[{"field": "price", "order": "desc"}, {"field": "title", "order": "asc"}], limit=10, offset=10,   # 全局第 11-20 名,不是"第二页内部排序" ) res = client.search( collection_name="products", data=[query_vector], limit=50, output_fields=["title", "price"], order_by_fields=[{"field": "price", "order": "asc"}], )

search 聚合:分桶、算指标、逐桶取样,一次调用完成: from pymilvus import SearchAggregation, TopHits agg = SearchAggregation( fields=["brand"], size=10,                                   # 返回前 10 个桶 metrics={"avg_price": {"avg": "price"}},   # 每个品牌的平均价格 order=[{"_count": "desc"}],                # 桶按落入样本数降序 top_hits=TopHits(size=3, sort=[{"_score": "desc"}]),  # 每桶 3 条样本; IP/cosine 用 desc, L2 用 asc ) res = client.search( collection_name="products", data=[query_vector], search_aggregation=agg,   # 聚合模式下检索规模由聚合规格派生, limit 不生效 )

07

什么场景需要它

RAG 与 Agent。Agent 问数据库的并不总是:给我最相关的几份文档。还有可能询问知识库里关于这个主题的记录,按产品线是怎么分布的?这类统计性问题过去要么答不了,要么靠拉全量。

Search 聚合让这层信息可以和召回本身一起返回。更有意思的是,它还可以成为召回质量的一个在线信号。同一个问题检索回来几十个 Chunk:如果大部分结果高度集中在两三份文档里,这通常意味着检索结果形成了比较明确的证据集中区;如果结果分散在几十个来源,每个桶只有零星样本,那么这次召回本身可能就缺乏明显共识。

当然,这不代表仅凭桶分布就能判断答案是否正确,但它给 RAG 和 Agent 多了一个以前获取成本很高的观察维度。当这类结构信息可以伴随每次 Search 低成本返回,它就不再只是一个离线分析指标,也可以参与 Agent 自己的检索决策:继续回答,还是调整 Query 再搜一次。

电商与内容推荐。商品检索是最直接的场景。还是开头那个页面:筛选器、排序、分布,全部都能随检索请求原生返回,应用层的 pandas 后处理可以全部删掉。

日志与安全分析。安全排查时, 相似事件检索是排查的第一步:拿一条可疑日志,找出全库里和它相似的事件。但真正要回答的问题在第二步:这些相似事件集中在哪几台主机?最早出现在什么时候?按日期分桶看是在收敛还是在扩散?过去这两步跨两个系统——向量库负责相似性,导出之后进分析引擎数分布。现在一次请求同时拿到「相似的事件」和「事件的分布」,排查从两条链路变成一次往返。

运营与数据探查。对过滤结果直接 count、avg,不再为了一个数字把数据导出一遍。

尾声

把聚合和排序加入 Milvus,并不意味着向量数据库要变成一个通用分析数据库。

Milvus 3.0 解决的是 围绕在线检索和查询产生的结构化计算,而不是取代完整 OLAP 引擎。Join、窗口函数、复杂子查询仍然不在这个能力边界内。

真正的大规模离线分析,依然应该交给专业分析引擎。例如通过外表和存储共享,让 Spark 等系统直接读取同一份底层数据,是另一条架构路径,也是 Milvus 3.0 更大的故事线之一。

聚合本身也有明确的类型限制:

* 分组键支持整数和字符串;Search 聚合还支持布尔;

* 浮点数不能作为分组键;

* sum / avg 只接受数值字段;

* min / max 支持数值和字符串;

* Array、JSON、向量字段不能直接参与聚合。

Query 聚合目前可以按照分组键排序,但按聚合结果——例如 count(*)——排序仍在演进;如果不指定排序,分组返回顺序不保证。

Search 聚合目前也不支持 Hybrid Search。

另外,前面提到的统计口径最后再强调一次:search 聚合的桶和指标基于每桶保留的样本,统计的是本次召回的候选集,不是全量数据——它回答「这次召回长什么样」,不回答「库里的分布」。要精确的全量统计,用 query 聚合。 作者介绍韩淳Senior Software Engineer at Zilliz 阅读推荐 官宣开源|Milvus 3.0 正式发布 Milvus 3.0 开源解读之backfill|亿级 AI 数据,如何做高效特征回填 Milvus 3.0 开源解读|从Kafka、Pulsar到Woodpecker,数据库如何低延迟、低成本的写入 Milvus 3.0 开源解读之Manifest|AI 数据管理,应该彻底放弃文件中心架构

!Image 3: 图片

!Image 4: 图片 阅读原文

查看原文 → 發佈: 2026-08-10 18:30:00 收錄: 2026-08-11 00:00:47

🤖 問 AI

針對這篇文章提問,AI 會根據文章內容回答。按 Ctrl+Enter 送出。