「Elasticsearch + Lucene」搜索引擎的架构、倒排索引和搜索过程

2020-09-10 10:22:05 浏览数 (1)

从一个浪漫的故事开始

许多年前,一个名叫Shay Banon的开发者,带着新婚妻子去伦敦生活,在得知妻子想从事厨师工作后,准备利用自己所学为妻子开发一个食谱搜索引擎,他开始使用Lucene的一个早期版本。但是尝试之后,他发现直接使用Lucene给没有任何开发经验的妻子而言是非常困难的,因此Shay 开始对Lucene进行封装。不久他发布了他的第一个基于Lucene的用java编写的开源项目 Compass。后来Shay找到了一份跟高性能和分布式有关的工作,然后发现这份工作对实时、分布式搜索引擎的需求尤为突出,于是他决定重写Compass,把它变为一个独立的服务并取名Elasticsearch,再到后来Elasticsearch发布了第一个公开版本,从此以后,Elasticsearch已经成为了 Github 上最活跃的开源项目之一。据说,Shay的妻子还在等着她的食谱搜索引擎,而他已经在大公司忙的“一发不可收拾”…

浪漫的故事开启了技术的起飞 。。。

那有人会问这个创始人Shay为什么使用的是Apache Lucene而不是再自己开发一个全文搜索库。对于这个问题,猜想是因为Lucene比较成熟,高性能,可扩展,轻量级以及强大的功能。Lucene内核可以创建为单个Java库文件,并且不依赖第三方代码,用户可以使用它提供的各种所见即所得的全文检索功能进行索引和搜索操作。当然,Lucene还有很多扩展,它们提供了各种各样的功能,例如多语言处理、拼写检查、高亮显示等。如果不需要这些额外的特性,可以下载单个的Lucene core库文件,直接在应用程序中使用它

Apache Lucene的架构与索引和搜索过程

Lucene 架构

Lucene 组件

被索引的文档用Document对象表示IndexWriter通过函数addDocument将文档添加到索引中,实现创建索引的过程Lucene的索引是反向索引当用户查询请求时,Query代表用户查询语句IndexSearcher通过函数search搜索Lucene IndexIndexSearcher计算Term Weight和Score并且将结果返回给用户返回给用户的文档集合用TopDocsCollector表示索引创建过程如下

创建一个IndexWriter用来写索引文件,它有几个参数,INDEX_DIR就是索引文件存放的位置,Analyzer便是用来对文档进行分析和语言处理的分词器。创建一个Document代表我们要索引的文档。将不同的Field加入到文档中。我们知道,一篇文档有多种信息,如题目、作者、内容、修改时间等。不同类型的信息用不同的Field来表示,在本例中,一共有两类信息进行了索引,一个是文件路径path,一个是文件内容contents。其中FileReader的SRC_FILE就表示要索引的源文件。IndexWriter调用函数addDocument将索引写入到索引文件夹中

搜索过程如下:

IndexReader将磁盘上的索引信息读入到内存,INDEX_DIR就是索引文件存放的位置。创建IndexSearch准备进行搜索。创建Analyer用来对查询语句进行词法分析和语言处理。创建QueryParser用来对查询语句进行语法分析。QueryParser调用parser进行语法分析,形成查询语法树,放到Query中。IndexSearcher调用search对查询语法树Query进行搜索,得到结果TopScoreDocCollector

Elasticsearch架构图与介绍

  • Gateway代表ElasticSearch索引的持久化存储方式。在Gateway中,ElasticSearch默认先把索引存储在内存中,然后当内存满的时候,再持久化到Gateway里。当ES集群关闭或重启的时候,它就会从Gateway里去读取索引数据。比如LocalFileSystem和HDFS、AS3等。
  • DistributedLucene Directory,它是Lucene里的一些列索引文件组成的目录。它负责管理这些索引文件。包括数据的读取、写入,以及索引的添加和合并等。
  • River,代表是数据源。是以插件的形式存在于ElasticSearch中。
  • Mapping,映射的意思,非常类似于静态语言中的数据类型。比如我们声明一个int类型的变量,那以后这个变量只能存储int类型的数据。比如我们声明一个double类型的mapping字段,则只能存储double类型的数据。Mapping不仅是告诉ElasticSearch,哪个字段是哪种类型。还能告诉ElasticSearch如何来索引数据,以及数据是否被索引到等。
  • Index Module,Elasticsearch里的索引概念是名词而不是动词,在elasticsearch里它支持多个索引。优点类似于关系数据库里面每一个服务器可以支持多个数据库是一个道理,在每一索引下面又可以支持多种类型,这又类似于关系数据库里面的一个数据库可以有多张表一样。但是本质上和关系数据库还是有很大的区别,我们这里暂时可以这么理解
  • Search Module,搜索查询模块。
  • Disvcovery,主要是负责集群的master节点发现。比如某个节点突然离开或进来的情况,进行一个分片重新分片等。这里有个发现机制。发现机制默认的实现方式是单播和多播的形式,即Zen,同时也支持点对点的实现。另外一种是以插件的形式,即EC2。
  • Scripting,即脚本语言。包括很多,这里不多赘述。如mvel、js、python等。
  • Transport,代表ElasticSearch内部节点,代表跟集群的客户端交互。包括Thrift、Memcached、Http等协议 RESTful Style API,通过RESTful方式来实现API编程。
  • 3rd plugins,代表第三方插件。
  • Java(Netty),是开发框架。
  • JMX,是监控。

Elasticsearch核心概念

索引 Index

ES中的索引类似关系型数据库中的数据库,里面存放用户文档数据。因为ES是封装的Lucene,所以底层还是有Lucene的一个或者多个索引组成,数据的增删改查也是有底层的Lucene完成,ES中的分片或副本实际上就是一个Lucene索引。

文档 Document

文档是ES中存储数据的主体,ES中所有的操作都是建立在文档的基础上的,每个文档都是由各种Field组成,每个Field有一个名称和一个或多个值构成。文档展示给用户就是一个JSON对象。

类型 Type

ES中Type是一种逻辑上的概念,类似关系型数据库中的表,每个文档都属于某一种类型,如果没有定义,会有默认值,这里的类型相当于数据库当中的表,ES的每个索引可以包含多种类型。

映射 Mapping

映射类似关系型数据库中的schema,用于定义field的属性,如字段类型,是否分词等。这里有一点和关系型数据库不同的是ES会在用户没有定义字段属性的情况下,自动嗅探该字段的类型进行自动识别。

集群 Cluster

多个ES节点工作在一起组成一个集群。ES对于集群的支持几乎是无缝的,这也是ES重要的竞争优点之一。

节点 Node

一个ES实例就是一个节点,说的再简单点,一台部署了ES服务的机器就是一个节点,同时,一台机器如果硬件资源允许也可以同时启动多个ES实例。ES中每个节点都和集群(如果是多个节点的集群)中的其他节点相互通信,了解所有文档的存储位置并能转发用户的请求到对应的数据节点上。

分片 Shard

因为ES是分布式架构,类似于HDFS的存储方式,所以数据被打散存储在集群的多个节点上,一个分片实际上就是底层Lucene的一个索引,这里说的分片指的是ES中的主分片(因为还有副本分片一说),分片的方式是ES自动完成,用户可以指定分片的数量,主分片一旦指定就不能修改,因为ES打散数据的方式是和索引创建时指定的主分片数量有关(参考公式:shard = hash(routting) % number_of_primary_shards进行文档分配),后期改变会导致分片中的数据不可搜索。

副本 Replia

副本就是分片的一个拷贝,不仅能提高自身容灾,另外,请求量很大的情况下,副本可以分担主Shard压力,承担查询功能。副本个数还以在创建完索引后灵活调整。

倒排索引

ElasticSearch中一个重要的概念 : 倒排索引(Inverted Index)也叫反向索引,有反向索引必有正向索引。通俗地来讲,正向索引是通过key找value,反向索引则是通过value找key

首先弄懂几个概念,如果类比现代汉语词典的话,那么Term就相当于词语Term Dictionary相当于汉语词典本身Term Index相当于词典的目录索引Posting List相当于词语在字典的页数集合

  • Term(单词):一段文本经过分析器分析以后就会输出一串单词,这一个一个的就叫做Term(直译为:单词)
  • Term Dictionary(单词字典):顾名思义,它里面维护的是Term,可以理解为Term的集合
  • Term Index(单词索引):为了更快的找到某个单词,我们为单词建立索引。B-Tree通过减少磁盘寻道次数来提高查询性能,Elasticsearch也是采用同样的思路,直接通过内存查找term,不读磁盘,但是如果term太多,term dictionary也会很大,放内存不现实,于是有了Term Index,就像字典里的索引页一样,A开头的有哪些term,分别在哪页,可以理解term index是一颗树:
  • Posting List(倒排列表):倒排列表记录了出现过某个单词的所有文档的文档列表及单词在该文档中出现的位置信息,每条记录称为一个倒排项(Posting)。根据倒排列表,即可获知哪些文档包含某个单词。(PS:实际的倒排列表中并不只是存了文档ID这么简单,还有一些其它的信息,比如:词频(Term出现的次数)、偏移量(offset)等,可以想象成是Python中的元组,或者Java中的对象)

关系型数据库

ElasticSearch

数据库

索引

类型

文档

字段

正排索引: 根据文档ID查询单词 倒排索引: 根据单词查询文档ID,返回多个对应的页面.

ElasticSearch的核心就是搜索,而搜索的核心就是倒排索引。

write(写) and create(创建)操作实现原理

1)先写入buffer,在buffer里的时候数据是搜索不到的;同时将数据写入translog日志文件

2)如果buffer快满了,或者到一定时间,就会将buffer数据refresh到一个新的segment file中,但是此时数据不是直接进入segment file的磁盘文件的,而是先进入os cache的。这个过程就是refresh。每隔1秒钟,es将buffer中的数据写入一个新的segment file,每秒钟会产生一个新的磁盘文件,segment file,这个segment file中就存储最近1秒内buffer中写入的数据,但是如果buffer里面此时没有数据,那当然不会执行refresh操作咯,每秒创建换一个空的segment file,如果buffer里面有数据,默认1秒钟执行一次refresh操作,刷入一个新的segment file中,操作系统里面,磁盘文件其实都有一个东西,叫做os cache,操作系统缓存,就是说数据写入磁盘文件之前,会先进入os cache,先进入操作系统级别的一个内存缓存中去,只要buffer中的数据被refresh操作,刷入os cache中,就代表这个数据就可以被搜索到了为什么叫es是准实时的?

NRT,near real-time,准实时。默认是每隔1秒refresh一次的,所以es是准实时的,因为写入的数据1秒之后才能被看到。可以通过es的restful api或者java api,手动执行一次refresh操作,就是手动将buffer中的数据刷入os cache中,让数据立马就可以被搜索到。只要数据被输入os cache中,buffer就会被清空了,因为不需要保留buffer了,数据在translog里面已经持久化到磁盘去一份了。

3)只要数据进入os cache,此时就可以让这个segment file的数据对外提供搜索了

4)重复1~3步骤,新的数据不断进入buffer和translog,不断将buffer数据写入一个又一个新的segment file中去,每次refresh完buffer清空,translog保留。随着这个过程推进,translog会变得越来越大。当translog达到一定长度的时候,就会触发commit操作。buffer中的数据,倒是好,每隔1秒就被刷到os cache中去,然后这个buffer就被清空了。所以说这个buffer的数据始终是可以保持住不会填满es进程的内存的。每次一条数据写入buffer,同时会写入一条日志到translog日志文件中去,所以这个translog日志文件是不断变大的,当translog日志文件大到一定程度的时候,就会执行commit操作。

5)commit操作发生第一步,就是将buffer中现有数据refresh到os cache中去,清空buffer6)将一个commit point写入磁盘文件,里面标识着这个commit point对应的所有segment file

7)强行将os cache中目前所有的数据都fsync到磁盘文件中去ranslog日志文件的作用是什么?就是在你执行commit操作之前,数据要么是停留在buffer中,要么是停留在os cache中,无论是buffer还是os cache都是内存,一旦这台机器死了,内存中的数据就全丢了。所以需要将数据对应的操作写入一个专门的日志文件,translog日志文件中,一旦此时机器宕机,再次重启的时候,es会自动读取translog日志文件中的数据,恢复到内存buffer和os cache中去。

8)将现有的translog清空,然后再次重启启用一个translog,此时commit操作完成。默认每隔30分钟会自动执行一次commit,但是如果translog过大,也会触发commit。整个commit的过程,叫做flush操作。我们可以手动执行flush操作,就是将所有os cache数据刷到磁盘文件中去。不叫做commit操作,flush操作。es中的flush操作,就对应着commit的全过程。我们也可以通过es api,手动执行flush操作,手动将os cache中的数据fsync强刷到磁盘上去,记录一个commit point,清空translog日志文件。

9)translog其实也是先写入os cache的,默认每隔5秒刷一次到磁盘中去,所以默认情况下,可能有5秒的数据会仅仅停留在buffer或者translog文件的os cache中,如果此时机器挂了,会丢失5秒钟的数据。但是这样性能比较好,最多丢5秒的数据。也可以将translog设置成每次写操作必须是直接fsync到磁盘,但是性能会差很多。

10)buffer每次refresh一次,就会产生一个segment file,所以默认情况下是1秒钟一个segment file,segment file会越来越多,此时会定期执行merge11)每次merge的时候,会将多个segment file合并成一个,同时这里会将标识为deleted的doc给物理删除掉,然后将新的segment file写入磁盘,这里会写一个commit point,标识所有新的segment file,然后打开segment file供搜索使用,同时删除旧的segment file。Update和Delete实现原理删除和更新操作也是写操作。但是,Elasticsearch中的文档是不可变的(immutable),因此不能删除或修改。那么,如何删除/更新文档呢?磁盘上的每个分段(segment)都有一个.del文件与它相关联。当发送删除请求时,该文档未被真正删除,而是在.del文件中标记为已删除。此文档可能仍然能被搜索到,但会从结果中过滤掉。当分段合并时(我们将在后续的帖子中包括段合并),在.del文件中标记为已删除的文档不会被包括在新的合并段中。现在,我们来看看更新是如何工作的。创建新文档时,Elasticsearch将为该文档分配一个版本号。对文档的每次更改都会产生一个新的版本号。当执行更新时,旧版本在.del文件中被标记为已删除,并且新版本在新的分段中编入索引。旧版本可能仍然与搜索查询匹配,但是从结果中将其过滤掉。

Read:查询阶段(Query Phase)下面是详细流程图

简单描述:

查询,GET某一条数据,写入了某个document,这个document会自动给你分配一个全局唯一的id,doc id,同时也是根据doc id进行hash路由到对应的primary shard上面去。也可以手动指定doc id,比如用订单id,用户id。你可以通过doc id来查询,会根据doc id进行hash,判断出来当时把doc id分配到了哪个shard上面去,从那个shard去查询:

1)客户端发送请求到任意一个node,成为coordinate node

2)coordinate node对document进行路由,将请求转发到对应的node,此时会使用round-robin随机轮询算法,在primary shard以及其所有replica中随机选择一个,让读请求负载均衡

3)接收请求的node返回document给coordinate node

4)coordinate node返回document给客户端

es 搜索数据过程(底层倒排索引)

1)客户端发送请求到一个coordinate node

2)协调节点将搜索请求转发到所有的shard对应的primary shard或replica shard也可以

3)query phase:每个shard将自己的搜索结果(其实就是一些doc id),返回给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果

4)fetch phase:接着由协调节点,根据doc id去各个节点上拉取实际的document数据,最终返回给客户端

參考資料:

「1」https://mbd.baidu.com/newspage/data/landingsuper?context={"nid":"news_9176382130971232748"}&n_type=1&p_from=3 「2」https://blog.csdn.net/GoSaint/article/details/89947797 「3」https://blog.csdn.net/moshowgame/article/details/99413448

0 人点赞