「聚合分析:从指标统计到多维下钻」

讲解 ES 聚合分析:metric 与 bucket 聚合的分类与语法、嵌套聚合与 pipeline 聚合、date_histogram 做时序统计、Terms 桶多维下钻,以及聚合结果如何对接 Kibana 可视化。

聚合是 Elasticsearch 除检索外的另一半能力:检索回答「哪些文档命中」,聚合回答「这些文档呈现什么分布」。本文覆盖 metric、bucket、pipeline 三类聚合,讲解嵌套聚合与日期直方图的用法,并给出对接 Kibana 可视化的最佳实践。

1. 聚合分析基础

一句话总结: 聚合把命中的文档集合按统计口径折叠成指标与分组,与 query 搭配完成分析与下钻。

1.1 聚合的定位与结构

聚合(Aggregation)紧跟在 query 之后执行:先由 query 筛出候选文档,再对候选集合做统计。请求体中聚合与查询并列,一条请求既能拿到搜索结果又能拿到统计结果。

聚合类型作用输出常见例子
metric单值统计一个数值avg、sum、max、cardinality
bucket分组一组桶terms、range、date_histogram
pipeline跨桶计算对桶再加工avg_bucket、bucket_script

1.2 最小的聚合请求

{
  "size": 0,
  "aggs": {
    "avg_price": {
      "avg": { "field": "price" }
    }
  }
}

size 设为 0 只返回聚合不返回命中文档,能显著降低传输与解析开销,纯分析场景应默认如此。响应中的 aggregations 与 hits 并列,每个聚合以自定义名字(此处 avg_price)为键返回。

2. metric 聚合

一句话总结: metric 聚合把字段折叠成单值或多值指标,是最底层的统计单元。

2.1 单值指标

{
  "size": 0,
  "aggs": {
    "total_amount": { "sum":  { "field": "amount" } },
    "avg_amount":   { "avg":  { "field": "amount" } },
    "max_amount":   { "max":  { "field": "amount" } },
    "min_amount":   { "min":  { "field": "amount" } },
    "count":        { "value_count": { "field": "amount" } }
  }
}

sum、avg、max、min 对数值字段做简单统计,value_count 统计字段非空的文档数,可用于计算均值之外的基数校验。所有指标基于 Doc Values 读取,无需加载 _source。

2.2 stats 与 percentiles

{
  "size": 0,
  "aggs": {
    "price_stats": { "stats": { "field": "price" } },
    "price_percentiles": {
      "percentiles": { "field": "price", "percents": [50, 90, 95, 99] }
    }
  }
}

stats 一次返回 count/min/max/avg/sum 五个指标,percentiles 输出分位数,适合衡量响应时间与价格分布。分位数默认用 TDigest 算法,内存占用远小于全量排序,但对极端值存在近似误差,精度与内存可调。

2.3 cardinality 去重基数

{
  "size": 0,
  "aggs": {
    "unique_users": {
      "cardinality": { "field": "user_id", "precision_threshold": 1000 }
    }
  }
}

cardinality 用 HyperLogLog 近似去重计数,precision_threshold 控制精确与内存的平衡,默认 3000、上限 40000。近似基数统计在亿级 UV 场景误差在 1% 以内,远比精确 Set 省内存。

3. bucket 聚合

一句话总结: bucket 聚合把文档分入各桶,是分组的骨架,也是嵌套聚合与下钻的起点。

3.1 terms 分组

{
  "size": 0,
  "aggs": {
    "by_category": {
      "terms": { "field": "category", "size": 10, "order": { "_count": "desc" } }
    }
  }
}

terms 按字段值分组,size 控制返回桶数而非分桶总数,默认返回按文档数降序的前 10 桶。terms 只能在 keyword 或启用 fielddata 的字段上分组;text 字段必须用 keyword 子字段。size 设得越大,查询越慢、内存越高,前几桶够用时不必拉满。

3.2 range 与 date_range

{
  "size": 0,
  "aggs": {
    "price_ranges": {
      "range": {
        "field": "price",
        "ranges": [
          { "to": 100 },
          { "from": 100, "to": 500 },
          { "from": 500 }
        ]
      }
    }
  }
}

range 按数值区间分桶,边界遵循 to 开区间、from 闭区间的约定,多段区间常用来生成价格分布直方图。date_range 用法相同,只是字段是日期。

3.3 histogram 数值直方图

{
  "size": 0,
  "aggs": {
    "amount_histogram": {
      "histogram": { "field": "amount", "interval": 50 }
    }
  }
}

histogram 按固定步长生成等宽直方图桶,适合看金额、时长的分布形态。interval 过小会生成海量空桶或稀疏桶,先做 percentile 摸清量级再定步长。

4. 嵌套聚合

一句话总结: 嵌套聚合在桶内再做子聚合,实现「分组后再统计」,是下钻分析的核心结构。

4.1 桶内统计

{
  "size": 0,
  "aggs": {
    "by_category": {
      "terms": { "field": "category", "size": 5 },
      "aggs": {
        "avg_amount": { "avg": { "field": "amount" } },
        "order_count": { "value_count": { "field": "order_id" } }
      }
    }
  }
}

terms 内嵌 aggs 后,每个分类桶内分别计算平均金额与订单数,响应是两级嵌套结构。嵌套深度与桶数量线性放大计算量,三层以上桶数往往爆炸,务必用 size 与 filter 收口。

4.2 多层下钻

{
  "size": 0,
  "aggs": {
    "by_category": {
      "terms": { "field": "category", "size": 5 },
      "aggs": {
        "by_brand": {
          "terms": { "field": "brand", "size": 5 },
          "aggs": {
            "max_sales": { "max": { "field": "sales" } }
          }
        }
      }
    }
  }
}

类目下再按品牌细分,最后统计品牌内最大销量,即多维下钻的经典形态:类目 → 品牌 → 指标。桶数按 5 × 5 增长,设计下钻路径时始终问一句「每一层是否真的需要全部桶」。

4.3 filter 与 filters 桶

{
  "size": 0,
  "aggs": {
    "high_value": {
      "filter": { "range": { "amount": { "gte": 500 } } },
      "aggs": {
        "avg_amount": { "avg": { "field": "amount" } }
      }
    }
  }
}

filter 桶先过滤再统计,相当于对子集做聚合;filters 桶则一次给出多个过滤条件的对比统计,常用于把 VIP、普通、退款三类订单分开比较。

5. pipeline 聚合

一句话总结: pipeline 聚合消费其他聚合的输出桶做二次计算,实现均值嵌套、比率与滑动平均。

5.1 avg_bucket 与 bucket_script

{
  "size": 0,
  "aggs": {
    "orders_per_day": {
      "date_histogram": {
        "field": "create_time",
        "calendar_interval": "day"
      },
      "aggs": {
        "daily_total": { "sum": { "field": "amount" } }
      }
    },
    "avg_daily_total": {
      "avg_bucket": { "buckets_path": "orders_per_day>daily_total" }
    }
  }
}

avg_bucket 对子聚合产生的桶值再求均值,buckets_path 用 > 号引用嵌套路径,是求「每日平均销售额」这类复合指标的标准写法。bucket_script 则对两个桶值做四则运算,例如算点击率 click / (click + view)。

5.2 moving_avg 滑动平均

{
  "size": 0,
  "aggs": {
    "sales_per_day": {
      "date_histogram": {
        "field": "create_time",
        "calendar_interval": "day"
      },
      "aggs": {
        "daily_total": { "sum": { "field": "amount" } },
        "smooth_total": {
          "moving_avg": { "buckets_path": "daily_total", "window": 7 }
        }
      }
    }
  }
}

moving_avg 对前 window 个桶做滑动平均,平滑日粒度数据的抖动,适合看趋势。滑动平均依赖桶的有序性,date_histogram 天然有序,terms 桶需要显式排序才能用于 pipeline。

5.3 pipeline 的三种形式

pipeline 聚合分成三类:父聚合(avg_bucket、sum_bucket)依赖同一级子聚合的输出;兄弟聚合(bucket_script、bucket_selector)在同级聚合中引用其他兄弟;另有 percentiles_bucket 等按桶聚合输出。bucket_selector 常用来过滤掉异常桶,例如只保留日销售额超过阈值的日子。

6. 日期直方图与时序统计

一句话总结: date_histogram 把事件按时间分桶,是日志分析与时序统计的地基。

6.1 calendar_interval 与 fixed_interval

{
  "size": 0,
  "aggs": {
    "by_hour": {
      "date_histogram": {
        "field": "create_time",
        "calendar_interval": "hour",
        "time_zone": "+08:00"
      }
    }
  }
}

calendar_interval 支持 second/minute/hour/day/week/month/quarter/year 等日历粒度,month 会自动对齐月首;fixed_interval 支持任意毫秒/秒/分钟数,如 90m。跨时区的日分桶必须显式指定 time_zone,否则按 UTC 对齐造成「上午数据被算进前一天」。

6.2 缺桶与偏移

{
  "size": 0,
  "aggs": {
    "by_day": {
      "date_histogram": {
        "field": "create_time",
        "calendar_interval": "day",
        "min_doc_count": 0,
        "offset": "+8h"
      }
    }
  }
}

min_doc_count 为 0 时补出没有文档的空桶,让折线连续;offset 平移桶边界,例如把「自然日」切成「结算日」。补空桶会增加响应体积,聚合跨度很大时建议在应用层补 0 而非让 ES 全量输出。

6.3 基于时序的环比

{
  "size": 0,
  "aggs": {
    "today": {
      "filter": { "range": { "create_time": { "gte": "2026-09-30T00:00:00+08:00", "lt": "2026-10-01T00:00:00+08:00" } } },
      "aggs": { "total": { "sum": { "field": "amount" } } }
    },
    "yesterday": {
      "filter": { "range": { "create_time": { "gte": "2026-09-29T00:00:00+08:00", "lt": "2026-09-30T00:00:00+08:00" } } },
      "aggs": { "total": { "sum": { "field": "amount" } } }
    }
  }
}

环比可直接写成两个 filter 桶,各自统计后交给应用算增长率;需要同比时同理补上月同日区间。数据量极大时可把常用统计预聚合到汇总索引,避免每次全量扫分片。

7. Kibana 可视化对接

一句话总结: Kibana 把聚合响应渲染成图表,聚合设计直接决定可视化能画什么。

7.1 从聚合到图表

Kibana Lens 与 Discover 的每个可视化背后都是一条带 aggs 的搜索请求:折线图对应 date_histogram + metric,柱状图对应 terms + metric,饼图对应 terms 占比。设计聚合时想清楚「X 轴是什么桶、Y 轴是什么指标」,图表能力就定了。

{
  "size": 0,
  "aggs": {
    "date_buckets": {
      "date_histogram": { "field": "create_time", "calendar_interval": "day" },
      "aggs": {
        "amount_sum": { "sum": { "field": "amount" } }
      }
    }
  }
}

7.2 数据源与仪表盘

Kibana 通过数据视图(Data View)绑定索引模式,把字段类型映射成可聚合字段。数值字段在可视化中自动提供 sum/avg 等指标选项,keyword 字段提供 terms 分桶。仪表盘上的多个面板共用同一数据视图时,聚合口径要一致,避免「图表 A 用 doc 计数、图表 B 用 sum」造成的口径混乱。

7.3 聚合性能红线

场景风险缓解
terms size 过大内存飙升、结果不准用 top hits 替代或加 order 约束
嵌套三层以上桶数量爆炸收敛 size、前置 filter
大跨度 date_histogram空桶海量min_doc_count=0 慎用
cardinality 大基数HLL 近似误差提高 precision_threshold
每次全量扫分片查询变慢预聚合汇总索引

聚合请求应在 Kibana 的 Request Inspector 中确认实际发送的 DSL,指标聚合优先走 Doc Values,避免使用需要加载 _source 的字段。

8. 总结

一句话总结: 聚合分析以 metric 为指标、bucket 为分组、pipeline 为跨桶加工,配合日期直方图与 Kibana 完成从指标统计到多维下钻的完整分析闭环。

环节要点
聚合结构query 先筛后聚,size=0 纯分析
metricavg/sum/stats/percentiles/cardinality 单层统计
bucketterms/range/histogram 分组骨架
嵌套聚合桶内再聚,多维下钻
pipelineavg_bucket/bucket_script 跨桶加工
时序统计date_histogram 指定日历粒度与时区
Kibana 对接可视化背后即聚合请求,口径需一致
性能红线桶数收敛、嵌套受限、避免全量扫描

聚合把散落的日志与业务数据折叠成可读的指标和分组。先明确分析口径,再选对聚合类型,最后用 Kibana 把结果画成图表;当聚合成为瓶颈时,优先考虑预聚合索引与收敛桶规模。聚合语法的上游可阅读《倒排索引与分词原理》与《Query DSL 与相关性打分》,索引与字段设计参考《数据建模与 Mapping 设计》。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「elasticsearch」更多文章

  1. 「搜索服务架构:从索引到容错」
  2. 「安全加固与访问控制:从角色到审计」
  3. 「地理空间搜索:从坐标到地图」