Elasticsearch 用 `collapse` 做结果去重与分组展示(含 inner\_hits、分页、重排、二级折叠)
Elasticsearch 折叠查询(Collapse)功能摘要:该功能用于按指定字段去重并返回每组Top结果,支持单值keyword或数值类型字段。基础用法通过collapse.field指定分组字段,配合sort选择组代表。可展开每组结果(inner_hits),支持多视图展示,但需注意性能开销。分页需使用search_after且排序字段必须与分组字段相同。支持与重排(rescore)配合使
基础用法:按字段折叠,取每组 Top1
要求:用于折叠的字段必须是单值
keyword或数值类型,并且 开启 doc_values。
GET my-index-000001/_search
{
"query": { "match": { "message": "GET /search" } },
"collapse": { "field": "user.id" },
"sort": [
{ "http.response.bytes": { "order": "desc" } }
],
"from": 0
}
collapse.field:按user.id折叠sort:挑“代表结果”的排序依据(这里取响应字节数最大者)from:折叠后的组起始偏移
注意
hits.total仍是未折叠的总命中数;不同组数目未知。- 折叠只作用于顶层 hits,不影响 aggregations。
展开每组:inner_hits(单视图 & 多视图)
单视图展开:拿每组最近 5 条
GET my-index-000001/_search
{
"query": { "match": { "message": "GET /search" } },
"collapse": {
"field": "user.id",
"inner_hits": {
"name": "most_recent",
"size": 5,
"sort": [ { "@timestamp": "desc" } ]
},
"max_concurrent_group_searches": 4
},
"sort": [ { "http.response.bytes": { "order": "desc" } } ]
}
要点:
inner_hits.size:每组取多少条inner_hits.sort:组内排序(如最新)max_concurrent_group_searches:限制展开阶段的并发,避免压垮集群
多视图展开:同一组返回两种代表
"collapse": {
"field": "user.id",
"inner_hits": [
{
"name": "largest_responses",
"size": 3,
"sort": [ { "http.response.bytes": { "order": "desc" } } ]
},
{
"name": "most_recent",
"size": 3,
"sort": [ { "@timestamp": { "order": "desc" } } ]
}
]
}
性能警告
每个折叠命中 * 每个 inner_hit 都会触发额外子查询;组合多、组多时会显著放慢检索。用并发阈值控制规模。
分页:search_after + 折叠(必须同字段、且无次级排序)
使用
search_after做折叠分页时,sort与collapse.field必须相同,且不允许次级排序字段。
GET my-index-000001/_search
{
"query": { "match": { "message": "GET /search" } },
"collapse": { "field": "user.id" },
"sort": [ "user.id" ],
"search_after": ["dd5ce1ad"]
}
与重排配合:对每组代表做 rescore
重排器仅对每个折叠组的 Top 文档执行。为获得最稳定的全局顺序,建议索引时将折叠字段作为 routing key,尽量让同组文档落在同一分片。
索引时设置 routing(示例)
POST /my-index-000001/_doc?routing=xyz
{
"@timestamp": "2099-11-15T13:12:00",
"message": "You know for search!",
"user.id": "xyz"
}
折叠 + 重排
GET /my-index-000001/_search
{
"query": { "match": { "message": "you know for search" } },
"collapse": { "field": "user.id" },
"rescore": {
"window_size": 50,
"query": {
"rescore_query": {
"match_phrase": { "message": "you know for search" }
},
"query_weight": 0.3,
"rescore_query_weight": 1.4
}
}
}
提醒:Rescorers 不会作用于
inner_hits,只作用于折叠后的代表文档。
二级折叠:对 inner_hits 再折叠
二级折叠只在
inner_hits中生效,且二级折叠不允许再带inner_hits。
示例:先按 geo.country_name 折叠,再在组内按 user.id 折叠并取 3 条:
"collapse": {
"field": "geo.country_name",
"inner_hits": {
"name": "by_location",
"collapse": { "field": "user.id" },
"size": 3
}
}
评分追踪:track_scores
折叠通常配合字段排序,此时默认不计算 _score。如果你需要同时拿到相关性得分,设置:
"track_scores": true
不支持与坑点清单
- ❌
collapse与scroll不能同时使用 - ⚠️ 字段必须单值
keyword/数值+ doc_values - ⚠️
hits.total是未折叠的总数;组总数未知 - ⚠️ 折叠不影响聚合结果
- ⚠️
inner_hits展开是“每组 * 每 inner_hit”额外请求,注意并发与配额 - ⚠️
search_after折叠分页:排序字段必须等于折叠字段,且无次级排序 - ⚠️ Rescore 只作用于组代表;为稳定性,建议按折叠字段 routing
实战模板(拷走即用)
1)按用户去重 + 展开两种子视图 + 限并发
GET logs-*/_search
{
"query": { "match": { "message": "GET /search" } },
"collapse": {
"field": "user.id",
"inner_hits": [
{ "name": "recent", "size": 3, "sort": [ { "@timestamp": "desc" } ] },
{ "name": "largest", "size": 3, "sort": [ { "http.response.bytes": "desc" } ] }
],
"max_concurrent_group_searches": 4
},
"sort": [ { "http.response.bytes": "desc" } ],
"track_scores": true
}
2)折叠分页(严格同字段排序)
GET logs/_search
{
"query": { "match_all": {} },
"collapse": { "field": "user.id" },
"sort": [ "user.id" ],
"size": 50,
"search_after": ["<last_user_id_from_prev_page>"]
}
3)折叠 + 重排(代表文档二次排序)
GET docs/_search
{
"query": { "match": { "content": "vector search" } },
"collapse": { "field": "author_id" },
"rescore": {
"window_size": 100,
"query": {
"rescore_query": {
"match_phrase": { "content": "vector search" }
},
"rescore_query_weight": 1.2
}
},
"sort": [ { "publish_at": "desc" } ]
}
性能与稳定性建议
- 结果“摇摆不定”?给折叠字段加路由,把同组文档尽量放同一分片;或者确保排序字段确定且覆盖良好。
- 展开很慢?减少
inner_hits.size、合并视图、调小max_concurrent_group_searches、加预过滤query。 - 需要相关性?
track_scores: true;但注意性能开销。 - 需要稳定分页?
search_after+ 同字段排序;避免二级排序。
FAQ
Q:为什么总命中数比返回条数大很多?
A:hits.total 是未折叠计数;你看到的是“组代表”的数量。
Q:如何每组返回 N 条?
A:用 inner_hits.size=N;若要不同视图(最新/最大),用 inner_hits 数组给出多个配置。
Q:重排是否会影响 inner_hits?
A:不会。Rescorer 仅应用在折叠后的代表文档。
更多推荐
所有评论(0)