Elasticsearch应用(十)
1.为什么需要聚合操作
聚合可以让我们极其方便的实现对数据的统计、分析、运算,例如:
- 什么品牌的手机最受欢迎?
- 这些手机的平均价格、最高价格、最低价格?
- 这些手机每月的销售情况如何?
2.什么是聚合
聚合(aggregations)可以实现对文档数据的统计,分析,运算,在做聚合的时候往往是与文档过滤一起用的,一般不会对整个索引库进行聚合操作
3.聚合语法
4.聚合分类
- 桶聚合(Bucket): 用来对文档做分组
- TermAggregation: 按照文档字段值分组
- Date Histogram: 按照日期阶梯分组,例如一周为一组,或者一月为一组
- 度量聚合(Metric): 用以计算一些值,比如:最大值,最小值,平均值等
- Avg: 求平均值
- Max: 求最大值
- Min: 求最小值
- Stats: 同时求max,min,avg,sum等
- 管道聚合(pipeline): 其他聚合的结果为基础做聚合
- Matrix: 支持对多个字段的操作并提供一个结果矩阵
5.桶聚合
介绍
按照一定的规则,将文档分配到不同的桶中,从而达到分类的目的。ES提供的一些常见的
Bucket Aggregation
TermAggregation
需求
统计所有数据中的酒店品牌有几种,此时可以根据酒店品牌的名称做聚合
示例
默认排序规则
默认情况下,Bucket聚合会统计Bucket内的文档数量,记为_count,并且按照_count降序排序。我们可以修改结果排序方式:
注意
默认情况下,Bucket聚合是对索引库的所有文档做聚合,我们可以限定要聚合的文档范围,只要添加query条件即可
分词字段不行,必须是不能分词的字段
聚合还会返回查询的文档,所以建议设置size为0,因为聚合一般不需要返回文档,这样能提高性能
6.度量聚合
介绍
—些数学运算,可以对文档字段进行统计分析,类比MySQL中的 min(),max(),sum() 操作
States
介绍
同时求max,min,avg,sum等
需求
获取每个品牌的用户评分的min,max,avg等
示例
按照子聚合字段排序
7.聚合的作用范围
介绍
ES聚合分析的默认作用范围是query的查询结果集,同时ES还支持以下方式改变聚合的作用范围:Filter,Post Filter,Global
query
{
"size": 0,
"query": {
"range": {
"age": {
"gte": 20
}
}
},
"aggs": {
"jobs": {
"terms": {
"field": "job.keyword"
}
}
}
}
Filter
{
"size": 0,
"aggs": {
"older_person": {
"filter": {
"range": {
"age": {
"from": 35
}
}
},
"aggs": {
"jobs": {
"terms": {
"field": "job.keyword"
}
}
}
},
"all_jobs": {
"terms": {
"field": "job.keyword"
}
}
}
}
Post Filter
{
"aggs": {
"jobs": {
"terms": {
"field": "job.keyword"
}
}
},
"post_filter": {
"match": {
"job.keyword": "Dev Manager"
}
}
}
Global
{
"size": 0,
"query": {
"range": {
"age": {
"gte": 40
}
}
},
"aggs": {
"jobs": {
"terms": {
"field": "job.keyword"
}
},
"all": {
"global": {},
"aggs": {
"salary_avg": {
"avg": {
"field": "salary"
}
}
}
}
}
}
8.ES聚合分析不精准
介绍
ElasticSearch在对海量数据进行聚合分析的时候会损失搜索的精准度来满足实时性的需求
原因
从Terms聚合分析的执行流程来看
数据分散到多个分片,聚合是每个分片的取Top X,导致结果不精准。ES可以不每个分片Top X,而是全量聚合,但势必这会有很大的性能问题
如何提高聚合精确度
- 设置主分片为1
- 调大 shard_size 值
- 将size设置为全量值,来解决精度问题
- 使用Clickhouse/ Spark 进行精准聚合
设置主分片为1
注意
注意7.x版本已经默认为1
适用场景
数据量小的小集群规模业务场景
调大 shard_size 值
介绍
设置 shard_size 为比较大的值,官方推荐:size*1.5+10。shard_size 值越大,结果越趋近于精准聚合结果值。此外,还可以通过show_term_doc_count_error参数显示最差情况下的错误值,用于辅助确定 shard_size 大小
参数说明
- size:是聚合结果的返回值,客户期望返回聚合排名前三,size值就是 3
- shard_size: 每个分片上聚合的数据条数。shard_size 原则上要大于等于size
适用场景
数据量大、分片数多的集群业务场景
Terms聚合返回值
- doc_count_error_upper_bound : 被遗漏的term 分桶,包含的文档,有可能的最大值
- sum_other_doc_count: 除了返回结果 bucket的terms以外,其他 terms 的文档总数(总数-返回的总数)
将size设置为全量值,来解决精度问题
介绍
将size设置为2的32次方减去1也就是分片支持的最大值,来解决精度问题
原因
1.x版本,size等于 0 代表全部,高版本取消 0 值,所以设置了最大值(大于业务的全量值)
缺点
全量带来的弊端就是:如果分片数据量极大,这样做会耗费巨大的CPU 资源来排序,而且可能会阻塞网络
适用场景
对聚合精准度要求极高的业务场景,由于性能问题,不推荐使用
使用Clickhouse/ Spark 进行精准聚合
适用场景:数据量非常大、聚合精度要求高、响应速度快的业务场景
9.聚合性能优化
总览
- 启用 eager global ordinals 提升高基数聚合性能
- 插入数据时对索引进行预排序
- 使用节点查询缓存
- 使用分片请求缓存
- 拆分聚合,使聚合并行化
启用 eager global ordinals 提升高基数聚合性能
介绍
高基数聚合 。高基数聚合场景中的高基数含义:一个字段包含很大比例的唯一值
本质
启用 eager_global_ordinals 时,会在刷新(refresh)分片时构建全局序号。这将构建全局序号的成本从搜索阶段转移到了数据索引化(写入)阶段
应用场景
global ordinals 中文翻译成全局序号,是一种数据结构,应用场景如下:
- 基于 keyword,ip 等字段的分桶聚合,包含:terms聚合、composite 聚合等
- 基于text 字段的分桶聚合(前提条件是:fielddata 开启),因为text默认不能分桶聚合
- 基于父子文档 Join 类型的 has_child 查询和 父聚合
global ordinals 使用一个数值代表字段中的字符串值,然后为每一个数值分配一个bucket(分桶)
创建索引的同时开启:eager_global_ordinals
PUT /my‐index
{
"mappings": {
"properties": {
"tags": {
"type": "keyword",
"eager_global_ordinals": true
}
}
}
注意
开启 eager_global_ordinals 会影响写入性能,因为每次刷新时都会创建新的全局序号。为了最大程度地减少由于频繁刷新建立全局序号而导致的额外开销,请调大刷新间隔refresh_interval
动态调整刷新频率的方法如下
该招数的本质是:以空间换时间
PUT my‐index/_settings
{
"index": {
"refresh_interval": "30s"
}
插入数据时对索引进行预排序
介绍
- Index sorting(索引排序)可用于在插入时对索引进行预排序,而不是在查询时再对索引进行排序,这将提高范围查询(range query)和排序操作的性能
- 在 Elasticsearch 中创建新索引时,可以配置如何对每个分片内的段进行排序
版本要求
这是 Elasticsearch 6.X 之后版本才有的特性
示例
PUT /index
{
"settings": {
"index": {
"sort.field": "create_time",
"sort.order": "desc"
}
},
"mappings": {
"properties": {
"create_time": {
"type": "date"
}
}
}
}
注意
预排序将增加 Elasticsearch 写入的成本。在某些用户特定场景下,开启索引预排序会导致大约 40%-50% 的写性能下降。也就是说,如果用户场景更关注写性能的业务,开启索引预排序不是一个很好的选择
使用节点查询缓存
介绍
节点查询缓存(Node query cache)可用于有效缓存过滤器(filter)操作的结果。如果多
次执行同一 filter 操作,这将很有效,但是即便更改过滤器中的某一个值,也将意味着需要计算新的过滤器结果
例如,由于 “now” 值一直在变化,因此无法缓存在过滤器上下文中使用 “now” 的查询
无法适用缓存示例
PUT /my_index/_doc/1
{
"create_time":"2022‐05‐11T16:30:55.328Z"
}
#下面的示例无法使用缓存
GET /my_index/_search
{
"query": {
"constant_score": {
"filter": {
"range": {
"create_time": {
"gte": "now‐1h",
"lte": "now"
}
}
}
}
}
}
使用缓存示例
PUT /my_index/_doc/1
{
"create_time":"2022‐05‐11T16:30:55.328Z"
}
# 下面的示例就可以使用节点查询缓存
GET /my_index/_search
{
"query": {
"constant_score": {
"filter": {
"range": {
"create_time": {
"gte": "now‐1h/m",
"lte": "now/m"
}
}
}
}
}
}
上述示例中的“now-1h/m” 就是 datemath 的格式
如果当前时间 now 是:16:31:29,那么range query 将匹配 my_date 介于:15:31:00 和
15:31:59 之间的时间数据。同理,聚合的前半部分 query 中如果有基于时间查询,或者后
半部分 aggs 部分中有基于时间聚合的,建议都使用 datemath 方式做缓存处理以优化性
能
使用分片请求缓存
介绍
聚合语句中,设置:size:0,就会使用分片请求缓存缓存结果。size = 0 的含义是:只返
回聚合结果,不返回查询结果
示例
GET /es_db/_search
{
"size": 0,
"aggs": {
"remark_agg": {
"terms": {
"field": "remark.keyword"
}
}
}
}
拆分聚合,使聚合并行化
介绍
Elasticsearch 查询条件中同时有多个条件聚合,默认情况下聚合不是并行运行的。当为每
个聚合提供自己的查询并执行 msearch 时,性能会有显著提升。因此,在 CPU 资源不是
瓶颈的前提下,如果想缩短响应时间,可以将多个聚合拆分为多个查询,借助:msearch
实现并行聚合
常规的聚合实现
GET /employees/_search
{
"size": 0,
"aggs": {
"job_agg": {
"terms": {
"field": "job.keyword"
}
},
"max_salary": {
"max": {
"field": "salary"
}
}
}
}
msearch拆分多个语句的聚合实现
GET _msearch
{"index":"employees"}
{"size":0,"aggs":{"job_agg":{"terms":{"field": "job.keyword"}}}}
{"index":"employees"}
{"size":0,"aggs":{"max_salary":{"max":{"field": "salary"}}}}