第三阶段 23 · 嵌套与子聚合 / top_hits

阶段:第三阶段 / 聚合能力

ES:子聚合 / top_hits | PostgreSQL:GROUP BY 后再聚合 / 窗口函数近似


1. 概念

聚合可以层层嵌套 :桶聚合里再放桶聚合或指标聚合,

相当于 SQL 里多级 GROUP BY 再算指标。

top_hits 是一种特殊子聚合,能取出每个桶内的前 N 条原始文档

近似 SQL 窗口函数 ROW_NUMBER() OVER (PARTITION BY ...) 取 top N。


2. PostgreSQL 对照

sql 复制代码
-- 多级分组:先 geo 再 region,算金额
SELECT geo_cd, region, SUM(net_amount) AS total
FROM salesdata
GROUP BY geo_cd, region;

-- 每个 geo 取金额最高的一条(窗口函数)
SELECT * FROM (
  SELECT *, ROW_NUMBER() OVER (PARTITION BY geo_cd ORDER BY net_amount DESC) rn
  FROM salesdata
) t WHERE rn = 1;

3. ES DSL

多级分组

复制代码
GET salesdata_idx/_search
{
  "size": 0,
  "aggs": {
    "by_geo": {
      "terms": { "field": "geo_cd", "size": 10 },
      "aggs": {
        "by_region": {
          "terms": { "field": "region", "size": 10 },
          "aggs": {
            "total": { "sum": { "field": "net_amount" } }
          }
        }
      }
    }
  }
}

每个桶取 top N 文档(top_hits)

复制代码
GET salesdata_idx/_search
{
  "size": 0,
  "aggs": {
    "by_geo": {
      "terms": { "field": "geo_cd", "size": 10 },
      "aggs": {
        "top_orders": {
          "top_hits": {
            "size": 1,
            "sort": [ { "net_amount": "desc" } ],
            "_source": [ "invoice_number", "net_amount" ]
          }
        }
      }
    }
  }
}

4. Spring Boot 实现

java 复制代码
@Component
public class Doc23NestedAgg {

    @Autowired
    private ElasticsearchClient elasticsearchClient;

    /** 每个 geo 取金额最高的一条订单 */
    public Map<String, Map<String, Object>> topOrderByGeo(String indexName) throws IOException {
        SearchResponse<Void> resp = elasticsearchClient.search(s -> s
                .index(indexName)
                .size(0)
                .aggregations("by_geo", a -> a
                        .terms(t -> t.field("geo_cd").size(10))
                        .aggregations("top_orders", sub -> sub
                                .topHits(h -> h
                                        .size(1)
                                        .sort(so -> so.field(f -> f.field("net_amount").order(SortOrder.Desc)))))),
                Void.class);

        Map<String, Map<String, Object>> result = new LinkedHashMap<>();
        for (StringTermsBucket bucket :
                resp.aggregations().get("by_geo").sterms().buckets().array()) {
            String geo = bucket.key().stringValue();
            var hits = bucket.aggregations().get("top_orders").topHits().hits().hits();
            if (!hits.isEmpty()) {
                // top_hits 的 source 是 JsonData,按需转 Map
                result.put(geo, hits.get(0).source().to(Map.class));
            }
        }
        return result;
    }
}

import:...aggregations.StringTermsBucketco.elastic.clients.elasticsearch._types.SortOrder

top_hits 命中在 .topHits().hits().hits(),source 是 JsonData,用 .to(Map.class) 转换。


5. 坑与最佳实践

  1. 嵌套层数别太深:多级 terms 桶数是乘积关系,容易爆内存(桶爆炸)。
  2. top_hits 只在桶内取少量文档:它是每桶单独排序取 top N,别当分页用。
  3. 子聚合排序 :桶间排序用父 termsorder,引用子聚合名即可。
  4. size: 0:外层不要文档,只要聚合结果。
  5. 高基数 + 多级分组 :优先考虑 composite 聚合分页,避免一次性返回海量桶。

下一篇

24-filter-bucket聚合.md

相关推荐
vHelios3 天前
【电商项目】商品搜索开发复盘(1):根据需求拆解搜索接口的设计逻辑
java·微服务·es
cfm_291412 天前
Logstash 8.x 入门实战
安全·es
倒流时光三十年18 天前
第五阶段 40 · 深分页与性能(from/size、search_after、scroll、PIT)
es
倒流时光三十年19 天前
第三阶段 29 · suggester 自动补全(搜索框联想 / 拼写纠错)
es
倒流时光三十年20 天前
第三阶段 24 · filter / filters 桶聚合
es
倒流时光三十年20 天前
第二阶段 17 · multi_match 多字段匹配
es
倒流时光三十年20 天前
第二阶段 19 · _source 字段裁剪
es
倒流时光三十年22 天前
第二阶段 15 · prefix / wildcard / fuzzy 模糊匹配
es
粗体鱼22 天前
RAG/Agent 记忆混合检索多路召回:RRF 算法与Chunk RRF、Document RRF如何决策TopK
postgresql·milvus·es·rag·rff·mermory