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

  下边的例子就表示查找 hostwenyuanblog.com 的所有记录。

POST /wenyuanblog-2018.03.02/_search
{
  "query": {
    "match": {
      "host": "wenyuanblog.com"
    }
  }
}

3. multi_match

  在多个字段上执行相同的 match 查询,下边的例子就表示查询 hosthttp_referer 字段中包含 wenyuanblog.com 的记录。

GET /wenyuanblog-2018.03.02/_search
{
  "query": {
    "multi_match": {
      "query": "wenyuanblog.com",
      "fields": [
        "host",
        "http_referer"
      ]
    }
  }
}

4. query_string

  可以在查询里边使用AND或者OR来完成复杂的查询。
  下边的例子表示查找 hosta.wenyuanblog.com 或者 b.wenyuanblog.com 的所有记录。

GET /wenyuanblog-2018.03.02/_search
{
  "query": {
    "query_string": {
      "query": "(a.wenyuanblog.com) OR (b.wenyuanblog.com)",
      "fields": [
        "host"
      ]
    }
  }
}

  也可以组合更多的条件完成更复杂的查询请求。
  下边的例子表示查询(hosta.wenyuanblog.com)或者是(hostb.wenyuanblog.comstatus404)的所有记录。

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。
  下面是一个组合查询的例子,我们要查询 hostwenyuanblog.comhttp_x_forworded_for47.97.12.69status不为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进行验证和查错。
  更复杂的应用这里就不列举出来了,必要时候官方文档是最好的资料。


  目录