elasticsearch 基础 —— 请求体查询

2023-01-07,,,

请求查询

简易 查询 —query-string search— 对于用命令行进行即席查询(ad-hoc)是非常有用的。 然而,为了充分利用查询的强大功能,你应该使用 请求体 search API, 之所以称之为请求体查询(Full-Body Search),因为大部分参数是通过 Http 请求体而非查询字符串来传递的。

请求体查询 —下文简称 查询—不仅可以处理自身的查询请求,还允许你对结果进行片段强调(高亮)、对所有或部分结果进行聚合分析,同时还可以给出 你是不是想找 的建议,这些建议可以引导使用者快速找到他想要的结果。

空查询

让我们以 最简单的 search API 的形式开启我们的旅程,空查询将返回所有索引库(indices)中的所有文档:

GET /_search

    {}  

  这是一个空的请求体。

只用一个查询字符串,你就可以在一个、多个或者 _all 索引库(indices)和一个、多个或者所有types中查询:

GET /index_2014*/type1,type2/_search

    {}

同时你可以使用 from 和 size 参数来分页:

GET /_search

    {

      "from": 30,

      "size": 10

    }

一个带请求体的 GET 请求?

某些特定语言(特别是 JavaScript)的 HTTP 库是不允许 GET 请求带有请求体的。 事实上,一些使用者对于 GET 请求可以带请求体感到非常的吃惊。

而事实是这个RFC文档 RFC 7231— 一个专门负责处理 HTTP 语义和内容的文档 — 并没有规定一个带有请求体的 GET 请求应该如何处理!结果是,一些 HTTP 服务器允许这样子,而有一些 — 特别是一些用于缓存和代理的服务器 — 则不允许。

对于一个查询请求,Elasticsearch 的工程师偏向于使用 GET 方式,因为他们觉得它比 POST 能更好的描述信息检索(retrieving information)的行为。然而,因为带请求体的 GET 请求并不被广泛支持,所以 search API 同时支持 POST 请求:

POST /_search

    {

      "from": 30,

      "size": 10

    }

类似的规则可以应用于任何需要带请求体的 GET API。

我们将在聚合 聚合 章节深入介绍聚合(aggregations),而现在,我们将聚焦在查询。

相对于使用晦涩难懂的查询字符串的方式,一个带请求体的查询允许我们使用 查询领域特定语言(query domain-specific language) 或者 Query DSL 来写查询语句。

查询表达式

查询表达式(Query DSL)是一种非常灵活又富有表现力的 查询语言。 Elasticsearch 使用它可以以简单的 JSON 接口来展现 Lucene 功能的绝大部分。在你的应用中,你应该用它来编写你的查询语句。它可以使你的查询语句更灵活、更精确、易读和易调试。

要使用这种查询表达式,只需将查询语句传递给 query 参数:

GET /_search

    {

        "query": YOUR_QUERY_HERE

    }

空查询(empty search) —{}— 在功能上等价于使用 match_all 查询, 正如其名字一样,匹配所有文档:

GET /_search

    {

        "query": {

            "match_all": {}

        }

    }

查询语句的结构

一个查询语句 的典型结构:

{

        QUERY_NAME: {

            ARGUMENT: VALUE,

            ARGUMENT: VALUE,...

        }

    }

如果是针对某个字段,那么它的结构如下:

{

        QUERY_NAME: {

            FIELD_NAME: {

                ARGUMENT: VALUE,

                ARGUMENT: VALUE,...

            }

        }

    }

举个例子,你可以使用 match 查询语句 来查询 tweet 字段中包含 elasticsearch 的 tweet:

{

        "match": {

            "tweet": "elasticsearch"

        }

    }

{

        "match": {

            "tweet": {

                "query": "elasticsearch"

            }

        }

    }

完整的查询请求如下:

GET /_search

    {

        "query": {

            "match": {

                "tweet": "elasticsearch"

            }

        }

    }

{

        "query": {

            "match": {

                "tweet": {

                    "query": "elasticsearch"

                }

            }

        }

    }

合并查询语句

查询语句(Query clauses) 就像一些简单的组合块 ,这些组合块可以彼此之间合并组成更复杂的查询。这些语句可以是如下形式:

叶子语句(Leaf clauses) (就像 match 语句) 被用于将查询字符串和一个字段(或者多个字段)对比。
复合(Compound) 语句 主要用于 合并其它查询语句。 比如,一个 bool 语句 允许在你需要的时候组合其它语句,无论是 must 匹配、 must_not 匹配还是 should 匹配,同时它可以包含不评分的过滤器(filters):

{

        "bool": {

            "must":     { "match": { "tweet": "elasticsearch" }},

            "must_not": { "match": { "name":  "mary" }},

            "should":   { "match": { "tweet": "full text" }},

            "filter":   { "range": { "age" : { "gt" : 30 }} }

        }

    }

一条复合语句可以合并 任何 其它查询语句,包括复合语句,了解这一点是很重要的。这就意味着,复合语句之间可以互相嵌套,可以表达非常复杂的逻辑。

例如,以下查询是为了找出信件正文包含 business opportunity 的星标邮件,或者在收件箱正文包含business opportunity 的非垃圾邮件:

{

        "bool": {

            "must": { "match":   { "email": "business opportunity" }},

            "should": [

                { "match":       { "starred": true }},

                { "bool": {

                    "must":      { "match": { "folder": "inbox" }},

                    "must_not":  { "match": { "spam": true }}

                }}

            ],

            "minimum_should_match": 1

        }

    }

到目前为止,你不必太在意这个例子的细节,我们会在后面详细解释。最重要的是你要理解到,一条复合语句可以将多条语句 — 叶子语句和其它复合语句 — 合并成一个单一的查询语句。

查询与过滤

Elasticsearch 使用的查询语言(DSL) 拥有一套查询组件,这些组件可以以无限组合的方式进行搭配。这套组件可以在以下两种情况下使用:过滤情况(filtering context)和查询情况(query context)。

当使用于 过滤情况 时,查询被设置成一个“不评分”或者“过滤”查询。即,这个查询只是简单的问一个问题:“这篇文档是否匹配?”。回答也是非常的简单,yes 或者 no ,二者必居其一。

created 时间是否在 2013 与 2014 这个区间?
status 字段是否包含 published 这个单词?
lat_lon 字段表示的位置是否在指定点的 10km 范围内?

当使用于 查询情况 时,查询就变成了一个“评分”的查询。和不评分的查询类似,也要去判断这个文档是否匹配,同时它还需要判断这个文档匹配的有 _多好_(匹配程度如何)。 此查询的典型用法是用于查找以下文档:

查找与 full text search 这个词语最佳匹配的文档
包含 run 这个词,也能匹配 runs 、 running 、 jog 或者 sprint
包含 quick 、 brown 和 fox 这几个词 — 词之间离的越近,文档相关性越高
标有 lucene 、 search 或者 java 标签 — 标签越多,相关性越高

一个评分查询计算每一个文档与此查询的 _相关程度_,同时将这个相关程度分配给表示相关性的字段 `_score`,并且按照相关性对匹配到的文档进行排序。这种相关性的概念是非常适合全文搜索的情况,因为全文搜索几乎没有完全 “正确” 的答案。

自 Elasticsearch 问世以来,查询与过滤(queries and filters)就独自成为 Elasticsearch 的组件。但从 Elasticsearch 2.0 开始,过滤(filters)已经从技术上被排除了,同时所有的查询(queries)拥有变成不评分查询的能力。

然而,为了明确和简单,我们用 "filter" 这个词表示不评分、只过滤情况下的查询。你可以把 "filter" 、 "filtering query" 和 "non-scoring query" 这几个词视为相同的。

相似的,如果单独地不加任何修饰词地使用 "query" 这个词,我们指的是 "scoring query" 。

性能差异

过滤查询(Filtering queries)只是简单的检查包含或者排除,这就使得计算起来非常快。考虑到至少有一个过滤查询(filtering query)的结果是 “稀少的”(很少匹配的文档),并且经常使用不评分查询(non-scoring queries),结果会被缓存到内存中以便快速读取,所以有各种各样的手段来优化查询结果。

相反,评分查询(scoring queries)不仅仅要找出 匹配的文档,还要计算每个匹配文档的相关性,计算相关性使得它们比不评分查询费力的多。同时,查询结果并不缓存。

多亏倒排索引(inverted index),一个简单的评分查询在匹配少量文档时可能与一个涵盖百万文档的filter表现的一样好,甚至会更好。但是在一般情况下,一个filter 会比一个评分的query性能更优异,并且每次都表现的很稳定。

过滤(filtering)的目标是减少那些需要通过评分查询(scoring queries)进行检查的文档。

如何选择查询与过滤

通常的规则是,使用 查询(query)语句来进行 全文 搜索或者其它任何需要影响 相关性得分 的搜索。除此以外的情况都使用过滤(filters)。

最重要的查询

虽然 Elasticsearch 自带了很多的查询,但经常用到的也就那么几个。我们将在 深入搜索 章节详细讨论那些查询的细节,接下来我们对最重要的几个查询进行简单介绍。

match_all 查询

match_all 查询简单的 匹配所有文档。在没有指定查询方式时,它是默认的查询:

{ "match_all": {}}

它经常与 filter 结合使用--例如,检索收件箱里的所有邮件。所有邮件被认为具有相同的相关性,所以都将获得分值为 1 的中性 `_score`。

match 查询

无论你在任何字段上进行的是全文搜索还是精确查询,match 查询是你可用的标准查询。

如果你在一个全文字段上使用 match 查询,在执行查询前,它将用正确的分析器去分析查询字符串:

{ "match": { "tweet": "About Search" }}

如果在一个精确值的字段上使用它, 例如数字、日期、布尔或者一个 not_analyzed 字符串字段,那么它将会精确匹配给定的值:

{ "match": { "age":    26           }}

    { "match": { "date":   "2014-09-01" }}

    { "match": { "public": true         }}

    { "match": { "tag":    "full_text"  }}

对于精确值的查询,你可能需要使用 filter 语句来取代 query,因为 filter 将会被缓存。接下来,我们将看到一些关于 filter 的例子。

不像我们在 轻量 搜索 章节介绍的字符串查询(query-string search), match 查询不使用类似 +user_id:2 +tweet:search 的查询语法。它只是去查找给定的单词。这就意味着将查询字段暴露给你的用户是安全的;你需要控制那些允许被查询字段,不易于抛出语法异常。

multi_match 查询

multi_match 查询可以在多个字段上执行相同的 match 查询:

{

        "multi_match": {

            "query":    "full text search",

            "fields":   [ "title", "body" ]

        }

    }

range 查询

range 查询找出那些落在指定区间内的数字或者时间:

{

        "range": {

            "age": {

                "gte":  20,

                "lt":   30

            }

        }

    }

被允许的操作符如下:

gt

大于

gte

大于等于

lt

小于

lte

小于等于

term 查询

term 查询被用于精确值 匹配,这些精确值可能是数字、时间、布尔或者那些 not_analyzed 的字符串:

{ "term": { "age":    26           }}

    { "term": { "date":   "2014-09-01" }}

    { "term": { "public": true         }}

    { "term": { "tag":    "full_text"  }}

term 查询对于输入的文本不 分析 ,所以它将给定的值进行精确查询。

terms 查询

terms 查询和 term 查询一样,但它允许你指定多值进行匹配。如果这个字段包含了指定值中的任何一个值,那么这个文档满足条件:

{ "terms": { "tag": [ "search", "full_text", "nosql" ] }}

和 term 查询一样,terms 查询对于输入的文本不分析。它查询那些精确匹配的值(包括在大小写、重音、空格等方面的差异)。

exists 查询和 missing 查询

exists 查询和 missing 查询被用于查找那些指定字段中有值 (exists) 或无值 (missing) 的文档。这与SQL中的 IS_NULL (missing) 和 NOT IS_NULL (exists) 在本质上具有共性:

{

        "exists":   {

            "field":    "title"

        }

    }

这些查询经常用于某个字段有值的情况和某个字段缺值的情况。

组合多查询

现实的查询需求从来都没有那么简单;它们需要在多个字段上查询多种多样的文本,并且根据一系列的标准来过滤。为了构建类似的高级查询,你需要一种能够将多查询组合成单一查询的查询方法。

你可以用 bool 查询来实现你的需求。这种查询将多查询组合在一起,成为用户自己想要的布尔查询。它接收以下参数:

must

文档 必须 匹配这些条件才能被包含进来。

must_not

文档 必须不 匹配这些条件才能被包含进来。

should

如果满足这些语句中的任意语句,将增加 _score ,否则,无任何影响。它们主要用于修正每个文档的相关性得分。

filter

必须 匹配,但它以不评分、过滤模式来进行。这些语句对评分没有贡献,只是根据过滤标准来排除或包含文档。

由于这是我们看到的第一个包含多个查询的查询,所以有必要讨论一下相关性得分是如何组合的。每一个子查询都独自地计算文档的相关性得分。一旦他们的得分被计算出来, bool 查询就将这些得分进行合并并且返回一个代表整个布尔操作的得分。

下面的查询用于查找 title 字段匹配 how to make millions 并且不被标识为 spam 的文档。那些被标识为 starred 或在2014之后的文档,将比另外那些文档拥有更高的排名。如果 _两者_ 都满足,那么它排名将更高:

{

        "bool": {

            "must":     { "match": { "title": "how to make millions" }},

            "must_not": { "match": { "tag":   "spam" }},

            "should": [

                { "match": { "tag": "starred" }},

                { "range": { "date": { "gte": "2014-01-01" }}}

            ]

        }

    }

如果没有 must 语句,那么至少需要能够匹配其中的一条 should 语句。但,如果存在至少一条 must 语句,则对 should 语句的匹配没有要求。

增加带过滤器(filtering)的查询

如果我们不想因为文档的时间而影响得分,可以用 filter 语句来重写前面的例子:

{

        "bool": {

            "must":     { "match": { "title": "how to make millions" }},

            "must_not": { "match": { "tag":   "spam" }},

            "should": [

                { "match": { "tag": "starred" }}

            ],

            "filter": {

              "range": { "date": { "gte": "2014-01-01" }}  

            }

        }

    }

  range 查询已经从 should 语句中移到 filter 语句

通过将 range 查询移到 filter 语句中,我们将它转成不评分的查询,将不再影响文档的相关性排名。由于它现在是一个不评分的查询,可以使用各种对 filter 查询有效的优化手段来提升性能。

所有查询都可以借鉴这种方式。将查询移到 bool 查询的 filter 语句中,这样它就自动的转成一个不评分的 filter 了。

如果你需要通过多个不同的标准来过滤你的文档,bool 查询本身也可以被用做不评分的查询。简单地将它放置到 filter 语句中并在内部构建布尔逻辑:

{

        "bool": {

            "must":     { "match": { "title": "how to make millions" }},

            "must_not": { "match": { "tag":   "spam" }},

            "should": [

                { "match": { "tag": "starred" }}

            ],

            "filter": {

              "bool": {   

                  "must": [

                      { "range": { "date": { "gte": "2014-01-01" }}},

                      { "range": { "price": { "lte": 29.99 }}}

                  ],

                  "must_not": [

                      { "term": { "category": "ebooks" }}

                  ]

              }

            }

        }

    }

  将 bool 查询包裹在 filter 语句中,我们可以在过滤标准中增加布尔逻辑

通过混合布尔查询,我们可以在我们的查询请求中灵活地编写 scoring 和 filtering 查询逻辑。

constant_score 查询

尽管没有 bool 查询使用这么频繁,constant_score 查询也是你工具箱里有用的查询工具。它将一个不变的常量评分应用于所有匹配的文档。它被经常用于你只需要执行一个 filter 而没有其它查询(例如,评分查询)的情况下。

可以使用它来取代只有 filter 语句的 bool 查询。在性能上是完全相同的,但对于提高查询简洁性和清晰度有很大帮助。

{

        "constant_score":   {

            "filter": {

                "term": { "category": "ebooks" }  

            }

        }

    }

 term 查询被放置在 constant_score 中,转成不评分的 filter。这种方式可以用来取代只有 filter 语句的 bool 查询。

验证查询

查询可以变得非常的复杂,尤其 和不同的分析器与不同的字段映射结合时,理解起来就有点困难了。不过 validate-query API 可以用来验证查询是否合法。

GET /gb/tweet/_validate/query

    {

       "query": {

          "tweet" : {

             "match" : "really powerful"

          }

       }

    }

以上 validate 请求的应答告诉我们这个查询是不合法的:

{

      "valid" :         false,

      "_shards" : {

        "total" :       1,

        "successful" :  1,

        "failed" :      0

      }

    }

理解错误信息

为了找出 查询不合法的原因,可以将 explain 参数 加到查询字符串中:

GET /gb/tweet/_validate/query?explain  

    {

       "query": {

          "tweet" : {

             "match" : "really powerful"

          }

       }

    }

 explain 参数可以提供更多关于查询不合法的信息。

很明显,我们将查询类型(match)与字段名称 (tweet)搞混了:

{

      "valid" :     false,

      "_shards" :   { ... },

      "explanations" : [ {

        "index" :   "gb",

        "valid" :   false,

        "error" :   "org.elasticsearch.index.query.QueryParsingException:

                     [gb] No query registered for [tweet]"

      } ]

    }

理解查询语句

对于合法查询,使用 explain 参数将返回可读的描述,这对准确理解 Elasticsearch 是如何解析你的 query 是非常有用的:

GET /_validate/query?explain

    {

       "query": {

          "match" : {

             "tweet" : "really powerful"

          }

       }

    }

我们查询的每一个 index 都会返回对应的 explanation ,因为每一个 index 都有自己的映射和分析器:

{

      "valid" :         true,

      "_shards" :       { ... },

      "explanations" : [ {

        "index" :       "us",

        "valid" :       true,

        "explanation" : "tweet:really tweet:powerful"

      }, {

        "index" :       "gb",

        "valid" :       true,

        "explanation" : "tweet:realli tweet:power"

      } ]

    }

从 explanation 中可以看出,匹配 really powerful 的 match 查询被重写为两个针对 tweet 字段的 single-term 查询,一个single-term查询对应查询字符串分出来的一个term。

当然,对于索引 us ,这两个 term 分别是 really 和 powerful ,而对于索引 gb ,term 则分别是 realli 和 power 。之所以出现这个情况,是由于我们将索引 gb 中 tweet 字段的分析器修改为 english分析器。

elasticsearch 基础 —— 请求体查询的相关教程结束。

《elasticsearch 基础 —— 请求体查询.doc》

下载本文的Word格式文档,以方便收藏与打印。