Elasticsearch(ES)是一个基于 Lucene构建的开源、分布式、 RESTful接口全文搜索引擎。 Elasticsearch还是一个分布式文档数据库,其中每个字段均是被索引的数据且可被搜索,它能够扩展至数以百计的服务器存储以及处理PB级的数据。它可以在很短的时间内存储、搜索和分析大量的数据。
1、Lucene倒排索引
1.1、什么是倒排索引
倒排索引源于实际应用中需要根据属性的值来查找记录。这种索引表中的每一项都包括个属性值和具有该属性值的各记录的地址。由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因而称为倒排索引( inverted index)。带有倒排索引的文件我们称为倒排索引文件,简称倒排文件( inverted file)。
倒排索引中的索引对象是文档或者文档集合中的单词等,用来存储这些单词在一个文档或者一组文档中的存储位置,是对文档或者文档集合的一种最常用的索引机制。
1.2、倒排索引原理
搜索引擎的关键步骤就是建立倒排索引,倒排索引一般表示为一个关键词,然后是它的频度(出现的次数)、位置(出现在哪一篇文章或网页中,及有关的日期,作者等信息),好比一本书的目录、标签一般。读者想看哪一个主题相关的章节,直接根据目录即可找到相关的页面。不必再从书的第一页到最后一页,一页一页地查找。
Lucene使用的是倒排文件索引结构,下面用例子介绍该结构及相应的生成算法。
假设有两篇文章1和文章2。
文章1的内容为: Tom lives in Guangzhou, I live in Guangzhou too.
文章2的内容为: He once lived in Shanghai
1.2.1、取得关键词
由于 Lucene是基于关键词索引和查询的,首先我们要取得这两篇文章的关键词,通常我们需要如下处理措施:
- 我们现在有的是文章内容,即一个字符串,我们先要找出字符串中的所有单词,即分词。英文单词由于用空格分隔,比较好处理。中文单词间由于是连在一起的,所以需要特殊的分词处理。
- 文章中的“in”“once”“to”等词没有什么实际意义,中文中的“的”“是”等字通常也无具体含义,这些不代表概念的词是可以过滤掉的。
- 用户通常希望查“He”时能把含“he”和“HE”的文章也找出来,所以所有单词需要统一大小写。
- 用户通常希望查“live”时能把含“ lives”和“ lived”的文章也找出来,所以需要把“ lives”,“ lived”还原成“live”。
- 文章中的标点符号通常不表示某种概念,也可以过滤掉。
在 Lucene中以上措施由 Analyzer类完成。经过上面处理后,得到如下结果:
文章1的所有关键词为:[tom] [live] [guangzhou] [i] [live] [guangzhou]
文章2的所有关键词为:[he][live][shanghai]
1.2.2、建立倒排索引
有了关键词后,我们就可以建立倒排索引了。上面的对应关系是:“文章号”对“文章中所有关键词”。倒排索引把这个关系倒过来,变成:“关键词”对“拥有该关键词的所有文章号”。
文章1和文章2经过倒排后的对应关系见下表。
倒排索引关键词文章号对应关系示例
关键词 | 文章号 |
guangzhou | 1 |
he | 2 |
i | 1 |
live | 1,2 |
shanghai | 2 |
tom | 1 |
通常仅知道关键词在哪些文章中出现还不够,我们还需要知道关键词在文章中出现的次数和位置,通常有两种位置:
- 字符位置,即记录该词是文章中第几个字符(优点是显示并定位关键词快)。
- 关键词位置,即记录该词是文章中第几个关键词(优点是节约索引空间、词组查询快), Lucene中记录的就是这种位置。
加上“出现频率”和“出现位置”信息后,我们的索引结构参见下表。
倒排索引关键词频率位置示例
关键词 | 文章号 | 出现位置 |
guangzhou | 1[2] | 3,6 |
he | 2[1] | 1 |
i | 1[1] | 4 |
live | 1[2] 2[1] | 2,5 2 |
shanghai | 2[1] | 3 |
tom | 1[1] | 1 |
以live这行为例,我们说明一下该结构:live在文章1中出现了2次,文章2中出现了一次,它的出现位置为“2,5,2”这表示什么呢?我们需要结合文章号和出现频率来分析,文章1中出现了2次,那么“2,5”就表示live在文章1中出现的两个位置,文章2中出现了一次,剩下的“2”就表示live是文章2中的第2个关键字。
以上就是 Lucene索引结构中最核心的部分。我们注意到关键字是按字符顺序排列的(Lucene没有使用B树结构),因此 Lucene可以用二元搜索算法快速定位关键词。
1.2.3、实现
实现时, Lucene将上面三列分别作为词典文件(Term Dictionary)、频率文件( frequencies)、位置文件(positions)保存。其中词典文件不仅保存了每个关键词,还保留了指向频率文件和位置文件的指针,通过指针可以找到该关键字的频率信息和位置信息。
Lucene中使用了field的概念,用于表达信息所在位置(如标题中、文章中、URL中)在建索引中,该field信息也记录在词典文件中,每个关键词都有一个field信息,因为每个关键字一定属于一个或多个field。
1.2.4、压缩算法
为了减小索引文件的大小, Lucene对索引还使用了压缩技术。
首先,对词典文件中的关键词进行了压缩,关键词压缩为<前缀长度,后缀>,例如:当前词为“阿拉伯语”,上一个词为“阿拉伯”,那么“阿拉伯语”压缩为<3,语>。
其次大量用到的是对数字的压缩,数字只保存与上一个值的差值(这样可以减少数字的长度,进而减少保存该数字需要的字节数)。例如当前文章号是16389(不压缩要用3个字节保存),上一文章号是16382,压缩后保存7(只用一个字节)。
1.2.5、应用场景
下面我们可以通过对该索引的查询来解释一下为什么要建立索引。
假设要查询单词“live”, Lucene先对词典二元査找、找到该词,通过指向频率文件的指针读出所有文章号,然后返回结果。词典通常非常小,因而,整个过程的时间是毫秒级的。
而用普通的顺序匹配算法,不建索引,而是对所有文章的内容进行字符串匹配,这个过程将会相当缓慢,当文章数目很大时,时间往往是无法忍受的。
2、Elasticsearch术语及概念
2.1、索引词(term)
在 Elasticsearch中索引词(term)是一个能够被索引的精确值。foo、Foo、FOO几个单词是不同的索引词。索引词(term)是可以通过term查询进行准确的搜索。
2.2、文本(text)
文本是一段普通的非结构化文字。通常,文本会被分析成一个个的索引词,存储在Elasticsearch的索引库中。为了让文本能够进行搜索,文本字段需要事先进行分析;当对文本中的关键词进行查询的时候,搜索引擎应该根据搜索条件搜索出原文本。
2.3、分析(analysis)
分析是将文本转换为索引词的过程,分析的结果依赖于分词器。比如: FOO BAR、Foo-Bar和 foo bar这几个单词有可能会被分析成相同的索引词foo和bar,这些索引词存储在 Elasticsearch的索引库中。当用FoO:bAR进行全文搜索的时候,搜索引擎根据匹配计算也能在索引库中搜索出之前的内容。这就是 Elasticsearch的搜索分析。
2.4、集群(cluster)
集群由一个或多个节点组成,对外提供服务,对外提供索引和搜索功能。在所有节点,一个集群有一个唯一的名称默认为“Elasticsearch”。此名称是很重要的,因为每个节点只能是集群的一部分,当该节点被设置为相同的集群名称时,就会自动加入集群。当需要有多个集群的时候,要确保每个集群的名称不能重复,否则,节点可能会加入错误的集群。请注意,一个节点只能加入一个集群。此外,你还可以拥有多个独立的集群,每个集群都有其不同的集群名称。例如,在开发过程中,你可以建立开发集群库和测试集群库,分别为开发、测试服务。 Elasticsearch集群结构见图1-1。
2.5、节点(node)
一个节点是一个逻辑上独立的服务,它是集群的一部分,可以存储数据,并参与集群的索引和搜索功能。就像集群一样,节点也有唯一的名字,在启动的时候分配。如果你不想要默认名称,你可以定义任何你想要的节点名。这个名字在管理中很重要,在网络中Elasticsearch集群通过节点名称进行管理和通信。一个节点可以被配置加入一个特定的集群。默认情况下,每个节点会加入名为 Elasticsearch的集群中,这意味着如果你在网络上启动多个节点,如果网络畅通,他们能彼此发现并自动加入一个名为 Elasticsearch的集群中。在一个集群中,你可以拥有多个你想要的节点。当网络没有集群运行的时候,只要启动任何一个节点,这个节点会默认生成一个新的集群,这个集群会有一个节点。
2.6、路由(routing)
当存储一个文档的时候,它会存储在唯一的主分片中,具体哪个分片是通过散列值进行选择。默认情况下,这个值是由文档的ID生成。如果文档有一个指定的父文档,则从父文档ID中生成,该值可以在存储文档的时候进行修改。
2.7、分片(shard)
分片是单个 Lucene实例,这是 Elasticsearch管理的比较底层的功能。索引是指向主分片和副本分片的逻辑空间。对于使用,只需要指定分片的数量,其他不需要做过多的事情。在开发使用的过程中,我们对应的对象都是索引, Elasticsearch会自动管理集群中所有的分片,当发生故障的时候, Elasticsearch会把分片移动到不同的节点或者添加新的节点。
一个索引可以存储很大的数据,这些空间可以超过一个节点的物理存储的限制。例如,十亿个文档占用磁盘空间为1TB。仅从单个节点搜索可能会很慢,还有一台物理机器也不定能存储这么多的数据。为了解决这一问题, Elasticsearch将索引分解成多个分片。当你创建一个索引,你可以简单地定义你想要的分片数量。每个分片本身是一个全功能的、独立的单元,可以托管在集群中的任何节点。
2.8、主分片(primary shard)
每个文档都存储在一个分片中,当你存储一个文档的时候,系统会首先存储在主分片中,然后会复制到不同的副本中。默认情况下,一个索引有5个主分片。你可以事先制定分片的数量,当分片一旦建立,则分片的数量不能修改。
2.9、副本分片(replica shard)
每一个分片有零个或多个副本。副本主要是主分片的复制,其中有两个目的
- 增加高可用性:当主分片失败的时候,可以从副本分片中选择一个作为主分片。
- 提高性能:当查询的时候可以到主分片或者副本分片中进行查询。默认情况下,一个主分片配有一个副本,但副本的数量可以在后面动态地配置增加。副本分片必须部署在不同的节点上,不能部署在和主分片相同的节点上。
分片主要有两个很重要的原因是:
- 允许水平分割扩展数据。
- 允许分配和并行操作(可能在多个节点上)从而提高性能和吞吐量。
这些很强大的功能对用户来说是透明的,你不需要做什么操作,系统会自动处理。
2.10、复制(rep|ica)
复制是一个非常有用的功能,不然会有单点问题。当网络中的某个节点出现问题的时候,复制可以对故障进行转移,保证系统的高可用。因此, Elasticsearch允许你创建一个或多个拷贝,你的索引分片就形成了所谓的副本或副本分片。
复制是重要的,主要的原因有:
- 它提供了高可用性,当节点失败的时候不受影响。需要注意的是,一个复制的分片不会存储在同一个节点中。
- 它允许你扩展搜索量,提高并发量,因为搜索可以在所有副本上并行执行。
每个索引可以拆分成多个分片。索引可以复制零个或者多个分片。一旦复制,每个索引就有了主分片和副本分片。分片的数量和副本的数量可以在创建索引时定义。当创建索引后,你可以随时改变副本的数量,但你不能改变分片的数量。
默认情况下,每个索引分配5个分片(在Elasticsearch7中默认分片和副本都是1。)和一个副本,这意味着你的集群节点至少要有两个节点,你将拥有5个主要的分片和5个副本分片共计10个分片。
每个 Elasticsearch分片是一个 Lucene的索引。有文档存储数量限制,你可以在一个单一的Lucene索引中存储的最大值为 lucene-5843,极限是2147483519(=Integer.max_value-128)个文档。你可以使用_cat/shards API监控分片的大小。
2.11、索引(index)
索引是具有相同结构的文档集合。例如,可以有一个客户信息的索引,包括一个产品目录的索引,一个订单数据的索引。在系统上索引的名字全部小写,通过这个名字可以用来执行索引、搜索、更新和删除操作等。在单个集群中,可以定义多个你想要的索引。索引结构参见下图。
2.12、类型(type)
在索引中,可以定义一个或多个类型,类型是索引的逻辑分区。在一般情况下,一种类型被定义为具有一组公共字段的文档。例如,让我们假设你运行一个博客平台,并把所有的数据存储在一个索引中。在这个索引中,你可以定义一种类型为用户数据,一种类型为博客数据,另一种类型为评论数据。
2.13、文档(document)
文档是存储在 Elasticsearch中的一个JSON格式的字符串。它就像在关系数据库中表的一行。每个存储在索引中的一个文档都有一个类型和一个ID,每个文档都是一个JSON对象,存储了零个或者多个字段,或者键值对。原始的JSON文档被存储在一个叫作_source
的字段中。当搜索文档的时候默认返回的就是这个字段。
2.14、映射(mapping)
映射像关系数据库中的表结构,每一个索引都有一个映射,它定义了索引中的每一个字段类型,以及一个索引范围内的设置。一个映射可以事先被定义,或者在第一次存储文档的时候自动识别。
2.15、字段(field)
文档中包含零个或者多个字段,字段可以是一个简单的值(例如字符串、整数、日期),也可以是一个数组或对象的嵌套结构。字段类似于关系数据库中表的列。每个字段都对应个字段类型,例如整数、字符串、对象等。字段还可以指定如何分析该字段的值。
2.16、来源字段(source field)
默认情况下,你的原文档将被存储在_source这个字段中,当你查询的时候也是返回这个字段。这允许你可以从搜索结果中访问原始的对象,这个对象返回一个精确的JSON字符串,这个对象不显示索引分析后的其他任何数据。
2.17、主键(ID)
ID是一个文件的唯一标识,如果在存库的时候没有提供ID,系统会自动生成一个ID,文档的 index/type/id必须是唯一的。