深入 Elasticsearch(09):搜索之上的实时分析
上一篇解决了 Query DSL 的查询体系——全文、精确、模糊、地理查询和 bool 组合。这一篇进入在搜索结果之上怎样做实时统计分析。
搜索返回匹配文档的列表。但很多场景不只需要文档列表,还需要统计信息:按类别分组计数、平均价格、日期趋势。ES 的 Aggregation 框架在搜索结果之上提供实时分析能力,不需要把数据导出到 OLAP 系统。
本文只抓一个问题:Aggregation 的三类聚合怎样工作,以及嵌套组合怎样构建复杂分析。
三类聚合
ES 的聚合分为三类,各有不同职责:
1 | |
Bucket 聚合按某种规则把文档分组——类似 SQL 的 GROUP BY。常见的 bucket 聚合:
| 聚合 | 分桶规则 | 类比 SQL |
|---|---|---|
terms |
按字段值分组 | GROUP BY field |
date_histogram |
按时间间隔分组 | GROUP BY DATE_TRUNC(field, interval) |
range |
按数值范围分组 | CASE WHEN … |
histogram |
按固定数值间隔分组 | WIDTH_BUCKET |
filters |
按多个查询条件分组 | CASE WHEN query1 THEN … |
Metric 聚合在文档集合上计算统计值——类似 SQL 的聚合函数:
| 聚合 | 计算 | 类比 SQL |
|---|---|---|
avg |
平均值 | AVG(field) |
sum |
求和 | SUM(field) |
min / max |
最小 / 最大值 | MIN / MAX |
value_count |
值计数 | COUNT(field) |
cardinality |
去重计数(近似) | COUNT(DISTINCT field) |
percentiles |
百分位数 | PERCENTILE_CONT |
stats |
min + max + avg + sum + count | 组合 |
Pipeline 聚合对其他聚合的输出做二次计算——没有直接的 SQL 对应:
| 聚合 | 作用 |
|---|---|
derivative |
对父聚合的 metric 做差值(环比增长) |
moving_avg |
移动平均 |
cumulative_sum |
累积求和 |
bucket_sort |
对桶按 metric 排序 |
bucket_selector |
按条件过滤桶 |
聚合与 Doc Values
聚合需要遍历大量文档的字段值来做分组和计算。这个需求恰好是 Doc Values 的设计目标——列式存储让聚合只需要顺序扫描一列数据。
倒排索引回答"哪些文档包含这个词",Doc Values 回答"这个文档的某个字段值是什么"。聚合用的是后者。
text 字段默认没有 Doc Values(因为分词后的词项列表通常不适合聚合)。对 text 字段做聚合需要启用 fielddata——这会把字段值加载到 JVM 堆内存中,代价高昂。推荐用 multi-field 同时映射为 keyword 类型,对 keyword 子字段做聚合。
嵌套聚合
聚合可以嵌套:在 bucket 内再放 bucket 或 metric。这是 ES 聚合框架最强大的特性——通过嵌套组合构建复杂分析。
三层嵌套的数据流向——外层 Bucket 把文档分桶,桶内嵌套子 Bucket 再分一层,最内层 Metric 在最细粒度的桶上算统计值:
graph TD
A["搜索结果集<br/>全部匹配文档"] --> B["date_histogram<br/>按月分桶"]
B --> C1["2026-01 桶<br/>3 docs"]
B --> C2["2026-02 桶<br/>3 docs"]
C1 --> D1["terms: electronics"]
C1 --> D2["terms: books"]
C2 --> D3["terms: electronics"]
C2 --> D4["terms: books"]
C2 --> D5["terms: clothing"]
D1 --> E1["avg_price: 224.99"]
D2 --> E2["avg_price: 29.99"]
D3 --> E3["avg_price: 599.99"]
D4 --> E4["avg_price: 19.99"]
D5 --> E5["avg_price: 79.99"]
style A fill:#f0f0f0,stroke:#333
style B fill:#4a90d9,color:#fff
style C1 fill:#5ba0e0,color:#fff
style C2 fill:#5ba0e0,color:#fff
style D1 fill:#7bc47f
style D2 fill:#7bc47f
style D3 fill:#7bc47f
style D4 fill:#7bc47f
style D5 fill:#7bc47f
style E1 fill:#f5a623
style E2 fill:#f5a623
style E3 fill:#f5a623
style E4 fill:#f5a623
style E5 fill:#f5a623
蓝色节点是 Bucket 聚合(分桶),绿色是子 Bucket(再分桶),橙色是 Metric 聚合(算值)。每一层嵌套对应 JSON 请求里的一层 aggs。
实验:创建一个带日期和分类的 index,做多层嵌套聚合:
1 | |
三层嵌套聚合:按月分桶 → 每月内按分类分桶 → 每个分类计算平均价格:
1 | |
size: 0 表示不返回搜索结果,只返回聚合。返回结构:
1 | |
聚合的执行路径
在分布式环境中,聚合和搜索一样走 scatter-gather:
1 | |
Pipeline 聚合的数据流和前两类不同——它不直接操作文档,而是接收其他聚合的输出作为输入:
graph LR
A["文档集合"] --> B["date_histogram<br/>按月分桶"]
B --> C["每月 sum<br/>月销售额"]
C --> D["cumulative_sum<br/>累积销售额"]
C --> E["derivative<br/>环比增长"]
subgraph "Bucket + Metric"
B
C
end
subgraph "Pipeline"
D
E
end
style A fill:#f0f0f0,stroke:#333
style B fill:#4a90d9,color:#fff
style C fill:#f5a623
style D fill:#e74c3c,color:#fff
style E fill:#e74c3c,color:#fff
Pipeline 聚合(红色)的输入是 Metric 聚合(橙色)的输出,不是原始文档。cumulative_sum 对每个桶的 sum 做累加,derivative 对相邻桶的 sum 做差值——两者都需要按顺序遍历桶序列,所以只能用在有序的 bucket 聚合(如 date_histogram、histogram)之上。
terms 聚合的精度问题:每个 shard 返回本地 top-N 的桶,coordinating node 合并。如果一个低频词在每个 shard 上都排不进 top-N,但全局加起来频率很高,它可能被遗漏。shard_size(默认 size × 1.5 + 10)可以增大每个 shard 返回的桶数来提高精度。
cardinality 聚合使用 HyperLogLog++ 算法做近似去重计数,结果不精确但内存开销恒定。精确去重在大数据量下成本过高。
模式提炼:分桶-度量-管道三层聚合
1 | |
| 层次 | 输入 | 输出 | 类比 |
|---|---|---|---|
| Bucket | 文档集合 | 多个分组(桶) | SQL GROUP BY |
| Metric | 一个桶内的文档 | 统计值(数字) | SQL AVG/SUM |
| Pipeline | 多个桶的 metric | 衍生统计值 | 窗口函数 |
工程迁移表
| 概念 | Elasticsearch | SQL (MySQL/PG) | Solr | MongoDB |
|---|---|---|---|---|
| 分桶 | Bucket aggregation | GROUP BY | Facet / JSON Facet | $group |
| 度量 | Metric aggregation | AVG/SUM/COUNT | stats facet | $avg / $sum |
| 嵌套分桶 | 嵌套 aggs | 子查询 / 窗口函数 | sub-facet | 嵌套 $group |
| 近似去重 | cardinality (HLL++) | COUNT(DISTINCT)(精确) | 无原生 | $addToSet(精确) |
| 百分位 | percentiles (t-digest) | PERCENTILE_CONT | 无原生 | 无原生 |
| 二次计算 | Pipeline aggs | 窗口函数 | 无 | $setWindowFields |
| 数据来源 | Doc Values(列式) | 行扫描或索引 | DocValues | 文档扫描 |
常见误解
误解一:聚合可以用在 text 字段上。 默认不行。text 字段没有 Doc Values。需要在 mapping 中配置 multi-field 的 keyword 子字段,对 keyword 做聚合。
误解二:cardinality 聚合是精确的。 cardinality 使用 HyperLogLog++ 算法,结果是近似值。precision_threshold 参数(默认 3000)控制精度——低于此值时通常精确,高于此值时有误差。
误解三:聚合总是很快。 聚合需要遍历所有匹配文档的 Doc Values。如果匹配文档数量很大(百万级),聚合可能耗时较长。用 query 缩小范围是优化聚合性能的第一步。
练习
-
在 agg-demo index 上做一个
terms聚合,按 category 分组并显示文档数。对比 ES 结果和等价 SQLSELECT category, COUNT(*) FROM agg_demo GROUP BY category。 -
在
date_histogram聚合的基础上嵌套一个summetric,计算每月总销售额。再用cumulative_sumpipeline 聚合计算累积销售额。 -
用
cardinality聚合统计不同 category 的数量,对比value_count的结果,理解去重计数和普通计数的区别。
系列导航
| 上一篇 | 下一篇 |
|---|---|
| Query DSL 深入:从 match 到 bool 的查询体系 | 分片与路由:数据分布的核心机制 |
