基础用法:按字段折叠,取每组 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 做折叠分页时,sortcollapse.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

不支持与坑点清单

  • collapsescroll 不能同时使用
  • ⚠️ 字段必须单值 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 仅应用在折叠后的代表文档

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐