读书笔记《elasticsearch-server-third-edition》扩展您的查询知识
在上一章中,我们深入探讨了 Elasticsearch 查询功能。我们详细讨论了如何查询 Elasticsearch,并了解了 Elasticsearch 查询的工作原理。我们现在知道了这个伟大的搜索引擎的基本查询和复合查询,以及每种查询类型的配置选项是什么。我们还知道何时使用我们的查询,我们讨论了一些用例以及哪些查询可以用来处理它们。本章致力于扩展我们的查询知识。在本章结束时,您将学习以下主题:
什么是过滤以及如何使用它
什么是突出显示以及如何使用它
荧光笔有哪些类型,它们带来了什么好处
如何验证您的查询
如何对查询结果进行排序
什么是查询重写以及如何控制它
在上一章中,我们讨论了各种类型的查询。共同点是我们总是希望首先获得最好的结果。这是与标准数据库方法的主要区别,其中每个文档都与查询匹配。在数据库的世界里,我们不问文档有多好;我们唯一的兴趣在于返回的结果。当谈到全文搜索引擎时,情况就不同了——我们不仅对结果感兴趣,而且对它们的质量感兴趣。原因很明显,我们在非结构化数据中搜索,使用的文本字段使用语言分析、词干提取等。正因为如此,我们查询的初始结果,在大多数情况下,给出的结果远非最佳。这就是为什么当我们谈论搜索时,我们谈论精度和文档召回。
另一方面,有时我们希望将整个文档子集限制为选定的部分。例如,在图书馆中,我们可能只想搜索可用的书籍,其余的不重要。有时,为给定字段忙着计算的分数,只会干扰整体分数,在准确性方面没有任何意义。在这种情况下,过滤器应该用于限制查询结果,但不干扰计算的分数。
在 Elasticsearch 2.0 之前,过滤器是独立于查询的实体。实际上,几乎每个查询在过滤器中都有自己的对应项。有 term
查询和 term
过滤器,bool
查询和bool
过滤器、range
查询和 range
过滤器等等上。从用户的角度来看,查询和过滤器之间最重要的区别是评分。过滤器不计算分数,这导致过滤器很容易缓存并且效率更高。但是这种差异对用户来说非常不方便。随着 Elasticsearch 2.0 的发布及其对 Lucene 5.3 的使用,过滤器查询以及一些允许我们使用过滤器的查询类型被弃用。让我们讨论一下现在过滤是如何工作的,以及我们可以做些什么来实现与以前在 Elasticsearch 2.0 中相同或更好的性能。
在 Elasticsearch 2.0 中,查询可以通过选择更有效的执行方式来计算分数或省略它。在许多情况下,此行为是根据使用 查询的上下文自动完成的。这是关于包含过滤器部分的查询,它根据某些条件删除文档。这些文档在返回的结果中是不必要的,应该在不影响总分的情况下尽快跳过。多亏了这一点,在丢弃一些文档后,我们可以只关注其余的文档,计算它们的分数,并在返回之前对它们进行排序。这种情况的示例可以是布尔查询的 must_not
子句。与 must_not
子句匹配的文档将从返回的结果集中移除,因此对 bool 这部分匹配的文档计算分数
查询将是一项额外的、不必要的且性能无效的工作。
所有这些变化最好的一点是,我们不需要关心是否要使用过滤。 Elasticsearch 和底层 Apache Lucene 库负责为我们选择正确的执行方法。
正如我们在 第 3 章中的 复合查询 部分中提到的 a>,搜索您的数据, Elasticsearch 2.0 中的 bool
查询允许我们添加过滤器通过添加 filter
部分并在该部分中包含 一个查询来显式地添加。这非常方便,如果我们希望有一部分查询需要匹配,但我们对这些文档的分数不感兴趣。
让我们看一下以下查询:
我们看到一个简单的查询, 应该返回我们图书馆中所有可供借阅的书籍,这意味着具有 的文档是可用的
字段设置为 true
。现在让我们将其与以下查询进行比较:
此查询返回所有书籍,但它还包含 filter
部分,它告诉 Elasticsearch 我们只是 对现有书籍感兴趣。该查询将返回与我们之前看到的查询相同的结果,当然,当仅查看文档数量和返回哪些文档时。不同的是分数。对于我们的示例数据,两个查询都返回两本书。第一个查询返回的结果如下所示:
如果您查看每个查询中文档的分数,您会注意到差异。在简单的 term
查询中,Elasticsearch(实际上是 Lucene 库)对第一个文档的得分为 1.0
,并且第二个得分 0.30685282
。这不是一个完美的解决方案,因为可用性检查或多或少是二进制的,我们不希望它干扰分数。这就是为什么 在这种 案例中第二个查询更好的原因。使用 bool
查询和过滤,不计算 filter
元素的分数,两个文档的分数相同,即1.0
。
您可能听说过或看过highlighting。当您在 World Wide Web (WWW) 上使用更大或更小的公共搜索引擎时,您甚至可能不知道您实际上在使用突出显示。当我们谈论全文搜索上下文中的突出显示时,我们通常是指显示查询中的哪些单词或短语在结果文档中匹配。例如,如果我们使用 Google 并搜索单词 lucene,,我们会在搜索结果中看到该单词加粗:
它在 Microsoft Bing 搜索引擎上更加明显:
在本章中,我们将了解如何使用 Elasticsearch 突出显示功能来增强我们的应用程序的突出显示结果。
除了进行查询并查看 Elasticsearch 返回的结果之外,没有更好的方法来显示突出显示的工作原理。所以让我们这样做。我们假设我们希望突出显示在文档的 title
字段中匹配的术语,以增加用户的搜索体验。现在您已经从上到下了解了示例数据,所以让我们再次 重用相同的数据集。我们希望匹配 title
字段中的术语 crime
并且我们希望获得突出显示的结果。可以实现此目的的最简单查询之一如下所示:
上述查询的响应如下:
如您所见,除了关于匹配查询的文档的 标准信息外,我们还有一个名为highlight 的新部分
。 Elasticsearch 使用
<em>
HTML 标签作为高亮部分的开头,并使用它的关闭对应物来关闭高亮部分。这是 Elasticsearch 的默认行为,但我们将学习如何改变它。
为了执行突出显示,该字段的原始内容需要存在。我们必须设置我们 将用于突出显示的字段。这是通过标记要存储的字段或使用包含这些字段的 _source
字段来完成的。如果该字段设置为存储在映射中,将使用存储的版本,否则 Elasticsearch 将尝试使用 _source
字段并提取需要突出显示的字段。
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
查询,在这种情况下突出显示赢了不被退回。
高亮机制的默认行为可能并不适合所有人——并非所有人都希望拥有 <em>
和 </ em>
标签用于高亮显示。因此,Elasticsearch 允许我们更改默认行为并更改用于该目的的标签。为此,我们应该将 pre_tags
和 post_tags
属性设置为我们希望高亮开始和结束的代码片段;例如,通过 <b>
和 </b>
。 pre_tags
和 post_tags
属性是数组,因此我们可以 提供多个开始和结束标签,Elasticsearch 将使用每个定义的标签来突出显示不同的单词。例如,如果我们想使用 <b>
作为开始标签, </b>
作为结束标签,我们的查询将如下所示:
Elasticsearch 对上述查询返回的结果如下:
如您所见,title
术语
code> 字段被我们选择的标签包围。犯罪
Elasticsearch 允许我们通过公开两个属性控制返回的突出显示片段的数量及其大小。第一个是number_of_fragments,
,它定义了Elasticsearch返回的片段数量(默认为5
)。将此属性设置为 0
会导致返回整个字段,这对于短字段可能很方便,但对于较长的字段来说很昂贵。第二个属性是 fragment_size
,它允许我们指定突出显示的片段的最大长度(以字符为单位),默认为 100
。
我们之前讨论的突出显示属性既可以在全局基础上设置,也可以在每个字段基础上设置。全局 将用于所有不会覆盖它们的字段,并且应该与 fields
部分突出显示,例如 ,如下所示:
您还可以设置每个字段的属性。例如,如果我们想为除了 title
字段之外的所有字段保留默认行为,我们将执行以下操作:
如您所见,我们没有将 属性放在与 fields
部分相同的级别,而是将它位于指定标题 field
行为的空 JSON 对象中。 当然,每个字段都可以使用不同的属性进行配置。
有时可能需要(尤其是在使用多个突出显示的字段时)只显示与我们的查询匹配的字段。为了有这样的行为,我们需要将 require_field_match
属性设置为 true
。将此属性设置为 false
将导致突出显示所有术语,即使字段与查询不匹配也是如此。
要了解它是如何工作的,让我们创建一个名为 users
的新索引,并在其中索引单个文档。我们将通过发送以下命令来做到这一点:
因此,假设我们要突出显示前面两个字段中的命中。我们将查询发送到新索引的命令如下所示:
请注意,我们只在 name
字段上突出显示。这是因为我们的查询只匹配该字段。让我们看看如果我们将 require_field_match
属性设置为 false
并使用类似于以下的命令会发生什么:
如您所见,Elasticsearch 现在在两个字段中都返回了突出显示。
在某些用例中,您的查询很复杂且不适合突出显示,但您仍希望 使用突出显示功能。在这种情况下,Elasticsearch 允许我们根据使用 highlight_query
属性提供的不同查询来突出显示结果。使用不同的突出显示查询的示例如下所示:
是时候讨论第三个 可用的荧光笔了。它是在 Elasticsearch 0.90.6 中添加的,与之前的略有不同。当字段定义将 index_options
设置为 offsets
时,会自动使用 PostingsHighlighter
。为了说明 PostingsHighlighter
的工作原理,我们将创建一个具有适当配置的简单索引,以允许荧光笔工作。我们将通过使用 以下命令来做到这一点:
如果一切顺利,我们应该有一个新的索引和映射。映射定义了两个字段:一个名为 contents
,第二个名为 contents.ps
。在第二种情况下,我们使用 index_options
属性打开了偏移量。这意味着 Elasticsearch 将为 contents
字段使用标准荧光笔,为 contents.ps
字段使用帖子荧光笔。
为了看到差异,我们将使用维基百科中描述伯明翰历史的片段来索引单个文档。我们通过运行以下命令来做到这一点:
最后一步是使用两个荧光笔发送查询。我们可以使用以下 命令在单个请求中完成:
如果一切顺利,您会在 Elasticsearch 返回的响应中找到以下代码段:
如您所见,两个荧光笔都找到了所需单词的出现。不同之处在于,发帖高亮显示返回更智能的片段——它检查句子边界。
让我们再尝试一个查询:
我们搜索了短语中心
。正如您所料,两种荧光笔的结果会有所不同。对于标准荧光笔,在 contents
字段上运行,我们将在响应中找到以下短语:
如您所见,标准荧光笔划分了给定的短语并突出显示了各个术语。此外,并非所有出现的术语 centres
和 of
都被突出显示,而只有构成短语的那些出现。
另一方面,PostingsHighlighter
返回以下突出显示的片段:
这是显着的区别。 PostingsHighlighter
突出显示与查询匹配的所有术语,而不是 仅突出显示构成短语的术语,并返回整个句子。这是一个非常好的功能,尤其是当您想在应用程序的 UI 中为用户显示突出显示结果时。
有时您无法完全控制发送到 Elasticsearch 的查询。查询可以从多个标准生成,使它们成为怪物甚至更糟。它们可以由某种向导生成,这使得故障排除和查找故障部分并导致查询失败变得困难。由于这些用例,Elasticsearch 公开了 Validate API,它可以帮助我们验证查询并诊断潜在问题。
Validate API 的使用非常简单。我们没有将查询发送到 _search
REST 端点,而是将其发送到 _validate/query
端点。就是这样。让我们看看下面的命令:
本书Chapter 3, 搜索你的数据< /em>。前面的命令将告诉 Elasticsearch 对其进行验证并返回有关其有效性的信息。 Elasticsearch 对上述命令的响应将类似于以下命令:
查看 valid
属性。它设置为 false
。出问题了。让我们再次执行一次查询验证,并在查询中添加 explain
参数:
现在从 Elasticsearch 返回的结果更加详细:
现在一切都清楚了。在我们的示例 中,我们不正确地引用了 range
属性。
Note
您可能想知道为什么在我们的 curl
查询中我们使用了 --data-binary
参数。在向 Elasticsearch 发送查询时,此参数会正确保留换行符。这意味着行号和列号保持不变,更容易发现错误。在其他情况下,–d
参数更方便,因为它更短。
Validate API 还可以检测其他错误,例如,数字格式不正确或其他与映射相关的问题。不幸的是,对于我们的应用程序,由于错误消息缺乏结构,因此不容易检测到问题所在。
Validate API 支持标准 Elasticsearch 查询支持的大部分参数,其中 包括:explain
、 ignore_unavailable
, allow_no_indices
, expand_wildcards
, operation_threading
, analyzer
, analyze_wildcard
, default_operator
, df
、lenient
、lowercase_expanded_terms
和 rewrite
。
到目前为止,我们已经运行了我们的查询,按照每个文档的分数确定的顺序获得了结果。但是,对于所有用例来说,这还不够。 能够根据字段值对结果进行排序真的很方便。例如,当您通常搜索日志或基于时间的数据时,您可能希望首先获得最新的数据。除此之外,Elasticsearch 允许我们控制文档的排序方式,不仅可以使用字段值,还可以使用更复杂的排序方式,例如 使用脚本或对具有多个值的字段进行排序。我们将在本节中介绍所有内容。
让我们看看下面的查询,它返回 所有至少有一个指定 的书a>单词:
在后台,我们可以想象 Elasticsearch 看到前面的查询如下:
查看前面查询中突出显示的部分。这是 Elasticsearch 使用的默认排序方式。为了更好的可见性,我们可以稍微改变格式并显示突出显示的片段,如下所示:
上一节定义了文档在结果列表中的排序方式。在这种情况下,Elasticsearch 将在结果列表顶部显示得分最高的文档。最简单的修改是通过将 sort
部分更改为以下部分来反转排序:
默认排序很无聊,不是吗?因此,让我们将其更改为根据文档中存在的字段值进行排序。让我们选择 title
字段,这意味着我们查询的 sort
部分将如下所示:
不幸的是,这并没有按预期工作。虽然 Elasticsearch 对文档进行了排序,但排序有些奇怪。仔细看看回复。对于每个文档,Elasticsearch 都会返回有关排序的信息;例如,对于 Crime and Punishment
这本书,返回的文档类似于以下代码:
如果你比较 title
字段和返回的排序信息,一切都应该清楚了。 Elasticsearch 在分析过程中,将字段拆分为多个标记。由于排序是使用单个令牌完成的,因此 Elasticsearch 会选择其中一个生成的令牌。它通过按字母顺序对这些标记进行排序并选择第一个标记来尽其所能。这就是为什么在排序值中,我们只找到一个单词而不是 title 的整个内容
字段。如果您想了解 Elasticsearch 在使用不同字段进行排序时的表现,可以尝试 copies
等字段:
通常,最好使用未分析的字段进行排序。我们可以使用具有多个值的字段进行排序,但是在大多数情况下,它没有多大意义并且用途有限。
作为使用两个不同字段的示例,一个用于排序,另一个用于搜索,让我们更改我们的 title
字段。更改后的 title
字段定义如下所示:
更改映射中的 title
字段后(我们使用了与 Chapter 3 中相同的映射, Searching Your Data) 并重新索引数据,我们可以尝试对 title 进行排序。 sort
字段,看看它是否有效。为此,我们需要发送以下查询:
现在,它工作正常。如您所见,我们使用了新字段,即 title.sort
字段。我们将其设置为不进行 分析,因此在 Elasticsearch 的索引中该字段只有一个标记。
在 Elasticsearch 的响应中,每个 文档都包含有关用于排序的值的信息。例如,让我们看一下使用 title
字段进行排序的查询返回的文档之一:
查询中用于获取上述文档的排序如下:
但是,因为我们正在对包含多个值的分析字段进行排序,所以排序定义实际上等价于更长的形式,如下所示:
mode
定义在对具有多个值的字段进行排序时应使用哪个标记进行比较。我们可以选择的可用值是:
min
:排序将使用最小值(或基于文本的字段上的第一个字母值)max
:排序将使用最大值(或基于文本的字段上的最后一个字母值)avg
:排序将使用平均值median
:排序将使用中值sum
:排序将使用字段中所有值的总和
请注意,在请求和响应中,sort
以数组的形式给出。这表明我们可以使用几种不同的排序。 Elasticsearch 将使用排序定义列表中的下一个元素来确定与前一个排序子句具有相同值的文档之间的排序。因此,如果我们在 title
字段中具有相同的值,则文档将按 排序我们指定的字段。例如,如果我们想获取副本最多的文档,然后按标题排序,我们将运行以下查询:
当某些匹配查询的文档没有我们要排序的字段时怎么办?默认情况下,没有给定字段的文档在升序的情况下首先返回,在降序的情况下最后返回。然而,有时这并不是我们想要实现的。
当我们对数字字段使用排序 时,我们可以更改缺少字段的文档的默认 Elasticsearch 行为。例如,让我们看一下以下查询:
请注意我们查询的 sort
部分的扩展形式。我们添加了 missing
参数。通过将 missing
参数设置为 _last
,Elasticsearch 会将没有给定字段的文档放在结果列表的底部。将 missing
参数设置为 _first
将导致 Elasticsearch 将没有给定字段的文档放在结果列表的顶部。值得一提的是,除了 _last
和 _first
值之外,Elasticsearch 还有 允许我们使用任何数字。在这种情况下,没有定义字段的文档将被视为具有此给定值的文档。
正如我们在上一节中提到的,Elasticsearch 允许我们使用具有多个值的字段进行排序。我们可以控制如何使用脚本进行比较。我们通过向 Elasticsearch 展示如何计算应该用于排序的值来做到这一点。假设我们要按 tags
字段中索引的第一个值进行排序。让我们看一下下面的示例查询(注意,运行下面的查询需要将 script.inline
属性设置为 on
in elasticsearch.yml
文件):
在前面的示例中,我们用列表中应该足够低的字符的 Unicode 代码替换了每个不存在的值。这段代码的主要思想是检查我们的数组是否至少包含一个元素。如果是,则返回数组中的第一个值。如果数组为空,我们返回应该放在 结果列表底部的Unicode 字符。除了 script
参数之外,这个排序选项还需要我们指定 order
(在我们的例子中是升序)和 type
参数将用于比较(我们从脚本返回 string
)。
在调试查询时,了解所有查询的执行方式非常有价值。正因为如此,我们决定 包含关于查询重写如何在 Elasticsearch 中工作、使用它的原因以及如何控制它的部分。如果您曾经使用过 查询,例如prefix
查询和wildcard
查询,基本上任何被称为 multiterm 的查询(由多个词组成的查询),你已经使用查询重写,即使您可能不知道它。出于性能原因,Elasticsearch 确实会重写。重写过程是将原始的、昂贵的查询更改为一组从 Apache Lucene 角度来看成本要低得多的查询,从而加快查询执行速度。
说明重写过程如何在内部完成的最佳方法是查看示例并查看使用哪些术语而不是原始查询术语。我们将使用以下命令将三个文档索引到我们的 library_it
索引:
我们想要找到所有以字母 s
开头的文档。就这么简单,我们对 library_it
索引运行以下查询:
我们使用了一个简单的 prefix
查询;我们已经说过我们想在 title
字段中找到所有带有字母 s
的文档。我们还使用了 rewrite
属性来指定查询 rewrite
方法,但我们现在跳过它,因为我们将讨论本节后面部分中此参数的可能值。
作为对上一个查询的响应,我们得到以下信息:
如您所见,作为响应,我们得到了两个文档,其中 title
字段的内容以所需字符开头。我们没有明确指定映射,因此我们依赖 Elasticsearch 为我们选择映射类型的能力。众所周知,对于文本字段,Elasticsearch 使用默认分析器。这意味着我们文档中的术语将小写,因此,我们在前缀中使用了小写字母
查询(请记住,prefix
查询不被分析)。
现在让我们退后一步,再看看 Apache Lucene。如果您还记得 Lucene 倒排索引是从什么构建的,您可以看出它包含一个术语、计数和 文档指针(如果你不记得了,请参考全文搜索部分" href="#" linkend="ch01">第 1 章,Elasticsearch 集群入门)。因此,让我们看看索引的简化视图如何查找我们放入 library_it
索引的前面数据:
您在带有 Term 文本的 列中看到的内容非常重要。如果您查看 Elasticsearch 和 Apache Lucene 内部结构,您会发现我们的 prefix
查询被重写为以下 Lucene 查询:
我们可以使用 Elasticsearch API 检查重写的部分。首先,我们可以通过 运行以下命令来使用Explain API:
结果如下:
我们可以控制如何在内部重写查询。为此,我们将 rewrite
参数放在负责实际查询的 JSON 对象中。例如:
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 方法也会导致 Booleanshould
子句被创建,类似于scoring_boolean
rewrite 方法,这个方法也可以命中最大布尔子句限制。top_terms_N
:一种 rewrite 方法,将每个生成的术语转换为布尔值布尔查询中的 should
子句并保持查询计算的分数。但是,与scoring_boolean
重写方法不同,它只保留N
个得分最高的术语,以避免碰到 最大布尔子句限制并提高最终查询性能。top_terms_boost_N
:类似于top_terms_N
的重写方法一,但不计算分数。相反,文档的分数等于boost
属性的值(默认为one
)。
例如,如果我们希望 我们的示例查询使用 top_terms_N
和 N
等于 2
,我们的查询将如下所示:
如果您查看 Elasticsearch 返回的结果,您会注意到,与我们的初始查询不同,文档的得分与默认的 1.0
不同:
分数不同于默认的 1.0
因为我们使用了 top_terms_N
重写类型并且这种类型的查询重写保留了分数N 个得分最高的术语。
在我们完成本章的查询重写部分之前,我们应该问自己最后一个问题:何时使用哪种重写类型?这个问题的答案很大程度上取决于你的用例,但是,总而言之,如果你能忍受更低的精度和相关性(但更高的性能),你可以选择顶部 N 重写方法。如果您需要高精度并因此需要更多 相关查询(但性能较低),请选择布尔方法。