欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

ElasticSearch ——结构化查询

程序员文章站 2022-03-06 12:58:03
...

之前笔者写过的所有的查询都属于命令行查询,但是不利于复杂的查询,而且一般在项目开发中不使用命令行查询方式,只有在调试测试时使用简单命令行查询,但是,如果想要善用搜索,我们必须使用请求体查询 (request body search)API。之所以这么称呼,是因为大多数的参数以 JSON 格式所容纳而非查询字符串。请求体查询,并不仅仅用来处理查询,而且还可以高亮返回结果中的片段,并且给出帮助你的用户找寻最好结果的相关数据建议。

空查询

我们以最简单的 search API 开始,空查询将会返回索引中所有的文档。

GET /_search
{}

同字符串查询一样,可以查询一个,多个或 _all 索引 (indices) 或类型 (types):

也可以使用 from 及 size 参数进行分页:

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

需要注意的是:携带内容的 GET 请求?

任何一种语言( 特别是js )的 HTTP 库都不允许 GET 请求中携带交互数据。 事实上,有些用户很惊讶 GET 请求中居然会允许携带交互数据。

真实情况是,http://tools.ietf.org/html/rfc7231#page-24[RFC 7231], 一份规定 HTTP语义及内容的 RFC 中并未规定 GET 请求中允许携带交互数据! 所以,有些 HTTP 服务允许这种行为,而另一些(特别是缓存代理),则不允许这种行为。

Elasticsearch 的作者们倾向于使用 GET 提交查询请求,因为他们觉得这个词相比 POST 来说,能更好的描述这种行为。然而,因为携带交互数据的 GET 请求并不被广泛支持,所以 searchAPI 同样支持 POST 请求,类似于这样:

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

这个原理同样应用于其他携带交互数据的 GET API 请求中。

结构化查询 Query DSL

结构化查询是一种灵活的,多表现形式的查询语言。 Elasticsearch 在一个简单的JSON 接口中用结构化查询来展现 Lucene 绝大多数能力。 你应当在你的产品中采用这种方式进行查询。它使得你的查询更加灵活,精准,易于阅读并且易于 debug。

使用结构化查询,你需要传递 query 参数:

GET /_search
{
  "query": 发查询体放置于此即可
}

空查询 - {} - 在功能上等同于使用 match_all 查询子句,正如其名字一样,匹配所有的文档:

GET /_search
{
  "query": {
      "match_all": {}  #查询体
  }
}

查询子句

一个查询子句一般使用这种结构:

#整个属于查询体
{
  QUERY_NAME(查询命令): {
      ARGUMENT: VALUE,
      ARGUMENT: VALUE,...
  }
}

或指向一个指定的字段:

#整个属于查询体
{
  QUERY_NAME(查询命令): {
      FIELD_NAME(匹配字段): {
          ARGUMENT: VALUE,
          ARGUMENT: VALUE,...
      }
  }
}

例如,你可以使用 match 查询子句用来找寻在 tweet 字段中找寻包含,elasticsearch的成员:

{
  "match"(查询命令): {
      "tweet": "elasticsearch"
  }
}

完整的查询请求会是这样:

GET /_search
{
  "query": {
      "match": {
          "tweet": "elasticsearch"
      }
  }
}

合并多子句

查询子句就像是搭积木一样,可以合并简单的子句为一个复杂的查询语句,比如:

叶子子句( leaf clauses )(比如 match 子句)用以在将查询字符串与一个字段(或多字段)进行比较复合子句( compound )用以合并其他的子句。例如,bool 子句允许你合并其他的合法子句,must,must_not 或者 should,如果可能的话:{
  "bool": {
      "must":     { "match": { "tweet": "elasticsearch" }},
      "must_not": { "match": { "name":  "mary" }},
      "should":   { "match": { "tweet": "full text" }}
  }}

复合子句能合并任意其他查询子句,包括其他的复合子句。这就意味着复合子句可以相互嵌套,从而实现非常复杂的逻辑。

以下实例查询的是邮件正文中含有“business opportunity”字样的星标邮件或收件箱中正文中含有“business opportunity”字样的非垃圾邮件:

#整个属于查询体{
  "bool": {
      "must": { "match":      { "email": "business opportunity" }},
      "should": [
           { "match":         { "starred": true }},
           { "bool": {
                 "must":      { "folder": "inbox" },
                 "must_not":  { "spam": true }
           }}
      ],
      "minimum_should_match": 1
  }}

查询与过滤

Elasticsearch 使用的 DSL 具有一组称为查询的组件,它们可以混合并以无穷组合进行匹配。这一组组件可以在两个上下文中使用:过滤上下文和查询上下文。

当用于过滤上下文时,该查询被称为“非评分”或“过滤”查询。也就是说,查询只询问问题:“此文档是否匹配?”。答案总是一个简单的二进制 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 中的单独组件。从 Elasticsearch 2.0 开始,过滤器在技术上被消除,并且所有查询都获得了成为非评分的能力。

然而,为了清楚和简单,将使用 term“过滤器”来表示在非评分过滤上下文中使用的查询。可以将 term“过滤器”,“过滤查询”和“非评分查询”视为相同。类似地,如果单独使用 term“查询”而不使用限定符,指的是“评分查询”。

关于具体的 Query DSL 变化可以查看 Query DSL changes(https://www.elastic.co/guide/en/elasticsearch/reference/2.4/breaking_20_query_dsl_changes.html#_filter_auto_caching)。

性能差异

过滤查询是对集合包含/排除的简单检查,这使得计算非常快。当您的过滤查询中至少有一个是“稀疏”(匹配文档较少)时,可以利用各种优化,并且可以将经常使用的非评分查询缓存在内存中以便更快地访问。

相比之下,评分查询不仅必须找到匹配的文档,而且还要计算每个文档的相关程度,这通常使得他们比他们的非评分对手更重。此外,查询结果不可缓存。

由于倒排索引,只匹配几个文档的简单评分查询可能与跨越数百万个文档的过滤器一样好或更好。然而,一般来说,过滤器将胜过评分查询。

过滤的目的是减少必须由评分查询检查的文档的数量。

什么情况下使用

作为一般规则,对全文搜索或任何会影响相关性分数的条件使用查询子句,并对其他所有条件使用过滤器。

最重要的查询过滤语句

match_all 查询

match_all 查询只匹配所有文档。如果未指定任何查询,则是使用的默认查询:

{“match_all”:{}}

此查询经常与过滤器结合使用,例如,用于检索收件箱文件夹中的所有电子邮件。所有文件被认为是同等相关的,所以他们都获得1的中性分数。

match 查询

match 查询是一个标准查询,不管你需要全文本查询还是精确查询基本上都要用到它。

如果你使用 match 查询一个全文本字段,它会在真正查询之前用分析器先分析 match 一下查询字符:

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

如果用 match下指定了一个确切值,在遇到数字,日期,布尔值或者 not_analyzed 的字符串时,它将为你搜索你给定的值:

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

提示: 做精确匹配搜索时,你最好用过滤语句,因为过滤语句可以缓存数据。

不像我们在 ElasticSearch ——简单搜索笔记中介绍的字符查询,match 查询不可以用类似 "+usid: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 用于按照精确值进行搜索,无论是数字,日期,布尔值还是未分析的精确值字符串字段:

{ "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 查询类似,不对输入文本执行分析。它正在寻找精确匹配(包括大小写,重音,空格等)。

exists and missing 查询

exists 和 missing 查询用于查找指定字段具有一个或多个值(exists)或没有任何值(missing)的文档。
它在本质上类似于 SQL 中的 IS_NULL(缺失)和 NOT IS_NULL(存在):

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

这些查询经常用于仅在存在字段时应用条件,以及在缺少条件时应用不同的条件。

查询与过滤条件的合并

现实世界的搜索请求从来不简单;他们使用各种输入文本搜索多个字段,并根据条件数组进行过滤。要构建复杂的搜索,您需要一种将多个查询组合到一个搜索请求中的方法。

要做到这一点,你可以使用 bool 询。此查询在用户定义的布尔组合中将多个查询组合在一起。此查询接受以下参数:

bool 过滤

bool 过滤可以用来合并多个过滤条件查询结果的布尔逻辑,它包含一下操作符:

must :: 多个查询条件的完全匹配,相当于 and。

must_not :: 多个查询条件的相反匹配,相当于 not。

should :: 至少有一个查询条件匹配, 相当于 or。

这些参数可以分别继承一个过滤条件或者一个过滤条件的数组:

{
  "bool": {
      "must":     { "term": { "folder": "inbox" }},
      "must_not": { "term": { "tag":    "spam"  }},
      "should": [
                  { "term": { "starred": true   }},
                  { "term": { "unread":  true   }}
      ]
  }}

因为这是我们见过的第一个包含其他查询的查询,所以我们需要谈论分数是如何组合的。每个子查询子句将单独计算文档的相关性分数。一旦计算了这些分数,bool查询将将分数合并在一起,并返回表示布尔运算的总分数的单个分数。

以下查询将会找到 title 字段中包含 "how to make millions",并且 tag 字段没有被标为 "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" }}}
      ]
  }}

提示: 如果 bool 查询下没有 must 子句,那至少应该有一个 should 子句。但是如果有must 子句,那么没有 should 子句也可以进行查询。

添加过滤查询

如果我们不希望文档的日期影响评分,我们可以重新排列前面的示例以使用过滤子句:

{
  "bool": {
      "must":     { "match": { "title": "how to make millions" }},
      "must_not": { "match": { "tag":   "spam" }},
      "should": [
          { "match": { "tag": "starred" }}
      ],
      "filter": {
        "range": { "date": { "gte": "2014-01-01" }}
      }
  }}

范围查询已从 should 子句中移出并进入过滤器子句。

通过将范围查询移动到过滤子句中,我们将其转换为非评分查询。它将不再为文档的相关性排名贡献分数。并且因为它现在是一个非评分查询,它可以使用可用于过滤器的各种优化,这应该提高性能。

任何查询都可以以这种方式使用。只需将查询移动到 bool 查询的过滤器子句中,它就会自动转换为非评分过滤器。

如果你需要过滤许多不同的标准,bool 查询本身可以用作非评分查询。只需将它放在过滤器子句中,并继续构建布尔逻辑:

{
  "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" }}
            ]
        }
      }
  }}

通过在 filter 子句中嵌入 bool 查询,我们可以为我们的过滤条件添加布尔逻辑。

constant_score 查询

尽管不像 bool 查询那样经常使用,但是 constant_score 查询在你的工具箱中仍然有用。查询对所有匹配的文档应用静态,常数得分。它主要用于当你想执行一个过滤器,没有别的(例如没有评分查询)。你可以使用它而不是一个只有过滤器子句的 bool。性能将是相同的,但它可以帮助查询简单/清晰。

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

验证查询

查询语句可以变得非常复杂,特别是与不同的分析器和字段映射相结合后,就会有些难度。

validate API 可以验证一条查询语句是否合法。

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

以上请求的返回值告诉我们这条语句是非法的:

{
"valid" :         false,
"_shards" : {
  "total" :       1,
  "successful" :  1,
  "failed" :      
}}

理解错误信息

要找出为什么它无效,请将 explain 参数添加到查询字符串:

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

显然,我们已经将查询(match)类型与字段名称(tweet)混淆:

{
"valid" :     false,
"_shards" :   { ... },
"explanations" : [ {
  "index" :   "gb",
  "valid" :   false,
  "error" :   "org.elasticsearch.index.query.QueryParsingException:
               [gb] No query registered for [tweet]"
} ]}

理解查询语句

使用 explain 参数具有返回(有效)查询的可读描述的附加优点,这对理解 Elasticsearch 如何解释查询是有用的:

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

为每个我们查询的索引返回一个解释,因为每个索引可以有不同的映射和分析器:

{
"valid" :         true,
"_shards" :       { ... },
"explanations" : [ {
  "index" :       "us",
  "valid" :       true,
  "explanation" : "tweet:really tweet:powerful" }, {   "index" :       "gb",   "valid" :       true,   "explanation" : "tweet:realli tweet:power"
} ]}

从解释中,您可以看到查询字符串的 match 查询 really powerful 已被重写为对 tweet 字段的两个单项查询,每个 term 一个。

此外,对于我们的索引,这两个 term 是 really 和 powerful 的,而对于 gb 索引,term 是realli 和 power。原因是我们改变了 gb 索引中的 tweet 字段以使用 english 分析器。