vlambda博客
学习文章列表

读书笔记《elasticsearch-server-third-edition》扩展您的查询知识

Chapter 4. Extending Your Querying Knowledge

在上一章中,我们深入探讨了 Elasticsearch 查询功能。我们详细讨论了如何查询 Elasticsearch,并了解了 Elasticsearch 查询的工作原理。我们现在知道了这个伟大的搜索引擎的基本查询和复合查询,以及每种查询类型的配置选项是什么。我们还知道何时使用我们的查询,我们讨论了一些用例以及哪些查询可以用来处理它们。本章致力于扩展我们的查询知识。在本章结束时,您将学习以下主题:

  • 什么是过滤以及如何使用它

  • 什么是突出显示以及如何使用它

  • 荧光笔有哪些类型,它们带来了什么好处

  • 如何验证您的查询

  • 如何对查询结果进行排序

  • 什么是查询重写以及如何控制它

Filtering your results


在上一章中,我们讨论了各种类型的查询。共同点是我们总是希望首先获得最好的结果。这是与标准数据库方法的主要区别,其中每个文档都与查询匹配。在数据库的世界里,我们不问文档有多好;我们唯一的兴趣在于返回的结果。当谈到全文搜索引擎时,情况就不同了——我们不仅对结果感兴趣,而且对它们的质量感兴趣。原因很明显,我们在非结构化数据中搜索,使用的文本字段使用语言分析、词干提取等。正因为如此,我们查询的初始结果,在大多数情况下,给出的结果远非最佳。这就是为什么当我们谈论搜索时,我们谈论精度和文档召回。

另一方面,有时我们希望将整个文档子集限制为选定的部分。例如,在图书馆中,我们可能只想搜索可用的书籍,其余的不重要。有时,为给定字段忙着计算的分数,只会干扰整体分数,在准确性方面没有任何意义。在这种情况下,过滤器应该用于限制查询结果,但不干扰计算的分数。

在 Elasticsearch 2.0 之前,过滤器是独立于查询的实体。实际上,几乎每个查询在过滤器中都有自己的对应项。有 term 查询和 term 过滤器,bool 查询和bool 过滤器、range 查询和 range 过滤器等等上。从用户的角度来看,查询和过滤器之间最重要的区别是评分。过滤器不计算分数,这导致过滤器很容易缓存并且效率更高。但是这种差异对用户来说非常不方便。随着 Elasticsearch 2.0 的发布及其对 Lucene 5.3 的使用,过滤器查询以及一些允许我们使用过滤器的查询类型被弃用。让我们讨论一下现在过滤是如何工作的,以及我们可以做些什么来实现与以前在 Elasticsearch 2.0 中相同或更好的性能。

The context is the key

在 Elasticsearch 2.0 中,查询可以通过选择更有效的执行方式来计算分数或省略它。在许多情况下,此行为是根据使用 查询的上下文自动完成的。这是关于包含过滤器部分的查询,它根据某些条件删除文档。这些文档在返回的结果中是不必要的,应该在不影响总分的情况下尽快跳过。多亏了这一点,在丢弃一些文档后,我们可以只关注其余的文档,计算它们的分数,并在返回之前对它们进行排序。这种情况的示例可以是布尔查询的 must_not 子句。与 must_not 子句匹配的文档将从返回的结果集中移除,因此对 bool 这部分匹配的文档计算分数 查询将是一项额外的、不必要的且性能无效的工作。

所有这些变化最好的一点是,我们不需要关心是否要使用过滤。 Elasticsearch 和底层 Apache Lucene 库负责为我们选择正确的执行方法。

Explicit filtering with bool query

正如我们在 第 3 章中的 复合查询 部分中提到的 a>,搜索您的数据, Elasticsearch 2.0 中的 bool 查询允许我们添加过滤器通过添加 filter 部分并在该部分中包含 一个查询来显式地添加。这非常方便,如果我们希望有一部分查询需要匹配,但我们对这些文档的分数不感兴趣。

让我们看一下以下查询:

curl -XGET 'localhost:9200/library/book/_search?pretty' -d '{
  "query" : {
    "term" : {
      "available" : true
    }
  }
}'

我们看到一个简单的查询, 应该返回我们图书馆中所有可供借阅的书籍,这意味着具有 的文档是可用的 字段设置为 true。现在让我们将其与以下查询进行比较:

curl -XGET 'localhost:9200/library/book/_search?pretty' -d '{
  "query" : {
    "bool" : {
      "must" : {
        "match_all" : { }
      },
      "filter" : {
        "term" : {
          "available" : true
         }
      }
    }
  }
}'

此查询返回所有书籍,但它还包含 filter 部分,它告诉 Elasticsearch 我们只是 对现有书籍感兴趣。该查询将返回与我们之前看到的查询相同的结果,当然,当仅查看文档数量和返回哪些文档时。不同的是分数。对于我们的示例数据,两个查询都返回两本书。第一个查询返回的结果如下所示:

{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 2,
    "max_score" : 1.0,
    "hits" : [ {
      "_index" : "library",
      "_type" : "book",
      "_id" : "4",
      "_score" : 1.0,
      "_source" : {
        "title" : "Crime and Punishment",
        "otitle" : "Преступлéние и наказáние",
        "author" : "Fyodor Dostoevsky",
        "year" : 1886,
        "characters" : [ "Raskolnikov", "Sofia Semyonovna Marmeladova" ],
        "tags" : [ ],
        "copies" : 0,
        "available" : true
      }
    }, {
      "_index" : "library",
      "_type" : "book",
      "_id" : "1",
      "_score" : 0.30685282,
      "_source" : {
        "title" : "All Quiet on the Western Front",
        "otitle" : "Im Westen nichts Neues",
        "author" : "Erich Maria Remarque",
        "year" : 1929,
        "characters" : [ "Paul Bäumer", "Albert Kropp", "Haie Westhus", "Fredrich Müller", "Stanislaus Katczinsky", "Tjaden" ],
        "tags" : [ "novel" ],
        "copies" : 1,
        "available" : true,
        "section" : 3
      }
    } ]
  }
}

第二个 查询的结果如下所示:

{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 2,
    "max_score" : 1.0,
    "hits" : [ {
      "_index" : "library",
      "_type" : "book",
      "_id" : "4",
      "_score" : 1.0,
      "_source" : {
        "title" : "Crime and Punishment",
        "otitle" : "Преступлéние и наказáние",
        "author" : "Fyodor Dostoevsky",
        "year" : 1886,
        "characters" : [ "Raskolnikov", "Sofia Semyonovna Marmeladova" ],
        "tags" : [ ],
        "copies" : 0,
        "available" : true
      }
    }, {
      "_index" : "library",
      "_type" : "book",
      "_id" : "1",
      "_score" : 1.0,
      "_source" : {
        "title" : "All Quiet on the Western Front",
        "otitle" : "Im Westen nichts Neues",
        "author" : "Erich Maria Remarque",
        "year" : 1929,
        "characters" : [ "Paul Bäumer", "Albert Kropp", "Haie Westhus", "Fredrich Müller", "Stanislaus Katczinsky", "Tjaden" ],
        "tags" : [ "novel" ],
        "copies" : 1,
        "available" : true,
        "section" : 3
      }
    } ]
  }
}

如果您查看每个查询中文档的分数,您会注意到差异。在简单的 term 查询中,Elasticsearch(实际上是 Lucene 库)对第一个文档的得分为 1.0,并且第二个得分 0.30685282。这不是一个完美的解决方案,因为可用性检查或多或少是二进制的,我们不希望它干扰分数。这就是为什么 在这种 案例中第二个查询更好的原因。使用 bool 查询和过滤,不计算 filter 元素的分数,两个文档的分数相同,即1.0

Highlighting


您可能听说过或看过highlighting。当您在 World Wide Web (WWW) 上使用更大或更小的公共搜索引擎时,您甚至可能不知道您实际上在使用突出显示。当我们谈论全文搜索上下文中的突出显示时,我们通常是指显示查询中的哪些单词或短语在结果文档中匹配。例如,如果我们使用 Google 并搜索单词 lucene,,我们会在搜索结果中看到该单词加粗:

读书笔记《elasticsearch-server-third-edition》扩展您的查询知识

它在 Microsoft Bing 搜索引擎上更加明显:

读书笔记《elasticsearch-server-third-edition》扩展您的查询知识

在本章中,我们将了解如何使用 Elasticsearch 突出显示功能来增强我们的应用程序的突出显示结果。

Getting started with highlighting

除了进行查询并查看 Elasticsearch 返回的结果之外,没有更好的方法来显示突出显示的工作原理。所以让我们这样做。我们假设我们希望突出显示在文档的 title 字段中匹配的术语,以增加用户的搜索体验。现在您已经从上到下了解了示例数据,所以让我们再次 重用相同的数据集。我们希望匹配 title 字段中的术语 crime 并且我们希望获得突出显示的结果。可以实现此目的的最简单查询之一如下所示:

curl -XGET 'localhost:9200/library/book/_search?pretty' -d '{
 "query" : {
  "match" : {
   "title" : "crime"
  }
 },
 "highlight" : {
  "fields" : {
   "title" : {}
  }
 }
}'

上述查询的响应如下:

{
  "took" : 16,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 0.5,
    "hits" : [ {
      "_index" : "library",
      "_type" : "book",
      "_id" : "4",
      "_score" : 0.5,
      "_source" : {
        "title" : "Crime and Punishment",
        "otitle" : "Преступлéние и наказáние",
        "author" : "Fyodor Dostoevsky",
        "year" : 1886,
        "characters" : [ "Raskolnikov", "Sofia Semyonovna Marmeladova" ],
        "tags" : [ ],
        "copies" : 0,
        "available" : true
      },
      "highlight" : {
        "title" : [ "<em>Crime</em> and Punishment" ]
      }
    } ]
  }
}

如您所见,除了关于匹配查询的文档的 标准信息外,我们还有一个名为highlight 的新部分 。 Elasticsearch 使用 <em> HTML 标签作为高亮部分的开头,并使用它的关闭对应物来关闭高亮部分。这是 Elasticsearch 的默认行为,但我们将学习如何改变它。

Field configuration

为了执行突出显示,该字段的原始内容需要存在。我们必须设置我们 将用于突出显示的字段。这是通过标记要存储的字段或使用包含这些字段的 _source 字段来完成的。如果该字段设置为存储在映射中,将使用存储的版本,否则 Elasticsearch 将尝试使用 _source 字段并提取需要突出显示的字段。

Under the hood

Elasticsearch 在后台使用 Apache Lucene,突出显示是该库的功能之一。 Lucene 提供了三种高亮实现: 标准的一种,我们刚刚使用过;第二个叫做 FastVectorHighlighter (https://lucene.apache.org/core/5_4_0/highlighter/org/apache/lucene/search/vectorhighlight/FastVectorHighlighter.html),其中需要术语向量和位置才能工作;第三个叫做 PostingsHighlighter (http://lucene.apache.org/core/5_4_0/highlighter/org/apache/lucene/search/postingshighlight/PostingsHighlighter.html)。 Elasticsearch 会自动选择正确的荧光笔实现。如果该字段配置了 term_vector 属性设置为 with_positions_offsets, FastVectorHighlighter将会被使用。如果该字段配置了 index_options 属性设置为 offsets, PostingsHighlighter将会被使用。否则,Elasticsearch 将使用标准荧光笔。

使用哪种荧光笔取决于您的数据、查询和所需的性能。标准荧光笔是一种一般用例。但是,如果要突出显示包含大量数据的字段,推荐使用 FastVectorHighlighter。要记住的是,它需要存在术语向量,这将使您的索引略大。最后,也推荐用于自然语言突出显示的最快的荧光笔是 PostingsHighlighter。但是,要记住的是 PostingsHighlighter 不支持复杂的查询,例如 match_phrase_prefix 查询,在这种情况下突出显示赢了不被退回。

Forcing highlighter type

虽然 Elasticsearch 为我们选择了 高亮类型,但如果我们真的< /a> 想要。为此,我们需要将 type 属性设置为以下值之一:

  • plain:当这个值为 设置时,Elasticsearch 将使用标准的荧光笔

  • fvh:设置此值后,Elasticsearch 将尝试使用 FastVectorHighlighter 。它将需要为用于突出显示的字段打开术语向量。

  • postings:当设置了这个 值时,Elasticsearch 将尝试使用 PostingsHighlighter 。它将需要为用于突出显示的字段打开偏移量

例如,要使用标准荧光笔,我们将运行以下查询:

curl -XGET 'localhost:9200/library/book/_search?pretty' -d '{
 "query" : {
  "term" : {
   "title" : "crime"
  }
 },
"highlight" : {
  "fields" : {
   "title" : { "type" : "plain" }
  }
 }
}'

Configuring HTML tags

高亮机制的默认行为可能并不适合所有人——并非所有人都希望拥有 <em></ em> 标签用于高亮显示。因此,Elasticsearch 允许我们更改默认行为并更改用于该目的的标签。为此,我们应该将 pre_tagspost_tags 属性设置为我们希望高亮开始和结束的代码片段;例如,通过 <b></b>pre_tagspost_tags 属性是数组,因此我们可以 提供多个开始和结束标签,Elasticsearch 将使用每个定义的标签来突出显示不同的单词。例如,如果我们想使用 <b> 作为开始标签, </b> 作为结束标签,我们的查询将如下所示:

curl -XGET 'localhost:9200/library/book/_search?pretty' -d '{
 "query" : {
  "term" : {
   "title" : "crime"
  }
},
 "highlight" : {
  "pre_tags" : [ "<b>" ],
  "post_tags" : [ "</b>" ],
  "fields" : {
   "title" : {}
  }
 }
}'

Elasticsearch 对上述查询返回的结果如下:

{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 0.5,
    "hits" : [ {
      "_index" : "library",
      "_type" : "book",
      "_id" : "4",
      "_score" : 0.5,
      "_source" : {
        "title" : "Crime and Punishment",
        "otitle" : "Преступлéние и наказáние",
        "author" : "Fyodor Dostoevsky",
        "year" : 1886,
        "characters" : [ "Raskolnikov", "Sofia Semyonovna Marmeladova" ],
        "tags" : [ ],
        "copies" : 0,
        "available" : true
      },
      "highlight" : {
        "title" : [ "<b>Crime</b> and Punishment" ]
      }
    } ]
  }
}

如您所见,title 术语犯罪 code> 字段被我们选择的标签包围。

Controlling highlighted fragments

Elasticsearch 允许我们通过公开两个属性控制返回的突出显示片段的数量及其大小。第一个是number_of_fragments,,它定义了Elasticsearch返回的片段数量(默认为5)。将此属性设置为 0 会导致返回整个字段,这对于短字段可能很方便,但对于较长的字段来说很昂贵。第二个属性是 fragment_size,它允许我们指定突出显示的片段的最大长度(以字符为单位),默认为 100

使用这些属性的示例查询 如下所示:

curl -XGET 'localhost:9200/library/book/_search?pretty' -d '{
 "query" : {
  
"term" : {
   "title" : "crime"
  }
 },
 "highlight" : {
  "fields" : {
   "title" : { "fragment_size" : 200, "number_of_fragments" : 0 }
  }
 }
}'

Global and local settings

我们之前讨论的突出显示属性既可以在全局基础上设置,也可以在每个字段基础上设置。全局 将用于所有不会覆盖它们的字段,并且应该与 fields 部分突出显示,例如 ,如下所示:

curl -XGET 'localhost:9200/library/book/_search?pretty' -d '{
 "query" : {
  "term" : {
   "title" : "crime"
  }
 },
 "highlight" : {
  "pre_tags" : [ "<b>" ],
  "post_tags" : [ "</b>" ],
  
"fields" : {
   "title" : {}
  }
 }
}'

您还可以设置每个字段的属性。例如,如果我们想为除了 title 字段之外的所有字段保留默认行为,我们将执行以下操作:

curl -XGET 'localhost:9200/library/book/_search?pretty' -d '{
 "query" : {
  "term" : {
   "title" : "crime"
  }
 },
 "highlight" : {
  "fields" : {
   "title" : { "pre_tags" : [ "<b>" ], "post_tags" : [ "</b>" ] }
  }
 }
}'

如您所见,我们没有将 属性放在与 fields 部分相同的级别,而是将它位于指定标题 field 行为的空 JSON 对象中。 当然,每个字段都可以使用不同的属性进行配置。

Require matching

有时可能需要(尤其是在使用多个突出显示的字段时)只显示与我们的查询匹配的字段。为了有这样的行为,我们需要将 require_field_match 属性设置为 true。将此属性设置为 false 将导致突出显示所有术语,即使字段与查询不匹配也是如此。

要了解它是如何工作的,让我们创建一个名为 users 的新索引,并在其中索引单个文档。我们将通过发送以下命令来做到这一点:

curl -XPUT 'http://localhost:9200/users/user/1' -d '{
 "name" : "Test user",
 "description" : "Test document"
}'

因此,假设我们要突出显示前面两个字段中的命中。我们将查询发送到新索引的命令如下所示:

curl -XGET 'localhost:9200/users/_search?pretty' -d '{
 "query" : {
  "term" : {
   "name" : "test"
  }
 },
 "highlight" : {
  "fields" : {
   "name" : { "pre_tags" : ["<b>"], "post_tags" : ["</b>"] },
   "description" : { "pre_tags" : ["<b>"], "post_tags" : ["</b>"] }
  }
 }
}'

上述查询 的结果如下:

{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 0.19178301,
    "hits" : [ {
      "_index" : "users",
      "_type" : "user",
      "_id" : "1",
      "_score" : 0.19178301,
      "_source":{
        "name" : "Test user",
        "description" : "Test document"
      },
      "highlight" : {
        "name" : [ "<b>Test</b> user" ]
      }
    } ]
  }
}

请注意,我们只在 name 字段上突出显示。这是因为我们的查询只匹配该字段。让我们看看如果我们将 require_field_match 属性设置为 false 并使用类似于以下的命令会发生什么:

curl -XGET 'localhost:9200/users/_search?pretty' -d '{
 "query" : {
  "term" : {
   "name" : "test"
  }
 },
 "highlight" : {
  "require_field_match" : false,
  "fields" : {
   "name" : { "pre_tags" : ["<b>"], "post_tags" : ["</b>"] },
   "description" : { "pre_tags" : ["<b>"], "post_tags" : ["</b>"] }
  }
 }
}'

现在我们看一下修改后的查询结果:

{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 0.19178301,
    "hits" : [ {
      "_index" : "users",
      "_type" : "user",
      "_id" : "1",
      "_score" : 0.19178301,
      "_source":{
        "name" : "Test user",
        "description" : "Test document"
      },
      "highlight" : {
        "name" : [ "<b>Test</b> user" ],
        "description" : [ "<b>Test</b> document" ]
      }
    } ]
  }
}

如您所见,Elasticsearch 现在在两个字段中都返回了突出显示。

Custom highlighting query

在某些用例中,您的查询很复杂且不适合突出显示,但您仍希望 使用突出显示功能。在这种情况下,Elasticsearch 允许我们根据使用 highlight_query 属性提供的不同查询来突出显示结果。使用不同的突出显示查询的示例如下所示:

curl -XGET 'localhost:9200/library/book/_search?pretty' -d '{
 "query" : {
  "term" : {
   "title" : "crime"
  }
 },
 "highlight" : {
  "fields" : {
   "title" : { 
    "highlight_query" : {
     "term" : {
      "title" : "punishment"
     }
    }
   }
  }
 }
}'

上述查询将导致 title 中突出显示术语 punishment 字段,而不是 crime 字段。

The Postings highlighter

是时候讨论第三个 可用的荧光笔了。它是在 Elasticsearch 0.90.6 中添加的,与之前的略有不同。当字段定义将 index_options 设置为 offsets 时,会自动使用 PostingsHighlighter。为了说明 PostingsHighlighter 的工作原理,我们将创建一个具有适当配置的简单索引,以允许荧光笔工作。我们将通过使用 以下命令来做到这一点:

curl -XPUT 'localhost:9200/hl_test'
curl -XPOST 'localhost:9200/hl_test/doc/_mapping' -d '{
 "doc" : {
  "properties" : {
   "contents" : {
    "type" : "string",
    "fields" : {
     "ps" : { "type" : "string", "index_options" : "offsets" }
    }
   }
  }
 }
}'

如果一切顺利,我们应该有一个新的索引和映射。映射定义了两个字段:一个名为 contents,第二个名为 contents.ps。在第二种情况下,我们使用 index_options 属性打开了偏移量。这意味着 Elasticsearch 将为 contents 字段使用标准荧光笔,为 contents.ps 字段使用帖子荧光笔。

为了看到差异,我们将使用维基百科中描述伯明翰历史的片段来索引单个文档。我们通过运行以下命令来做到这一点:

curl -XPUT localhost:9200/hl_test/doc/1 -d '{
  "contents" : "Birmingham''s early history is that of a remote and marginal area. The main centres of population, power and wealth in the pre-industrial English Midlands lay in the fertile and accessible river valleys of the Trent, the Severn and the Avon. The area of modern Birmingham lay in between, on the upland Birmingham Plateau and within the densely wooded and sparsely populated Forest of Arden."
}'

最后一步是使用两个荧光笔发送查询。我们可以使用以下 命令在单个请求中完成:

curl 'localhost:9200/hl_test/_search?pretty' -d '{
 "query": {
  "term": {
   "contents.ps": "modern"
  }
 },
 "highlight": {
  "require_field_match" : false,
  "fields": {
   "contents": {},
   "contents.ps" : {}
  }
 }
}'

如果一切顺利,您会在 Elasticsearch 返回的响应中找到以下代码段:

"highlight" : {
 "contents" : [ " valleys of the Trent, the Severn and the Avon. The area of <em>modern</em> Birmingham lay in between, on the upland" ],
 "contents.ps" : [ "The area of <em>modern</em> Birmingham lay in between, on the upland Birmingham Plateau and within the densely wooded and sparsely populated Forest of Arden." ]
}

如您所见,两个荧光笔都找到了所需单词的出现。不同之处在于,发帖高亮显示返回更智能的片段——它检查句子边界。

让我们再尝试一个查询:

curl 'localhost:9200/hl_test/_search?pretty' -d '{
 "query": {
  "match_phrase": {
   "contents.ps": "centres of"
  }
 },
 "highlight": {
  "require_field_match" : false,
  "fields": {
   "contents": {},
   "contents.ps": {}
  }
 }
}'

我们搜索了短语中心。正如您所料,两种荧光笔的结果会有所不同。对于标准荧光笔,在 contents 字段上运行,我们将在响应中找到以下短语

"Birminghams early history is that of a remote and marginal area. The main <em>centres</em> <em>of</em> population"

如您所见,标准荧光笔划分了给定的短语并突出显示了各个术语。此外,并非所有出现的术语 centresof 都被突出显示,而只有构成短语的那些出现。

另一方面,PostingsHighlighter 返回以下突出显示的片段:

"Birminghams early history is that <em>of</em> a remote and marginal area.", "The main <em>centres</em> <em>of</em> population, power and wealth in the pre-industrial English Midlands lay in the fertile and accessible river valleys <em>of</em> the Trent, the Severn and the Avon.", "The area <em>of</em> modern Birmingham lay in between, on the upland Birmingham Plateau and within the densely wooded and sparsely populated Forest <em>of</em> Arden."

这是显着的区别。 PostingsHighlighter 突出显示与查询匹配的所有术语,而不是 仅突出显示构成短语的术语,并返回整个句子。这是一个非常好的功能,尤其是当您想在应用程序的 UI 中为用户显示突出显示结果时。

Validating your queries


有时您无法完全控制发送到 Elasticsearch 的查询。查询可以从多个标准生成,使它们成为怪物甚至更糟。它们可以由某种向导生成,这使得故障排除和查找故障部分并导致查询失败变得困难。由于这些用例,Elasticsearch 公开了 Validate API,它可以帮助我们验证查询并诊断潜在问题。

Using the Validate API

Validate API 的使用非常简单。我们没有将查询发送到 _search REST 端点,而是将其发送到 _validate/query 端点。就是这样。让我们看看下面的命令:

curl -XGET 'localhost:9200/library/_validate/query?pretty' --data-binary '{
 "query" : {
  "bool" : {
    "must" : {
      "term" : {
        "title" : "crime"
      }
    },
    "should" : {
      "range : {
        "year" : {
          "from" : 1900,
          "to" : 2000
        }
      }
    },
    "must_not" : {
      "term" : {
        "otitle" : "nothing"
      }
    }
  }
 }
}'

本书Chapter 3, 搜索你的数据< /em>。前面的命令将告诉 Elasticsearch 对其进行验证并返回有关其有效性的信息。 Elasticsearch 对上述命令的响应将类似于以下命令:

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

查看 valid 属性。它设置为 false。出问题了。让我们再次执行一次查询验证,并在查询中添加 explain 参数:

curl -XGET 'localhost:9200/library/_validate/query?pretty&explain' --data-binary '{
 "query" : {
  "bool" : {
    "must" : {
      "term" : {
        "title" : "crime"
      }
    },
    "should" : {
      "range : {
        "year" : {
          "from" : 1900,
          "to" : 2000
        }
      }
    },
    "must_not" : {
      "term" : {
        "otitle" : "nothing"
      }
    }
  }
 }
}'

现在从 Elasticsearch 返回的结果更加详细:

{
  "valid" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "failed" : 0
  },
  "explanations" : [ {
    "index" : "library",
    "valid" : false,
    "error" : "[library] QueryParsingException[Failed to parse]; nested: JsonParseException[Illegal unquoted character ((CTRL-CHAR, code 10)): has to be escaped using backslash to be included in name\n at [Source: org.elasticsearch.transport.netty.ChannelBufferStreamInput@1110d090; line: 10, column: 18]];; com.fasterxml.jackson.core.JsonParseException: Illegal unquoted character ((CTRL-CHAR, code 10)): has to be escaped using backslash to be included in name\n at [Source: org.elasticsearch.transport.netty.ChannelBufferStreamInput@1110d090; line: 10, column: 18]"
  } ]
}

现在一切都清楚了。在我们的示例 中,我们不正确地引用了 range 属性。

Note

您可能想知道为什么在我们的 curl 查询中我们使用了 --data-binary 参数。在向 Elasticsearch 发送查询时,此参数会正确保留换行符。这意味着行号和列号保持不变,更容易发现错误。在其他情况下,–d 参数更方便,因为它更短。

Validate API 还可以检测其他错误,例如,数字格式不正确或其他与映射相关的问题。不幸的是,对于我们的应用程序,由于错误消息缺乏结构,因此不容易检测到问题所在。

Validate API 支持标准 Elasticsearch 查询支持的大部分参数,其中 包括:explainignore_unavailable, allow_no_indices, expand_wildcards, operation_threading, analyzer, analyze_wildcard, default_operator, dflenientlowercase_expanded_termsrewrite

Sorting data


到目前为止,我们已经运行了我们的查询,按照每个文档的分数确定的顺序获得了结果。但是,对于所有用例来说,这还不够。 能够根据字段值对结果进行排序真的很方便。例如,当您通常搜索日志或基于时间的数据时,您可能希望首先获得最新的数据。除此之外,Elasticsearch 允许我们控制文档的排序方式,不仅可以使用字段值,还可以使用更复杂的排序方式,例如 使用脚本或对具有多个值的字段进行排序。我们将在本节中介绍所有内容。

Default sorting

让我们看看下面的查询,它返回 所有至少有一个指定 的书a>单词:

curl -XGET 'localhost:9200/library/book/_search?pretty' -d '{
  "query" : {
    "terms" : {
      "title" : [ "crime", "front", "punishment" ]
    }
  }
}'

在后台,我们可以想象 Elasticsearch 看到前面的查询如下:

curl -XGET 'localhost:9200/library/book/_search?pretty' -d '{
  "query" : {
    "terms" : {
      "title" : [ "crime", "front", "punishment" ]
    }
  },
  "sort" : { "_score" : "desc" }
}'

查看前面查询中突出显示的部分。这是 Elasticsearch 使用的默认排序方式。为了更好的可见性,我们可以稍微改变格式并显示突出显示的片段,如下所示:

"sort" : [
  { "_score" : "desc" }
]

上一节定义了文档在结果列表中的排序方式。在这种情况下,Elasticsearch 将在结果列表顶部显示得分最高的文档。最简单的修改是通过将 sort 部分更改为以下部分来反转排序:

 "sort" : [
   { "_score" : "asc" }
 ]

Selecting fields used for sorting

默认排序很无聊,不是吗?因此,让我们将其更改为根据文档中存在的字段值进行排序。让我们选择 title 字段,这意味着我们查询的 sort 部分将如下所示:

"sort" : [
  { "title" : "asc" }
]

不幸的是,这并没有按预期工作。虽然 Elasticsearch 对文档进行了排序,但排序有些奇怪。仔细看看回复。对于每个文档,Elasticsearch 都会返回有关排序的信息;例如,对于 Crime and Punishment 这本书,返回的文档类似于以下代码:

    {
      "_index" : "library",
      "_type" : "book",
      "_id" : "4",
      "_score" : null,
      "_source" : {
        "title" : "Crime and Punishment",
        "otitle" : "Преступлéние и наказáние",
        "author" : "Fyodor Dostoevsky",
        "year" : 1886,
        "characters" : [ "Raskolnikov", "Sofia Semyonovna Marmeladova" ],
        "tags" : [ ],
        "copies" : 0,
        "available" : true
      },
      "sort" : [ "punishment" ]
    }

如果你比较 title 字段和返回的排序信息,一切都应该清楚了。 Elasticsearch 在分析过程中,将字段拆分为多个标记。由于排序是使用单个令牌完成的,因此 Elasticsearch 会选择其中一个生成的令牌。它通过按字母顺序对这些标记进行排序并选择第一个标记来尽其所能。这就是为什么在排序值中,我们只找到一个单词而不是 title 的整个内容 字段。如果您想了解 Elasticsearch 在使用不同字段进行排序时的表现,可以尝试 copies 等字段:

curl -XGET 'localhost:9200/library/book/_search?pretty' -d '{
 "query" : { 
  "terms" : {
   "title" : [ "crime", "front", "punishment" ]
  } 
 },
 "sort" : [
  { "copies" : "asc" }
 ]
}'

通常,最好使用未分析的字段进行排序。我们可以使用具有多个值的字段进行排序,但是在大多数情况下,它没有多大意义并且用途有限。

作为使用两个不同字段的示例,一个用于排序,另一个用于搜索,让我们更改我们的 title 字段。更改后的 title 字段定义如下所示:

"title" : {
  "type": "string",
  "fields": {
    "sort": { "type" : "string", "index": "not_analyzed" }
  }
}

更改映射中的 title 字段后(我们使用了与 Chapter 3 中相同的映射, Searching Your Data) 并重新索引数据,我们可以尝试对 title 进行排序。 sort 字段,看看它是否有效。为此,我们需要发送以下查询:

{
  "query" : { 
    "match_all" : { }
  },
  "sort" : [
    {"title.sort" : "asc" }
  ]
}

现在,它工作正常。如您所见,我们使用了新字段,即 title.sort 字段。我们将其设置为不进行 分析,因此在 Elasticsearch 的索引中该字段只有一个标记。

Sorting mode

在 Elasticsearch 的响应中,每个 文档都包含有关用于排序的值的信息。例如,让我们看一下使用 title 字段进行排序的查询返回的文档之一:

    {
      "_index" : "library",
      "_type" : "book",
      "_id" : "1",
      "_score" : null,
      "_source" : {
        "title" : "All Quiet on the Western Front",
        "otitle" : "Im Westen nichts Neues",
        "author" : "Erich Maria Remarque",
        "year" : 1929,
        "characters" : [ "Paul Bäumer", "Albert Kropp", "Haie Westhus", "Fredrich Müller", "Stanislaus Katczinsky", "Tjaden" ],
        "tags" : [ "novel" ],
        "copies" : 1,
        "available" : true,
        "section" : 3
      },
      "sort" : [ "all" ]
    }

查询中用于获取上述文档的排序如下:

"sort" : [
  { "title" : "asc" }
]

但是,因为我们正在对包含多个值的分析字段进行排序,所以排序定义实际上等价于更长的形式,如下所示:

"sort" : [
  { "title" : { "order" : "asc", "mode" : "min" }
]

mode 定义在对具有多个值的字段进行排序时应使用哪个标记进行比较。我们可以选择的可用值是:

  • min:排序将使用最小值(或基于文本的字段上的第一个字母值)

  • max:排序将使用最大值(或基于文本的字段上的最后一个字母值)

  • avg:排序将使用平均值

  • median:排序将使用中值

  • sum:排序将使用字段中所有值的总和

    Note

    medianavgsum 等模式对于数值计算很有用多值字段,但对于基于文本的字段没有多大意义。

请注意,在请求和响应中,sort 以数组的形式给出。这表明我们可以使用几种不同的排序。 Elasticsearch 将使用排序定义列表中的下一个元素来确定与前一个排序子句具有相同值的文档之间的排序。因此,如果我们在 title 字段中具有相同的值,则文档将按 排序我们指定的字段。例如,如果我们想获取副本最多的文档,然后按标题排序,我们将运行以下查询:

curl -XGET 'localhost:9200/library/book/_search?pretty' -d '{
  "query" : {
    "terms" : {
      "title" : [ "crime", "front", "punishment" ]
    }
  },
  "sort" : [
    { "copies" : "desc" }, { "title" : "asc" }
  ]
}'

Specifying behavior for missing fields

当某些匹配查询的文档没有我们要排序的字段时怎么办?默认情况下,没有给定字段的文档在升序的情况下首先返回,在降序的情况下最后返回。然而,有时这并不是我们想要实现的。

当我们对数字字段使用排序 时,我们可以更改缺少字段的文档的默认 Elasticsearch 行为。例如,让我们看一下以下查询:

curl -XGET 'localhost:9200/library/book/_search?pretty' -d '{
  "query" : {
    "match_all" : { }
  },
  "sort" : [
    { 
      "section" : {
        "order" : "asc",
        "missing" : "_last"
      }
    }
  ]
}'

请注意我们查询的 sort 部分的扩展形式。我们添加了 missing 参数。通过将 missing 参数设置为 _last,Elasticsearch 会将没有给定字段的文档放在结果列表的底部。将 missing 参数设置为 _first 将导致 Elasticsearch 将没有给定字段的文档放在结果列表的顶部。值得一提的是,除了 _last_first 值之外,Elasticsearch 还有 允许我们使用任何数字。在这种情况下,没有定义字段的文档将被视为具有此给定值的文档。

Dynamic criteria

正如我们在上一节中提到的,Elasticsearch 允许我们使用具有多个值的字段进行排序。我们可以控制如何使用脚本进行比较。我们通过向 Elasticsearch 展示如何计算应该用于排序的值来做到这一点。假设我们要按 tags 字段中索引的第一个值进行排序。让我们看一下下面的示例查询(注意,运行下面的查询需要将 script.inline 属性设置为 on in elasticsearch.yml 文件):

curl -XGET 'localhost:9200/library/book/_search?pretty' -d '{
  "query" : { 
    "match_all" : { }
  },
  "sort" : {
    "_script" : {
      "script" : "doc[\"tags\"].values.size() > 0 ? doc[\"tags\"].values[0] : \"\u19999\"",
       "type" : "string",
       "order" : "asc"
     }
  }
}'

在前面的示例中,我们用列表中应该足够低的字符的 Unicode 代码替换了每个不存在的值。这段代码的主要思想是检查我们的数组是否至少包含一个元素。如果是,则返回数组中的第一个值。如果数组为空,我们返回应该放在 结果列表底部的Unicode 字符。除了 script 参数之外,这个排序选项还需要我们指定 order(在我们的例子中是升序)和 type 参数将用于比较(我们从脚本返回 string)。

Calculate scoring when sorting

默认情况下,Elasticsearch 假定当您使用排序时,分数完全不重要。通常这是一个很好的假设;当文档的重要性由排序公式给出时,为什么还要进行额外的计算。然而,有时您想知道 文档与当前查询的关系有多好,即使文档以不同的顺序呈现。这时应该使用 track_scores 参数并将其设置为 true。使用它的示例查询如下所示:

curl -XGET 'localhost:9200/library/book/_search?pretty' -d '{
  "query" : {
    "match_all" : { }
  },
  "track_scores" : true,
  "sort" : [
    { "title" : { "order" : "asc" }}
 ]
}'

前面的查询计算每个文档的分数。事实上,在我们的示例中,分数很无聊并且总是等于 1.0 因为 match_all 查询处理所有文件平等。

Query rewrite


在调试查询时,了解所有查询的执行方式非常有价值。正因为如此,我们决定 包含关于查询重写如何在 Elasticsearch 中工作、使用它的原因以及如何控制它的部分。如果您曾经使用过 查询,例如prefix 查询和wildcard 查询,基本上任何被称为 multiterm 的查询(由多个词组成的查询),你已经使用查询重写,即使您可能不知道它。出于性能原因,Elasticsearch 确实会重写。重写过程是将原始的、昂贵的查询更改为一组从 Apache Lucene 角度来看成本要低得多的查询,从而加快查询执行速度。

Prefix query as an example

说明重写过程如何在内部完成的最佳方法是查看示例并查看使用哪些术语而不是原始查询术语。我们将使用以下命令将三个文档索引到我们的 library_it 索引:

curl -XPOST 'localhost:9200/library_it/book/1' -d '{"title": "Solr 4 Cookbook"}'
curl -XPOST 'localhost:9200/library_it/book/2' -d '{"title": "Solr 3.1 Cookbook"}'
curl -XPOST 'localhost:9200/library_it/book/3' -d '{"title": "Mastering Elasticsearch"}'

我们想要找到所有以字母 s 开头的文档。就这么简单,我们对 library_it 索引运行以下查询:

curl -XGET 'localhost:9200/library_it/_search?pretty' -d '{
 "query" : {
  "prefix" : {
   "title" : {
    "prefix" : "s",
    "rewrite" : "constant_score_boolean"
   }
  }
 }
}'

我们使用了一个简单的 prefix 查询;我们已经说过我们想在 title 字段中找到所有带有字母 s 的文档。我们还使用了 rewrite 属性来指定查询 rewrite 方法,但我们现在跳过它,因为我们将讨论本节后面部分中此参数的可能值。

作为对上一个查询的响应,我们得到以下信息:

{
  "took" : 13,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 2,
    "max_score" : 1.0,
    "hits" : [ {
      "_index" : "library_it",
      "_type" : "book",
      "_id" : "2",
      "_score" : 1.0,
      "_source" : {
        "title" : "Solr 3.1 Cookbook"
      }
    }, {
      "_index" : "library_it",
      "_type" : "book",
      "_id" : "1",
      "_score" : 1.0,
      "_source" : {
        "title" : "Solr 4 Cookbook"
      }
    } ]
  }
}

如您所见,作为响应,我们得到了两个文档,其中 title 字段的内容以所需字符开头。我们没有明确指定映射,因此我们依赖 Elasticsearch 为我们选择映射类型的能力。众所周知,对于文本字段,Elasticsearch 使用默认分析器。这意味着我们文档中的术语将小写,因此,我们在前缀中使用了小写字母 查询(请记住,prefix 查询不被分析)。

Getting back to Apache Lucene

现在让我们退后一步,再看看 Apache Lucene。如果您还记得 Lucene 倒排索引是从什么构建的,您可以看出它包含一个术语、计数和 文档指针(如果你不记得了,请参考全文搜索部分" href="#" linkend="ch01">第 1 章Elasticsearch 集群入门)。因此,让我们看看索引的简化视图如何查找我们放入 library_it 索引的前面数据:

读书笔记《elasticsearch-server-third-edition》扩展您的查询知识

您在带有 Term 文本的 列中看到的内容非常重要。如果您查看 Elasticsearch 和 Apache Lucene 内部结构,您会发现我们的 prefix 查询被重写为以下 Lucene 查询:

ConstantScore(title:solr)

我们可以使用 Elasticsearch API 检查重写的部分。首先,我们可以通过 运行以下命令来使用Explain API:

curl -XGET 'localhost:9200/library_it/book/1/_explain?pretty' -d '{
 "query" : {
  "prefix" : {
   "title" : {
    "prefix" : "s",
    "rewrite" : "constant_score_boolean"
   }
  }
 }
}'

结果如下:

{
  "_index" : "library_it",
  "_type" : "book",
  "_id" : "1",
  "matched" : true,
  "explanation" : {
    "value" : 1.0,
    "description" : "sum of:",
    "details" : [ {
      "value" : 1.0,
      "description" : "ConstantScore(title:solr), product of:",
      "details" : [ {
        "value" : 1.0,
        "description" : "boost",
        "details" : [ ]
      }, {
        "value" : 1.0,
        "description" : "queryNorm",
        "details" : [ ]
      } ]
    }, {
      "value" : 0.0,
      "description" : "match on required clause, product of:",
      "details" : [ {
        "value" : 0.0,
        "description" : "# clause",
        "details" : [ ]
      }, {
        "value" : 1.0,
        "description" : "_type:book, product of:",
        "details" : [ {
          "value" : 1.0,
          "description" : "boost",
          "details" : [ ]
        }, {
          "value" : 1.0,
          "description" : "queryNorm",
          "details" : [ ]
        } ]
      } ]
    } ]
  }
}

我们可以看到 Elasticsearch 使用了 一个常数分数 查询与术语 solrtitle 字段。

Query rewrite properties

我们可以控制如何在内部重写查询。为此,我们将 rewrite 参数放在负责实际查询的 JSON 对象中。例如:

curl -XGET 'localhost:9200/library/book/_search?pretty' -d '{
   "query" : {
    "prefix" : {
      "title" : "s",
      "rewrite" : "constant_score_boolean"
    }
  }
}'

rewrite 属性可以采用以下值:

  • scoring_boolean:这个重写方法将每个生成的术语翻译成布尔应该布尔查询中的 子句。这种重写方法导致为每个文档计算分数。因此,这种方法可能对 CPU 要求很高。另请注意,对于包含多个术语的查询,它可能会超出布尔查询限制,该限制设置为 1024。可以通过设置 elasticsearch.yml 文件中的 index.query.bool.max_clause_count 属性来更改默认的布尔查询限制。但是,请记住,生成的布尔查询越多,查询性能可能越低。

  • constant_score:这个重写方法选择constant_score_boolean或< code class="literal">constant_score_filter 取决于查询并考虑性能。这也是完全未设置 rewrite 属性时的默认行为。

  • constant_score_boolean:这个重写方法类似于scoring_boolean 重写之前描述的方法,但对 CPU 的要求较低,因为不计算评分,而是每个词都会收到等于查询提升的分数(默认情况下为 one,并且可以使用 boost 属性设置)。因为这个 rewrite 方法也会导致 Boolean should 子句被创建,类似于 scoring_boolean rewrite 方法,这个方法也可以命中最大布尔子句限制。

  • top_terms_N:一种 rewrite 方法,将每个生成的术语转换为布尔值 布尔查询中的 should 子句并保持查询计算的分数。但是,与 scoring_boolean 重写方法不同,它只保留 N 个得分最高的术语,以避免碰到 最大布尔子句限制并提高最终查询性能。

  • top_terms_blend_freqs_N:一种重写方法,将每个术语转换为布尔查询,并将 术语视为具有相同的词频。

  • top_terms_boost_N:类似于top_terms_N的重写方法一,但不计算分数。相反,文档的分数等于 boost 属性的值(默认为 one)。

例如,如果我们希望 我们的示例查询使用 top_terms_NN 等于 2,我们的查询将如下所示:

curl -XGET 'localhost:9200/library/book/_search?pretty' -d '{
  "query" : {
    "prefix" : {
     "title" : {
      "prefix" :"s",
      "rewrite" : "top_terms_2"
     }
    }
  }
}'

如果您查看 Elasticsearch 返回的结果,您会注意到,与我们的初始查询不同,文档的得分与默认的 1.0 不同:

{
  "took" : 4,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 0.15342641,
    "hits" : [ {
      "_index" : "library",
      "_type" : "book",
      "_id" : "3",
      "_score" : 0.15342641,
      "_source" : {
        "title" : "The Complete Sherlock Holmes",
        "author" : "Arthur Conan Doyle",
        "year" : 1936,
        "characters" : [ "Sherlock Holmes", "Dr. Watson", "G. Lestrade" ],
        "tags" : [ ],
        "copies" : 0,
        "available" : false,
        "section" : 12
      }
    } ]
  }
}

分数不同于默认的 1.0 因为我们使用了 top_terms_N 重写类型并且这种类型的查询重写保留了分数N 个得分最高的术语。

在我们完成本章的查询重写部分之前,我们应该问自己最后一个问题:何时使用哪种重写类型?这个问题的答案很大程度上取决于你的用例,但是,总而言之,如果你能忍受更低的精度和相关性(但更高的性能),你可以选择顶部 N 重写方法。如果您需要高精度并因此需要更多 相关查询(但性能较低),请选择布尔方法。

Summary


您刚刚完成的那一章再次关注查询。我们使用了过滤器并了解了突出显示是什么以及如何使用它。我们了解了什么是荧光笔类型以及它们如何帮助我们。我们验证了我们的查询,并了解了 Elasticsearch 如何帮助我们对结果进行排序。最后,我们讨论了查询重写,它给我们带来了什么,以及我们如何控制它。

在下一章中,我们将回到索引主题。我们将讨论索引复杂的 JSON 对象,例如树状结构和索引非平面数据。我们将准备 Elasticsearch 来处理文档之间的关系,我们将使用 Elasticsearch API 来更新索引的结构。