Elasticsearch Query DSL入门
列举几个最基础的 DSL 语句,所有长达成百上千行的 DSL 都是由这些基础语法组合起来的。
一、环境
- Ubuntu 14.04/16、04
- JDK1.8
- Elasticsearch 5.3
- Kibana 5.3.2
二、DSL介绍
Query DSL 又叫查询表达式,是一种非常灵活又富有表现力的查询语言,采用 JSON 接口的方式实现丰富的查询,并使你的查询语句更灵活、更精确、更易读且易调试。
我平时喜欢借助 Kibana 来执行 DSL 语句,它能够辅助自己开发,也能用于 debug 和研究。
实际项目中一般封装一下第三方引擎来实现业务,而这些语法转换成 DSL 后经常有成百上千行,但其实一点也不复杂。不管是过滤、聚合还是嵌套,只要理解了最基本的语法,都是很好解读的。
这篇文章主要还是记录下我平时常用的几个 DSL,防止隔得时间久了,裸写 DSL 不一定写得出来。
三、全文查询
1. match_all
/_search 查找整个ES中所有索引的内容,/ 前面可以加上索引名,多个索引名之前用英文逗号分割。
下面这个语法是 match_all 查询,Kibana 能够自动补全代码,最简单。
GET /_search
{
"query": {
"match_all": {}
}
}
2. match
下边的例子就表示查找 host 为 wenyuanblog.com 的所有记录。
POST /wenyuanblog-2018.03.02/_search
{
"query": {
"match": {
"host": "wenyuanblog.com"
}
}
}
3. multi_match
在多个字段上执行相同的 match 查询,下边的例子就表示查询 host 或 http_referer 字段中包含 wenyuanblog.com 的记录。
GET /wenyuanblog-2018.03.02/_search
{
"query": {
"multi_match": {
"query": "wenyuanblog.com",
"fields": [
"host",
"http_referer"
]
}
}
}
4. query_string
可以在查询里边使用 AND 或者 OR 来完成复杂的查询。
下边的例子表示查找 host 为 a.wenyuanblog.com 或者 b.wenyuanblog.com 的所有记录。
GET /wenyuanblog-2018.03.02/_search
{
"query": {
"query_string": {
"query": "(a.wenyuanblog.com) OR (b.wenyuanblog.com)",
"fields": [
"host"
]
}
}
}
也可以组合更多的条件完成更复杂的查询请求。
下边的例子表示查询(host 为 a.wenyuanblog.com)或者是(host 为 b.wenyuanblog.com 且 status 为 404)的所有记录。
GET /wenyuanblog-2018.03.02/_search
{
"query": {
"query_string": {
"query": "host:a.wenyuanblog.com OR (host:b.wenyuanblog.com AND status:404)"
}
}
}
与其相类似的还有个 simple_query_string 的关键字,可以将 query_string 中的 AND 或 OR 用 + 或 | 这样的符号替换掉。
5. term
term 可以用来精确匹配,精确匹配的值可以是数字、时间、布尔值或者是设置了 not_analyzed 不分词的字符串。
GET /wenyuanblog-2018.03.02/_search
{
"query": {
"term": {
"status": {
"value": 404
}
}
}
}
term 对输入的文本不进行分析,直接精确匹配输出结果,如果要同时匹配多个值可以使用 terms。
GET /wenyuanblog-2018.03.02/_search
{
"query": {
"terms": {
"status": [
403,
404
]
}
}
}
6. range
range 用来查询落在指定区间内的数字或者时间。
下边的例子表示搜索所有状态为 200 到 399 之间的数据,这里的操作符主要有四个 gt 大于,gte 大于等于,lt 小于,lte 小于等于。
GET /wenyuanblog-2018.03.02/_search
{
"query": {
"range": {
"status": {
"gte": 200,
"lte": 399
}
}
}
}
当使用日期作为范围查询时,我们需要注意下日期的格式,官方支持的日期格式主要有两种:
① 时间戳,注意是毫秒粒度。
GET /wenyuanblog-2018.03.02/_search
{
"query": {
"range": {
"@timestamp": {
"gte": 1519920000000,
"lte": 1519956000000,
"format": "epoch_millis"
}
}
}
}
② 日期字符串。
GET /wenyuanblog-2018.03.02/_search
{
"query": {
"range": {
"@timestamp": {
"gte": "2018-03-02 00:00:00",
"lte": "2018-03-03",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd",
"time_zone": "+08:00"
}
}
}
}
选择哪种方式根据实际情况决定,我们业务中用时间戳的情况居多。
如果采用日期字符串的方式,那么可以使用 format 字段指定匹配的日期格式,如果格式有多个就用 || 分开,像例子中那样,不过建议用同样的日期格式。
如果日期中缺少年月日这些内容,那么缺少的部分会用 unix 的开始时间(即 1970年1月1日)填充,当你将 “format”:”dd” 指定为格式时,那么 “gte”:10 将被转换成 1970-01-10T00:00:00.000Z。
Elasticsearch 中默认使用的是 UTC 时间,所以我们在使用时要通过 time_zone 来设置好时区,以免出错。
7. exists
查询出存在某字段的文档。
GET /wenyuanblog-2018.03.02/_search
{
"query": {
"bool": {
"must": [
{
"exists": {
"field": "visitor.name"
}
}
]
}
},
"from": 0,
"size": 100
}
8. bool组合查询
通常我们可能需要将很多个条件组合在一起查出最后的结果,这个时候就需要使用 ES 提供的 bool 来实现了。
布尔查询支持的子查询类型共有四种,分别是:must,should,must_not 和 filter。
must:类似于 SQL 中的 AND ,必须包含;
must_not:类似于 SQL 中的 NOT,必须不包含;
should:文档应该匹配 should 子句查询的一个或多个;
filter:过滤器,文档必须匹配该过滤条件,跟 must 子句的唯一区别是,filter 不会对结果进行相关性评分 _score,换言之当我们的业务中无相关性的要求时,建议查询的过程中多用 filter。
下面是一个组合查询的例子,我们要查询 host 为 wenyuanblog.com 且 http_x_forworded_for 为 47.97.12.69 且 status 不为 200 的所有数据。
GET /wenyuanblog-2018.03.02/_search
{
"query": {
"bool": {
"filter": [
{
"match": {
"host": "wenyuanblog.com"
}
},
{
"match": {
"http_x_forwarded_for": "47.97.12.69"
}
}
],
"must_not": {
"match": {
"status": 200
}
}
}
}
}
这里再说一下 should。通常情况下,should 子句是数组字段,包含多个 should 子查询,默认情况下,匹配的文档必须满足其中一个子查询条件。
但我们可以通过显式设置布尔查询的参数 minimum_should_match 的值,从而改变默认匹配行为。该参数控制一个文档必须匹配的 should 子查询的数量,它有很多种配置方式:
如果设置为数字 3,表示至少需要匹配 3 个 should 子句;如果设置为一个百分比,例如 “minimum_should_match”:75%,则至少满足 75% 且向下取整(5个 should 子句,5*75%=3.75,向下取整为 3,也就是至少匹配 3 个 should 子句)。
下面是个例子。(注:在 bool query 中 minimum_should_match 只能紧跟在 should 的后面,放其他地方会出异常)
GET /wenyuanblog-2018.03.02/_search
{
"query": {
"bool": {
"should": [
{
"match": {
"host": "a.wenyuanblog.com"
}
},
{
"match": {
"host": "b.wenyuanblog.com"
}
},
{
"match": {
"host": "c.wenyuanblog.com"
}
}
],
"minimum_should_match": 2
}
}
}
9. sort
sort 是排序,也是很常用的查询,这里我举个按时间(@timestamp)倒叙查询的例子。
GET /wenyuanblog-2018.03.02/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"@timestamp": {
"order": "desc"
}
}
]
}
四、聚合查询
1. 分桶
根据 host 字段的值进行分桶(有点类似于 SQL 中的 group by),这里的 host_bucket 是我给该桶起的名字。
GET /wenyuanblog-2018.03.02/_search
{
"query": {
"match_all": {}
},
"aggs": {
"host_bucket": {
"terms": {
"field": "host"
}
}
}
}
2. 度量
计算出 latency 字段的最大值(metric 有点类似于 SQL 的 avg、max、min),这里的 max_latency 是我给该度量起的名字。
GET /wenyuanblog-2018.03.02/_search
{
"query": {
"match_all": {}
},
"aggs": {
"max_latency": {
"max": {
"field": "latency"
}
}
}
}
五、业务应用
实际业务中的一些需求,属于较综合的查询语法。
1. 聚合结果进行排序
关键词:aggregations,terms,order。
先过滤出 host 字段值为 “wenyuanblog.com” 的记录,然后对 源IP 进行分桶聚合,最后根据聚合查询到的 文档数量倒序排序。
GET /wenyuanblog-2019.05.*/_search
{
"query": {
"bool": {
"filter": [
{
"range": {
"@timestamp": {
"to": 1557503999000,
"from": 1557417600000
}
}
},
{
"term": {
"host": "wenyuanblog.com"
}
}
]
}
},
"from": 0,
"size": 0,
"aggregations": {
"src_ip_bucket": {
"terms": {
"field": "http.src_ip.dotted",
"size": 10,
"order": {
"_count": "desc"
}
}
}
}
}
2. IP范围过滤+分桶+度量+脚本+排序
关键词:range,aggregations,terms,metric,script,order。
先根据 IP范围 过滤,同时排除指定 host;
接着对 源IP 进行分桶聚合,再计算 进流量、出流量 在该时间段内的总和,用脚本计算出 总流量(进流量+出流量) ,最后根据 总流量(进流量+出流量) 数值大小进行倒序排序。
使用groovy:
GET /wenyuanblog-2019.05.*/_search
{
"query": {
"bool": {
"filter": [
{
"bool": {
"must": [
{
"range": {
"@timestamp": {
"to": 1557503999000,
"from": 1557417600000
}
}
},
{
"range": {
"http.src_ip.dotted": {
"to": "10.255.255.255",
"from": "10.0.0.0"
}
}
},
{
"bool": {
"must_not": [
{
"terms": {
"host": [
"a.wenyuanblog.com",
"b.wenyuanblog.com"
]
}
}
]
}
}
]
}
}
]
}
},
"from": 0,
"size": 0,
"aggs": {
"appid_bucket": {
"terms": {
"field": "http.src_ip.dotted",
"order": {
"sum_total_bytes": "desc"
},
"size": 100
},
"aggs": {
"sum_in_bytes": {
"sum": {
"field": "http.in_bytes"
}
},
"sum_out_bytes": {
"sum": {
"field": "http.out_bytes"
}
},
"sum_total_bytes": {
"sum": {
"script": {
"lang": "groovy",
"inline": "doc['http.in_bytes'].value + doc['http.out_bytes'].value"
}
}
}
}
}
}
}
使用 painless,修改 script 部分如下:
"sum_total_bytes": {
"sum": {
"script": {
"lang": "painless",
"inline": "doc['tcp.in_bytes'].value + doc['tcp.out_bytes'].value"
}
}
}
以前一直用 groovy,因为比较好用。但最近两年的某个版本开始将其 Deprecation 了,官方推荐使用 painless。
painless 也能实现一些脚本语法,具体可以上官网查询。
3. IP范围过滤+聚合过滤+度量
关键词:range,aggregations,filter,metric。
先根据 IP范围 过滤,然后开始聚合,筛选出 目的端口 是 80、443 的数据,再计算这些数据中(80、443端口) 进流量、出流量 在该时间段内的总和。
至于为什么要在聚合中进行过滤而不是在聚合前就过滤,是因为下面语句只是完整 dsl 的一部分,该查询业务还要同时对其它端口进行聚合操作。
GET /wenyuanblog-2019.05.*/_search
{
"query": {
"bool": {
"filter": [
{
"bool": {
"must": [
{
"range": {
"@timestamp": {
"to": 1557503999000,
"from": 1557417600000
}
}
},
{
"range": {
"http.src_ip.dotted": {
"to": "10.255.255.255",
"from": "10.0.0.0"
}
}
}
]
}
}
]
}
},
"from": 0,
"size": 0,
"aggs": {
"dport_80_443": {
"filter": {
"bool": {
"must": [
{
"terms": {
"http.dport": [
80,
443
]
}
}
]
}
},
"aggs": {
"sum_in_bytes": {
"sum": {
"field": "http.in_bytes"
}
},
"sum_out_bytes": {
"sum": {
"field": "http.out_bytes"
}
}
}
}
}
}
六、总结
ES 很强大,它支持的查询有很多,这里我只是列举了平时在 Kibana 中经常调试的 DSL。
还有很多实际业务中经常用到的模糊查询(wildcard、regexp、prefix)、nested 查询、多层聚合等等,基本上都是先封装第三方引擎,然后开发时转成 DSL 并 copy 到 Kibana 进行验证和查错。
更复杂的应用这里就不列举出来了,必要时候官方文档是最好的资料。