Elasticsearch中的倒排索引和读写操作原理解析

目录

前言

一、倒排索引

二、Elasticsearch中的基本概念

三、写操作原理

四、读操作原理


前言

在实际的生产环境中,起初启用Elasticsearch(下称ES)是为了解决模糊查询的问题。具体业务场景为大量抓取回来的短视频内容、热门微博、公众号文章、小红书笔记、信息流新闻文章等,需要支持用户模糊查找,而随着每日新增的内容越来越多,这些信息已经积累到单个媒体数千万近亿的数量,因此依靠MySQL的模糊查询是无法满足性能上的要求,考虑引入对应的搜索引擎来解决,于是就将数据的特定字段迁移至ES以支持快速高效的模糊查询,并将查询得到的ID取回MySQL匹配再将详细内容返回。

 

一、倒排索引

那么,为什么Elasticsearch能够支持高效的模糊查询?

事实上,为了支持模糊查询,用户输入关键词之后,需要快速定位到这些词对应的词条,思路与MySQL的LIKE一样,但是MySQL没有实现对应的方案以支持快速定位。而ES在这块上略有不同,利用倒排索引(Inverted Index)可以直接获取到文档的ID。

那么,为什么Elasticsearch是采用的倒排索引?

按照一般的索引检索信息的方式,比如原始的数据源假设都是以文档的形式被分开,文档1拥有一段内容,文档2也富含一段内容,文档3同样如此。然后给定一个关键词,要搜索出与此关键词相关的文档,自然而然我们联想到的办法就是一个个文档的内容去比较,判断是否含有此关键词,如果含有则返回这个文档的索引地址,如果不是接着用后面的文档去比,这就有点类似于字符串的匹配类似。 很显然,当数据量非常巨大的时候,这种方式并不适用。原来的这种方式可以理解为是索引-->关键词,而倒排索引的形式则是关键词--->索引位置,也就是说,给出一个关键词信息,我能立马根据倒排索引的信息得出他的位置。

事实上,每一个文档都会对应一个ID,而倒排索引会按照指定语法对每一个文档进行分词,然后维护一张表,列举所有文档中出现的terms以及它们出现的文档ID和出现频率。搜索时同样会对关键词进行同样的分词分析,然后查表得到结果。

如下图所示:

Term(单词):一段文本经过分析器分析以后就会输出一串单词,这一个一个的就叫做Term

Term Dictionary(单词字典):顾名思义,它里面维护的是Term,可以理解为Term的集合

Term Index(单词索引):为了更快的找到某个单词,我们为单词建立索引

Posting List(倒排列表):倒排列表记录了出现过某个单词的所有文档的文档列表及单词在该文档中出现的位置信息,每条记录称为一个倒排项(Posting)。根据倒排列表,即可获知哪些文档包含某个单词。(PS:实际的倒排列表中并不只是存了文档ID这么简单,还有一些其它的信息,比如:词频(Term出现的次数)、偏移量(offset)等,可以想象成是Python中的元组,或者Java中的对象)

举个栗子:
比如,有两篇文档内容如下,
D1:我是中国人,中国真好!
D2:我爱中国。
假设现在已经分了词,去了停词什么的(分词的结果不一定对,举个例子说明问题),则两篇文档的关键词如下:
AD1 :【我】【是】 【中国人】【中国】 【真】【好】
AD2 : 【我】【爱】【中国】
则倒排索引结构如下:

比如你要找"中国",首先会在索引里面找,找出含有“中国”的文档,然后对这两个文档分别计算和查询语句的相关度,按结果排序,然后返回呈现给用户。

这里所述倒排索引是针对非结构化的文档构造的,而在ES中存储的文档是基于JSON格式的,因此索引结构会更为复杂。简单来说,ES对于JSON文档中的每一个field都会构建一个对应的倒排索引。

那么,到底什么是倒排索引?
通过文档找词,比如我要在一篇文章中找“南京”,这个叫做正排。而通过词找含有这个词的文档,比如我想在某10篇文档中找哪些文档有“南京”这个词,这个叫做倒排。
倒排索引是一种数据结构,列出每个单词所在的文档和它们在每篇文档中出现的频次,优化的倒排索引一般还要包括单词在出现文档中的位置,这为搜索短语提供了可能。用户按关键词查询时,系统只需要在索引中找到该单词,就可以找到对应的文档。
比如,当用户输入查询关键词“迪奥 口红”时,系统可以通过倒排索引查找到所有包含“迪奥”和“口红”的文档,然后,对两个文档集合取交集得到同时出现“迪奥”和“口红”两个单词的文档,再根据位置信息,确定那些“迪奥”恰好出现在“口红”之前的文档,从而得到最终查询结果。

倒排索引构建的常用算法有两种:BSBI(基于磁盘的外部排序构建索引)算法和SPIMI(内存单遍扫描构建索引)算法,关于两种算法的原理和实现本文先不做阐述。

 

二、Elasticsearch中的基本概念

索引(Index)

ES将数据存储于一个或多个索引中,索引是具有类似特性的文档的集合,是一个逻辑命名空间。类比传统的关系型数据库领域来说,索引相当于SQL中的一个数据库,或者一个数据存储方案(schema)。索引由其名称(必须为全小写字符)进行标识,并通过引用此名称完成文档的创建、搜索、更新及删除操作。一个ES集群中可以按需创建任意数目的索引。

类型(Type)

类型是索引内部的逻辑分区(category/partition),然而其意义完全取决于用户需求。因此,一个索引内部可定义一个或多个类型(type)。一般来说,类型就是为那些拥有相同的域的文档做的预定义。例如,在索引中,可以定义一个用于存储用户数据的类型,一个存储日志数据的类型,以及一个存储评论数据的类型。类比传统的关系型数据库领域来说,类型相当于“表”。

文档(Document)

文档是索引和搜索的原子单位,它是包含了一个或多个域(Field)的容器,基于JSON格式进行表示。文档由一个或多个域组成,每个域拥有一个名字及一个或多个值,有多个值的域通常称为“多值域”。每个文档可以存储不同的域集,但同一类型下的文档至应该有某种程度上的相似之处。

关系型数据库MySQL与ES在结构上的对比参考如下:

 field类比关系数据库里的field,每个field 都有自己的字段类型。

mapping类比关系型数据库中的 schema 概念,mapping 定义了 index 中的 type。mapping 可以显示的定义,也可以在 document 被索引时自动生成,如果有新的 field,Elasticsearch 会自动推测出 field 的type并加到mapping中。

节点(Node)

一个运行中的 Elasticsearch 实例称为一个节点,而集群是由一个或者多个拥有相同cluster.name配置的节点组成, 它们共同承担数据和负载的压力。使用的是多播或单播方式发现 cluster 并加入。

集群(Cluster)

包含一个或多个拥有相同集群名称的node,其中包含一个master node。

ES集群中的节点有三种不同的类型:

主节点:负责管理集群范围内的所有变更,例如增加、删除索引,或者增加、删除节点等。 主节点并不需要涉及到文档级别的变更和搜索等操作。可以通过属性node.master进行设置。

数据节点:存储数据和其对应的倒排索引。默认每一个节点都是数据节点(包括主节点),可以通过node.data属性进行设置。

协调节点:如果node.master和node.data属性均为false,则此节点称为协调节点,用来响应客户请求,均衡每个节点的负载

分片(Shard)

一个索引中的数据保存在多个分片中,相当于水平分表。一个分片便是一个Lucene 的实例,它本身就是一个完整的搜索引擎。我们的文档被存储和索引到分片内,但是应用程序是直接与索引而不是与分片进行交互。

ES实际上就是利用分片来实现分布式。分片是数据的容器,文档保存在分片内,分片又被分配到集群内的各个节点里。 当你的集群规模扩大或者缩小时, ES会自动的在各节点中迁移分片,使得数据仍然均匀分布在集群里。

一个分片可以是主分片或者副本分片。 索引内任意一个文档都归属于一个主分片,所以主分片的数目决定着索引能够保存的最大数据量。一个副本分片只是一个主分片的拷贝。 副本分片作为硬件故障时保护数据不丢失的冗余备份,并为搜索和返回文档等读操作提供服务。

在索引建立的时候就已经确定了主分片数,但是副本分片数可以随时修改。

默认情况下,一个索引会有5个主分片,而其副本可以有任意数量。

主分片和副本分片的状态决定了集群的健康状态。每一个节点上都只会保存主分片或者其对应的一个副本分片,相同的副本分片不会存在于同一个节点中。如果集群中只有一个节点,则副本分片将不会被分配,此时集群健康状态为yellow,存在丢失数据的风险。

如下图所示,有3个节点,3个主分片,1份副本

如下图所示,又增加了一份副本

如下图所示,其中NODE1节点出现了故障

实际上,每一个分片(shard)还会进一步拆分为分段(segment)。这是ES写入文档所采用的机制造成的结果。

 

三、写操作原理

当用户向一个节点提交了一个索引新文档的请求,节点会计算新文档应该加入到哪个分片(shard)中。每个节点都存储有每个分片存储在哪个节点的信息,因此协调节点会将请求发送给对应的节点。注意这个请求会发送给主分片,等主分片完成索引,会并行将请求发送到其所有副本分片,保证每个分片都持有最新数据。

每次写入新文档时,都会先写入内存缓冲区(In-memory buffer中,并将这一操作写入一个translog文件(transaction log)中,此时如果执行搜索操作,这个新文档还不能被索引到。

如下图所示,图中的一个Lucene索引包含了3个段和一个提交点,当新增数据的时候新文档被写入内存缓冲区,同时操作被写入translog

ES会每隔1秒时间(这个时间可以修改)进行一次刷新操作(refresh),此时在这1秒时间内写入内存的新文档都会被写入一个文件系统缓存(filesystem cache中,并构成一个分段(segment)。此时这个segment里的文档可以被搜索到,但是尚未写入硬盘,即如果此时发生断电,则这些文档可能会丢失。

如图所示,在执行刷新后清空内存,新文档被写入文件系统缓存

不断有新的文档写入,则这一过程将不断重复执行。每隔一秒将生成一个新的segment,而translog文件将越来越大。

如下图所示,translog不断加入新文档记录:

直到每隔30分钟或者translog文件变得很大时,则执行一次fsync操作。此时所有在文件系统缓存中的segment将被写入磁盘,而translog将被删除(此后会生成新的translog)。

如下图所示,执行fsyncsegment写入磁盘,清空内存和translog

为什么要引入translog机制?

因为在两次fsync操作之间,存储在内存和文件系统缓存中的文档是不安全的,一旦出现断电这些文档就会丢失。所以ES引入了translog来记录两次fsync之间所有的操作,这样机器从故障中恢复或者重新启动,ES便可以根据translog进行还原。

但是,由于translog本身也是文件,存在于内存当中,如果发生断电一样也会丢失。因此,ES会每隔5秒时间或者在一次写入请求完成后将translog写入磁盘。可以认为一个对文档的操作一旦写入磁盘便是安全的可以复原的,因此只有在当前操作记录被写入磁盘,ES才会将操作成功的结果返回发送此操作请求的客户端。

segment合并

试想一下,由于每一秒就会生成一个新的segment,于是很快将会有大量的segment生成。而对于一个分片进行查询请求,将会轮流查询分片中的所有segment,如此一来将会大大降低搜索的效率。因此,ES会自动启动合并segment的工作机制,将一部分相似大小的segment合并成一个新的大segment。合并的过程实际上是创建了一个新的segment,当新segment被写入磁盘,所有被合并的旧segment被清除。

如下图所示,合并segment

如下图所示,在合并完成后删除旧的segment,而新的segment可供搜索

 

四、读操作原理

查询的过程大体上分为查询(query)和取回(fetch)两个阶段。这个节点的任务是广播查询请求到所有相关分片,并将它们的响应整合成全局排序后的结果集合,这个结果集合会返回给客户端。

查询阶段

当一个节点接收到一个搜索请求,则这个节点就变成了协调节点。如下图所示,查询过程分布式搜索

第一步是广播请求到索引中每一个节点的分片拷贝。 查询请求可以被某个主分片或某个副本分片处理,协调节点将在之后的请求中轮询所有的分片拷贝来分摊负载。

每个分片将会在本地构建一个优先级队列。如果客户端要求返回结果排序中从第from名开始的数量为size的结果集,则每个节点都需要生成一个from+size大小的结果集,因此优先级队列的大小也是from+size。分片仅会返回一个轻量级的结果给协调节点,包含结果集中的每一个文档的ID和进行排序所需要的信息。

协调节点会将所有分片的结果汇总,并进行全局排序,得到最终的查询排序结果。此时查询阶段结束。

取回阶段

查询过程得到的是一个排序结果,标记出哪些文档是符合搜索要求的,此时仍然需要获取这些文档返回客户端。

协调节点会确定实际需要返回的文档,并向含有该文档的分片发送get请求;分片获取文档返回给协调节点;协调节点将结果返回给客户端。如下图所示,分布式搜索的取回阶段

相关性计算

在搜索过程中对文档进行排序,需要对每一个文档进行打分,判别文档与搜索条件的相关程度。在旧版本的ES中默认采用TF/IDF(term frequency/inverse document frequency)算法对文档进行打分。

 

参考文献:

https://blog.csdn.net/BDuck2014/article/details/100084936

https://blog.csdn.net/zkyfcx/article/details/79998197

 

猜你喜欢

转载自blog.csdn.net/weixin_43230682/article/details/107470391