Elastic Search学习系列04-请求体查询

Table of Contents

1 前言

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

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

2 空查询

空查询将返回所有索引库(indices)中的所有文档:

curl -X GET "localhost:9200/_search?pretty" -H 'Content-Type: application/json' -d'
{}
'

可以在一个、多个或者_all索引库(indices)和一个、多个或者所有types中查询:

GET /index_2014*/type1,type2/_search
{}

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

GET /_search
{
  "from": 30,
  "size": 10
}

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

3 查询表达式

查询表达式(Query DSL)是一种非常灵活又富有表现力的查询语言。Elasticsearch使用它可以以简单的JSON接口来展现Lucene功能的绝大部分。

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

GET /_search
{
    "query": YOUR_QUERY_HERE
}

查询语句的结构

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

{
    QUERY_NAME: {
        ARGUMENT: VALUE,
        ARGUMENT: VALUE,...
    }
}

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

{
    QUERY_NAME: {
        FIELD_NAME: {
            ARGUMENT: VALUE,
            ARGUMENT: VALUE,...
        }
    }
}

合并查询语句

查询语句(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 }} }
    }
}

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

4 查询与过滤

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

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

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

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

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

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

性能差异

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

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

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

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

如何选择查询与过滤

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

5 常用查询

5.1 match_all 查询

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

{ "match_all": {}}

5.2 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将会被缓存。

5.3 multi_match 查询

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

{
    "multi_match": {
        "query":    "full text search",
        "fields":   [ "title", "body" ]
    }
}

5.4 range 查询

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

{
    "range": {
        "age": {
            "gte":  20,
            "lt":   30
        }
    }
}

被允许的操作符如下:

  • gt 大于
  • gte 大于等于
  • lt 小于
  • lte 小于等于

5.5 term 查询

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

{ "term": { "age":    26           }}
{ "term": { "date":   "2014-09-01" }}
{ "term": { "public": true         }}
{ "term": { "tag":    "full_text"  }}

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

5.6 terms 查询

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

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

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

5.7 exists 查询和 missing 查询

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

{
    "exists":   {
        "field":    "title"
    }
}

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

6 组合查询

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

你可以用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了。

constant_score 查询

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

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

{
    "constant_score":   {
        "filter": {
            "term": { "category": "ebooks" } 
        }
    }
}

7 验证查询

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

GET /gb/tweet/_validate/query
{
   "query": {
      "tweet" : {
         "match" : "really powerful"
      }
   }
}

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

{
  "valid" : false
}

理解错误信息

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

GET /gb/tweet/_validate/query?explain 
{
   "query": {
      "tweet" : {
         "match" : "really powerful"
      }
   }
}

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

{
  "valid" : false,
  "error" : "ParsingException[unknown query [tweet]]; nested: NamedObjectNotFoundException[[3:17] unknown field [tweet]];; org.elasticsearch.common.xcontent.NamedObjectNotFoundException: [3:17] unknown field [tweet]"
}

理解查询语句

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

curl -X GET "localhost:9200/_validate/query?explain&pretty" -H 'Content-Type: application/json' -d'
{
   "query": {
      "match" : {
         "tweet" : "really powerful"
      }
   }
}
'

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

"explanations" : [
    {
      "index" : ".apm-agent-configuration",
      "valid" : true,
      "explanation" : """MatchNoDocsQuery("unmapped fields [tweet]")"""
    },
    {
      "index" : ".apm-custom-link",
      "valid" : true,
      "explanation" : """MatchNoDocsQuery("unmapped fields [tweet]")"""
    }
]