读书笔记《elasticsearch-blueprints》构建您自己的电子商务解决方案
在第一章中,我们讨论了如何将文档添加到 Elasticsearch 实例以及如何创建字段并决定分析器。所有这些都是让您入门的步骤。接下来的两章将帮助您熟悉创建电子商务搜索解决方案的过程。电子商务彻底改变了购物方式,这样的交易系统提供的便利非常吸引人们。亚马逊、eBay 和 Flipkart 都是该领域的先驱。 Elasticsearch 为构建此类平台提供了出色的支持,您可以在其中展示您拥有的东西。有许多技术可以将用户体验推得更高,最终可以反映这样一个系统的整体性能。这就是我们本章要处理的内容。看一下本章的内容:
构建文档
查询或过滤器——正确的选择
基于日期范围的过滤器
基于奖品范围的过滤器
基于类别的过滤器/构面
构建示例电子商务文档
数据在 Elasticsearch 的文档中建模。这意味着单个项目,无论它是像公司、国家或产品这样的实体,都必须建模为单个文档。一个文档可以包含与其相关联的任意数量的字段和值。此信息围绕 JSON 格式建模,帮助我们使用数组、对象或简单值来表达数据的行为。
本质上,JSON 支持字符串、数字、对象、数组甚至布尔值等格式。 Elasticsearch 在其之上添加了各种其他格式,例如日期、IP 等。您将在下表中找到此类受支持类型的列表。使用日期类型来存储日期值将帮助我们进行有效的特定于日期的操作,例如日期范围和在其之上的聚合。如果我们使用默认的字符串类型,这是无法做到的。其他自定义类型也是如此,例如 IP、geo_point 等。
类型 |
定义 |
---|---|
细绳 |
文本 |
整数 |
32 位整数 |
长 |
64 位整数 |
漂浮 |
IEEE 浮点数 |
双倍的 |
双精度浮点数 |
布尔值 |
对或错 |
日期 |
日期/时间存储为纪元 |
地理点 |
纬度/经度 |
无效的 |
空值 |
IPv4 |
IP 版本 4 |
有必要让 Elasticsearch 知道特定字段将保存什么类型的数据。我们看到了如何将类型配置传递给 Elasticsearch 实例。除此之外,您还可以使用各种其他配置来微调您的整体搜索性能。我们可能会在适当的时候看到一些配置。但是,学习所有这些配置参数是值得的,并且在您尝试微调搜索性能时会很有用。
想象一下自己处于需要并想要构建购物应用程序的场景中。构建此类应用程序的第一步是为您的产品信息编制索引。在这里,最好围绕单个产品对文档进行建模。因此,单个文档代表与产品相关的所有数据,例如产品名称、描述、制造日期等。
首先,让我们创建索引:
在这里,我们假设 Elasticsearch 实例在本地机器上运行,或者更确切地说,在 localhost 上运行。我们创建一个名为 products
的索引,其中包含一个分片和一个副本。这意味着我们的数据不会跨分片进行分区;相反,单个分片将处理它。这意味着将来不可能跨添加到集群的新机器进行横向扩展。一个副本确保分片的副本也在其他地方维护。
现在,让我们进行映射。
这里,products
是索引,product
是类型:
在这里,我们将 单个产品的信息建模为文档,并创建了各种字段来保存该信息。让我们看看这些字段是什么以及我们如何对待它们:
name
:此字段存储我们产品的名称。即使我们为它提供一个单词,我们也应该能够按此名称进行搜索。因此,如果名称是联想笔记本电脑,即使用户只给出了Lenovo
这个词,这个文档也应该匹配。因此,它必须经过一个称为 analysis 的过程,其中有资格表示此字符串的标记是 已选中。我们稍后会详细讨论这个问题。但是,我们需要了解默认情况下会发生此过程,除非您进行其他配置。description
:此字段包含产品的描述,应与名称字段相同。dateOfManufactoring
:这是该产品的制造日期。在这里,如果我们不将该字段声明为日期,则会假定它是一个字符串。这种方法的问题在于,当我们尝试对该字段进行范围选择时,它不是查看日期值,而是查看其字典值(根据字母或字典顺序计算),这将为我们提供错误的结果。这意味着在字符串类型的情况下,两个日期范围之间的日期搜索不会给出准确的结果。因此,我们需要将该字段声明为日期,并将该字段存储为 Unix 纪元格式。可是等等!日期格式有很多种。 Elasticsearch 将如何理解正确的格式并解析出正确的日期值?为此,您需要提供格式作为格式属性。使用这种格式,解析日期字符串并计算纪元值。此外,所有查询和聚合都通过这个解析的日期值解决并发生,因此我们得到了实际结果。price
:此字段具有数字形式的价格值。productType
:该字段存储产品类型如Laptop
、Tab
、等等,作为一个字符串。但是,此字符串没有中断,因此聚合结果是有意义的。这里需要注意的是,当我们将这个字段设为not_analyzed
时,它在token级别是不可搜索的。这意味着如果产品类型是Large Laptop
,则Laptop
这个词的搜索查询不会给你一个匹配,而是确切的词Large Laptop
单独会给你一个匹配。但是,通过这种方法,聚合可以很好地工作。totalBuy
:这是我们维护的一个字段,用于跟踪为此字段购买的商品数量。imageURL
:我们将这个产品的图像存储在外部图像数据库中,并提供访问它的 URL。由于我们不会对该字段进行搜索或聚合,因此禁用该字段的索引是安全的。这意味着该字段不可搜索,但可检索。
我们已经学习了如何在 Elasticsearch 中索引数据。假设您索引了您希望的信息。如果您想查看索引的概览,可以安装 Elasticsearch-head 作为索引的简单前端。这是索引信息后示例搜索结果的样子:
使用 Elasticsearch 的最大 优势是您可以控制数据的级别。灵活的模式让您可以定义自己的方式来处理您的信息。因此,用户可以有绝对的自由来定义用户的虚拟文档将持有的字段和类型(在我们的例子中,是一个特定的产品)。
搜索的基本思想是缩小您拥有的文档子集。在 Elasticsearch 上下文中,这意味着根据各种条件,您可能希望从一个索引或一组索引中选择一个 文档集。查询和过滤器为您提供便利。
如果您已经阅读了参考指南或其他一些 Elasticsearch 文档,您可能已经注意到同一组操作可能适用于查询和过滤器。那么,即使查询和过滤器给出的操作集几乎相同,它们的区别因素是什么?让我们来了解一下。
在查询中,一个匹配的文档可能比另一个匹配的文档更匹配。在过滤器中,所有匹配的文档都被平等对待。
这意味着有一种方法可以对与查询匹配的文档与另一个文档匹配进行评分或排名。这是通过计算一个分值来完成的,该值告诉您特定文档与查询的匹配程度。如果查询是更好的匹配,则给出更高的分数,如果它是一个较小的匹配,则给出较低的分数。这样,我们可以识别最佳匹配并在分页中使用它。
对于电子商务网站,成功取决于输入流量转化为购买的百分比。客户搜索他/她有兴趣购买的东西,如果我们不能在首页本身向他展示最相关的结果,那么将搜索转换为购买的机会就很小。大多数情况下,没有一个客户会查看第二页或后续页面以获得最佳选择。他们会假设其他页面中的产品不如当前页面重要,并将在那里放弃搜索。因此,我们必须使用查询来使我们的结果顺序与用户更相关。
但是等等,过滤器的优点是什么?让我们来探索一下。
因此,对于 结构化搜索,例如日期范围、数字 范围,以及以此类推,在没有得分的情况下,过滤器是我们的人。必须注意的是,过滤器可用于许多领域。他们是:
查询:过滤器可用于查询。请注意,像查询一样,在查询 DSL(领域特定语言),过滤器没有单独的部分。相反,您需要将 您的过滤器嵌入到
constant_score
查询类型或filtered_query
类型。评分:Elasticsearch 为您提供了一种称为
function_score
查询的查询类型。使用这种查询类型的功能,我们可以使用过滤器并根据过滤器匹配来提高分数。后置过滤器:应用于搜索结果,但不应用于聚合的输入。这意味着即使聚合的范围是它的查询,我们也可以通过添加后过滤器来修改这种行为。后过滤器仅适用于搜索结果或点击,而不适用于聚合输入。
Aggregations:我们还可以在聚合中指定过滤器来过滤桶内的文档。
这里要注意的一个非常有趣的点是过滤器被缓存并独立于上下文使用。这意味着一旦您在查询中使用过滤器并在聚合或后过滤器中重用相同的过滤器,就会命中相同的缓存而不是计算结果。
为了搜索,我们使用了大量的文档,而我们的兴趣只在于该文档集的一个子集。这可以基于一组约束和条件。搜索并不止于此。您可能对获取查询结果的快照视图感兴趣。在我们的例子中,如果用户搜索 dell
,他/她可能会对查看不同的独特产品类型及其文档数量感兴趣。这称为聚合。通过这种方式,我们增强了搜索体验并使其更具探索性。在这里,我们 尝试发现各种查询选项,通过这些选项我们可以表达我们的需求并将其传达给 Elasticsearch。
在我们的搜索应用程序中,我们公开了一个可用于搜索的搜索框。我们抽象出有关搜索哪个字段或我们搜索的字段的优先级是什么的信息。让我们看看最适合这个搜索框的查询类型。
匹配查询是开始查询的理想位置。它可用于搜索许多字段类型,例如 数字、字符串甚至日期。让我们看看如何使用它来提供搜索输入框。假设用户对关键字 laptop
进行了搜索。搜索该关键字的字段名称和描述确实有意义,而对价格或日期字段进行相同操作是没有意义的。
一个单词 laptop
在所有字段中的简单匹配查询如下:
等等,我们不会也在日期和价格字段上使用 _all
搜索吗?我们不打算……在这种情况下。请记住,我们在 include_in_all
作为 false
搜索除名称和描述字段之外的所有字段。这将确保这些字段值不会流向 _all
。
亲爱的,我们能够搜索对我们有意义的字符串字段,并得到简洁的结果。但是,现在,管理层的要求发生了变化。与其以同等优先级对待名称字段和描述字段,我更愿意将名称字段置于描述之上。这意味着对于文档匹配,如果该单词出现在名称字段中,则使该文档与文档更相关,其中有效的匹配仅在字段描述上。让我们看看如何使用匹配查询的变化来实现它。
多字段匹配 查询规定搜索多个字段而不是单个字段。等等,它不止于此。您还可以为每个字段指定优先级或重要性。这有助于我们告诉 Elasticsearch 比其他人更好地对待某些字段匹配:
在这里,我们要求 Elasticsearch 匹配字段名称和描述上的单词 laptop
,但与描述字段上的匹配相比,匹配字段名称的相关性更高。
让我们考虑以下文件:
文件 A:
名称:联想笔记本电脑
说明:这是一款很棒的产品,得到了联想的很高评价
文件 B:
名称:联想包
描述:这些都是很棒的笔记本电脑包,联想评分很高
搜索词 laptop
将在 Document A 而不是 Document B 上产生更好的匹配,这在现实世界的场景中非常有意义。
接下来,当我搜索 Lenovo 的 时,我希望看到与之相关的不同产品类型。这更像是根据结果制作报告,但是由于这使您的数据更易于探索和易于理解,因此可以安全地将其视为增强搜索 能力。
因此,每当我搜索某些内容时,我都希望在我的查询结果中看到以下报告,或者更确切地说,是我在以下信息中的结果汇总:
不同类型的
productType
各种预定义价格范围内的产品数量
每年基于制造日期的文件数量
为此,我们需要构建不同的聚合来捕获这些报告。
我们应该提供聚合的格式如下:
一旦我们使用这个 JSON 触发搜索请求,我们就可以按以下格式检索结果:
考虑到何时使用过滤器和 何时选择对于查询,让我们考虑一些场景,看看 Elasticsearch 如何实现最佳过滤。按日期范围、奖品或部门过滤通常会在电子商务视图的用例中弹出。看下图的左侧:
检查新来者或从库中选择旧经典歌曲可能需要基于日期范围的过滤机制。 Elasticsearch 通过提供日期范围过滤器来提供内置工具来进行过滤。术语过滤器对字符串执行相同的操作,字符串可以是任何东西,例如部门或类别。数字过滤器过滤数字,可用于奖品等。
这个片段 展示了如何在 Elasticsearch 中实现基于日期范围的 过滤:
这些是范围过滤器中采用的参数,您可以使用它们来指定范围偏移量:
参数 |
意义 |
---|---|
|
大于或等于 |
|
比...更棒 |
|
小于或等于 |
|
少于 |
现在,让我们继续,看看如何实现一个 奖品范围过滤器。在一些电子商务网站中。考虑以下电子商务网站的屏幕截图:
在前面的截图中,您可以在左侧看到一个价格范围过滤器——手边标签。通过单击或指定价格范围,将显示该范围内的产品。这不过是一个数字范围过滤器。
数值范围过滤器的实现几乎类似于 Elasticsearch 中的日期范围过滤器。以下代码片段显示了如何实现数值范围过滤器。这是示例;假设您有一个本质上是数字的 age
字段:
在阅读了将要遵循的过滤器的实现之后,您将完全理解该片段。目前,我们刚刚向您展示了如何在 Elasticsearch 中的数字字段中实现过滤器。
上表中显示的相同参数也适用于此处。
通常,我们发现 自己处于需要使用多个条件进行搜索的位置,其中每个条件都可以表示为单独的查询。在本节中,让我们看看如何进行这样的搜索组合。
表达多个搜索条件的一种简单方法是使用 bool
查询。 bool
查询允许您使用其他查询的组合,其中每个条件都可以使用布尔子句放置。有三个布尔子句:
必须
应该
must_not
must
子句建议构成查询必须出现在匹配的文档中。如果查询中没有 must
子句,则可以给出 should
子句的组合。此外,您可以设置应匹配的 should
子句的最小数量。顾名思义,must_not
表示该子句不应与文档匹配。
让我们看看如何在 Elasticsearch 中使用 bool
查询:
在这里,我们使用 bool
查询来选择制造日期在 not 之间的所有笔记本电脑1999 年到 2014 年。然后,我们 提升了黄色或联想的笔记本电脑。
有时,您可能希望为用户提供基于自定义逻辑而非 Elasticsearch 采用的默认评分排序的结果集。假设您想显示一些与您的搜索查询匹配的项目,这些项目根据奖品按升序或降序排序。 Elasticsearch 可以帮助您轻松做到这一点。让我们看看 Elasticsearch 中如何使用各种排序方法。
排序时我们应该记住的一件事是,您排序所基于的特定字段应该加载到内存中。如果您有足够的资源进行分类,那就太好了。排序基本上是在字段级别完成的。 Elasticsearch 支持对数组或多值字段执行排序。
但是,如果排序字段是数组或具有多值,则可以使用以下任何函数将这些值的结果组合到单个文档中,以生成该文档的排序值:
分钟
最大
sum
平均
顾名思义,min
根据执行的排序选择最小值。同样,max
排序基于最大值。如果指定了 sum
模式,则所有值的总和是排序的基础。类似地,avg
取所有值的平均值,并根据平均值进行排序。
看看排序是如何完成的:
Elasticsearch 中还有另一个排序选项,称为 _geo_distance
。它根据与特定位置的距离对结果进行排序。它将帮助您找到最近的点——例如,如果您正在搜索您所在位置附近的餐馆,您可以得到根据每个点的距离排序的结果来自您所在位置的结果。这是一个示例代码:
还有另一种类型的排序可以使用脚本完成。我们可能会寻求您可以排序的外部脚本的帮助。如果 Elasticsearch 提供的默认排序无法满足您的需求,这是一种使用自定义排序技术的方法。关键字 _script
表示使用脚本进行排序。看这个例子:
默认情况下,当使用 sort
时,分数会被忽略。如果你想跟踪分数,你可以使用 track_scores
应该设置为 true
。
让我们想象一下这种情况;首先,用户查询所有笔记本电脑。用户收到 1,000 个结果,并通过 10 个结果的第一页。现在,添加了一些额外的文档,在后台,我们得到了 1,200 个匹配的结果,而不是 1,000 个匹配的结果。因此,当用户打开下一页时,他们会看到与第一页有一些重叠,或者大多数情况下会看到基于分数或聚合的一些不一致的结果。第一页显示的联想笔记本电脑数量为 20
,但在第二页,这一数字增加到 50
。
这是因为在提供第一页和第二页之间添加了额外的文件。
如果要使用滚动 API,则需要在查询字符串中指定滚动参数。此滚动参数指示 Elasticsearch 应保持搜索上下文活动的时间。
看看这个例子:
执行查询时,我们会得到包含 scroll_id
的搜索结果。它可以传递给滚动 API 以获取下一批结果。
如果您对排序顺序不感兴趣,可以将滚动 API 与扫描类型一起使用:
在前面的示例中,您会注意到我们为搜索请求提供了时间单位 1m
(一分钟)。您会想知道这是否就是处理整个 结果集所需的全部内容。当然不。我们 在请求中给出的时间是单独处理上一批结果所需的时间。每次调用滚动 API 都会设置一个新的到期时间。
自动完成 在您输入字段时提供建议。此功能在搜索操作期间非常有用,因为它节省了用户的时间,还使他们能够一次性查看各种其他相关选项。 prefix
查询是实现这一点的一种方法。 prefix
查询匹配具有包含具有相同前缀的术语的字段的文档。
您很自然会想到为什么我们不使用 prefix
查询来解决自动完成问题,或者更确切地说,需要单独的 API 和模块来实现自动完成。这里的主要原因是性能。自动完成的理想数据结构是 有限状态自动机 (FST),这将在下一节中解释,但是我们 并没有在 Lucene 的 FST 中存储令牌。因此,维护并拥有一个用于自动完成的单独模块并维护不同的数据结构来存储与自动完成相关的数据是合理的。需要注意的是,与自动完成相关的数据完全存储在主存储器中。
让我们以 Zomato 之类的网站的场景 为例,它可以帮助我们搜索酒店。作为第一步,需要使用自动完成建议。当用户输入时,自动提示器应该能够完成或提供对最终用户尝试输入的酒店名称的建议。特征 这个特性如下:
下图显示了单词 chick
的自动完成示例:
考虑到这个 要求,让我们看看 我们如何在Elasticsearch 中实现它。
Elasticsearch 为您提供了开箱即用的自动完成功能。请记住,此功能未在 Lucene 索引上实现,因此,不能对其应用任何查询或过滤器。因此,我们可以使用 Elasticsearch 进行上下文无关的自动建议。仅出于这个目的,Elasticsearch 就有一个与之关联的完成类型。完成类型接受多个输入并将它们映射到单个输出。这更像是告诉建议者“嘿,如果用户开始输入这些词中的任何一个 [chicken, chili, tandoori],则向他显示输出建议为 Chicken chili tandoori。”
现在,让我们看看这是如何完成的。首先,您需要在映射中指定我们正在处理的自动完成字段。因此,该类型的映射如下所示:
在这里,首先我们定义了一个 name
字段,它将保存餐厅的实际名称,然后是 suggest
字段。我们选择了名为 restaurantSuggest
的 suggest
字段。 suggest
字段接受 n 个输入字符串和单个输出字符串。这是将多个字符串映射到单个输出字符串。我们还可以指定一个负载,它通常引用来自 的原始文档,其中产生了这个建议。添加此字段不是强制性的。但是,请记住,自动建议不会提供太多来自其来源的上下文信息,在有效负载中添加文档 ID 或其他内容可能是个好主意。每个被索引的文档都有一个唯一的 ID,称为文档 ID。我们应该提供这个,否则 Elasticsearch 会自动为其分配一个唯一的 ID)。在我们的例子中,我们将在有效负载中包含文档 ID,以便我们可以返回并选择原始文档,以防用户选择特定的自动完成建议:
在这里,您可以看到,我们索引了三个关于餐馆的文档。现在,让我们看看如何向用户请求未完成文本输入的建议:
在这里,我们要求 ES 根据 restaurantSuggest
字段自动完成文本 roy
。结果如下:
我们收到了两个结果以及与之关联的有效负载,即生成它的文档的 ID。请注意,此操作的速度比 普通查询请求快得多。这个是因为我们刚才使用的FST存储结构。