1 聚合的概述
- ElasticSearch 除了搜索功能外,还提供针对其数据的统计分析功能,这一功能通过**聚合(aggregations)**实现;
- 作用 :能够极其方便地实现对数据的统计、分析、运算;
- 例 :
- 什么品牌的手机最受欢迎?(用于分析类别维度的"热门程度")
- 这些手机的平均价格、最高价格、最低价格?(用于分析数值维度的"统计指标")
- 这些手机每月的销售情况如何?(用于分析时间维度的"趋势变化")
1.1 聚合的使用场景
- 聚合查询的适用场景非常广泛,典型包括商业智能、数据挖掘、日志分析 等领域,以下是具体行业/场景的应用示例:
-
电商平台的销售分析:统计每个地区的销售额、每个用户的消费总额、每个产品的销售量等,用于了解销售情况和趋势
-
社交媒体的用户行为分析:统计每个用户的发布次数、转发次数、评论次数等;还可按地区、时间、话题等维度拆分分析,用于理解用户行为和趋势
-
物流企业的运输分析:统计每个区域的运输量、每个车辆的运输次数、每个司机的行驶里程等,用于了解运输情况和优化运输效率
-
金融企业的交易分析:统计每个客户的交易总额、每个产品的销售量、每个交易员的业绩等,用于了解交易情况和优化业务流程
-
智能家居的设备监控分析:统计每个设备的使用次数、每个家庭的能源消耗量、每个时间段的设备使用率等,用于了解用户需求和优化设备效能
-
1.2 基本语法
-
聚合查询的语法结构与ElasticSearch其他查询相似,通常包含以下三部分:
-
查询条件 :指定需要聚合的文档,可使用标准的ElasticSearch查询语法,如
term、match、range等; -
聚合函数 :指定要执行的聚合操作,如
sum(求和)、avg(求平均)、min(最小值)、max(最大值)、terms(桶聚合,用于分组统计)、date_histogram(基于日期的直方图聚合)等;每个聚合命令都会生成一个聚合结果; -
聚合嵌套:聚合命令可以嵌套,以便更细粒度地分析数据;
-
-
语法示例结构:
jsonGET <index_name>/_search { "aggs": { "<aggs_name>": { // 聚合名称需要自己定义 "agg_type": { "field": "<field_name>" } } } }-
aggs_name:聚合函数的名称,由使用者自定义
-
agg_type :聚合种类,比如是桶聚合(
terms)或者是指标聚合(avg、sum、min、max等) -
field_name:字段名称或者叫域名,即要对哪个字段执行聚合操作
-
2 聚合的分类
2.1 概述与示例数据
-
ElasticSearch 的聚合主要分为三类,每类都有明确的功能和类比场景;
-
Metric Aggregation(指标聚合) :执行数学运算,对文档字段进行统计分析,类似 MySQL 中的
min()、max()、sum()等聚合函数操作;-
MySQL 语句:
SELECT MIN(price), MAX(price) FROM products(统计产品价格的最小值和最大值); -
ElasticSearch DSL 类比实现(以"求价格平均值"为例):
json{ "aggs":{ "avg_price":{ // 聚合名称 "avg":{ // 求平均值 "field":"price" } } } }
-
-
Bucket Aggregation(桶聚合) :将满足特定条件的文档集合放入"桶"中,每个桶关联一个
key,类似 MySQL 中的GROUP BY操作;-
MySQL 语句:
SELECT size, COUNT(*) FROM products GROUP BY size(按产品尺寸分组并统计数量); -
ElasticSearch DSL 类比实现(以"按尺寸分组"为例):
json{ "aggs": { "by_size": { // 聚合名称 "terms": { // 桶聚合 "field": "size" } } } }
-
-
Pipeline Aggregation(管道聚合) :对其他聚合的结果进行二次聚合,实现更复杂的多维度统计分析;
-
-
示例数据
-
索引与映射操作
-
删除索引 (可选清理操作):
DELETE /employees -
创建索引并定义映射:
jsonPUT /employees { "mappings": { "properties": { "age": { "type": "integer" }, // 整数类型,存储年龄 "gender": { "type": "keyword" }, // keyword类型,存储性别(适合精确匹配、分组) "job": { // 文本类型,同时开启keyword子字段(用于分组、聚合) "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 50 } } }, "name": { "type": "keyword" }, // keyword类型,存储姓名(精确匹配、分组) "salary": { "type": "integer" } // 整数类型,存储薪资 } } }
-
-
批量插入文档:通过
_bulkAPI 批量插入 20 条员工数据,每条数据包含name、age、job、gender、salary字段,示例片段如下:jsonPUT /employees/_bulk { "index" : { "_id" : "1" } } { "name" : "Emma","age":32,"job":"Product Manager","gender":"female","salary":35000 } { "index" : { "_id" : "2" } } { "name" : "Underwood","age":41,"job":"Dev Manager","gender":"male","salary": 50000} { "index" : { "_id" : "3" } } { "name" : "Tran","age":25,"job":"Web Designer","gender":"male","salary":18000 } { "index" : { "_id" : "4" } } { "name" : "Rivera","age":26,"job":"Web Designer","gender":"female","salary": 22000} { "index" : { "_id" : "5" } } { "name" : "Rose","age":25,"job":"QA","gender":"female","salary":18000 } { "index" : { "_id" : "6" } } { "name" : "Lucy","age":31,"job":"QA","gender":"female","salary": 25000} { "index" : { "_id" : "7" } } { "name" : "Byrd","age":27,"job":"QA","gender":"male","salary":20000 } { "index" : { "_id" : "8" } } { "name" : "Foster","age":27,"job":"Java Programmer","gender":"male","salary": 20000} { "index" : { "_id" : "9" } } { "name" : "Gregory","age":32,"job":"Java Programmer","gender":"male","salary":22000 } { "index" : { "_id" : "10" } } { "name" : "Bryant","age":20,"job":"Java Programmer","gender":"male","salary": 9000} { "index" : { "_id" : "11" } } { "name" : "Jenny","age":36,"job":"Java Programmer","gender":"female","salary":38000 } { "index" : { "_id" : "12" } } { "name" : "Mcdonald","age":31,"job":"Java Programmer","gender":"male","salary": 32000} { "index" : { "_id" : "13" } } { "name" : "Jonthna","age":30,"job":"Java Programmer","gender":"female","salary":30000 } { "index" : { "_id" : "14" } } { "name" : "Marshall","age":32,"job":"Javascript Programmer","gender":"male","salary": 25000} { "index" : { "_id" : "15" } } { "name" : "King","age":33,"job":"Java Programmer","gender":"male","salary":28000 } { "index" : { "_id" : "16" } } { "name" : "Mccarthy","age":21,"job":"Javascript Programmer","gender":"male","salary": 16000} { "index" : { "_id" : "17" } } { "name" : "Goodwin","age":25,"job":"Javascript Programmer","gender":"male","salary": 16000} { "index" : { "_id" : "18" } } { "name" : "Catherine","age":29,"job":"Javascript Programmer","gender":"female","salary": 20000} { "index" : { "_id" : "19" } } { "name" : "Boone","age":30,"job":"DBA","gender":"male","salary": 30000} { "index" : { "_id" : "20" } } { "name" : "Kathy","age":29,"job":"DBA","gender":"female","salary": 20000}
-
2.2 指标聚合
-
指标聚合分为单值分析 和多值分析两类,各自包含不同的聚合函数:
-
单值分析:只输出一个分析结果,包含以下聚合函数
-
min(最小值)、max(最大值)、avg(平均值)、sum(求和) -
Cardinality(类似 SQL 中的distinct Count,用于统计去重后的数量)
-
-
多值分析:输出多个分析结果,包含以下聚合函数
-
stats(基础统计,包含 min、max、avg、sum、count)、extended stats(扩展统计,增加方差、标准差等) -
percentile(百分位,如计算第 50 百分位数即中位数)、percentile rank(百分位排名) -
top hits(返回排在前面的文档示例)
-
-
-
多指标聚合 。例:查询员工的最低、最高和平均工资。通过同时定义
max、min、avg三个单值聚合,分别统计薪资的最大值、最小值和平均值:jsonPOST /employees/_search { "size": 0, // 不返回匹配查询条件的原始文档数据,只返回聚合(或其他统计类)结果 "aggs": { "max_salary": { "max": { "field": "salary" } }, "min_salary": { "min": { "field": "salary" } }, "avg_salary": { "avg": { "field": "salary" } } } } -
多值聚合:对 salary 进行统计(stats 聚合)。通过
stats聚合一次性输出薪资的多个统计指标(min、max、avg、sum、count):jsonPOST /employees/_search { "size": 0, "aggs": { "stats_salary": { "stats": { "field": "salary" } } } } -
Cardinality 聚合:对搜索结果去重(统计不同岗位的数量)。通过
cardinality聚合统计job.keyword字段的去重数量(即不同岗位的种类数):jsonPOST /employees/_search { "size": 0, "aggs": { "cardinate": { "cardinality": { "field": "job.keyword" } } } }
2.3 桶聚合
2.3.1 概述
-
桶聚合是按照一定规则,将文档分配到不同的"桶"中,从而达到分类统计的目的;
-
ElasticSearch 提供了多种桶聚合方式,可分为以下类别:
-
Terms 聚合:基于字段的"词项"进行分组(类似 SQL 的
GROUP BY);- 字段支持要求:
keyword类型字段默认支持 fielddata,可直接用于 Terms 聚合;text类型字段需在 Mapping 中显式开启 fielddata,且会按照分词后的结果进行分桶;
- 字段支持要求:
-
数字类型相关的桶聚合
-
Range / Date Range:对数字或日期字段按"区间"分组(如年龄分"20-30岁""30-40岁"区间); -
Histogram(直方图) / Date Histogram:对数字或日期字段按"固定间隔"分组(如薪资每 5000 为一个区间,日期每天为一个区间);
-
-
-
桶聚合支持嵌套,即可以在一个桶内再做分桶,实现更细粒度的多层级分析;
-
桶聚合的应用非常广泛,典型场景包括:
-
对数据进行分组统计:如按照地区、年龄段、性别等字段分组统计数量或指标
-
对时间序列数据进行时间段分析:如按照每小时、每天、每月、每季度、每年等时间段拆分分析
-
对各种标签信息分类并统计数量:如电商商品评价的"好评标签""差评标签"分类统计(如图中电商商品评价的"时尚简约""漂亮大方"等标签分组)
-
-
下图中展示了电商平台"商品评价"的实际场景,其中:
-
评价标签(如"时尚简约(50)""漂亮大方(34)")是通过Terms 聚合实现的:将包含相同标签的评价文档归入同一"桶",并统计每个桶的文档数量(即标签出现次数)
-
评价等级(好评、中评、差评)的分组统计,也是桶聚合的典型应用,通过对"评价等级"字段分组,实现各等级评价的数量统计

-
2.3.2 例:获取 job 的分类信息
-
通过
terms聚合对job.keyword字段进行分组统计,实现岗位类别的分类分析:jsonGET /employees/_search { "size": 0, // 不返回匹配查询条件的原始文档数据,只返回聚合(或其他统计类)结果 "aggs": { "jobs": { // 自定义聚合名称 "terms": { "field": "job.keyword" // 对岗位的keyword字段进行分组 } } } } -
聚合支持以下关键属性来定制分析逻辑:
-
field :指定聚合操作的字段(如上述示例中的
job.keyword) -
size:指定返回的聚合结果数量(控制输出的"桶"数量)
-
order:指定聚合结果的排序方式
-
-
默认情况下,Bucket 聚合会统计每个桶内的文档数量(记为
_count),并按_count降序排序。可以通过order属性自定义排序逻辑,示例如下:jsonGET /employees/_search { "size": 0, "aggs": { "jobs": { "terms": { "field": "job.keyword", "size": 10, // 返回前10个岗位分类的聚合结果 "order": { "_count": "desc" // 按文档数量降序排序(也可改为"asc"升序) } } } } }
2.3.3 限定聚合范围
-
通过
query子句可以过滤出符合条件的文档,再对这些文档执行聚合操作。下面示例中是对薪资≥10000元 的员工文档,按岗位(job.keyword)进行Terms聚合:jsonGET /employees/_search { "query": { "range": { // 范围查询,筛选薪资≥10000的文档 "salary": { "gte": 10000 } } }, "size": 0, // 不返回原始文档,只返回聚合结果 "aggs": { "jobs": { "terms": { "field": "job.keyword", // 对岗位的keyword字段分组 "size": 10, // 返回前10个岗位分类 "order": { "_count": "desc" // 按文档数量降序排序 } } } } } -
问题:直接对Text字段执行Terms聚合会失败。Text字段默认不支持聚合操作,若强行执行会抛出异常。下面示例中对
job(Text类型)执行Terms聚合:jsonPOST /employees/_search { "size": 0, "aggs": { "jobs": { "terms": { "field": "job" // 直接对Text字段聚合,会报错 } } } } -
异常原因:
Text fields are not optimised for operations that require per-document field data like aggregations...(Text字段默认不支持聚合,需改用keyword字段或开启fielddata);
-
解决:对Text字段开启fielddata支持。通过更新Mapping,为Text字段启用
fielddata,使其支持聚合:jsonPUT /employees/_mapping { "properties": { "job": { "type": "text", "fielddata": true // 开启fielddata,支持聚合 } } } -
开启后,可对Text字段的分词结果执行Terms聚合:
jsonPOST /employees/_search { "size": 0, "aggs": { "jobs": { "terms": { "field": "job" // 对Text字段的分词结果分组 } } } } -
对
job.keyword(Keyword类型)和job(开启fielddata的Text类型)执行Terms聚合,分桶总数会不同:-
job.keyword是按"完整岗位名称"分组(如"Java Programmer"作为一个桶); -
job(Text类型,开启fielddata)是按"分词结果"分组(如"Java""Programmer"会被拆分为不同桶);
-
-
可通过
cardinality聚合对比去重数量,示例:jsonPOST /employees/_search { "size": 0, "aggs": { "cardinate": { "cardinality": { // 统计去重后的数量 "field": "job" // 对比job和job.keyword的结果差异 } } } }
2.3.4 Range 聚合
-
按照数字范围对文档进行分桶,且支持自定义桶的
key; -
例:按工资范围分桶(自定义桶标识)
jsonPOST employees/_search { "size": 0, "aggs": { "salary_range": { "range": { "field": "salary", // 聚合字段为薪资 "ranges": [ { "to": 10000 }, // 薪资 < 10000 { "from": 10000, "to": 20000 }, // 10000 ≤ 薪资 < 20000 { "key": ">20000", "from": 20000 } // 自定义桶标识,薪资 ≥ 20000 ] } } } }
2.3.5 Histogram聚合
-
按照固定数值间隔 对数字字段分桶,可通过
extended_bounds定义桶的范围; -
例:工资按 5000 为间隔分桶(范围 0~100000)
jsonPOST employees/_search { "size": 0, "aggs": { "salary_histrogram": { "histogram": { "field": "salary", // 聚合字段为薪资 "interval": 5000, // 每 5000 为一个桶 "extended_bounds": { // 强制桶范围为 0~100000 "min": 0, "max": 100000 } } } } }
2.3.6 top_hits 聚合
-
获取分桶后,桶内最匹配的顶部文档列表(可指定数量和排序规则);
-
例:按岗位分组后,获取每个岗位中年龄最大的 3 名员工信息
jsonPOST /employees/_search { "size": 0, "aggs": { "jobs": { "terms": { "field": "job.keyword" // 按岗位分组 }, "aggs": { "old_employee": { "top_hits": { "size": 3, // 每个桶返回 3 条文档 "sort": [ // 按年龄降序排序 { "age": { "order": "desc" } } ] } } } } } }
2.3.7 嵌套聚合
-
核心逻辑 :在一个聚合的结果上,再嵌套执行另一层聚合,实现多维度分层分析;
-
示例 1:两层嵌套(岗位 + 薪资统计) 。按岗位分组后,对每个岗位的薪资执行
stats聚合(统计 min、max、avg 等):jsonPOST employees/_search { "size": 0, "aggs": { "Job_salary_stats": { "terms": { "field": "job.keyword" // 第一层:按岗位分组 }, "aggs": { "salary": { "stats": { "field": "salary" // 第二层:统计每个岗位的薪资指标 } } } } } } -
示例 2:三层嵌套(岗位 + 性别 + 薪资统计) 。按岗位分组 → 再按性别分组 → 对每个性别组的薪资执行
stats聚合:jsonPOST employees/_search { "size": 0, "aggs": { "Job_gender_stats": { "terms": { "field": "job.keyword" // 第一层:按岗位分组 }, "aggs": { "gender_stats": { "terms": { "field": "gender" // 第二层:按性别分组 }, "aggs": { "salary_stats": { "stats": { "field": "salary" // 第三层:统计每个性别组的薪资指标 } } } } } } } }
2.4 管道聚合
2.4.1 概述
-
管道聚合的核心是对聚合分析的结果再次进行聚合分析,其结果在原聚合中的输出位置分为两类:
-
Sibling(同级):结果与现有分析结果同级,包含以下类型:
Max Bucket、min Bucket、Avg Bucket、Sum BucketStats Bucket、Extended Stats BucketPercentiles Bucket
-
Parent(内嵌):结果内嵌到现有聚合分析结果之中,包含以下类型:
Derivative(求导)Cumulative Sum(累计求和)Moving Function(移动平均值)
-
2.4.2 min_bucket 示例
-
需求:在员工数最多的工种里,找出平均工资最低的工种;
-
实现:
jsonPOST employees/_search { "size": 0, "aggs": { "jobs": { // 第一层:按岗位分组 "terms": { "field": "job.keyword", "size": 10 }, "aggs": { "avg_salary": { // 第二层:统计每个岗位的平均薪资 "avg": { "field": "salary" } } } }, "min_salary_by_job": { // 管道聚合:找出平均薪资最低的岗位(与jobs同级) "min_bucket": { "buckets_path": "jobs>avg_salary" // 指定聚合路径 } } } } -
说明:
min_bucket用于求之前聚合结果的最小值;- 通过
buckets_path指定聚合路径,min_salary_by_job的结果与jobs聚合同级。
2.4.3 stats_bucket 示例
-
需求:对平均工资进行统计分析(含 min、max、avg、sum、count 等指标);
-
实现 :
jsonPOST employees/_search { "size": 0, "aggs": { "jobs": { "terms": { "field": "job.keyword", "size": 10 }, "aggs": { "avg_salary": { "avg": { "field": "salary" } } } }, "stats_salary_by_job": { // 管道聚合:对平均薪资执行多指标统计 "stats_bucket": { "buckets_path": "jobs>avg_salary" } } } }
2.4.4 percentiles_bucket 示例
-
需求:计算平均工资的百分位数(如第 25、50、75 百分位等)。
-
实现 :
jsonPOST employees/_search { "size": 0, "aggs": { "jobs": { "terms": { "field": "job.keyword", "size": 10 }, "aggs": { "avg_salary": { "avg": { "field": "salary" } } } }, "percentiles_salary_by_job": { // 管道聚合:计算平均薪资的百分位数 "percentiles_bucket": { "buckets_path": "jobs>avg_salary" } } } }
2.4.5 Cumulative Sum 示例
-
需求:对平均薪资进行累计求和。
-
实现 :
jsonPOST employees/_search { "size": 0, "aggs": { "age": { // 第一层:按年龄直方图分桶(间隔1岁) "histogram": { "field": "age", "min_doc_count": 0, "interval": 1 }, "aggs": { "avg_salary": { // 第二层:统计每个年龄桶的平均薪资 "avg": { "field": "salary" } }, "cumulative_salary": { // 管道聚合:对平均薪资累计求和(内嵌到age聚合中) "cumulative_sum": { "buckets_path": "avg_salary" } } } } } }
2.5 聚合的作用范围
2.5.1 概述
- ElasticSearch 聚合分析的默认作用范围是
query的查询结果集 ,同时支持以下方式改变聚合的作用范围:-
Filter:在聚合内部通过过滤器限定聚合的文档范围 -
Post Filter:在聚合之后对结果进行过滤,不影响聚合的作用范围 -
Global:聚合作用于所有文档 (无视query的过滤条件)
-
2.5.2 Query 作用范围(默认)
-
聚合作用于
query过滤后的文档集; -
例:对年龄≥20岁 的员工,按岗位(
job.keyword)进行 Terms 聚合:josnPOST employees/_search { "size": 0, "query": { "range": { "age": { "gte": 20 } } }, "aggs": { "jobs": { "terms": { "field": "job.keyword" } } } }
2.5.3 Filter 作用范围
-
在聚合内部通过
filter限定部分聚合的文档范围,同时可保留全局聚合; -
例:
-
older_person聚合仅作用于年龄≥35岁的员工,统计其岗位分布; -
all_jobs聚合作用于所有员工,统计全部岗位分布:javaPOST employees/_search { "size": 0, "aggs": { "older_person": { "filter": { "range": { "age": { "from": 35 } } }, "aggs": { "jobs": { "terms": { "field": "job.keyword" } } } }, "all_jobs": { "terms": { "field": "job.keyword" } } } }
-
2.5.4 Post Filter 作用范围
-
聚合作用于所有文档,聚合完成后再对结果进行过滤;
-
例:先统计所有岗位分布,再过滤出包含"Dev Manager"的结果
jsonPOST employees/_search { "aggs": { "jobs": { "terms": { "field": "job.keyword" } } }, "post_filter": { "match": { "job.keyword": "Dev Manager" } } }
2.5.5 Global 作用范围
-
聚合作用于所有文档 (无视
query的过滤条件); -
例:
query过滤"年龄≥40岁"的员工,但global聚合会统计所有这些员工的平均薪资(即查询范围内的全部文档)jsonPOST employees/_search { "size": 0, "query": { "range": { "age": { "gte": 40 } } }, "aggs": { "jobs": { "terms": { "field": "job.keyword" } }, "all": { "global": {}, "aggs": { "salary_avg": { "avg": { "field": "salary" } } } } } }
2.6 排序
2.6.1 概述
- 在 ElasticSearch 聚合(尤其是 Terms 聚合)中,可通过
order指定排序逻辑:-
默认情况下,按照桶内文档数量(
_count)降序排序 -
可通过
size指定返回的桶数量
-
2.6.2 按 _count 和 _key 排序
-
对"年龄≥20岁"的员工,按岗位(
job.keyword)分组后,先按桶内文档数(_count)升序、再按岗位名称(_key)降序排列:jsonPOST employees/_search { "size": 0, "query": { "range": { "age": { "gte": 20 } } }, "aggs": { "jobs": { "terms": { "field": "job.keyword", "order": [ { "_count": "asc" }, { "_key": "desc" } ] } } } }
2.6.2 按聚合指标(平均薪资)排序
-
先对岗位分组,再统计每个岗位的平均薪资(
avg_salary),最后按平均薪资降序排列岗位:jsonPOST employees/_search { "size": 0, "aggs": { "jobs": { "terms": { "field": "job.keyword", "order": [ { "avg_salary": "desc" } ] }, "aggs": { "avg_salary": { "avg": { "field": "salary" } } } } } }
2.6.3 按统计指标(薪资最小值)排序
-
先对岗位分组,再对每个岗位的薪资执行
stats聚合(获取 min、max、avg 等),最后按薪资最小值降序排列岗位:jsonPOST employees/_search { "size": 0, "aggs": { "jobs": { "terms": { "field": "job.keyword", "order": [ { "stats_salary.min": "desc" } ] }, "aggs": { "stats_salary": { "stats": { "field": "salary" } } } } } }
3 ES聚合分析不准确的原因及解决
3.1 原因
-
ES聚合分析不精准的根本原因:
- ElasticSearch 在对海量数据进行聚合分析时,会牺牲部分精准度来满足实时性需求;
- 其技术层面的核心原因是:数据分散在多个分片上,聚合时每个分片先取"Top X"(如前3名),再由协调节点合并结果。这种"分片级局部Top + 协调节点全局合并"的机制,会导致最终结果可能不精准(因为分片外的真实Top数据可能未被纳入计算);
-
在数据处理场景中,存在以下三角权衡关系,且只能同时满足其中两条:
-
Hadoop离线计算 :追求精确度 和大数据量处理 ,但实时性差;
-
近似计算(如ES聚合) :追求数据量 和实时性 ,但精确度有损失;
-
有限数据计算 :追求精确度 和实时性 ,但数据量有限;

-
-
以"获取Top 3岗位"的Terms聚合为例,执行流程和不精准性可拆解为:
-
执行流程
-
每个数据分片(如Node1、Node2、Node3)先各自计算本地的"Top 3"结果;
-
协调节点(Coordinating Nodes)收集所有分片的本地Top 3,再合并出最终的"Top 3";

-
-
这种流程会导致结果不精准,以下图为例:
-
分片1的本地数据:A(6)、B(4)、C(4)、D(3) → 本地Top 3是A、B、C;
-
分片2的本地数据:A(6)、B(2)、C(1)、D(3) → 本地Top 3是A、D、B;
-
协调节点合并后结果:A(12)、B(6)、C(4) → 但真实全量数据中,D可能有更高的真实计数(如分片1的D(3) + 分片2的D(3) = 6),却因未进入分片本地Top 3而被遗漏,最终导致返回的Top 3不精准;

-
-
3.2 解决方案一:设置主分片为1
- 原理:将索引的主分片数设置为1,避免数据分散在多个分片上,从而让聚合在单个分片的全量数据上执行,保证精准度;
- 注意点:ElasticSearch 7.x版本已默认将主分片数设为1;
- 适用场景:数据量小的小集群、小规模业务场景(数据量小,性能开销可接受)。
3.3 解决方案2:调大 shard_size 值
-
原理:
shard_size指每个分片上聚合的数据条数,需大于等于size(size是聚合结果的返回数量);- 官方推荐公式:
shard_size = size * 1.5 + 10。shard_size越大,结果越趋近于精准聚合结果;size:聚合结果的返回值(如期望返回前3名,size就是3);shard_size:每个分片上聚合的数据条数;
- 可通过
show_term_doc_count_error参数显示最差情况下的错误值,辅助确定shard_size大小;
-
适用场景:数据量大、分片数多的集群业务场景;
-
测试:使用Kibana的测试数据
jsonDELETE my_flights PUT my_flights { "settings": { "number_of_shards": 20 }, "mappings" : { "properties" : { "AvgTicketPrice" : { "type" : "float" }, "Cancelled" : { "type" : "boolean" }, "Carrier" : { "type" : "keyword" }, "Dest" : { "type" : "keyword" }, "DestAirportID" : { "type" : "keyword" }, "DestCityName" : { "type" : "keyword" }, "DestCountry" : { "type" : "keyword" }, "DestLocation" : { "type" : "geo_point" }, "DestRegion" : { "type" : "keyword" }, "DestWeather" : { "type" : "keyword" }, "DistanceKilometers" : { "type" : "float" }, "DistanceMiles" : { "type" : "float" }, "FlightDelay" : { "type" : "boolean" }, "FlightDelayMin" : { "type" : "integer" }, "FlightDelayType" : { "type" : "keyword" }, "FlightNum" : { "type" : "keyword" }, "FlightTimeHour" : { "type" : "keyword" }, "FlightTimeMin" : { "type" : "float" }, "Origin" : { "type" : "keyword" }, "OriginAirportID" : { "type" : "keyword" }, "OriginCityName" : { "type" : "keyword" }, "OriginCountry" : { "type" : "keyword" }, "OriginLocation" : { "type" : "geo_point" }, "OriginRegion" : { "type" : "keyword" }, "OriginWeather" : { "type" : "keyword" }, "dayOfWeek" : { "type" : "integer" }, "timestamp" : { "type" : "date" } } } } POST _reindex { "source": { "index": "kibana_sample_data_flights" }, "dest": { "index": "my_flights" } } GET my_flights/_count GET kibana_sample_data_flights/_search { "size": 0, "aggs": { "weather": { "terms": { "field":"OriginWeather", "size":5, "show_term_doc_count_error":true } } } } GET my_flights/_search { "size": 0, "aggs": { "weather": { "terms": { "field":"OriginWeather", "size":5, "shard_size":10, "show_term_doc_count_error":true } } } }
- 当未调整
shard_size(或shard_size较小时),聚合结果可能存在偏差; - 调大
shard_size后,通过show_term_doc_count_error可查看错误范围(doc_count_error_upper_bound是被遗漏的term分桶包含文档的最大可能值;sum_other_doc_count是除返回结果外其他term的文档总数),结果精准度显著提升;
- 当未调整
3.4 解决方案3:将 size 设置为全量值
- 原理 :将
size设置为2^32 - 1(分片支持的最大值),确保聚合包含所有可能的候选数据,从而解决精度问题; - 注意 :ElasticSearch 1.x版本中
size=0代表全量,高版本已取消该逻辑,因此需设置最大值; - 弊端:若分片数据量极大,会消耗巨大的CPU资源用于排序,且可能阻塞网络;
- 适用场景:对聚合精准度要求极高的业务场景(因性能问题,不推荐常规使用)。
3.5 解决方案4:使用Clickhouse/Spark进行精准聚合
- 原理:借助Clickhouse(列式存储、OLAP引擎)或Spark(分布式计算框架)的强大计算能力,在海量数据场景下实现精准聚合;
- 适用场景:数据量非常大、聚合精度要求高、响应速度快的业务场景(需依赖外部计算引擎,脱离ElasticSearch自身聚合能力)。
4 ElasticSearch 聚合性能优化
4.1 插入数据时对索引进行预排序
-
索引预排序(Index Sorting)在插入数据时对索引进行预排序 ,而非查询时再排序,以此提升范围查询(range query)和排序操作的性能;
-
这是 Elasticsearch 6.X 及之后版本才有的特性;
-
配置方法:创建新索引时,可配置每个分片内的段如何排序。例:
jsonPUT /my_index { "settings": { "index": { "sort.field": "create_time", // 指定排序字段(如创建时间) "sort.order": "desc" // 指定排序顺序(降序) } }, "mappings": { "properties": { "create_time": { "type": "date" // 字段类型为日期 } } } } -
预排序会增加 Elasticsearch 的写入成本:
- 在某些场景下,开启索引预排序会导致约 40%-50% 的写性能下降;
- 因此,若业务场景更关注写入性能,不建议开启索引预排序;
- 若业务更依赖范围查询和排序的读性能,则可考虑使用。
4.2 使用节点查询缓存
-
**节点查询缓存(Node Query Cache)用于有效缓存 过滤器(filter)**操作的结果。若多次执行同一
filter操作,缓存可大幅提升性能;但过滤器中任意值的修改,都需要重新计算并缓存新结果; -
使用方式 :在包含
filter的查询请求中,Elasticsearch 会自动尝试使用节点查询缓存优化性能。例:jsonGET /your_index/_search { "query": { "bool": { "filter": { "term": { "your_field": "your_value" } } } } }
4.3 使用分片请求缓存
-
触发条件 :在聚合语句中设置
size: 0(含义:只返回聚合结果,不返回查询结果),即可使用分片请求缓存存储结果; -
例:
jsonGET /es_db/_search { "size": 0, "aggs": { "remark_agg": { "terms": { "field": "remark.keyword" } } } }
4.4 拆分聚合,使聚合并行化
-
背景 :Elasticsearch 中多个聚合条件默认不是并行运行 的。若 CPU 资源充足,可将多个聚合拆分为多个查询,借助
_msearch实现并行聚合,从而缩短响应时间; -
常规多条件聚合(串行执行) :
jsonGET /employees/_search { "size": 0, "aggs": { "job_agg": { "terms": { "field": "job.keyword" } }, "max_salary": { "max": { "field": "salary" } } } } -
_msearch并行聚合(拆分后并行执行) :jsonGET _msearch {"index":"employees"} {"size":0,"aggs":{"job_agg":{"terms":{"field": "job.keyword"}}}} {"index":"employees"} {"size":0,"aggs":{"max_salary":{"max":{"field": "salary"}}}}