搜索聚合Compatible with Milvus 3.0.x
当购物者搜索“日常训练用的黑色跑鞋”时,近似最近邻(ANN)搜索会根据向量相似度对产品进行排序,并返回一个扁平化的Top-K列表。搜索结果虽然相关,但可能存在重复:在下面的示例中,前六个结果中有四个是A品牌的产品,而B品牌和C品牌各出现一次。
扁平列表无法直接提供基于分桶的汇总信息。应用程序可能需要根据保留候选项数量或平均价格对品牌进行比较,检查每个品牌的少量代表性产品,或将结果组织为多级分桶结构。
搜索聚合功能会根据选定的标量字段,将保留的ANN候选项组织到不同的桶中。在此示例中,每个品牌成为一个独立的桶。Milvus可以计算每个桶的统计数据、对桶进行排序,并附上代表性产品。应用程序通过result.agg_buckets 接口获取这种“桶优先”的响应结果。
一份平铺的跑鞋搜索结果被转化为一组可比的品牌分桶
搜索聚合不会对整个Collection进行精确的聚合处理。桶的存在、计数、指标、排序以及代表性命中结果均取决于人工神经网络(ANN)和分组阶段保留的候选结果。
工作原理
按桶键分组的 ANN 候选结果,并附带计数、指标和代表性命中结果
检索候选项。Milvus 运行 ANN 搜索以查找与查询向量最接近的实体。随后,分组阶段为每个完整的复合键保留数量有限的候选项。此按键划分的候选项配额为聚合树中任意位置的最大
TopHits.size值,或者当未配置top_hits时,为1。构建桶。
SearchAggregation.fields定义了桶键。每个字段值的唯一组合都会生成一个独立的键。如图所示,fields=["brand"]生成了(Brand A)、(Brand B)和(Brand C)这三个桶键。具有相同键的保留候选项属于同一个桶,并计入该桶的count。SearchAggregation.size限制了 Milvus 返回的桶的数量。计算并返回结果。每个返回的桶都包含其键和保留候选项的数量。Milvus 还可以计算配置的指标、对桶进行排序、返回代表性实体以及构建子桶。
result.agg_buckets中的每个AggregationBucket都会暴露key、count、metrics、hits和sub_groups。当启用搜索聚合时,普通的搜索命中列表为空。
在图中,TopHits.size=4 为每个键提供了4的候选预算,因此保留的4个品牌A候选结果生成count: 4 。为使图示简洁,生成的品牌A卡片仅显示了返回的4个代表性命中结果中的2个。
当sub_aggregation 生效时,Milvus会在每个父桶内重复步骤2和步骤3。人工神经网络(ANN)的召回率或每键候选预算的变化,可能会改变桶的数量、指标、排序、命中结果以及嵌套结果。
限制
在使用搜索聚合之前,请注意以下限制:
嵌套聚合:一个请求可包含一个根级
SearchAggregation,以及最多三个嵌套的sub_aggregation级别,总计最多四级。用于创建桶键的字段:
SearchAggregation.fields支持布尔型、整数型、VARCHAR以及TIMESTAMPTZ字段。它不支持FLOAT、DOUBLE、ARRAY、JSON、GEOMETRY、TEXT、向量或 Dynamic Field 字段。度量字段:
count接受"*"或任何非JSON且非 Dynamic Field,并在指定字段时跳过NULL的值。sum和avg接受整数和浮点数字段。min和max还额外接受字符串和TIMESTAMPTZ字段。“Top Hits”排序字段:
TopHits.sort支持可比较的布尔型、整数型、浮点型、字符串型以及TIMESTAMPTZ字段,此外还支持_score。它不支持ARRAY、JSON、GEOMETRY、向量型或 Dynamic Field。候选集预算:聚合树中任何位置的最大
TopHits.size值,即为每个完整复合键保留的候选集数量。如果没有任何级别配置top_hits,Milvus 将为每个键保留一个候选集。桶count和指标是根据这些保留的候选集计算得出的,因此更改TopHits.size可能会影响这些值。可为空的桶字段:
NULL的值会形成其自身的桶键。若要排除空桶,请在搜索请求中添加诸如brand is not null之类的过滤器。重复字段:同一字段不能出现在多个
SearchAggregation.fields列表中。例如,如果根聚合使用fields=["category"],则嵌套的sub_aggregation不能同时使用fields=["category"]。不支持的组合:搜索聚合不能与
offset、搜索迭代器、混合搜索、高亮器或分组搜索结合使用。返回条目:请将配置的结果条目最大数量保持在 10,000 条及以下。该最大值的计算方式如下:
number of query vectors × size at every aggregation level × largest TopHits.size at any level当未配置任何层级时,请将“
TopHits”的最后一个因子设为1。例如,一个查询向量、10个根桶、每个根桶5个子桶,以及每个子桶2个命中,其配置的最大值为:1 × 10 × 5 × 2 = 100
使用搜索聚合
根据您的目标选择一个示例:
| 转到 | 描述 | 关键设置 |
|---|---|---|
| 比较和排序存储桶 | 计算每个存储桶的统计数据以进行比较,然后根据指标、计数或键对返回的存储桶进行排序。 | fields,size,metrics,order |
| 显示每个桶的代表性结果 | 从每个桶中返回数量有限的实体,并根据标量字段或向量得分独立对这些实体进行排序。 | top_hits,TopHits.size,TopHits.sort |
| 多级分组结果 | 将结果组织为父级和子级桶,以便依次分析多个维度。 | sub_aggregation |
下面的示例使用了一个包含品牌、类别、颜色、价格和评分字段的产品Collection。所有品牌名称、产品名称、价格、评分和搜索结果均为合成示例数据。展开以下部分以创建该Collection并定义共享搜索变量。
from pymilvus import DataType, MilvusClient, SearchAggregation, TopHits
client = MilvusClient(
uri="http://localhost:19530",
token="root:Milvus",
)
collection_name = "product_search_aggregation"
if client.has_collection(collection_name):
client.drop_collection(collection_name)
schema = client.create_schema(auto_id=False, enable_dynamic_field=False)
schema.add_field("id", DataType.INT64, is_primary=True)
schema.add_field("embedding", DataType.FLOAT_VECTOR, dim=5)
schema.add_field("name", DataType.VARCHAR, max_length=200)
schema.add_field("brand", DataType.VARCHAR, max_length=100)
schema.add_field("category", DataType.VARCHAR, max_length=100)
schema.add_field("color", DataType.VARCHAR, max_length=50)
schema.add_field("price", DataType.DOUBLE)
schema.add_field("rating", DataType.DOUBLE)
schema.add_field("in_stock", DataType.BOOL)
index_params = client.prepare_index_params()
index_params.add_index(
field_name="embedding",
index_type="AUTOINDEX",
metric_type="COSINE",
)
client.create_collection(
collection_name=collection_name,
schema=schema,
index_params=index_params,
# Make preceding writes visible to searches from this client.
consistency_level="Session",
)
client.insert(
collection_name=collection_name,
data=[
{
"id": 1,
"embedding": [0.12, 0.42, 0.18, 0.66, 0.31],
"name": "Runner A1",
"brand": "Brand A",
"category": "running_shoes",
"color": "black",
"price": 129.99,
"rating": 4.7,
"in_stock": True,
},
{
"id": 2,
"embedding": [0.10, 0.39, 0.20, 0.61, 0.29],
"name": "Trail A2",
"brand": "Brand A",
"category": "running_shoes",
"color": "blue",
"price": 139.99,
"rating": 4.6,
"in_stock": True,
},
{
"id": 3,
"embedding": [0.14, 0.44, 0.19, 0.68, 0.33],
"name": "Runner B1",
"brand": "Brand B",
"category": "running_shoes",
"color": "white",
"price": 159.99,
"rating": 4.8,
"in_stock": True,
},
{
"id": 4,
"embedding": [0.16, 0.41, 0.22, 0.62, 0.30],
"name": "Runner C1",
"brand": "Brand C",
"category": "running_shoes",
"color": "red",
"price": 119.99,
"rating": 4.4,
"in_stock": False,
},
{
"id": 5,
"embedding": [0.48, 0.20, 0.59, 0.15, 0.71],
"name": "Jacket A1",
"brand": "Brand A",
"category": "jackets",
"color": "black",
"price": 99.99,
"rating": 4.5,
"in_stock": True,
},
{
"id": 6,
"embedding": [0.45, 0.18, 0.55, 0.17, 0.69],
"name": "Jacket B1",
"brand": "Brand B",
"category": "jackets",
"color": "blue",
"price": 89.99,
"rating": 4.3,
"in_stock": True,
},
{
"id": 7,
"embedding": [0.09, 0.38, 0.17, 0.60, 0.27],
"name": "Runner A3",
"brand": "Brand A",
"category": "running_shoes",
"color": "black",
"price": 159.99,
"rating": 4.8,
"in_stock": True,
},
{
"id": 8,
"embedding": [0.13, 0.43, 0.21, 0.65, 0.32],
"name": "Runner A4",
"brand": "Brand A",
"category": "running_shoes",
"color": "black",
"price": 149.99,
"rating": 4.9,
"in_stock": True,
},
],
)
client.load_collection(collection_name)
query_vector = [0.11, 0.40, 0.19, 0.64, 0.30]
search_params = {
"metric_type": "COSINE",
"params": {},
}
上述设置同时配置了COSINE 的向量索引和搜索参数。因此,后续示例将使用{"_score": "desc"} 来优先显示余弦相似度较高的结果。若使用L2 等距离度量,请使用{"_score": "asc"} 。
比较和排序桶
当您需要使用计算得出的统计数据比较检索到的实体组,并控制桶的返回顺序时,请使用此模式。在此示例中,Milvus 根据brand 对检索到的产品进行分组,为每个品牌桶计算价格指标,并按平均价格对桶进行排序。
若您的目标仅是通过针对每个字段值返回一个或多个实体来提高结果多样性,请改用“分组搜索”。
以下配置将创建最多三个品牌桶,为每个桶计算指标,并按平均价格对桶进行排序:
aggregation = SearchAggregation(
# Form one bucket for each distinct brand value.
fields=["brand"],
# Return up to three buckets at this aggregation level.
size=3,
# Calculate named metrics for every selected bucket.
metrics={
"product_count": {"count": "*"},
"avg_price": {"avg": "price"},
"min_price": {"min": "price"},
},
# Sort buckets by average price, highest first.
order=[
{"avg_price": "desc"},
# If average prices are equal, sort by bucket key in ascending order.
{"_key": "asc"},
],
)
将该对象传递给 `MilvusClient.search()` 的 `search_aggregation ` 参数:
result = client.search(
collection_name=collection_name,
data=[query_vector],
anns_field="embedding",
search_params=search_params,
output_fields=[
"name",
"brand",
"category",
"color",
"price",
"rating",
"in_stock",
],
search_aggregation=aggregation,
)
当设置search_aggregation 时,PyMilvus在result[0] 中不会返回普通实体命中结果。请改从result.agg_buckets[0] 中读取桶响应。output_fields 参数控制哪些标量字段会出现在每个返回的AggregationHit.fields 映射中;Milvus仍可使用未列在output_fields 中的指标源字段和排序字段。
以下输出截取自上述请求,并为便于阅读而序列化为 JSON 格式。PyMilvus 返回的是AggregationBucket 对象,而非 JSON。key 的值始终是键组件的有序列表,即使fields 中仅包含一个字段也是如此。这可保留复合键的字段顺序。
[
{
"key": [
{
"field_id": 103,
"field_name": "brand",
"value": "Brand B"
}
],
"count": 1,
"metrics": {
"product_count": 1,
"avg_price": 159.99,
"min_price": 159.99
},
"hits": [],
"sub_groups": []
},
{
"key": [
{
"field_id": 103,
"field_name": "brand",
"value": "Brand A"
}
],
"count": 1,
"metrics": {
"product_count": 1,
"avg_price": 129.99,
"min_price": 129.99
},
"hits": [],
"sub_groups": []
},
{
"key": [
{
"field_id": 103,
"field_name": "brand",
"value": "Brand C"
}
],
"count": 1,
"metrics": {
"product_count": 1,
"avg_price": 119.99,
"min_price": 119.99
},
"hits": [],
"sub_groups": []
}
]
对于本指南中的单个查询向量,请从 `result.agg_buckets[0]` 中读取返回的顶级桶。每个桶都会暴露其有序键组件、保留候选项 `count`、计算结果 `metrics`、代表样本 `hits` 以及嵌套桶 `sub_groups`。
请按以下方式读取配置:
| 设置 | 控制内容 | 在此示例中 |
|---|---|---|
fields | Milvus 如何生成桶键 | 针对每个不同的brand 值创建一个存储桶。 |
size | 返回的存储桶最大数量 | 最多返回三个品牌存储桶。 |
metrics | 为每个存储桶计算的统计数据 | 计算商品数量、平均价格和最低价格。 |
order | Milvus 对返回的桶进行排序的方式 | 按平均价格排序,然后使用桶键来打破平局。 |
当设置了 `search_aggregation ` 时,Milvus 会忽略 `limit `。使用根 `SearchAggregation.size ` 值来控制顶级分桶的数量。
采用这些设置后,Milvus 将按avg_price 从高到低的顺序返回品牌 B、品牌 A 和品牌 C 的桶。_key 条件仅在桶的平均价格相同时生效。由于此配置未定义top_hits ,每个桶的hits 列表均为空,且每个键的候选预算为1 。因此,显示的计数和指标描述的是每个品牌保留的一个候选项。当聚合需要更宽的按键指标窗口时,请将top_hits 配置为更大的TopHits.size 。
每个SearchAggregation.metrics 条目将一个用户定义的别名映射到{operation: source} :
| 源 | 支持的操作 | 行为 |
|---|---|---|
任何非JSON 且非Dynamic Field | count | 统计源字段非NULL 的保留候选项。 |
| 整数或浮点字段 | sum、avg 、min 、max | 基于非空的保留值进行计算。 |
字符串或TIMESTAMPTZ 字段 | min,max | 选择非空保留值的最小值或最大值。 |
"*" | count | 统计桶中每个保留候选项的数量。结果与bucket.count 一致。 |
_score | sum,avg,min,max | 对保留的候选项汇总 ANN 相似度或距离值。 |
SearchAggregation.order 支持以下键:
| 排序键 | 含义 |
|---|---|
| 度量别名 | 按在metrics 中同一聚合级别计算的值进行排序,例如avg_price 。 |
_count | 按每个桶中保留的候选项数量进行排序。 |
_key | 按桶键排序,而非按名为_key 的 Collection 字段排序。 |
每个order 条目都会将一个键映射到"asc" 或"desc" 。Milvus会按从首到尾的顺序评估多个条目。若省略order ,Milvus将保留来自保留候选集的桶发现顺序。
若要按向量匹配质量对桶进行排序,请先根据 `_score` 计算桶级指标,然后在 `order` 中使用该指标别名。您不能直接将 `_score ` 用作桶排序键,因为每个桶可能包含多个实体得分。例如,对于 `COSINE ` 或 `IP`:
aggregation = SearchAggregation(
fields=["brand"],
size=3,
metrics={"max_score": {"max": "_score"}},
order=[{"max_score": "desc"}],
)
对于L2 ,计算_score 的最小值,并按升序对指标别名进行排序,以便距离最小的桶排在最前面。
要创建复合桶键,请在同一个列表中传入多个字段名称:
aggregation = SearchAggregation(
# Combine brand and color to form a composite bucket key.
fields=["brand", "color"],
size=6,
)
此配置可生成诸如(Brand A, black) 、(Brand A, blue) 和(Brand B, white) 之类的键。只有当两个值的值完全匹配时,两个实体才会共享同一个桶。Milvus 会保留列表顺序,因此brand 是第一个键组件,color 是第二个。当在order 中使用_key 时,Milvus 会按相同顺序比较复合键的各个组件。请将多个字符串作为单个扁平列表传递;不支持嵌套列表。
size=6 是该聚合级别返回的复合桶的最大数量。示例数据包含五种不同的品牌-颜色组合,因此可以返回全部五种。在返回条目限制中,此请求贡献了1 query vector × 6 buckets × 1 = 6 配置的结果条目。
在单个SearchAggregation.fields 列表中包含多个字段,将在该聚合级别上生成一个复合桶键。若要创建父子桶层次结构,请使用嵌套聚合。
以下示例重新定义了aggregation 。将更新后的对象传递给相同的search_aggregation 参数,并重新执行搜索调用。
显示每个桶中的代表性结果
当应用程序需要显示每个桶中的实际产品时,请包含具有代表性的实体。在此示例中,Milvus 从每个品牌桶中返回最多两个产品,按评分排序,然后按向量得分排序。
请按以下方式配置 `TopHits `:
aggregation = SearchAggregation(
fields=["brand"],
size=3,
# Return and sort representative entities for each selected bucket.
top_hits=TopHits(
# Return up to two entities per bucket.
size=2,
# Apply sort criteria in list order.
sort=[
{"rating": "desc"},
{"_score": "desc"},
],
),
)
以下品牌 A 桶的数据摘自上述请求,并为便于阅读而序列化为 JSON 格式。
{
"key": [
{
"field_id": 103,
"field_name": "brand",
"value": "Brand A"
}
],
"count": 2,
"metrics": {},
"hits": [
{
"pk": 1,
"score": 0.99976646900177,
"fields": {
"brand": "Brand A",
"category": "running_shoes",
"color": "black",
"in_stock": true,
"name": "Runner A1",
"price": 129.99,
"rating": 4.7
}
},
{
"pk": 2,
"score": 0.9997048377990723,
"fields": {
"brand": "Brand A",
"category": "running_shoes",
"color": "blue",
"in_stock": true,
"name": "Trail A2",
"price": 139.99,
"rating": 4.6
}
}
],
"sub_groups": []
}
| 参数 | 用途 |
|---|---|
top_hits | 可选。用于配置此聚合层级的代表性实体。若省略,bucket.hits 将为空,且每个键的候选预算默认为 1。 |
TopHits.size | 从每个选定的桶中返回最多两个代表性实体,并将整个聚合树中每个键的候选预算设置为两个。 |
TopHits.sort | 根据列出的标准对每个桶内的实体进行排序。 |
当应用程序需要代表性实体,或者计数和指标需要更宽的按键候选窗口时,请配置 `top_hits `。较大的 `TopHits.size ` 值会同时增加候选预算,并提高“限制”中返回条目的最大计算量。
SearchAggregation.order 对桶进行排序,而“TopHits.sort ”则对每个桶内的保留实体进行排序。该排序顺序不会改变为“count ”和指标所保留的候选项。TopHits.sort 接受受支持的可比较标量字段名称以及内置的_score 字段,该字段表示ANN相似度或距离。Milvus会从头到尾评估sort 条目。 在此示例中,它按rating 从高到低对产品进行排序,仅当两个评分相同时才使用_score 。由于该配置使用了COSINE ,因此降序的_score 会将相似度更高的产品排在首位。
metrics 或TopHits.sort 所使用的字段不必出现在output_fields 中。Milvus 会在内部获取这些字段,但只有output_fields 中显式列出的字段才会被包含在每个返回命中结果的fields 映射中。主键和向量得分仍可通过AggregationHit.pk 和AggregationHit.score 获取。
每个返回的AggregationHit 都会在pk 中暴露其主键,在score 中暴露向量得分,并在fields 中暴露请求的输出字段。
多级分组结果
当需要在某一层级内嵌套另一层级桶时,请使用嵌套聚合。在此示例中,Milvus 首先创建类别桶,然后在每个类别内创建品牌桶。
子聚合仅接收分配给其父桶的实体。fields 控制每个聚合层级的桶键,而sub_aggregation 则创建父子层级结构。
以下配置将创建一个键为(running_shoes) 的类别桶。在此父桶内,子聚合会创建多个独立的品牌桶,其键分别为(Brand A) 、(Brand B) 和(Brand C) 等。
Parent bucket key:
(running_shoes)
Child bucket keys:
├── (Brand A)
├── (Brand B)
└── (Brand C)
每个级别均可独立使用多个字段。例如,在子聚合中使用fields=["brand", "color"] 将生成诸如(Brand A, black) 之类的复合子键。
以下配置实现了此层次结构:
aggregation = SearchAggregation(
fields=["category"],
size=2,
metrics={
"product_count": {"count": "*"},
"avg_price": {"avg": "price"},
},
order=[{"product_count": "desc"}],
# For each category bucket, group only its entities by brand.
sub_aggregation=SearchAggregation(
fields=["brand"],
size=3,
metrics={
"brand_count": {"count": "*"},
"avg_rating": {"avg": "rating"},
},
order=[{"avg_rating": "desc"}],
top_hits=TopHits(
size=2,
sort=[{"rating": "desc"}],
),
),
)
以下序列化片段展示了父桶running_shoes 及其子桶 Brand B。为简洁起见,省略了子桶 Brand A 和 Brand C。
{
"key": [
{
"field_id": 104,
"field_name": "category",
"value": "running_shoes"
}
],
"count": 4,
"metrics": {
"avg_price": 137.49,
"product_count": 4
},
"hits": [],
"sub_groups": [
{
"key": [
{
"field_id": 103,
"field_name": "brand",
"value": "Brand B"
}
],
"count": 1,
"metrics": {
"avg_rating": 4.8,
"brand_count": 1
},
"hits": [
{
"pk": 3,
"score": 0.9994542598724365,
"fields": {
"brand": "Brand B",
"category": "running_shoes",
"color": "white",
"in_stock": true,
"name": "Runner B1",
"price": 159.99,
"rating": 4.8
}
}
],
"sub_groups": []
}
]
}
显示的结果代表桶路径(running_shoes) → (Brand B) ,而非单个复合桶键(running_shoes, Brand B) 。
Milvus 首先根据product_count 排序,最多选出两个类别存储桶。然后,它在每个选定的类别中独立运行sub_aggregation ,并返回最多三个品牌存储桶,按avg_rating 排序。
在上述输出中:
- 根
running_shoes桶在其子复合键中包含四个保留候选项。其metrics包含根级别的avg_price和product_count值。 - 根桶的
sub_groups列表包含子品牌桶。显示的品牌B桶包含一个保留候选项及其自身的avg_rating和brand_count值。 - 根桶的
hits列表为空,因为根聚合未配置top_hits。品牌 B 子桶包含一个代表性命中,因为top_hits已在sub_aggregation中配置。
常见问题
桶计数和指标的准确性如何?
搜索聚合对保留的 ANN 候选项进行汇总,不会执行全 Collection 聚合。
候选项保留包含两个近似阶段。ANN 搜索可能会遗漏相关 Collection 实体,而分组阶段对于每个完整的复合键,最多仅保留TopHits.size 个候选项。如果没有任何级别配置了top_hits ,则每个键的限制为 1。
例如,假设一个 Collection 包含 5,000 件 A 品牌产品,其中许多与向量查询相关。如果聚合使用了TopHits(size=4) ,则 A 品牌桶对于每个完整复合键最多可保留四个候选项。其count 和指标描述的是这些保留的候选项,而不是所有相关的 A 品牌产品,也不是 Collection 中的全部 5,000 个实体。
当order 使用指标别名时,近似性尤为重要。搜索召回率的变动会改变指标值,从而改变哪些桶符合SearchAggregation.size 的条件。嵌套聚合会放大这一影响,因为每个子级别都是基于其父桶中可用的实体进行操作的。
如果您需要针对每个匹配实体的精确统计数据,请使用精确查询聚合工作流,而不是搜索聚合。
搜索聚合与分组搜索有何区别?
请根据应用程序的主要结果结构进行选择:
| 主要需求 | 推荐方案 | 需处理的响应 |
|---|---|---|
| 返回包含分组字段中重复值较少的标准排序实体列表 | 分组搜索 | 针对每个查询向量返回扁平化的搜索命中结果 |
| 将分组视为桶进行检查或比较,包括键、计数、指标、排序、代表性命中结果或子桶 | 搜索聚合 | AggregationBucket 对象位于result.agg_buckets |
即使搜索聚合配置了top_hits ,其主要响应仍为桶树。当应用程序已处理常规搜索命中结果,且主要追求结果多样性时,分组搜索依然有用。
这些API互斥。当在同一请求中将search_aggregation 与group_by_field 或group_by_fields 结合使用时,PyMilvus会抛出ParamError 异常。