ES基础--terms聚合

1.一般使用

基于 es-7.2版本

Terms聚合一般情况下是针对字段类型为 keyword 的聚合(text 类型的字段需要开启 fielddata ),用于对指定字段进行唯一匹配,如下:

1
2
3
4
5
6
7
8
9
10
11
GET indexName/_search
{
"aggs" : {
"terms_test" : {
"terms" : {
"field" : "fieldname",
"size": 3
}
}
}
}
  • indexName 为指定的索引名称;
  • terms_test 为自定义名称,后端程序API可以通过该名称获取到 bucket 结果集;
  • size指定了返回的 topK 条数。

其响应结果类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
......
"aggregations" : {
"test" : {
"doc_count_error_upper_bound" : 7,
"sum_other_doc_count" : 292,
"buckets" : [
{
"key" : "人民检察院",
"doc_count" : 114
},
{
"key" : "党和国家",
"doc_count" : 105
},
{
"key" : "菏泽市",
"doc_count" : 101
}
]
}
}
  • doc_count_error_upper_bound 表示没有在这次聚合中返回、但是可能存在的潜在聚合结果,其数量为7,可能会排在最后;

  • sum_other_doc_count 表示未参与本次聚合的文档数量,因为 ES 是分布式部署的,每个分片只返回 topK的结果,其余部分不会扫描。这里设置的 size 是3,所以每个分片只会聚合本分片中前三的数据。即有 292 个文档没有参与本次聚合,设置的 size 越大,返回结果越准确。但同时也会增加计算成本;

  • buckets 中的是本次 term 聚合的结果,这里的 doc_count 并不一定是准确的, 有时只是一个近似值,原因同上;

数据不准确的原因,本质上是因为分布式导致的,尤其是当获取 topK 数据时,节点内的 topK 准确不保证整体 topK 准确。 maxminavg 的聚合可以获取准确的结果,但 terms 对于分片数据只能取近似值。

可以通过以下方式提高准确度:

  • 不分片(实际情况不现实);
  • 增加 size 的大小;
  • 设置 shard_size ,该参数可以最小化 size 设置较大时的计算成本,设置后他会限制从每个分片取回的个数。例如 size 设置的取5个,shard_size 设置为取10个。此时分片会返回10个数据减小误差。shard_size 不能小于 size(没有意义),当它比 size 小时,es 会重写为 size 的大小。

2. show_term_doc_count_error

查询中可以设置该参数为 true ,此时返回的每一个 bucket 都会包含一个误差值的范围 ,在按照升序排序或按照子聚合排序时,es会无法计算该误差值,并返回 -1.

1
2
3
4
5
6
7
8
9
10
11
12
13
GET search_jw_words/_search
{
"size": 0,
"aggs": {
"terms_test": {
"terms": {
"field":"searchContent",
"show_term_doc_count_error": true,
"size": 30
}
}
}
}

在实际应用中其实会发现,排名越往后的数据,出现的误差越大,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"buckets" : [
{
"key" : "人民检察院",
"doc_count" : 265,
"doc_count_error_upper_bound" : 0
},
{
"key" : "菏泽市",
"doc_count" : 246,
"doc_count_error_upper_bound" : 0
},
{
"key" : "党和国家",
"doc_count" : 220,
"doc_count_error_upper_bound" : 0
}
......

3. 使用 include/exclude 来过滤结果中的指定内容

1
2
3
4
5
6
7
8
9
10
11
12
GET indexName/_search
{
"aggs" : {
"terms_test" : {
"terms" : {
"field" : "fieldname",
"exclude": ["张三","王五"],
"include": "李四"
}
}
}
}

在我测试的7.2版本中,两者不能同时使用,要么单独设置 exlude,要么单独设置 include

也可以使用正则表达式的方式进行过滤(一开始我想通过正则去除单字的结果,然后后再过滤给定的词典,但并不能起到组合的效果):

1
2
3
4
5
6
7
8
9
10
11
12
13
GET indexName/_search
{
"aggs" : {
"terms_test" : {
"terms" : {
"field" : "fieldname",
#正则表达式语法与正则查询的语法相同.
"include" : ".",
"exclude" : "water_.*"
}
}
}
}

4. 自定义排序

默认状态下,聚合结果会根据返回 bucket 中的 doc_count 来进行降序排序,想要更改的话可以使用以下方式:

官方文档提示:不推荐根据 count 来降序排,本身聚合是从分片取 topK 的行为,倒序会使准确度更低。

升序排序的查询方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
GET indexName/_search
{
"size": 0,
"aggs" : {
"terms_test" : {
"terms" : {
"field" : "fieldname",
# _count/_key 分别表示按数量、名称排序
"order" : { "_count" : "asc" }
}
}
}
}

使用子聚合排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET indexName/_search
{
"aggs" : {
"terms_test" : {
"terms" : {
"field" : "fieldname",
"order" : { "sub_agg" : "desc" }
},
#按子聚合的最大xx排序。
"aggs" : {
"sub_agg" : { "max" : { "field" : "fieldname2" } }
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GET /_search
{
"aggs" : {
"terms_test" : {
"terms" : {
"field" : "fieldname",
#使用 . 来确定排序依据
"order" : { "sub_agg.max" : "desc" }
},
#stats的结果会同时包含 count、max、min、avg、sum
"aggs" : {
"sub_agg" : { "stats" : { "field" : "fieldname2" } }
}
}
}
}

注意:pipeline 聚合由于本身机制问题不能用该方式。

官方文档声明为:子聚合的层级可以任意多,只要按照规定的命名方式,就可以完成排序,例如更深一层级的写法为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
GET indexName/_search
{
"aggs" : {
"terms_test" : {
"terms" : {
"field" : "fieldname",
"order" : { "sub1>sub2.avg" : "desc" }
},
"aggs" : {
"sub1" : {
#过滤范围只包含 "张三"
"filter" : { "term" : { "fieldname2" : "张三" }},
"aggs" : {
#stats的结果会同时包含 count、max、min、avg、sum
"sub2" : {
"stats" : { "field" : "fieldname3" }
}
}
}
}
}
}
}

order 规则需满足以下条件:(即 sub1>sub2>....sub100.avg

  • 聚合层级间使用 “>” ;
  • 属性间使用 “.” ;
  • 聚合名为自定义的名称。

另外 order 还可以使用数组的方式对多个内容进行排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
GET indexName/_search
{
"aggs" : {
"terms_test" : {
"terms" : {
"field" : "fieldname",
# 先按子聚合内容排序,再按doc_count排序
"order" : [{ "sub1>sub2.avg" : "desc" },{ "_count": "desc" }]
},
"aggs" : {
"sub1" : {
# 过滤范围只包含 "张三"
"filter" : { "term" : { "fieldname2" : "张三" }},
"aggs" : {
# stats的结果会同时包含 count、max、min、avg、sum
"sub2" : {
"stats" : { "field" : "fieldname3" }
}
}
}
}
}
}
}

5. 使用 script 脚本进行聚合

es提供了脚本支持—-内置 painless 脚本语言。(下次会单独解释下脚本如何使用),如下脚本也可以实现对指定字段的聚合。

1
2
3
4
5
6
7
8
9
10
11
12
13
GET indexName/_search
{
"aggs" : {
"terms_test" : {
"terms" : {
"script" : {
"source": "doc['fieldname'].value",
"lang": "painless"
}
}
}
}
}

要使用已经创建好的脚本的话:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET indexName/_search
{
"aggs" : {
"terms_test" : {
"terms" : {
"script" : {
"id": "my_script",
"params": {
"field": "fieldname"
}
}
}
}
}
}

针对 valuescript 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GET indexName/_search
{
"aggs" : {
"terms_test" : {
"terms" : {
"field" : "fieldname",
"script" : {
"source" : "'我是前缀: ' +_value",
"lang" : "painless"
}
}
}
}
}

6. 多字段聚合

terms 聚合不支持多字段。想要进行多字段聚合需要使用 script 脚本进行处理,或者使用 copy_to 字段(copy_to即老版本的 _all 字段,但更加灵活)。

7. collect_mode

collect_mode 分为深度优先搜索(depth_first)和广度优先搜索(breadth_first ),一般情况下默认使用depth_mode。但某些时候更适合breadth_first

场景例如:想要查询最受欢迎的10位演员,及其最常见的5位联合主演。虽然在这个数量级下只是获取50个结果,但每增加一个演员都会进行 n² 的增长:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GET indexName/_search
{
"aggs" : {
"actors" : {
"terms" : {
"field" : "actors",
"size" : 10
},
"aggs" : {
"costars" : {
"terms" : {
"field" : "actors",
"size" : 5
}
}
}
}
}
}

此时,可以使用 breath_first 模式来优化子聚合的加载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
GET indexName/_search
{
"aggs" : {
"actors" : {
"terms" : {
"field" : "actors",
"size" : 10,
# 此处可以选择的值包括 breadth_first 和 depth_first两种
"collect_mode" : "breadth_first"
},
"aggs" : {
"costars" : {
"terms" : {
"field" : "actors",
"size" : 5
}
}
}
}
}
}

breath_first 即先确定好10名最受欢迎的演员,然后再取对应的联合主演。而不是每一个演员都先去确定其联合主演。

8. tips

在多个索引上聚合时,聚合字段的类型可能在每个索引中都不相同。某些类型彼此兼容(integerlongfloatdouble),但是当类型混合使用十进制数和非十进制数时,terms聚合会将非十进制数提升为十进制数。这可能会导致数值的精度下降

9. API

本来想在这写下聚合的 API,但发现单独扯出来不太完整。后续单独写下 restHighClient 的基本用法吧。

0%