Elasticsearch

全文检索的复杂性

为了理解为什么全文搜索是一个很难解决的问题,让我们想一个例子。 假设你正在托管一个博客发布网站,其中包含数亿甚至数十亿的博客文章,每个博客文章包含数百个单词,类似于 CSDN。

执行全文搜索意味着任何用户都可以搜索 “java” 或 “学习编程” 之类的内容,并且你需要在几毫秒内找出出现这些单词的所有博客文章。 不仅如此,你还需要根据多种因素对这些博客文章进行评分,例如,这些单词在这些帖子中出现的频率,或者每个帖子有多少拍手或评论,或者你可能想在顶部显示最近写的帖子,或者你可能想突出显示某些顶级内容创建者,或者你可能想将这些单词出现在标题中的帖子放在更高的位置,等等。

另外,你知道用户可能会意外地犯错,因此你需要处理这个问题。 你还需要考虑单词的顺序,“学习 Java” 应该与 “Java 学习” 具有相似的含义,但有时顺序会更重要,例如 “二氧化碳” 可能与 “化碳二氧” 有很大不同 ”(这只是一个例子,我不知道这是不是一个词,我不懂化学)。

仅仅匹配单词也是行不通的。 有些词比其他词为帖子提供了更多的上下文。 例如,当用户搜索 “Java” 时,标题为 “学习 Java” 的博客文章是相关结果,但当用户仅搜索 “学习” 时,相关性就不那么高了。 当用户搜索 “编程” 时,这也是一篇相关的博客文章,即使该词从未出现在博客文章中!

这些挑战极其复杂,乍一看,它们似乎几乎无法搜索,但你打开一个订餐应用程序,在数千家餐厅的数万种菜肴中进行搜索,或者搜索执行特定工作的人员 每天在 Linkedin 上数亿用户中发挥作用,或在数十亿博客文章中搜索特定主题。

Elasticsearch 是一个旨在解决这个问题的数据库。 让我们看看它是如何工作的。

理解术语

在开始使用 Elasticsearch 之前,我们应该先熟悉一下术语。 为了更好地理解事情,让我们举个例子。 假设你在 Elasticsearch 上存储博客文章。

Nodes

节点只是单独的 Elasticsearch 进程。

通常,你会在每台机器上运行一个 Elasticsearch 进程,因此更容易将它们视为单独的服务器。 这些进程中的每一个都独立于其他进程运行,并且仅通过公共网络连接。 Elasticsearch 通常作为大型分布式系统运行,这意味着你通常会运行多台机器(或节点)。

一旦所有这些节点一起运行,它们就可以形成一个“集群 (cluster)”。 集群不仅仅是各个部分的总和; 它不仅仅是一定数量的孤立运行的节点。 相反,节点知道它们是集群的一部分,并在执行不同操作时相互通信。 在某种程度上,Elasticsearch 集群是一个全新的实体。

Elasticsearch 集群有大量的职责,例如存储文档、搜索这些文档、执行不同的分析和聚合任务、备份数据等。它还必须进行自我管理,例如确保哪些节点是健康的,哪些节点是健康的 因此,在任何大型集群中,为不同的操作域提供不同的节点非常重要。

虽然可能存在许多这样的区别,但其中一个明显的区别是存储数据并执行繁重的数据密集型任务的节点,例如搜索和拥有管理集群的专用节点、确保节点健康、决定将哪个文档发送到哪个节点等。创建这种区别很重要,因为这些节点甚至可能需要不同的硬件资源。 数据节点可能需要更大的机器,具有更高性能的网络和磁盘以及大量内存,而执行更多管理任务的节点可能有完全不同的要求。

存储数据和搜索的节点可以是 “数据 (data)” 节点,执行更多管理任务的节点可以称为 “主 (master)” 节点。

有关节点的更多描述,请阅读文章 “Elasticsearch 中的一些重要概念: cluster, node, index, document, shards 及 replica”。事实上,除了 data 及 master 节点之外,Elasticsearch 还有其它类型的节点。

索引和文档

文档是你存储在 Elasticsearch 中的简单 JSON 对象。 它们与关系数据库中的行或 MongoDB 中的单个文档同义。

对于我们的示例,单个文档可能如下所示 –

{"_id": "9a91473c-522e-4174-bf7f-f55293b8e526","post_title": "Learning about Elasticsearch","author_name": "Zhang san",.....}

索引是相似文档的集合。 它们与关系数据库中的表(其中每一行都是单个项目)和 MongoDB 中的集合同义。

因此,对于我们的示例,我们将有一个存储博客文章的索引。 我们称之为 blog_posts。 如果我们想存储一些其他数据,比如说用户,我们可以创建另一个索引,用户。 blog_posts 索引存储各种博客文章文档,每个文档都包含与博客文章相关的字段,而 users 索引存储包含 user_name、email 等字段的用户文档。

Shards – 分片

索引中的文档被分为多个分片。 每个分片存储索引文档的某个子集。 稍后我们会理解为什么将文档划分为多个分片很重要,但现在我们先关注分片的工作原理。

例如,假设我们有一些 blog_posts 文档。

如果我们为此索引创建三个分片(例如分片 A、分片 B、分片 C),那么我们所有的文档都将分为这三个分片。

然后,这些分片将驻留在集群中的不同数据节点中。

这很重要,因为将这些文档分布到多个分片中可以为你带来多种优势,

  1. 搜索可以并行化。 当用户想要执行搜索时,将搜索所有文档。 如果在单个服务器上搜索所有文档,这将非常耗时。 分片允许你将文档分布在多个服务器上,从而允许在不同的硬件上并行执行单个搜索。
  2. 其他查询,例如插入文档(在 Elasticsearch 中称为索引)或通过特定 ID 检索文档,将分布在所有节点之间。

然而,我们的架构仍然不完整。 如果一个节点死亡,它存储的分片(以及这些分片上的数据)将永远丢失。

让我们看看主分片 (primary shard) 和副本分片 (replica shard) 以更好地理解这一点。

主分片、副本分片和不同分片(distinct shards)

只是对到目前为止我们已经介绍过的内容的快速修订:单个分片包含多个文档。 例如,

每个分片都位于一个特定的节点上,

到目前为止,我们架构的一个问题是,如果某个特定节点(假设 10.192.0.3)挂掉或变得不可用,“分片 A”中的数据将永远丢失。 为了解决这个问题,我们引入 “副本分片” 和 “主分片” 的概念。 主分片是我们到目前为止一直在讨论的分片(现在将它们标记为“主(Primary)”),

副本分片 (replica shards) 是仅存储主分片与存储关联的相同文档的分片。 因此,副本分片只是 “复制” 或复制特定的主分片。

在上图中,你可以看到每个主分片都有一个关联的副本分片,并且每个副本分片存储与主分片相同的文档。 在这里,我们每个主分片有一个副本分片,但我们也可以将这个数字修改为更大 —— 每个主分片可以有两个副本。 现在,我们继续每个主分片一个副本。

这些副本分片不需要与主分片位于同一节点上(每个副本位于与其主分片不同的节点上是有意义的)。 主分片和副本分片都分布在集群的所有节点上。

在上图中,每个分片的主分片和副本分片存在于不同的节点上。 单节点故障不会导致数据不可用。 例如,如果节点 10.192.0.3 不可用,则分片 A 和分片 B 的数据都不会丢失。 分片 A 的数据在节点 10.192.0.2 上仍然可用,同样,分片 B 的数据在节点 10.192.0.1 上仍然可用。

这意味着我们的集群可以在单个节点丢失的情况下幸存下来。 然而,我们的集群可能无法在失去两个节点的情况下幸存下来。 例如,10.192.0.3 和10.192.0.2 节点同时丢失将导致分片 A 的文档完全不可用。 我们可以配置更高的复制,例如,每个主分片使用两个副本来缓解这种情况。 但现在,我们继续每个主分片一个副本。

最后,我们来看看 “不同分片(distinct shards)”。 不同分片只是一个术语,用于将相同的主分片和副本分组在一起。 因此,在我们当前的示例中,我们有三个主分片、三个副本分片(每个主分片 1 个副本)、六个总分片(三个主分片 + 三个副本)和三个不同的分片,

将主分片及其相应的副本分片分组为单个 “不同分片” 如此重要的原因将会变得清晰。 重申一下,“不同分片” 只是分片的逻辑分组,并且确实影响了我们到目前为止所绘制的架构。

我们来看几个真实的查询示例

为了结束架构讨论,让我们看看搜索查询和获取查询在我们的示例集群中如何工作。

第一步…

让我们看看执行搜索或获取查询时会发生什么。

这就是我们集群现在的样子,

客户端 API 向这些节点中的任何一个发送搜索或获取查询。 它发送查询的节点成为 “协调器(coordinator)” 节点。 更大的集群甚至可能有专用的协调器节点(专用协调器节点是不具有任何节点角色的节点,它只可以接收客户端的请求),但我们现在不需要这样做。在文章的开始部分,我们可以看到一个更为详细的架构图。

该协调器节点负责接收请求、与其他节点通信(如果需要)、组合从多个节点接收到的结果并返回结果。

搜索

搜索时,搜索查询必须命中所有不同的分片。 这是因为所有分片都使用它们所保存的文档单独在本地执行搜索。

然后,协调器节点将与多个节点通信,以从每个不同的分片获取数据。 回想一下,在我们的示例中,每个主分片有一个副本,因此查询仅命中集群中的一半分片(主分片或副本分片)。

有关详细的如何完成一个请求,请阅读文章 “Elasticsearch:数据是如何被读取的?”。

根据 id 来进行查询

当通过 ID 对特定文档执行查询时,协调器节点已经知道哪个分片将保存该文档,因此无需命中所有节点。 它只是将请求转发到存储数据的节点并将响应发送回客户端。这是因为每当一个文档进来后,根据文档的 id 会自动进行 hash 计算,并存放于计算出来的 shard 实例中,这样的结果可以使得所有的 shard 都比较有均衡的存储,而不至于有的 shard 很忙。

shard_num = hash(_routing) % num_primary_shards

我们可以根据文档的 id 来计算出来是哪一个 shard。

结论

这是对 Elasticsearch 架构的非常简单的介绍。希望大家能对 Elasticsearch 的集群架构有一个比较清楚的认识。更多关于 Elasticsearch 的术语及概念介绍,请详细阅读文章 “Elasticsearch 中的一些重要概念: cluster, node, index, document, shards 及 replica”。