# 查询
本文记录对于 ES document 的常见查询操作。
# 简单查询
ES 支持通过 GET 请求进行简单查询。相关 API :
GET /<index>/_search # 查询指定索引下的所有文档 GET /index1,index2/_search # 查询多个索引 GET /_search # 查询所有索引 GET /_search?q=message:hello # 可以在 URL 的查询字符串中加入 q 字段及查询参数
客户端向 ES 发送查询请求之后,收到的 ES 响应格式如下:
[root@CentOS ~]# curl -X GET 127.0.0.1:9200/test_log/_search?pretty { "took" : 14, # 本次查询的耗时,单位为 ms "timed_out" : false, # 该请求的处理过程是否超时 "_shards" : { "total" : 1, # 总共尝试查询多少个分片(包括主分片、副分片) "successful" : 1, # 有多少个分片成功了 "skipped" : 0, # 有多少个分片跳过了 "failed" : 0 # 有多少个分片失败了 }, "hits" : { "total" : { "value" : 8, # 查询条件匹配的文档数量(但实际返回的文档数可能受限制,更小) "relation" : "eq" # 这些文档与查询条件的关系 }, "max_score" : 1.0, # 这些文档的最大相关性 "hits" : [ # hits 字典中的 hits 列表包含了 ES 实际返回给客户端的文档 { "_index" : "test_log", "_type" : "_doc", "_id" : "ZhzMiXABzduhqPWX7mUX", "_score" : 1.0, # 一个正浮点数,表示该文档与查询条件的相关性 "_source" : { "pid" : 1, "level" : "INFO", "message": "process started." } }, ...
- ES 默认将 hits 列表中的各个文档按 _score 的值进行排序。
# DSL 查询
ES 提供了一种 DSL 查询语法,可以实现复杂的查询。相关 API :
GET /<index>/_search [request_body] POST /<index>/_delete_by_query [request_body] # 删除与 DSL 查询条件匹配的文档 ?proceed=proceed # 如果遇到文档 version conflict ,默认动作为 abort ,建议改为 proceed ?scroll_size=1000 # 每个 batch 大小,默认为 1000 。增加该值可以些许提高删除速度 ?wait_for_completion=false # 如果操作耗时过久,建议改为后台执行的 task POST /<index>/_update_by_query [request_body] # 修改与 DSL 查询条件匹配的文档
- 需要在请求报文 body 中填入 JSON 格式的查询参数。
- 如果省略 request_body ,则会匹配所有文档,相当于简单查询。
例:查询并修改文档
POST /my-index-1/_update_by_query { "query": { "wildcard": { "ip":{ "value": "10.0.0.*" } } }, "script": { "source": "ctx._source['hostname'] = 'CentOS'; ctx._source['release'] = '7'" } }
查询时,query 字段表示查询条件,还可添加其它字段,如下:
GET /test_log/_search { "query": {...}, # 关于 _source 字段 "_source": true, # 返回的每个文档是否包含内置字段 _source ,默认为 true "_source": false, # 不返回 doc["_source"] "_source": ["log*", "log.offset"], # 返回 doc["_source"] 时,只包含指定的一些子字段,可使用通配符 * 。如果指定的字段不存在,则返回结果中不包含该字段,但不会报错 "_source": { # 返回 doc["_source"] 时,包含一些子字段,排除一些子字段 "includes": [ "filed1", "filed2" ], "excludes": [ "filed3" ] }, # 默认情况下,返回的每个文档只包含 _index、_id、、_score、_source 这几个内置字段,不包含其它字段 # 可添加以下参数,额外返回一些字段。这些字段会作为 doc["fields"] 的子字段返回,且每个字段的值都放在一个 array 中 "fields": ["filed1"], # 从 doc["_source"] 提取一些字段,作为 doc["fields"] 的子字段返回 "docvalue_fields" : ["filed1"], # 返回哪些字段的 doc_values "stored_fields" : ["filed1"], # 返回哪些字段的 store_values "script_fields": { # 通过执行脚本,在返回的文档中加入临时生成的字段 "tmp_fiedl1": { "script": { "lang": "painless", "source": "doc['field1'].value * 2" } } } }
# match
- query 中可添加 match 查询子句,用于根据字段的值查询文档。
- 例:
GET /test_log/_search { "query" : { "match" : { "message" : { # 指定要查询的字段名称 "query": "hello" # 指定要查询的关键词,找出哪些文档的该字段的值包含该关键词 # "operator": "or", # 设置当 query_string 被分词之后,几个词之间的逻辑关系。可以取值为 or 或 and # "fuzziness": 0, # "max_expansions": 50, } } } }
- 没有其它查询参数时,可以简写为:
"match" : { "message" : "hello" }
- 查询的字段可以是以下形式:
"pid" : 1 # 字段的值可以是 string、number、boolean 等类型 "pid" : "1" # number 类型的值也可以写作 string 形式 "host.os.platform" : "centos" # 字段名可以通过 . 引用子字段
- 没有其它查询参数时,可以简写为:
- 常见的几种 match 子句:
"match" : {"message": "A B"} # 分词之后,查询存在这些词语之一的文档。这里是查询 name 包含 A 或 B 的文档
"match_phrase" : {"message": "A B"} # 分词之后,查询同时存在这些词语的文档,且词语的先后顺序一致
"match_all" : {} # 匹配所有文档
"multi_match" : { "fields": ["message", "msg*"] # 指定多个字段进行 match 查询。不指定 fields 则会查询所有字段,实现全文搜索 "query": "hello" , }
- 如果 query_string 包含保留字符
+ - = && || > < ! ( ) { } [ ] ^ " ~ * ? : \ /
,则需要用\
转义。采用 JSON 格式时需要用\\
转义。< >
总数不支持转义。
# term
- query 中可添加 term 查询子句,用于精确匹配字段值。
- 例:
GET /test_log/_search { "query": { "term": { "message": { "value": "hello" # 字段值必须等于它 # "boost": 1.0, # 控制相关性分数 _score 。默认取值为 1.0 ,取值小于 1 会降低得分,取值大于 1 会增加得分 } } } }
- 如果想查询取值等于空字符串 "" 的字段,则有以下几种方式:
GET /test_log/_search { "query": { "term": { "message.keyword": "" } } }
GET /test_log/_search { "query": { "bool": { "must": [ { "exists": { "field": "message" } } ], "must_not": [ { "wildcard": { "message": "*" } } ] } } }
# exists
- query 中可添加 exists 查询子句,用于查询存在指定字段名称的文档,但不管该字段的取值是否为空。
- 例:
GET /test_log/_search { "query": { "exists": { "field": "name" } } }
# range
- query 中可添加 range 查询子句,用于查询字段值在指定范围内的文档。
- 例:
GET /test_log/_search { "query": { "range": { "pid": { "gt": 10, # 大于 "lt": 20, # 小于 # "gte" : 20, # 大于等于 # "lte" : 20, # 小于等于 } } } }
# prefix
- query 中可添加 prefix 查询子句,用于进行前缀查询。
- 只能查询 text、keyword 类型的字段。
- 例:
GET /test_log/_search { "query": { "prefix": { "message": { "value": "hello" # 字段值需要以该前缀开头 } } } }
# wildcard
- query 中可添加 wildcard 查询子句,用于进行通配符查询。
- 例:
GET /test_log/_search { "query": { "wildcard": { "message": { "value": "hello*" # 字段值需要匹配该 pattern } } } }
- 可以使用通配符 ? 和 * 。
- 用于查询的 pattern 应该避免以通配符开头,否则会大幅增加查询的开销。
# regexp
- query 中可添加 regexp 查询子句,用于进行正则查询。
- 例:
GET /test_log/_search { "query": { "regexp": { "message": { "value": "hello.*" } } } }
- text 类型的 value 在存储时会被分词,因此 ES 只支持对单个单词进行正则匹配,不能匹配一条句子。
# fuzzy
- query 中可添加 fuzzy 查询子句,用于进行模糊查询。
- 模糊查询时,会将查询的值修改几个字符,生成一些变体,然后分别尝试 match 查询。
- 修改的字符数称为编辑距离。
- 每个字符区分大小写。
- 比如编辑距离为 1 时, "hello" 可以生成以下变体:
Hello # 替换一个字符 Hell # 移除一个字符 Heyllo # 在任意位置插入一个字符 eHllo # 交换两个相邻字符的位置
- 例:
GET /test_log/_search { "query": { "fuzzy": { "message": { "value": "hello" # 字段值需要与它模糊匹配 # "fuzziness": "AUTO", # 允许的最大编辑距离 # "max_expansions": 50, # 允许的最大变体数 } } } }
# bool
- query 中可添加 bool 子句,用于进行布尔查询。
- 例:
GET /test_log/_search { "query": { "bool": { "must": [{ "range": { "pid": { "gte": 10, "lte": 20 } } }, { "bool": { "should": [{ "match": { "message": "A" } }, { "match": { "message": "B" } } ], "minimum_should_match": 1 } } ], "must_not": [{ "match": { "pid": 1 } }] } } }
- match、exists、wildcard 等查询子句一次只能查询一个字段,查询多个字段时需要通过 bool 查询组合起来。
- bool 查询中可以组合使用以下查询子句:
must
子句是一个列表,文档必须符合其中列出的所有条件。must_not
子句是一个列表,文档必须不符合其中列出的所有条件。should
子句是一个列表,文档应该符合其中列出的条件。不能写在与 must 同一层级的位置,否则会失效。filter
子句
minimum_should_match
表示文档至少应该符合should
列表中的几个条件。- 默认值为 1 。
- 可以设置成任意正整数,比如 10 。不过实际生效的值不会超过
should
条件总数 n 。 - 可以设置成任意负整数,比如 -2 ,这表示实际生效的值等于 n-2 。
- 可以设置成百分比格式,比如 75% 、-25% ,这会向下取整。
# filter
- query 中可添加 filter 子句,用于添加过滤器,使 ES 只返回过滤后的文档。
- 例:
GET /test_log/_search { "query": { "bool": { "must": { "match_all": {} }, "filter": { "match": { "message": "hello" } } } } }
- query 中的查询子句称为 "查询上下文" ,filter 中的查询子句称为 "过滤器上下文" 。
- ES 只会返回同时匹配 "查询上下文" 和 "过滤器上下文" 的文档,不过文档的 _score 的值只由 "查询上下文" 决定,不受 "过滤器上下文" 影响。
# 分页查询
_search 查询命中较多文档时,客户端不一定能一次性处理这么多文档。可以让客户端进行分页查询,多次从 ES 获取文档,每次只获取少量文档。
分页查询的基本原理:
- 客户端发送查询请求到 ES 。
- ES 找出查询条件命中的所有文档 _id ,按 sort 条件进行排序,得到一个文档 _id 列表,暂存在内存中,称为上下文(context)。
- 客户端从 ES 获取第 n 至 m 个文档。
- ES 删除 context 。
分页查询虽然方便用户查看数据,但增加了客户端代码复杂度、服务器内存开销。
- 查询命中的文档数越多,context 占用的 ES 内存越多。因此建议:
- 禁止执行深度分页查询,例如 Google 搜索一般最多显示 50 页的结果。
- 通过 query、sort 等方式,将用户需要查看的文档排序到前几页。
- 查询命中的文档数越多,context 占用的 ES 内存越多。因此建议:
ES 提供了多种分页查询的方案:
- from :适合查询命中的文档数低于 10000 的情况,属于浅分页。
- scroll :适合查询命中的文档数超过 10000 的情况,属于深度分页。
- search_after :与 scroll 类似。
- ES v7 建议将 scroll 请求改为 PIT + search_after 请求,因为功能更多、占用内存更少。
# from
用法示例:
- 客户端发送一个查询请求:
GET /test_log/_search { "query": ..., "sort": [{ # 返回文档时,按指定字段的值排序,asc 表示升序,desc 表示降序 "_id": "asc" # ES v8 开始,内置字段 _id 默认禁用 fielddata ,因此不能按 _id 字段排序 }], "from": 0, # 返回从第几个编号开始的文档(从 0 开始编号) "size": 10, # 返回文档的最大数量,默认为 10 。比如查询命中 1000 个文档,只返回 10 个文档 # "track_total_hits": 10000, # ES v7.0 新增的参数,默认当查询命中 10000 个文档时就提前结束查询,返回结果,从而节省内存开销。将该参数设置为 true ,则允许查询所有文档 }
- 客户端可以发送多个查询请求,例如
{"from": 0, "size": 10}
、{"from": 10, "size": 10}
。这样逐渐增加 from 的值,就能遍历所有文档。
- 客户端发送一个查询请求:
优点:
- 既支持按顺序遍历文档,也支持跳页(直接跳转到第 n 页,查看第 n 至 m 个文档)。
缺点:
- 不能暂存某个时刻的 context ,因此多次分页查询时,不能保证一致性。
- 只适合浅分页。ES 默认给每个 index 配置了
"max_result_window": 10000
,限制 from + size 之和不能超过 10000 。
关于 sort 排序。
- 排序时,需要将所有文档的同一字段的值加载到 ES 内存中,因此应该避免对海量文档进行排序。
- 按 keyword 类型的字段进行排序时,会先比较前缀相同的字符串,再比较同位字符的 ASCII 码值。例如以下值从上到下按 asc 排序:
hello hello1 # 它与 hello 前缀相同,且多了一个字符,因此排序在 hello 之后 hello2 # 它与 hello1 只是最后一位字符不同,且最后一位字符的 ASCII 码值更大,因此排序在 hello1 之后 help
# scroll
用法示例:
- 客户端发送一个查询请求:
POST /test_log/_search?scroll=1m { "query": ..., "size": ..., "sort": ... }
- 这会让 ES 查询文档并排序,得到一个文档 _id 列表,暂存在内存中,称为 scroll context 。
- 然后 ES 会返回 scroll_id ,以及不超过 size 数量的文档,格式如下:
{ "_scroll_id": "******", "took": ..., "hits": ... }
- 客户端可以使用 scroll_id ,构造多个查询请求,从而获取这个 scroll context 中剩下的文档:
POST /_search/scroll { "scroll" : "1m", # 将该 scroll context 的 TTL 重置为 1m 。如果不填该参数,则会在执行该查询请求之后,立即删除该 scroll context "scroll_id" : "******" }
- 该查询请求中不能指定 index、size ,因为创建 scroll context 时已经固定了这些值。
- 然后 ES 会返回同一个 scroll_id ,以及不超过 size 数量的文档,格式如下:
{ "_scroll_id": "******", "took": ..., "hits": ... }
- 上面创建 scroll context 时,设置的 TTL 为 1m ,超时之后会被 ES 自动删除。
- 客户端也可以主动删除 scroll context :
DELETE /_search/scroll { "scroll_id" : "******" }
DELETE /_search/scroll/_all # 删除所有 scroll context
- 客户端也可以主动删除 scroll context :
- 客户端发送一个查询请求:
优点:
- 可以暂存某个时刻的 context ,因此多次分页查询时,能保证一致性。
缺点:
- 只支持单向遍历文档,不支持双向遍历,不支持像 from 查询一样跳页。因此 scroll 请求通常用于同步 index 数据,不适合供用户分页查询。
- 如果客户端同时执行大量 scroll 请求,则会创建大量 scroll context ,占用较多内存。
- ES v6 及更早版本,不限制 scroll context 的数量。
- ES v7 开始,默认限制整个 ES 集群最多存在 500 个 scroll context ,超过则拒绝新的 scroll 请求。配置示例:
PUT _cluster/settings { "persistent" : { "search.max_open_scroll_context": 500 }, "transient": { "search.max_open_scroll_context": 500 } }
- 可执行以下请求,查询当前的 scroll context 数量:
GET /_nodes/stats/indices/search
# search_after
用法示例:
- 客户端发送一个查询请求:ES 响应示例:
GET /test_log/_search { "query": ..., "size": ..., "sort": [ # 指定至少一个取值唯一的字段来排序 {"_id": "asc"} ], "search_after": ["!"] # 让 ES 返回排序值在这之后的文档。这里填 ! ,是因为它的 ASCII 码值小于所有 _id ,可以遍历所有文档 }
{ "took": ..., "hits": { "total": { "value": 10000, "relation": "gte" }, "max_score": null, "hits": [ { "_index": "test_log", "_id": "036Tq4oBqx4_-nLX85V1", "_score": null, "_source": { ... }, "sort": [ # 该文档参与排序的所有字段的值 "036Tq4oBqx4_-nLX85V1" ] }, ... } }
- 客户端读取 ES 响应中的 hits 列表,取出最后一个文档的 sort 值,构造下一个查询请求,就能从当前位置继续遍历文档:
GET /test_log/_search { "query": ..., "size": ..., "sort": [ {"_id": "asc"} ], "search_after": ["036Tq4oBqx4_-nLX85V1"] }
- 如果ES 响应的 hits 列表为空,则说明已经遍历完所有文档。
- 客户端发送一个查询请求:
优点:
- 与 scroll 查询相比,支持双向遍历,比如向前翻页,只需要客户端缓存之前的 search_after 值。
缺点:
- 不能像 from 查询一样跳页。除非客户端能预测第 n 页的排序值。
- 不能暂存某个时刻的 context ,因此多次分页查询时,不能保证一致性。
# PIT
ES v7.10 新增了 PIT(point in time),又称为 search context 。
- 用途:对 index 创建一个轻量级视图,记录当前时刻的所有文档 _id 。
- 使用 PIT ,客户端可以对某个时刻的所有文档进行多次不同的查询,忽略该时刻之后新增、减少的文档。
用法示例:
客户端请求创建一个 PIT :
POST /test_log/_pit?keep_alive=1m
- request body 为空。
- 然后 ES 会返回 pit_id ,格式如下:
{ "id": "******" }
客户端可以使用 pit_id ,构造多个查询请求,从而获取这个 PIT 中的文档:
POST /_search { "query": ..., "size": ..., "sort": ..., "pit": { "id": "******", "keep_alive": "1m" # 将该 PIT 的 TTL 重置为 1m }, # "from": ..., # "search_after": ... }
- 可以在 PIT 查询请求中加入 from 或 search_after 条件,从而实现分页查询。
上面创建 PIT 时,设置的 TTL 为 1m ,超时之后会被 ES 自动删除。
- 客户端也可以主动删除 PIT :
DELETE /_pit { "id": "******" }
- 客户端也可以主动删除 PIT :
scroll context 与 PIT 都是用于暂存文档 _id 列表,共同点如下:
- 优点:
- 暂存某个时刻的 scroll context 或 PIT 之后,多次分页查询时,能保证一致性。
- 缺点:
- 使用 scroll context 或 PIT 时,客户端只能遍历指定时刻的所有文档,不能发现之后的文档变化,因此不适合实时查询。
- 同时存在的 scroll context 或 PIT 越多,占用的 ES 内存越多。
- scroll context 或 PIT 长时间未关闭时,它引用的文档所在的 segment 也不能被 ES 删除、合并。
- 优点:
scroll context 与 PIT 的差异如下:
- scroll context 的工作流程:
- ES 查询文档并排序,得到一个文档 _id 列表,暂存在内存中。
- 客户端分批从 scroll context 获取文档。(此时不能改变查询条件)
- PIT 的工作流程:
- ES 直接暂存 index 下的所有文档 _id ,不进行查询、排序。
- 客户端向 PIT 进行多次不同的查询。(此时可以改变查询条件)
- 与 scroll context 对比,更推荐使用 PIT ,因为:
- PIT 功能更多,可以与 from 或 search_after 组合查询。
- PIT 更轻量级,占用的内存更少。
- scroll context 的工作流程:
# 聚合查询
- 聚合查询(aggregations):用于在查询时进行一些额外操作。
- 一个聚合查询中可以包含多个聚合操作。聚合操作分为三类:
- Metric
- Bucket
- Pipeline
- Metric、Bucket 聚合操作中,支持声明嵌套的子聚合查询。而 Pipeline 不支持。
# Metric
:指标聚合。用于统计 sum、avg、max、min 等指标。
- 例:统计 avgES 响应示例:
GET /test_log/_search { # "query": {}, # "size": 0, # 可以指定 size 为 0 ,使其不返回 query 的查询结果,只返回聚合结果 "aggs": { # 启用聚合查询 "my_agg_1": { # 声明一个聚合操作,自定义名称 "avg": { "field": "log.offset" } } } }
{ "took" : 1, "hits" : { ... }, "aggregations" : { "my_agg_1" : { "value" : 433553217 } } }
# Bucket
:桶聚合。基于字段值等条件,将查询到的文档分为多个组,称为桶。
- 例:terms 聚合,是根据指定字段的非重复值来分组
GET /test_log/_search { "size": 0, "aggs": { "my_agg_1": { "terms": { "field": "product", # "size": 10, # 限制最多返回的 bucket 数,默认为 10 # "min_doc_count": 1, # 每个 bucket 至少包含的文档数,不满足则不返回 # "order": {"_count": "asc"}, # bucket 的排序方式 # "show_term_doc_count_error": true # 是否在聚合结果中显示 sum_other_doc_count ,表示除了当前 bucket 以外的文档总数,包括分组失败的、bucket 排名超过 size 的 } } } }
# Pipeline
:管道聚合。用于处理其它聚合操作的输出。
- 例:avg_bucket 聚合,是统计一些 bucket 的平均值
POST /test_log/_search { "aggs": { "my_agg_1": { "date_histogram": { "field": "date", "calendar_interval": "month" }, "aggs": { "my_agg_2": { "sum": { "field": "price" } } } }, "my_agg_3": { "avg_bucket": { "buckets_path": "my_agg_1>my_agg_2" # 指定目标 bucket 的路径 } } } }