分布式搜索和分析引擎——Elasticsearch(高级查询语法)

cuixiaogang

本文将深入探讨Elasticsearch中的查询机制及其进阶用法,包括基础查询方式、span相关的搜索语法、子字段分词以及滚动搜索等。下文的查询示例基于真实的APK安全分析索引apk_v6

示例索引(apk_v6)

本篇所有示例基于以下索引。这是一个APK样本数据库的完整mapping,别名为apk,90分片,ES 7.15版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
{
"apk_v6": {
"aliases": {
"apk": {}
},
"mappings": {
"dynamic": "false",
"properties": {
"aboletag": { "type": "byte" },
"abroadcautious": { "type": "byte" },
"app_key": {
"type": "nested",
"properties": {
"app_key": { "type": "keyword" },
"sdk_name": { "type": "keyword" }
}
},
"app_key_count": { "type": "integer" },
"ave_ids": { "type": "long" },
"ave_sign": { "type": "short" },
"cert_md5": { "type": "keyword" },
"cert_name": { "type": "keyword" },
"cert_sha1": { "type": "keyword" },
"cert_valid_from": { "type": "keyword", "index": false },
"cert_valid_to": { "type": "keyword", "index": false },
"common_app_type": { "type": "short" },
"copycutsoft": { "type": "byte" },
"createtime": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss" },
"dex_compile": { "type": "short", "index": false },
"dex_md5": { "type": "keyword" },
"dex_sha1": { "type": "keyword" },
"dex_sha256": { "type": "keyword" },
"dex_size": { "type": "long" },
"dexapk_status": { "type": "short", "index": false },
"fphash": { "type": "keyword" },
"icon_md5": { "type": "keyword" },
"id": { "type": "long" },
"last_name": { "type": "keyword" },
"main_ave_ids": { "type": "long" },
"md5": { "type": "keyword" },
"mf_activity_count": { "type": "integer" },
"mf_activity_names": { "type": "keyword" },
"mf_app_name": { "type": "keyword" },
"mf_meta_data_count": { "type": "integer" },
"mf_meta_datas": {
"type": "nested",
"properties": {
"name": { "type": "keyword" },
"value": { "type": "keyword" }
}
},
"mf_metadata": {
"type": "nested",
"properties": {
"name": { "type": "keyword" },
"value": { "type": "keyword" }
}
},
"mf_metadata_count": { "type": "integer" },
"mf_permission_count": { "type": "integer" },
"mf_permission_names": { "type": "keyword" },
"mf_provider_count": { "type": "integer" },
"mf_providers": {
"type": "nested",
"properties": {
"authorities": { "type": "keyword" },
"name": { "type": "keyword" }
}
},
"mf_providers_nested": {
"type": "nested",
"properties": {
"authorities": { "type": "keyword" },
"name": { "type": "keyword" }
}
},
"mf_receiver_action_count": { "type": "integer" },
"mf_receiver_action_names": { "type": "keyword" },
"mf_receiver_count": { "type": "integer" },
"mf_receiver_names": { "type": "keyword" },
"mf_service_count": { "type": "integer" },
"mf_service_names": { "type": "keyword" },
"mfsha1": { "type": "keyword" },
"name": { "type": "keyword" },
"notaboletrain": { "type": "byte" },
"package_size": { "type": "long", "index": false },
"qvmsimi": { "type": "byte" },
"rurl_hosts": { "type": "keyword" },
"safe": { "type": "long" },
"sha1": { "type": "keyword" },
"sha256": { "type": "keyword" },
"sign": { "type": "short" },
"source": { "type": "keyword" },
"special_app_type": { "type": "short" },
"spread_cnt": { "type": "long" },
"spy_ids": { "type": "long" },
"status": { "type": "byte" },
"sub_ave_ids": { "type": "long" },
"surl_hosts": { "type": "keyword" },
"targets": { "type": "keyword" },
"tips": { "type": "keyword", "index": false },
"troy_type": { "type": "short" },
"uid": { "type": "keyword" },
"updatetime": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss" },
"verify": { "type": "byte" },
"version_code": { "type": "long" },
"virus_name": { "type": "keyword" },
"wd_apk_type": { "type": "keyword" },
"wd_app_type": { "type": "short" },
"wd_fake_prod": { "type": "keyword" },
"wd_level": { "type": "keyword" }
}
},
"settings": {
"index": {
"search": {
"slowlog": {
"threshold": {
"fetch": { "trace": "10ms" },
"query": { "trace": "10ms" }
}
}
},
"refresh_interval": "15s",
"indexing": {
"slowlog": {
"threshold": {
"index": { "trace": "20ms" }
}
}
},
"number_of_shards": "90",
"provided_name": "apk_v6",
"number_of_replicas": "1",
"version": { "created": "7150299" }
}
}
}
}

字段类型速查

分类 代表字段 类型 说明
哈希 md5、sha1、sha256、dex_md5、cert_sha1 keyword 精确匹配,不分词
名称 name、virus_name、mf_app_name、cert_name keyword APK包名、病毒名等
分类标签 source、wd_level、wd_apk_type、targets keyword 来源、等级、类型
状态标识 status、sign、ave_sign、verify byte/short 小数值枚举
数值 dex_size、spread_cnt、version_code、safe long 可做range/聚合
计数 app_key_count、mf_permission_count、mf_service_count integer 各类组件数量
时间 createtime、updatetime date 格式yyyy-MM-dd HH:mm:ss
嵌套 app_key{app_key,sdk_name} nested SDK信息
嵌套 mf_providers{name,authorities} nested Provider组件
嵌套 mf_meta_datas{name,value} nested 元数据键值对

注意:apk_v6的所有字符串字段都是keyword类型(不分词),因此聚合和排序时直接使用字段名即可,不需要加.keyword后缀。下文中match(分词搜索)、match_phrase、Span查询、match_phrase_prefixmatch_bool_prefix、高亮、子字段映射等依赖text分词的章节,使用通用字段(title/content)演示——实际使用时需要为目标字段配置text类型和分词器。

高级查询

在Elasticsearch的查询中,通常使用query作为顶层元素,顶层元素下的子句可分为查询子句(Query Clauses)复合查询子句(Compound Query Clauses)。前者直接匹配文档内容,后者用于组合多个查询或过滤条件。

核心查询子句

match

全文搜索,适用于text类型字段,支持分词和模糊匹配。会对查询文本进行分词处理(如中文按词、英文按空格),并计算文档相关性评分(_score)。apk_v6无text字段,此处用通用字段演示。

1
2
3
4
5
{
"match": {
"title": "Elasticsearch 教程"
}
}

term

精确匹配单个值,适用于keywordnumericdate等结构化字段。不分析查询文本,直接匹配倒排索引中的精确值。

1
2
3
4
5
{
"term": {
"md5": "d41d8cd98f00b204e9800998ecf8427e"
}
}

terms

匹配多个值中的任意一个,等价于SQL中的IN操作。

1
2
3
4
5
{
"terms": {
"wd_level": ["black", "gray", "white"]
}
}

range

范围查询,支持数值、日期等类型。

按时间范围查询最近30天创建的APK:

1
2
3
4
5
6
7
8
{
"range": {
"createtime": {
"gte": "2022-01-01 00:00:00",
"lte": "2022-01-31 23:59:59"
}
}
}

按数值范围查询DEX文件大小:

1
2
3
4
5
6
7
8
{
"range": {
"dex_size": {
"gte": 1000000,
"lte": 50000000
}
}
}

match_phrase

短语匹配,要求所有词条按顺序连续出现。适用于text类型字段,apk_v6无text字段,此处用通用字段演示。

1
2
3
4
5
{
"match_phrase": {
"content": "分布式系统"
}
}

exists

检查字段是否存在(即文档中是否包含该字段且值非null)。

查找有病毒名的APK样本:

1
2
3
4
5
{
"exists": {
"field": "virus_name"
}
}

复合查询子句

bool

组合多个查询条件,支持四种逻辑关系

  • must:必须匹配,贡献评分。
  • filter:必须匹配,不贡献评分(结果会被缓存)。
  • must_not:必须不匹配。
  • should:选择性匹配,满足任意条件即可。

查找2025年创建的、非白名单、标记为恶意的APK:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"bool": {
"must": [
{ "term": { "sign": 1 } }
],
"filter": [
{
"range": {
"createtime": {
"gte": "2025-01-01 00:00:00",
"lte": "2025-12-31 23:59:59"
}
}
}
],
"must_not": [
{ "term": { "wd_level": "white" } }
],
"should": [
{ "exists": { "field": "virus_name" } }
]
}
}

dis_max

取多个查询中的最高评分作为最终评分,避免不同字段的评分相互影响。

tie_breaker控制其他查询的评分权重(0~1),0表示只取最高分,1表示所有查询评分求和。

1
2
3
4
5
6
7
8
9
{
"dis_max": {
"queries": [
{ "term": { "name": "com.example.app" } },
{ "term": { "mf_app_name": "com.example.app" } }
],
"tie_breaker": 0.7
}
}

function_score

自定义评分逻辑,结合查询结果和自定义函数调整文档评分。

gauss是衰减函数,让越接近origin的文档得分越高。boost_mode控制函数结果与原始评分的合并方式:multiply(相乘)、sum(相加)、replace(替换)等。

按传播量加权、近期样本优先:

1
2
3
4
5
6
7
8
9
10
{
"function_score": {
"query": { "term": { "wd_level": "black" } },
"functions": [
{ "field_value_factor": { "field": "spread_cnt", "modifier": "log1p" } },
{ "gauss": { "createtime": { "origin": "now", "scale": "30d" } } }
],
"boost_mode": "multiply"
}
}

constant_score

将查询包装为过滤条件,返回固定评分(_score=1)。

boost设定固定评分值,默认为1.0。

1
2
3
4
5
6
{
"constant_score": {
"filter": { "term": { "wd_level": "black" } },
"boost": 2.0
}
}

其他特殊查询子句

multi_match

在多个字段上执行相同的查询,支持多种匹配模式。apk_v6无text字段,此处用通用字段演示。

fields^2表示该字段权重加倍。type支持多种模式:best_fields(取最高分字段,默认)、most_fields(累加所有字段分数)、cross_fields(跨字段当一个字段查)、phrase(短语匹配)、phrase_prefix(短语前缀)。

1
2
3
4
5
6
7
{
"multi_match": {
"query": "Elasticsearch 教程",
"fields": ["title", "content^2"],
"type": "best_fields"
}
}

nested

查询嵌套文档(处理nested类型的字段)。

查找包含特定SDK的APK:

1
2
3
4
5
6
7
8
9
10
11
12
{
"nested": {
"path": "app_key",
"query": {
"bool": {
"must": [
{ "term": { "app_key.sdk_name": "com.umeng.analytics" } }
]
}
}
}
}

查找包含特定meta-data的APK:

1
2
3
4
5
6
7
8
9
10
11
12
{
"nested": {
"path": "mf_meta_datas",
"query": {
"bool": {
"must": [
{ "term": { "mf_meta_datas.name": "com.google.android.gms.version" } }
]
}
}
}
}

wildcard

通配符查询,支持 *(任意字符)和 ?(单个字符)。

查找腾讯系包名的APK:

1
2
3
4
5
{
"wildcard": {
"name": "com.tencent.*"
}
}

注意:在keyword字段上做wildcard代价较高,尤其是前缀通配(*xxx),数据量大时慎用。

regexp

正则表达式查询。

1
2
3
4
5
{
"regexp": {
"uid": "[a-f0-9]{32}"
}
}

Span 查询

Span查询用于精确控制词条之间的位置关系和距离,比match_phrase更灵活。适合需要”两个词之间最多隔几个词”或”某个词不能出现在另一个词附近”这类场景。Span查询只能用于text分词字段,apk_v6无text字段,以下用通用字段演示。

span_term

最基础的span查询,匹配单个精确词条,是其他span查询的构建块。

1
2
3
{
"span_term": { "content": "elasticsearch" }
}

span_near

要求多个span查询在指定距离内按顺序(或不按顺序)出现。

slop指定允许的最大间隔词数,in_order控制是否要求按顺序。

1
2
3
4
5
6
7
8
9
10
{
"span_near": {
"clauses": [
{ "span_term": { "content": "分布式" } },
{ "span_term": { "content": "搜索" } }
],
"slop": 2,
"in_order": true
}
}

上面的查询要求”分布式”和”搜索”按顺序出现且中间最多隔2个词。

span_or

匹配多个span查询中的任意一个。

1
2
3
4
5
6
7
8
{
"span_or": {
"clauses": [
{ "span_term": { "content": "elasticsearch" } },
{ "span_term": { "content": "solr" } }
]
}
}

span_not

排除:匹配include但不匹配exclude所覆盖的位置范围。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"span_not": {
"include": {
"span_near": {
"clauses": [
{ "span_term": { "content": "分布式" } },
{ "span_term": { "content": "系统" } }
],
"slop": 1,
"in_order": true
}
},
"exclude": {
"span_term": { "content": "存储" }
}
}
}

span_containing

要求一个大范围的span包含一个小范围的span。常用于”A和B靠近且附近还有C”这类组合条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"span_containing": {
"big": {
"span_near": {
"clauses": [
{ "span_term": { "content": "分布式" } },
{ "span_term": { "content": "搜索" } }
],
"slop": 5,
"in_order": false
}
},
"little": {
"span_term": { "content": "引擎" }
}
}
}

Span查询对比

查询 作用 关键参数
span_term 单词条匹配
span_near 多词条按距离和顺序匹配 slopin_order
span_or 多个span取并集
span_not 排除特定位置范围 includeexclude
span_containing 大范围包含小范围 biglittle

模糊与前缀查询

fuzzy

基于编辑距离(Levenshtein Distance)的模糊匹配,容忍拼写错误。keyword字段也支持fuzzy查询。

fuzziness可以是具体数值(0、1、2)或AUTO(根据词长自动选择)。

1
2
3
4
5
6
7
8
{
"fuzzy": {
"name": {
"value": "com.tencet.mm",
"fuzziness": "AUTO"
}
}
}

prefix

前缀匹配,查询以指定字符串开头的词条。

1
2
3
4
5
{
"prefix": {
"name": { "value": "com.tencent" }
}
}

match_phrase_prefix

短语匹配 + 最后一个词做前缀补全,适合搜索框的自动补全场景。适用于text字段,apk_v6无text字段,此处用通用字段演示。

max_expansions限制最后一个词前缀展开的最大词条数,防止性能爆炸。

1
2
3
4
5
6
7
8
{
"match_phrase_prefix": {
"title": {
"query": "分布式搜",
"max_expansions": 50
}
}
}

match_bool_prefix

把查询文本分词后,前面的词用term匹配,最后一个词用prefix匹配。适合实时搜索联想。适用于text字段,apk_v6无text字段,此处用通用字段演示。

match_phrase_prefix的区别:不要求词条按顺序连续出现。

1
2
3
4
5
{
"match_bool_prefix": {
"title": "elastic sear"
}
}

ids

按文档_id精确查找。

1
2
3
4
5
{
"ids": {
"values": ["100001", "100002", "100003"]
}
}

子字段与多字段映射(fields)

同一个原始字段可以用不同的分词器建多个子字段,查询时按需选择。一个字段既能做全文搜索(text + 分词),又能做精确排序和聚合(keyword + 不分词)。apk_v6的字段均为keyword,未使用多字段映射,以下用通用字段演示。

映射定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word",
"fields": {
"keyword": {
"type": "keyword"
},
"english": {
"type": "text",
"analyzer": "english"
}
}
}
}
}
}

查询时的使用:

字段引用 类型 用途
title text(ik分词) 中文全文搜索
title.keyword keyword 精确匹配、排序、聚合
title.english text(english分词) 英文搜索(词干提取等)
1
2
3
4
5
6
7
8
9
10
{
"bool": {
"must": [
{ "match": { "title": "分布式搜索" } }
],
"filter": [
{ "term": { "title.keyword": "分布式搜索引擎" } }
]
}
}

注意:term查询不能直接用在text字段上(text字段存的是分词后的词条,和原始值不一致),要精确匹配必须走.keyword子字段。而像apk_v6这样字段本身就是keyword类型的,直接用字段名即可,不需要加.keyword后缀。

结果控制

_source 过滤

控制返回文档中包含哪些字段,减少网络传输。

1
2
3
4
{
"query": { "term": { "wd_level": "black" } },
"_source": ["md5", "name", "virus_name", "createtime", "spread_cnt"]
}

也可以用includes/excludes做更细粒度的控制:

1
2
3
4
5
6
{
"_source": {
"includes": ["md5", "name", "virus_name", "mf_*"],
"excludes": ["mf_activity_names", "mf_receiver_names"]
}
}

高亮(highlight)

在搜索结果中把匹配的关键词用标签包裹,方便前端展示。高亮主要用于text分词字段,apk_v6无text字段,此处用通用字段演示。

1
2
3
4
5
6
7
8
9
10
{
"query": { "match": { "content": "分布式搜索" } },
"highlight": {
"pre_tags": ["<em>"],
"post_tags": ["</em>"],
"fields": {
"content": {}
}
}
}

返回结果里会多一个highlight字段,内容是带标签的文本片段。

常用参数:

参数 作用
pre_tags / post_tags 高亮标签
fragment_size 每个片段的字符数(默认100)
number_of_fragments 返回几个片段(默认5)
type 高亮器类型:unified(默认)、plainfvh

排序(sort)

默认按_score降序排列。可以指定一个或多个字段排序,一旦指定了sort就不再计算_score(除非显式加上_score)。

按创建时间倒序、再按传播量倒序:

1
2
3
4
5
6
7
8
{
"query": { "term": { "wd_level": "black" } },
"sort": [
{ "createtime": { "order": "desc" } },
{ "spread_cnt": { "order": "desc" } },
"_id"
]
}

排序字段必须是keyword、数值、日期等可排序类型,text字段不能直接排序(要用.keyword子字段)。apk_v6的字段都是keyword或数值,直接用即可。

分页

from / size(浅分页)

最基本的分页方式,from是偏移量,size是每页条数。

1
2
3
4
5
{
"query": { "term": { "status": 1 } },
"from": 20,
"size": 10
}

from + size不能超过index.max_result_window(默认10000)。超过这个限制用下面两种深度分页方式。

scroll(滚动搜索)

适合一次性遍历大量数据(如数据导出、批量处理),不适合实时搜索。原理是创建一个快照,通过scroll_id逐批获取。

1
2
3
4
5
6
POST /apk/_search?scroll=5m
{
"query": { "term": { "wd_level": "black" } },
"size": 1000,
"sort": ["_doc"]
}

scroll=5m表示快照保持5分钟。sort: ["_doc"]按索引顺序遍历,效率最高。拿到第一批结果和_scroll_id后,后续请求:

1
2
3
4
5
POST /_search/scroll
{
"scroll": "5m",
"scroll_id": "DXF1ZXJ5QW5..."
}

反复调用直到返回hits.hits为空。用完必须清理:

1
2
3
4
DELETE /_search/scroll
{
"scroll_id": "DXF1ZXJ5QW5..."
}

search_after(实时深度分页)

适合实时翻页到很深的位置,用上一页最后一条的排序值作为起点。比scroll更轻量,不占服务端资源。

1
2
3
4
5
6
7
8
9
{
"query": { "term": { "wd_level": "black" } },
"size": 10,
"sort": [
{ "createtime": { "order": "desc" } },
{ "_id": "asc" }
],
"search_after": ["2025-05-15 10:30:00", "abc123"]
}

search_after的值是上一页最后一条文档的sort值数组。要求sort里必须有一个唯一字段(如_id)保证顺序确定。

三种分页方式对比

方式 适用场景 能否跳页 深度限制 资源占用
from/size 浅分页(前几页) 默认10000条
scroll 批量遍历/导出 不能 高(服务端保持快照)
search_after 实时深度翻页 不能

聚合(aggregations)

聚合是ES的分析能力核心,相当于SQL里的GROUP BY + 聚合函数。聚合和查询可以同时使用——query筛选文档范围,aggs在结果上做统计。

聚合类型

类型 说明 常用
桶聚合(Bucket) 按条件分组,类似GROUP BY terms、range、date_histogram、histogram、filter
指标聚合(Metric) 计算数值指标 avg、sum、min、max、cardinality、value_count、stats
管道聚合(Pipeline) 在其他聚合的结果上二次计算 avg_bucket、max_bucket、cumulative_sum

常用桶聚合

terms

按字段值分组统计,类似GROUP BY

统计各威胁等级的APK数量:

1
2
3
4
5
6
7
8
9
10
11
{
"size": 0,
"aggs": {
"by_level": {
"terms": {
"field": "wd_level",
"size": 10
}
}
}
}

size: 0表示不返回文档,只返回聚合结果。aggs里的size是返回的桶数量。注意这里直接用wd_level而不是wd_level.keyword——因为它本身就是keyword类型。

range

按数值范围分桶。

按DEX文件大小分档统计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"size": 0,
"aggs": {
"dex_ranges": {
"range": {
"field": "dex_size",
"ranges": [
{ "key": "<1MB", "to": 1048576 },
{ "key": "1-10MB", "from": 1048576, "to": 10485760 },
{ "key": ">10MB", "from": 10485760 }
]
}
}
}
}

date_histogram

按时间间隔分桶。

按月统计APK入库趋势:

1
2
3
4
5
6
7
8
9
10
11
12
{
"size": 0,
"aggs": {
"monthly": {
"date_histogram": {
"field": "createtime",
"calendar_interval": "month",
"format": "yyyy-MM"
}
}
}
}

calendar_interval用于自然周期(day、week、month、year),fixed_interval用于固定时长(1h、30m、7d)。

常用指标聚合

1
2
3
4
5
6
7
8
9
10
{
"size": 0,
"query": { "term": { "wd_level": "black" } },
"aggs": {
"avg_dex": { "avg": { "field": "dex_size" } },
"total_spread": { "sum": { "field": "spread_cnt" } },
"unique_certs": { "cardinality": { "field": "cert_sha1" } },
"version_stats": { "stats": { "field": "version_code" } }
}
}

stats一次返回count、min、max、avg、sum五个指标。cardinality是去重计数(近似值)。

嵌套聚合

桶聚合内部可以再嵌套指标聚合或子桶聚合,实现多维分析。

按威胁等级分组,每组内算平均DEX大小,并再按status细分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"size": 0,
"aggs": {
"by_level": {
"terms": { "field": "wd_level" },
"aggs": {
"avg_dex": { "avg": { "field": "dex_size" } },
"by_status": {
"terms": { "field": "status" }
}
}
}
}
}

nested类型聚合

对nested字段做聚合时,必须先用nested聚合进入嵌套文档上下文,才能对嵌套字段做terms等聚合。

统计各SDK出现的APK数量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"size": 0,
"aggs": {
"sdk_stats": {
"nested": { "path": "app_key" },
"aggs": {
"top_sdks": {
"terms": {
"field": "app_key.sdk_name",
"size": 20
}
}
}
}
}
}

post_filter

post_filter在聚合计算之后再过滤文档,只影响返回的hits,不影响聚合结果。典型场景:先聚合算出所有分类的统计数据,再只返回某个分类的文档。

聚合统计所有威胁等级的数量分布,但只返回black级别的文档:

1
2
3
4
5
6
7
8
9
10
11
{
"query": { "range": { "createtime": { "gte": "2022-01-01 00:00:00" } } },
"aggs": {
"all_levels": {
"terms": { "field": "wd_level" }
}
},
"post_filter": {
"term": { "wd_level": "black" }
}
}

聚合all_levels统计的是2022年以来所有等级的分布,但返回的hits只有black级别的文档。