2026年7月12日 · 5 分钟阅读

Elasticsearch 聚合分析与 Painless Scripting

从指标聚合到桶聚合、管道聚合和直方图,再到 Painless 脚本处理,完整覆盖 ES 的数据分析能力。

搜索只是 ES 的一半能力。另一半是聚合(Aggregations)——它让你在亿级数据上实时完成分组、统计、排序、百分比分析等操作,完全不需要外挂一个 OLAP 引擎。

按执行方式,聚合分为三类:

  • 指标聚合(Metric Aggregations):对一组文档计算统计值,类似 SQL 的聚合函数。
  • 桶聚合(Bucket Aggregations):把文档分组到不同的桶里,类似 SQL 的 GROUP BY。
  • 管道聚合(Pipeline Aggregations):对聚合结果再做一次聚合,类似子查询嵌套。

指标聚合(Metric Aggregations)

基本统计

GET product/_search
{
  "size": 0,
  "aggs": {
    "max_price": { "max": { "field": "price" } },
    "min_price": { "min": { "field": "price" } },
    "avg_price": { "avg": { "field": "price" } }
  }
}

一次性拿到多项统计

GET product/_search
{
  "size": 0,
  "aggs": {
    "price_stats": {
      "stats": { "field": "price" }
    }
  }
}

stats 会同时返回 count、min、max、avg、sum 五个值,非常方便。如果需要百分位,用 percentiles


桶聚合(Bucket Aggregations)

按字段分组 + 子聚合

计算每个商品类型的平均价格,再找出其中最低的那个:

GET product/_search
{
  "size": 0,
  "aggs": {
    "type_bucket": {
      "terms": {
        "field": "type.keyword"
      },
      "aggs": {
        "price_avg": {
          "avg": { "field": "price" }
        }
      }
    },
    "min_bucket": {
      "min_bucket": {
        "buckets_path": "type_bucket>price_avg"
      }
    }
  }
}

这个查询展示了聚合嵌套的精髓:

  1. type_bucket 先按 type 分组。
  2. 在每个组内计算 price_avg
  3. min_bucket 是一个管道聚合,对所有组的 price_avg 取最小值。

笛卡尔积分组

ES 支持在桶聚合内再嵌套另一个桶聚合,形成类似 SQL 多列 GROUP BY 的效果:

GET product/_search
{
  "size": 0,
  "aggs": {
    "type_bucket": {
      "terms": { "field": "type.keyword" },
      "aggs": {
        "name_bucket": {
          "terms": { "field": "name.keyword" }
        }
      }
    }
  }
}

先查询后聚合

聚合可以跟在 query 后面,只对满足条件的文档做聚合:

GET product/_search
{
  "query": {
    "range": { "price": { "gte": 1000 } }
  },
  "aggs": {
    "type_bucket": {
      "terms": { "field": "type.keyword" }
    }
  }
}

聚合排序

按字段名排序

GET product/_search?size=0
{
  "aggs": {
    "tags_aggs": {
      "terms": {
        "field": "tags.keyword",
        "size": 10,
        "order": { "_key": "asc" }
      }
    }
  }
}

_key 按字段值字母序排序,_count 按文档数排序。

嵌套聚合排序

二级分组内各自排序:

GET product/_search?size=0
{
  "aggs": {
    "first_sort": {
      "terms": {
        "field": "tags.keyword",
        "order": { "_count": "desc" }
      },
      "aggs": {
        "second_sort": {
          "terms": {
            "field": "type.keyword",
            "order": { "_count": "desc" }
          }
        }
      }
    }
  }
}

按子聚合结果排序

根据自定义统计值来排序——这是最灵活的排序方式:

GET product/_search?size=0
{
  "aggs": {
    "type_price": {
      "terms": {
        "field": "type.keyword",
        "order": {
          "agg_stats>stats.min": "asc"
        }
      },
      "aggs": {
        "agg_stats": {
          "filter": {
            "terms": { "tags.keyword": ["88vip", "tmall"] }
          },
          "aggs": {
            "stats": {
              "stats": { "field": "price" }
            }
          }
        }
      }
    }
  }
}

这里的意思是:按各 type 分组,在每个组内先过滤出 tags 为 88vip 或 tmall 的商品,计算它们的价格统计,然后按照这些统计的最小值(stats.min)对整个分组结果排序。


直方图与分布分析

范围直方图(Range)

手动指定区间范围:

GET product/_search
{
  "aggs": {
    "price_range": {
      "range": {
        "field": "price",
        "ranges": [
          { "from": 0, "to": 1000 },
          { "from": 1000, "to": 2000 },
          { "from": 2000, "to": 3000 },
          { "from": 3000, "to": 4000 }
        ]
      }
    }
  }
}

自动直方图(Histogram)

指定区间大小,ES 自动分桶:

GET product/_search
{
  "aggs": {
    "price_range": {
      "histogram": {
        "field": "price",
        "interval": 1000
      }
    }
  }
}

日期直方图(Date Histogram)

按时间维度聚合,支持年、季度、月、周、日、小时等:

GET product/_search
{
  "aggs": {
    "price_range": {
      "date_histogram": {
        "field": "date",
        "calendar_interval": "month",
        "extended_bounds": {
          "min": "2023-01",
          "max": "2023-06"
        }
      }
    }
  }
}

百分位排名(Percentile Ranks)

分析数据分布情况——比如想知道价格在 1000、2000、3000、4000 以内的商品占比:

GET product/_search
{
  "aggs": {
    "price_range": {
      "percentile_ranks": {
        "field": "price",
        "values": [1000, 2000, 3000, 4000]
      }
    }
  }
}

Painless Scripting:动态处理数据

有些场景没法通过固定 Mapping 解决,比如:更新时动态计算、查询时临时加工字段、复杂条件聚合。这时就需要 Painless——ES 内置的脚本语言。

文档更新

最简单的脚本——给指定文档的价格 +1:

POST product/_update/2
{
  "script": {
    "source": "ctx._source.price += 1"
  }
}

使用参数化脚本——避免拼接字符串、可复用、安全:

POST product/_update/2
{
  "script": {
    "lang": "painless",
    "source": "ctx._source.price += params.num",
    "params": { "num": 1 }
  }
}

配合 upsert——文档存在则更新,不存在则创建:

POST product/_update/2
{
  "script": {
    "lang": "painless",
    "source": "ctx._source.price += params.num",
    "params": { "num": 1 }
  },
  "upsert": {
    "name": "xiaomi nfc phone2",
    "type": "c",
    "price": 2999,
    "date": "2023-05-01",
    "desc": "xiaomi 2023 new1",
    "tags": ["88vip", "tmall", "newer"]
  }
}

动态计算字段

查询时根据现有字段临时生成新字段的值,不改变索引数据

GET product/_search
{
  "script_fields": {
    "discount_price": {
      "script": {
        "source": "doc['price'].value * 0.9"
      }
    }
  }
}

返回多个计算值:

GET product/_search
{
  "script_fields": {
    "price": { "script": { "source": "doc['price'].value" } },
    "discounts": {
      "script": {
        "source": "[doc['price'].value * 0.9, doc['price'].value * 0.8, doc['price'].value * 0.7]"
      }
    }
  }
}

存储脚本(Stored Script)

把脚本保存到 ES 中,以后通过 id 引用:

POST _scripts/calc_x
{
  "script": {
    "lang": "painless",
    "source": "doc.price.value + params.num"
  }
}

// 引用已存储的脚本
GET product/_search
{
  "script_fields": {
    "discount": {
      "script": {
        "id": "calc_x",
        "params": { "num": 1 }
      }
    }
  }
}

聚合中使用脚本

计算价格 ≤ 2000 的商品中,每个商品 tags 的数量之和:

GET product/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "range": { "price": { "lte": 2000 } }
      }
    }
  },
  "aggs": {
    "tag_agg": {
      "sum": {
        "script": {
          "source": "doc['tags.keyword'].length"
        }
      }
    }
  }
}

性能要点

  1. 聚合优先使用 keyword 字段text 字段需要 fielddata=true 才能聚合,这会把正排索引加载到 JVM 堆中,容易 OOM。
  2. doc_values 是正排索引,以列式存储在磁盘上,排序和聚合直接读这个,不需要加载原文档。如果某个字段确定不需要排序或聚合,设 "doc_values": false 可以省磁盘空间。
  3. 先过滤再聚合,用 filter / constant_score 提前减少聚合数据量,能显著提升性能。
  4. size: 0 在聚合查询上设置,表示不需要返回原始文档,只返回聚合结果。
  5. 脚本尽量参数化,避免编译开销——Painless 每次编译脚本有 150 个/分钟的软限制。

总结

  • 指标聚合解决”是多少”,桶聚合解决”分几组”,管道聚合解决”组与组之间的关系”。
  • 直方图是范围分组的利器:Range 精细可控、Histogram 自动分桶、Date Histogram 按时间线聚合。
  • Painless 让 ES 从一个搜索引擎变成了能执行任意逻辑的数据处理引擎,但也要谨慎使用——在大量文档上用脚本做聚合,性能开销不小。