聊聊ElasticSearch的倒排索引

2020-05-29  本文已影响0人  星空怎样

[toc]

为什么需要倒排索引

倒排索引也是索引。索引初衷都是为了快速检索到你要的数据。

每种数据库有自己需要解决的问题(或者说擅长的领域),对应的就有自己的数据结构,而不同的使用场景和数据结构,需要用不同的索引,才能起到最大化加快查询的目的

对MySQL来说,是B+树,对于ES/Lucene来说是倒排索引。

为什么叫倒排索引

在没有搜索引擎时,我们是直接输入网址,然后获取网站内容,这时我们的行文是:document->to->words,通过文章,获取里面的单词,此为倒排索引,forward index。

后来,我们希望能够输入一个单词,找到含有这个单词,或者和单词有关系的文章:word->to->documennts

于是我们把这种索引,称为inverted index,直译过来,应该叫反向索引,国内翻译成倒排索引

倒排索引的内部结构

首先,在数据生成的时候,比如爬虫爬到一篇文章,这里我们需要对这篇文章进行分析,将文本拆解成一个个单词。

这个过程很复杂,比如“生成还是死亡”,要如何让分词器自动将他分解为“生存”,“还是”,“死亡”三个词语,然后把“还是”,这个无意义的词语干掉。接着,把这两个词语已经它对应的文档id存下来:

word document Id
生存 1
死亡 1

接着爬虫继续,又爬到一个含有“生存”的文档,于是索引变成:

word document Id
生存 1,2
死亡 1

下次搜索“生存”,就会返回文档ID是1、2的两份文档。

上面这套索引的实现,只是最简单,想想看,这个世界上那么多单词,,每次搜索一个单词,都要全局遍历一遍,很明显不行。于是有了排序,我们需要对单词进行排序,像B+树一样,可以在页面实现二分查找。光排序还不行,单词都放在磁盘呢,磁盘IO非常慢,所以MySQL特意把索引缓存到了内存,但是ES不行,因为单词太多,所以是下面的结构:

image

ES的倒排索引,增加了最左边的一层字典树term index,他不存存储所有单词,只存储单词的前缀,通过字典树找到单词所在的块,也就是单词大概的位置,再在块里进行二分查找,找到对应的单词,在找到对应的文档列表。

当然内存,能省则省,所以ES还用了FST(Finite State Transducers)对他进一步压缩。

这里重点说一下最右边的Postinng List,别看只是存一个文档ID数组,但是他的设计,遇到的问题可不少。

Frame Of Reference

原生的Posting List有两个痛点:

压缩数据

我们简化一个ES要面对的问题,假设有这样的一个数组:[73, 300, 302, 332, 343, 372],如何把他进行压缩?

Es里,数据是按Segment存储的,每个Segment最多存65536个文档ID,所以文档ID的范围,从0到2^32 - 1,所以如果不进行任何处理,那么每个元素都会占用2 bytes,对应上面的数组就是6*2=12 bytes。

怎么压缩?

压缩,就是尽可能降低每个数据占用的空间,同时又能让信息不失真,能够还原回来

Step 1:Delta-encode 增量编码

我们只需要记录元素与元素之间的增量,于是数组变成了[73,227,2,30,11,29]

Step 2:Split into blockes 分隔成块

ES里面每个块是256个文档ID,这样可以保证每个块,增量编码后,每个元素都不会超过256(1 byte)。

为了方便演示,我们假设每个块是3个文档ID:
[73,227,2],[30,11,29]

Step 3:Bit packing 按需分配空间

对于第一个块,[73,227,2],最大元素是227,需要8 bits,那么给这个块每个元素,都分配8 bits的空间。
但是对于第二个块,[30,11,29],最大的元素才30,只需要5 bits,那么给每个元素只分配5 bits的空间足够。

这一步,可以说是把吝啬发挥到极致。

以上三个步骤,共同组成了一项编码技术,Frame Of Reference(FOR):

image

Roaring bitmaps

现在来说Posting List的第二个痛点,如何快速求交并集(inteersections and unions)

在ES中查询,通常不只有一个查询条件,比如想搜索:

注:即使没有多条件查询,ES也需要频繁求并集,因为ES是分片存储的

我么把ES遇到的问题,简化成一道算法题。

加入有下面三个数组:
[64,300,303,343]

[73,300,302,303,343,372]

[303,311,333,343]
求他们的交集

Integer 数组

直接用原始文档ID,可能会说,那就逐个遍历,遍历完就知道交集是什么了。

其实对于有序数组,用跳表(skip table)可以更搞笑,这里不展开了,因为不管是从性能,还是空间上考虑,Integer数组都不靠谱,假设有100M个文档ID,每个文档ID占2 bytes,那已经是200MB,而这些数据要放到内存中进行处理的,把这么大量的数据,从磁盘解压后丢到内存,内存肯定撑不住。

Bitmap

假设有这样的一个数组:

[3,6,7,10]

我们可以这样来表示:
[0,0,0,1,0,0,1,1,0,0,1]

我们用0表示角标对应的数组不存在,1表示存在
这样两个好处:

Roaring Bitmaps

Bitmap有个硬伤,就是不管有多少个文档,占用的空间都是一样的,之前说过,Posting List的每个Segement最多放65536个文档ID,举一个极端的例子,有一个数组,里面只有两个文档ID:[0,65535],用Bitmap,要怎么表示[1,0,0,0...(超多个0)...,0,0,1],你需要65536个bit,也就是65536/8=8192 bytes,而用Integer数组,只需要2*2 bytes=4 bytes,可见文档数量不多的时候,使用Integer数组更加节省内存。

算一下临界值,很简单文档数量多少,bitmap都需要8192 bytes,而Integer数组则和文档数量成线性相关,每个文档ID占2 bytes,所以:8192/2=4096,当文档数量少于4096时,用Integer数组,否则,用bitmap

image

注:补充一下Roaring bitmaps和之前讲的Frame Of Referrence的关系。Frame Of Referrence是压缩数据,减少磁盘占用空间,所以我们从磁盘取数据时,也需要一个反向过程,即解压,解压后才有我们上面看到的这样子的文档ID数组:[73,300,302,303,343,372],接着我们需要对数据进行处理,求交集或者并集,这时候数据是需要放到内存进行处理的,我们三个这样的数组,这些数组可能很大,而内存空间比磁盘还宝贵,于是需要更强有力的压缩算法,同时还要有利于快速的求交并集,于是有了Roaring bitmaps处理后,缓存到内存中ES称之为filter cache。

总结

每一种数据库都有自己要解决的问题(或者说擅长的领域),对应的就有自己的数据结构,而不同的使用场景和数据结构,需要用不同的索引,才能起到最大化加快查询目的。

上一篇下一篇

猜你喜欢

热点阅读