# 查询

本文记录对于 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 获取文档,每次只获取少量文档。

  • 分页查询的基本原理:

    1. 客户端发送查询请求到 ES 。
    2. ES 找出查询条件命中的所有文档 _id ,按 sort 条件进行排序,得到一个文档 _id 列表,暂存在内存中,称为上下文(context)。
    3. 客户端从 ES 获取第 n 至 m 个文档。
    4. ES 删除 context 。
  • 分页查询虽然方便用户查看数据,但增加了客户端代码复杂度、服务器内存开销。

    • 查询命中的文档数越多,context 占用的 ES 内存越多。因此建议:
      • 禁止执行深度分页查询,例如 Google 搜索一般最多显示 50 页的结果。
      • 通过 query、sort 等方式,将用户需要查看的文档排序到前几页。
  • ES 提供了多种分页查询的方案:

    • from :适合查询命中的文档数低于 10000 的情况,属于浅分页。
    • scroll :适合查询命中的文档数超过 10000 的情况,属于深度分页。
    • search_after :与 scroll 类似。
      • ES v7 建议将 scroll 请求改为 PIT + search_after 请求,因为功能更多、占用内存更少。

# from

  • 用法示例:

    1. 客户端发送一个查询请求:
      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 ,则允许查询所有文档
      }
      
    2. 客户端可以发送多个查询请求,例如 {"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

  • 用法示例:

    1. 客户端发送一个查询请求:
      POST /test_log/_search?scroll=1m
      {
        "query": ...,
        "size": ...,
        "sort": ...
      }
      
      • 这会让 ES 查询文档并排序,得到一个文档 _id 列表,暂存在内存中,称为 scroll context 。
      • 然后 ES 会返回 scroll_id ,以及不超过 size 数量的文档,格式如下:
        {
          "_scroll_id": "******",
          "took": ...,
          "hits": ...
        }
        
    2. 客户端可以使用 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": ...
        }
        
    3. 上面创建 scroll context 时,设置的 TTL 为 1m ,超时之后会被 ES 自动删除。
      • 客户端也可以主动删除 scroll context :
        DELETE /_search/scroll
        {
          "scroll_id" : "******"
        }
        
        DELETE /_search/scroll/_all   # 删除所有 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

  • 用法示例:

    1. 客户端发送一个查询请求:
      GET /test_log/_search
      {
        "query": ...,
        "size": ...,
        "sort": [               # 指定至少一个取值唯一的字段来排序
          {"_id": "asc"}
        ],
        "search_after": ["!"]   # 让 ES 返回排序值在这之后的文档。这里填 ! ,是因为它的 ASCII 码值小于所有 _id ,可以遍历所有文档
      }
      
      ES 响应示例:
      {
        "took": ...,
        "hits": {
          "total": {
            "value": 10000,
            "relation": "gte"
          },
          "max_score": null,
          "hits": [
            {
              "_index": "test_log",
              "_id": "036Tq4oBqx4_-nLX85V1",
              "_score": null,
              "_source": {
                ...
              },
              "sort": [   # 该文档参与排序的所有字段的值
                "036Tq4oBqx4_-nLX85V1"
              ]
            },
            ...
        }
      }
      
    2. 客户端读取 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 ,客户端可以对某个时刻的所有文档进行多次不同的查询,忽略该时刻之后新增、减少的文档。
  • 用法示例:

    1. 客户端请求创建一个 PIT :

      POST /test_log/_pit?keep_alive=1m
      
      • request body 为空。
      • 然后 ES 会返回 pit_id ,格式如下:
        {
          "id": "******"
        }
        
    2. 客户端可以使用 pit_id ,构造多个查询请求,从而获取这个 PIT 中的文档:

      POST /_search
      {
        "query": ...,
        "size": ...,
        "sort": ...,
        "pit": {
          "id":  "******",
          "keep_alive": "1m"  # 将该 PIT 的 TTL 重置为 1m
        },
        # "from": ...,
        # "search_after": ...
      }
      
      • 可以在 PIT 查询请求中加入 from 或 search_after 条件,从而实现分页查询。
    3. 上面创建 PIT 时,设置的 TTL 为 1m ,超时之后会被 ES 自动删除。

      • 客户端也可以主动删除 PIT :
        DELETE /_pit
        {
          "id": "******"
        }
        
  • scroll context 与 PIT 都是用于暂存文档 _id 列表,共同点如下:

    • 优点:
      • 暂存某个时刻的 scroll context 或 PIT 之后,多次分页查询时,能保证一致性。
    • 缺点:
      • 使用 scroll context 或 PIT 时,客户端只能遍历指定时刻的所有文档,不能发现之后的文档变化,因此不适合实时查询。
      • 同时存在的 scroll context 或 PIT 越多,占用的 ES 内存越多。
      • scroll context 或 PIT 长时间未关闭时,它引用的文档所在的 segment 也不能被 ES 删除、合并。
  • scroll context 与 PIT 的差异如下:

    • scroll context 的工作流程:
      1. ES 查询文档并排序,得到一个文档 _id 列表,暂存在内存中。
      2. 客户端分批从 scroll context 获取文档。(此时不能改变查询条件)
    • PIT 的工作流程:
      1. ES 直接暂存 index 下的所有文档 _id ,不进行查询、排序。
      2. 客户端向 PIT 进行多次不同的查询。(此时可以改变查询条件)
    • 与 scroll context 对比,更推荐使用 PIT ,因为:
      • PIT 功能更多,可以与 from 或 search_after 组合查询。
      • PIT 更轻量级,占用的内存更少。

# 聚合查询

  • 聚合查询(aggregations):用于在查询时进行一些额外操作。
  • 一个聚合查询中可以包含多个聚合操作。聚合操作分为三类:
    • Metric
    • Bucket
    • Pipeline
      • Metric、Bucket 聚合操作中,支持声明嵌套的子聚合查询。而 Pipeline 不支持。

# Metric

:指标聚合。用于统计 sum、avg、max、min 等指标。

  • 例:统计 avg
    GET /test_log/_search
    {
      # "query": {},
      # "size": 0,              # 可以指定 size 为 0 ,使其不返回 query 的查询结果,只返回聚合结果
      "aggs": {                 # 启用聚合查询
        "my_agg_1": {           # 声明一个聚合操作,自定义名称
          "avg": {
            "field": "log.offset"
          }
        }
      }
    }
    
    ES 响应示例:
    {
      "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 的路径
          }
        }
      }
    }