读书笔记《elasticsearch-server-third-edition》扩展您的索引结构
树木无处不在。如果您 开发一个电子商务商店应用程序,您的产品可能会使用类别来描述。关于类别的事情是,在大多数情况下,它们是分层的。有顶级类别,例如电子产品、音乐、书籍等。每个顶级类别都可以有许多子类别,例如小说和科学,这些类别可以更深入地研究科幻、爱情等。如果您查看文件系统,文件和目录也以树状结构排列。这本书也可以表示为一棵树:章节包含主题,主题分为子主题。所以我们周围的数据被排列成树状结构,正如你想象的那样,Elasticsearch 能够索引树状结构,以便我们可以更轻松地表示数据。让我们检查一下如何使用 path_analyzer
浏览此类数据。
如您所见,我们创建了一个类型——类别类型。我们将使用它来存储和索引有关我们的文档在树结构中的位置的信息。这个想法很简单——我们可以将文档的位置显示为路径,其方式与文件和目录在硬盘驱动器上的显示方式完全相同。例如,在汽车商店,我们可以有 /cars/passenger/sport
、/cars/passenger/camper
,或/cars/delivery_truck/
。然而,为了实现这一点,我们需要以两种不同的方式索引这条路径。首先,我们将使用一个名为 name
的未分析字段,以原始形式存储和索引路径名称。我们还将使用一个名为 path
的字段,它将使用我们定义的 path_analyzer
分析器来处理路径,因此它更容易搜索。
现在,让我们看看 Elasticsearch 在分析过程中将如何处理类别路径。为了看到这一点,我们 将使用以下命令行,该命令行使用中讨论的分析API了解第 6 章的解释信息部分,让你更好地搜索:
Elasticsearch 将返回以下结果:
可以看到,我们的类别路径 /cars/passenger/sport
被 Elasticsearch 处理并分为三个令牌。多亏了这一点,我们可以使用术语过滤器轻松找到属于给定类别或其子类别的每个文档。为了使示例完整,让我们使用以下命令索引一个简单的文档:
使用过滤器的示例如下:
请注意,我们还在 category.name
字段中索引了原始值。当我们想要 从特定路径查找文档时,这很方便,忽略层次结构中更深的文档。
并非所有数据都像我们迄今为止在书中使用的 示例一样平坦。您将遇到的大多数数据将在根 JSON 对象内具有一些结构和嵌套对象。当然,如果我们正在构建我们的系统,Elasticsearch 将成为其中的一部分,并且我们可以控制它的所有部分,我们可以创建一个便于 Elasticsearch 的结构。但即使在这种情况下,平面数据也不总是一种选择。值得庆幸的是,Elasticsearch 允许我们索引不平坦的数据,本节将向我们展示如何做到这一点。
假设我们有 以下数据(我们将其存储在名为 structured_data.json
的文件中):
如您所见,数据不是平面的——它包含数组和嵌套对象。如果我们想创建映射并使用我们目前掌握的知识,我们将不得不扁平化数据。然而,正如我们已经说过的,Elasticsearch 允许某种程度的结构,我们应该能够创建适用于前面示例的映射。
前面的示例 data 显示了结构化的 JSON 文件。正如您在示例中看到的,我们的根对象有一些额外的简单属性,例如englishTitle、isbn、year 和copies。这些将被索引为索引中的普通字段,我们已经知道如何处理它们(我们在 第 2 章,索引您的数据)。除此之外,它还有字符数组类型和作者对象。 author 对象中嵌套了另一个对象 - name 对象,它有两个属性:firstName
和 lastName
。如您所见,我们可以在彼此内部拥有多个嵌套对象。
我们已经使用了array 类型的数据,但是我们没有谈论它。默认情况下,Lucene 和 Elasticsearch 中的所有字段都是多值的,这意味着它们可以存储多个值。为了将这些字段发送到 Elasticsearch 的索引,我们使用 JSON 数组类型,它嵌套在方括号 [] 中。正如您在前面的示例中所看到的,我们为书中的字符使用了数组类型。
现在让我们看看我们之前展示的 book 对象的 映射是什么样子的。我们已经说过,索引数组不需要任何特殊的东西。因此,在 我们的例子中,要索引字符数据,我们需要添加类似于以下的字段定义:
没什么奇怪的!我们只是将属性部分嵌套在数组名称中(在我们的例子中是字符),并在那里定义字段。作为上述映射的结果,我们将在索引中获得 characters.name
多值字段。
我们为作者对象做类似的事情。我们使用与数据中存在的名称相同的名称来调用该部分。我们有 author
对象,但它也有 有 name< /code> 对象嵌套在其中,所以我们也这样做——我们只是在其中嵌套另一个对象。因此,我们对
author
字段的映射如下所示:
firstName
和 lastName
字段在索引中显示为 author.name.firstName
和 author.name.lastName
。
其余字段是简单的核心类型,因此我将跳过讨论它们,因为它们已经在 映射配置 部分中讨论过="link" href="#" linkend="ch02">第 2 章,索引您的数据。
嵌套对象在某些情况下会派上用场。基本上,使用嵌套对象,Elasticsearch 允许我们将多个文档连接在一起——一个主文档和多个从属文档。主文档和嵌套文档被索引在一起,它们被放置在索引的同一段中(实际上,在段内的同一块中,彼此靠近),这保证了我们对于这样的数据结构可以获得最佳性能. 同样适用于更改文档;除非您使用更新 API,否则您需要同时索引父文档和所有其他嵌套文档。
Note
如果您想了解更多关于嵌套对象如何在 Apache Lucene 级别上工作的信息,请参阅 Mike McCandless 在 http://blog.mikemccandless.com/2012/01 /searching-relational-content-with.html。
现在让我们继续我们的示例用例。想象一下,我们有一家卖衣服的商店,我们存储每件 T 恤的尺寸和颜色。我们的标准非嵌套映射将如下所示(存储在 cloth.json
中):
要在没有布料映射的情况下创建 shop
索引,我们运行以下命令:
现在想象一下,我们店里有一件 T 恤,只有红色的 XXL 码和黑色的 XL 码。所以我们的示例文档索引命令将如下所示:
然而,这样的数据结构存在一个问题。如果我们的一位客户搜索我们的商店以找到黑色的 XXL T 恤怎么办?让我们通过运行以下查询来检查(我们假设我们已经使用我们的映射来创建索引并且我们已经索引了我们的示例文档):
我们应该没有 结果,对吧?但实际上 Elasticsearch 返回了以下文档:
这是因为文档是匹配的——我们在大小字段和颜色字段中有我们正在搜索的值。当然,这不是我们想要得到的。
因此,让我们修改我们的映射以使用嵌套对象将颜色和大小分隔到不同的嵌套文档。最终的映射如下所示(我们将这些映射存储在 cloth_nested.json
文件中):
现在,我们将通过运行以下命令,使用修改后的映射创建名为 shop_nested
的第二个索引:
如您所见,我们在布料类型中引入了 一个新对象——变体之一,它是一个嵌套对象(type
属性设置为 nested
)。它基本上说我们将要索引嵌套文档。现在,让我们修改我们的文档。我们将向其中添加变体对象,该对象将存储具有两个属性的对象——大小和颜色。因此,我们修改后的示例产品的 index
命令将如下所示:
我们已经对文档进行了结构化,以便每个尺寸及其匹配的颜色都是一个单独的文档。但是,如果您运行我们之前的查询,它不会返回任何文档。这是因为为了查询嵌套文档,我们需要使用专门的查询。所以现在我们的查询如下所示:
现在,前面的查询将不会返回索引文档,因为我们没有大小等于 XXL 且颜色为黑色的嵌套文档。
让我们回到 查询上来简单讨论一下。如您所见,我们使用嵌套查询来搜索嵌套文档。 path
属性指定嵌套对象的名称(是的,我们可以有多个)。我们刚刚在嵌套类型下包含了一个标准查询部分。另请注意,我们为嵌套对象中的字段名称指定了完整路径,这在您进行多级嵌套时很方便,这也是可能的。
在查询期间处理嵌套文档时,还有一个附加属性。除了 path
属性,还有 score_mode
属性,它允许我们定义如何从嵌套的查询。 Elasticsearch 允许我们将 score_mode
属性 设置为以下值之一:
在上一节中,我们讨论了使用 Elasticsearch 来索引嵌套文档和父文档。但是,即使嵌套文档在 索引中作为单独文档进行索引,我们也无法更改单个嵌套文档(除非我们使用更新 API) . Elasticsearch 允许我们拥有真正的父子关系,我们将在下一节中对其进行研究。
让我们使用我们在讨论嵌套文档时使用的相同示例——假设的布料商店。 我们希望能够更新大小和颜色,而无需在 每次更改。我们将看到 如何使用 Elasticsearch 父子功能来实现这一点。
首先,我们必须创建一个子索引定义。要创建子映射,我们需要添加带有父类型名称的 _parent
属性,在我们的例子中是布料。在子文档中,我们想要有布料的尺寸和颜色。因此,将创建商店索引和变体类型的命令如下所示:
就这样。您无需指定将使用哪个字段将子文档连接到父文档。默认情况下,Elasticsearch 将为此使用文档的唯一标识符。如果您还记得之前的章节,关于唯一标识符的信息默认存在于索引中。
我们已经为我们的数据建立了索引, 现在我们需要使用适当的查询来将文档与存储在其子项中的数据进行匹配。这是因为,默认情况下,Elasticsearch 搜索文档而不查看父子关系。例如,以下查询将匹配我们索引的所有三个文档(两个孩子和一个父母):
这不是我们想要实现的,至少在大多数情况下是这样。通常,我们对具有与查询匹配的子级的父文档感兴趣。当然,Elasticsearch 通过专门类型的查询来提供此类功能。
想象一下,我们想要 获得 XXL 码的红色衣服。您还记得,布料的大小和颜色在子文档中都有索引,所以 我们需要一个专门的 has_child
查询,检查哪些父文档具有所需大小和颜色的子文档。因此,符合我们要求的示例查询如下所示:
查询很简单;它属于 has_child
类型,它告诉 Elasticsearch 我们要在子文档中进行搜索。为了指定我们感兴趣的子类型,我们用子类型的名称指定 type
属性。使用 query
属性提供查询。我们使用了一个标准的 bool
查询,我们已经讨论过了。查询的结果将只包含那些有子与我们匹配的父文档bool
查询。在我们的例子中,返回的单个文档如下所示:
has_child
查询允许我们提供额外的参数来控制其行为。找到的每个父文档都可能与一个或多个子文档相关联。这意味着每个子文档都可以影响结果分数。默认情况下,查询不关心子文档,有多少匹配,以及它们的内容是什么——只关心它们是否匹配查询。这可以通过使用 score_mode
参数来更改,该参数控制 has_child
查询的分数计算。该参数可以取的值是:
我们使用 sum
作为 score_mode
,这导致子文档对父文档的最终分数做出贡献 - contribution 是每个匹配查询的子文档的得分之和。
最后,我们可以限制需要匹配的子文档的数量;我们可以指定允许匹配的最大子文档数(max_children
属性)和最小子文档数(min_children
属性)需要匹配。说明这些参数用法的查询如下:
有时,我们对父文档不感兴趣,而是对子文档感兴趣。如果您想返回 与 中给定数据匹配的子文档父文档,Elasticsearch 为我们提供了一个查询 - has_parent
查询。它类似于 has_child
查询;但是,我们使用父文档类型的值指定 parent_type
属性,而不是 type
属性。例如,以下查询将返回我们已索引的两个子文档,但不返回父文档:
Elasticsearch 的响应将类似于以下响应:
与 has_child
查询类似,has_parent
查询也为我们提供了调整查询分数计算的可能性。在这种情况下,score_mode
只有两个选项:none,
默认的一个,查询计算的分数等于1.0和score
,根据父文档内容计算文档的 分数。在 has_parent
查询中使用 score_mode
的示例看起来 如下:
与上一个示例的一个区别是 score_mode
。如果您检查这些查询的结果,您会发现只有一个区别。第一个示例中所有文档的得分为 1.0,而前面查询返回的结果的得分等于 0.8784157。在这种情况下,找到的所有文档都具有相同的分数,因为它们具有共同的父文档。
使用 Elasticsearch 父子功能时,您必须了解它对性能的影响。您需要记住的第一件事是父文档和子文档 需要存储在同一个分片中才能使查询工作。如果您碰巧有单亲的大量子代,那么您最终可能会得到没有相似数量文档的分片。因此,您的查询性能在其中一个节点上可能会降低,从而导致整个查询变慢。另外,请记住,父子查询将比针对它们之间没有关系的文档运行的查询要慢。有一种方法可以通过急切加载所谓的全局序数来加速父子查询的连接,但会以内存为代价;但是,我们将在 的 Elasticsearch 缓存部分讨论该方法第 9 章,Elasticsearch 集群详解。
最后,第一个查询将使用 doc 值预加载和缓存文档标识符。这需要时间。为了提高使用父子关系的初始查询的性能,可以使用Warmer API。您可以在 Warming up 部分找到有关如何向 Elasticsearch 添加预热查询的更多信息linkend="ch10">第 10 章,管理您的集群。
在前面的章节中,我们 讨论了如何创建索引映射和索引数据。但是,如果您已经创建了映射并索引了数据,但您想修改索引的结构怎么办?当然有人可以说我们可以用新的映射创建一个新的索引,但这并不总是可行的,尤其是在生产环境中。这在某种程度上是可能的。例如,默认情况下,如果我们使用新字段索引文档,Elasticsearch 会将该字段添加到索引结构中。现在让我们看看如何手动修改索引结构。
Note
对于需要更改映射并且由于与当前索引结构冲突而无法更改的情况,使用别名非常好——包括读取和写入。我们将在 Chapter 10< 的 Index aliasing 部分讨论别名/a>,管理您的集群。
假设我们 在 user.json
文件中存储了以下用户索引映射:
如您所见,它非常简单。它只有一个属性来保存用户名。现在让我们创建一个名为 users
的 index
并使用前面的映射来创建我们的类型。为此,我们将运行以下命令:
如果一切顺利,我们将创建索引(称为 users
)和类型(称为 user
)。所以现在让我们尝试向映射中添加一个新字段。
为了说明 如何将新字段添加到我们的映射中,我们假设我们希望将电话号码添加到为每个用户存储的数据中。为此,我们需要向 /index_name/type_name/_mapping
REST 端点发送一个 HTTP PUT 命令,其中包含我们的新字段的正确正文。例如,要添加提到的电话字段,我们将 运行以下命令:
与我们之前运行的命令类似,如果一切顺利,我们应该在索引结构中添加一个新字段。
Note
当然,Elasticsearch 不会重新索引我们的数据或自动填充新添加的字段。它只会改变主节点持有的映射并将映射填充到集群中的所有其他节点,仅此而已。数据重新索引必须由我们或在我们的环境中索引数据的应用程序来完成。在那之前,旧文档不会有新添加的字段。记住这一点至关重要。如果您没有原始文档,您可以使用 _source
字段从 Elasticsearch 中获取原始数据并再次对其进行索引。
为确保一切正常,我们可以向 _mapping
REST 端点运行 GET
HTTP 请求,Elasticsearch 将返回适当的映射.在用户索引中为我们的用户类型获取映射 的示例命令如下所示:
我们的 users
索引结构包含两个字段:name
和 phone
。假设我们索引了一些数据,但一段时间后我们决定要在电话字段上进行搜索,并且我们想更改它的index
属性从 not_analyzed
到 分析
。因为我们已经知道如何更改索引结构,所以我们将运行以下命令:
Elasticsearch 会返回一个指示错误的响应,如下所示:
这是因为我们无法将设置为 not_analyzed
的字段更改为 analyzed
的字段。而不仅仅是 ,在大多数情况下,您将无法更新字段映射。这是一件好事,因为如果我们被允许更改这些设置,我们会混淆 Elasticsearch 和 Lucene。想象一下,我们已经有许多电话字段设置为 not_analyzed
的文档,我们可以将映射更改为分析。 Elasticsearch 不会更改 已编入索引的数据,但分析的查询将使用不同的逻辑进行处理,因此您将无法正确找到您的数据。
但是,为了给您一些禁止和禁止的示例,我们决定提及这两种情况的一些操作。例如,可以安全地进行以下修改:
添加新的类型定义
添加新字段
添加新的分析器
以下修改被禁止或无效:
为一个领域启用规范
更改要存储或不存储的字段
更改字段的类型(例如,从文本到数字)
将存储的字段更改为不存储,反之亦然
更改索引属性的值
更改已索引文档的分析器
请记住,前面提到的允许和不允许更新的示例并未提及更新 API 使用的所有可能性,如果您尝试执行的更新有效,您必须自己尝试。
你刚刚读完的那一章集中在索引操作和处理不平坦或文档之间有关系的数据上。我们从在 Elasticsearch 中索引树状结构和对象开始。我们还使用了嵌套对象并了解了何时可以使用它们。我们还使用了父子功能,并了解了这种方法与嵌套文档相比有何不同。最后,我们通过调用 API 修改了我们的索引结构,并了解了何时可以这样做。
在下一章中,我们将回到查询相关主题。我们将了解 Lucene 评分的工作原理、如何在 Elasticsearch 中使用脚本以及如何处理多语言数据。我们将使用提升来影响评分,我们将使用同义词来改善用户的搜索结果。最后,我们将看看我们可以做些什么来查看我们的文档是如何评分的。