倒排索引原理

一、 概念

倒排索引(Inverted index) 倒排索引是一种将词项映射到文档的数据结构,这与传统关系型数据库的工作方式不同。你可以把倒排索引当做面向词项的而不是面向文档的数据结构。(倒排索引源于实际应用中需要根据属性的值来查找记录。这种索引表中的每一项都包括一个属性值和具有该属性值的各记录的地址。由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因而称为倒排索引(inverted index))

  • 正排索引:是以文档对象的唯一 ID 作为索引,以文档内容作为记录的结构。
  • 倒排索引:Inverted index,指的是将文档内容中的单词作为索引,将包含该词的文档 ID 作为记录的结构。

二、 倒排索引实例

假设文档集合包含五个文档,每个文档内容如下图所示,在图中最左端一栏是每个文档对应的文档编号。我们的任务就是对这个文档集合建立倒排索引。

(1) 中文和英文等语言不同,单词之间没有明确分隔符号,所以首先要用分词系统将文档自动切分成单词序列。这样每个文档就转换为由单词序列构成的数据流,为了系统后续处理方便,需要对每个不同的单词赋予唯一的单词编号,同时记录下哪些文档包含这个单词,在如此处理结束后,我们可以得到最简单的倒排索引。在下图中,“单词ID”一栏记录了每个单词的单词编号,第二栏是对应的单词,第三栏即每个单词对应的倒排列表。比如单词“谷歌”,其单词编号为1,倒排列表为{1,2,3,4,5},说明文档集合中每个文档都包含了这个单词。

(2) 上图索引系统只记载了哪些文档包含某个单词,而事实上,索引系统还可以记录除此之外的更多信息。下图是一个相对复杂些的倒排索引,与上图的基本索引系统比,在单词对应的倒排列表中不仅记录了文档编号,还记载了单词频率信息(TF),即这个单词在某个文档中的出现次数,之所以要记录这个信息,是因为词频信息在搜索结果排序时,计算查询和文档相似度是很重要的一个计算因子,所以将其记录在倒排列表中,以方便后续排序时进行分值计算。在下图的例子里,单词“创始人”的单词编号为7,对应的倒排列表内容为:(3:1),其中的3代表文档编号为3的文档包含这个单词,数字1代表词频信息,即这个单词在3号文档中只出现过1次,其它单词对应的倒排列表所代表含义与此相同。

(3)实用的倒排索引还可以记载更多的信息,下图所示索引系统除了记录文档编号和单词频率信息外,额外记载了两类信息,即每个单词对应的“文档频率信息”(下图的第三栏)以及在倒排列表中记录单词在某个文档出现的位置信息。

 “文档频率”代表了在文档集合中有多少个文档包含某个单词,之所以要记录这个信息,其原因与单词频率信息一样,这个信息在搜索结果排序计算中是非常重要的一个因子。而单词在某个文档中出现的位置信息并非索引系统一定要记录的,在实际的索引系统里可以包含,也可以选择不包含这个信息,之所以如此,因为这个信息对于搜索系统来说并非必需的,位置信息只有在支持“短语查询”的时候才能够派上用场。

    以单词“拉斯”为例,其单词编号为8,文档频率为2,代表整个文档集合中有两个文档包含这个单词,对应的倒排列表为:{(3;1;<4>),(5;1;<4>)},其含义为在文档3和文档5出现过这个单词,单词频率都为1,单词“拉斯”在两个文档中的出现位置都是4,即文档中第四个单词是“拉斯”。

 什么是倒排索引?

三 、倒排索引数据结构与核心算法

 

倒排索引分为倒排表(posting_list)词项字典(term dictionary)词项索引(term index)三部分组成 ,其中倒排表是一个int类型的有序数组 ,存储了匹配某个term(词汇)的所有文档的id, 词项字典分为三部分组成 tip、tim、doc(各个部分的含义如上图)

四 、倒排索引的核心算法

  • 倒排表的压缩算法
    FOR(Frame Of Reference)
    RBM(RoaringBitmap)
  • 词项索引的检索原理:
    FST(Finit state Transducers)

4.1 FOR(Frame Of Reference)

Frame Of Reference 又叫增量编码压缩, 首先Elasticsearch要求倒排索引是有序的(也就是文档id是有序排列的) 如下图所示

  • 第一步 压缩前 文档id列表为 73 300 302 332 343 372 这样的一个有序的文档id集合
  • 第二步 然后es会先将这些文档id的delta(也就是差值)的值计算出来。227 = 300 - 73 ,2 = 302 - 300 依次类推 这样做的好处是可以缩小int的数值。
  • 第三步 将第二步计算出来的值进行分块, 每一个小块(73 227 2 为一个块 30 11 29 为另一个块) 取最大值 ,如第一个块最大的值是227 而2的8次方为256>227 所以这一个块中的(73 227 2) 每一个数字都可以用8个bit位来存储,另外还需要一个字节来标识 ,每一个数据块是用多少bit位来存储一个数字的(绿色标识的位置 ,这个标志位固定8个bit是为了解压缩的时候能够方便的知道每一个数据块是用多少bit位来存储一个数)
  • 压缩后一共大约是7个字节的样子 而没压缩之前是6*8=24个字节(int占4个字节)

FOR算法的核心思想是用减法来削减数值大小,从而达到降低空间存储。



4.2 RBM压缩算法

FOR算法的核心是用减法来缩减数值大小,但是减法一定能缩减大小吗?但数值大小很大时,减法能够达到的效果是不明显的,比如100W,200W,300W,相减后是100W,100W,100W,依然很大,这时的压缩效果很不理想,所以引入了RBM算法

RBM的核心就是通过除法来缩减数值大小,但是并不是直接的相除。
比如数组为1000,62101,131385,191173,196658
其中196658的二进制表示为0000 0000 0000 0011 0000 0000 0011 0010
然后将其高16位和低16位分别转换为10进制:
0000 0000 0000 0011 -> 3
0000 0000 0011 0010 -> 50
那么196658就转换成了(3,50)的表示形式,其效果就相当于除以2^16,商3余50

这里的计算用位运算会更快更好理解,除以2^n相当于将这个数的二进制向右位移n位(不含符号位),并且用0补足空位。容易得出196658二进制右移16位后为
0000 0000 0000 0000 0000 0000 0000 0011
也就是其高16位,前面用0补足,而被位移顶替掉的就是其余数0000 0000 0011 0010

因为商和余数都不超过16位,那么我们最大用16bit来存储足够了。也就是short类型,占用2个字节。因此商和余数都可以用一个short来盛装,那么所有的商就是一个short[],所有的余数就是一个short[][]将原数组除以2^16得:
(0,1000),(0,62101),(2,313),(2,980),(2,60101),(3,50)
转化为二维数组盛装
0: 1000,62101
2: 313,980,60101
3: 50

我们把每一个商所对应的余数short[]称之为一个容器Container,使用上述所说的short盛装也称为ArrayContainer

我们也容易观察发现到,每一个Container实际上都是有序数值数组,是不是能够联想到什么?
数组还能进行压缩吗?
数组能用FOR算法再压缩吗?
有别的方式再进行压缩吗?

首先回答前两个问题:数组肯定可以压缩,而且正是我们需要去做的;用FOR算法在这里进行压缩是可以的,不算错,但是我们说不合适,正如在FOR算法中介绍的那样,压缩的同时我们还有考虑解码时的效率,其实这里已经经过除法做了一次处理了,那么再用减法做一次处理,再解码时效率会降低不少,所以我们追求的是一种解码更加容器,但又能具备压缩能力的方法。
因此我们就可以使用bitmap来存储数据,按照规定一个Container的最大值是65534(这里为什么最大值是65534,思考一下,如果不明白往上看看原数组是怎么处理的),也就需要65535bit=8k的容器来存储,当然bitmap有个很明显的缺点,那就是无论Container中有多少个数,都要占用8k的大小,所以当数量不超过65535bit /16bit = 4096个时,使用short (16bit)来存储更划算,当每个Container的数量超过4096个时使用bitmap更加划算,那么使用bitmap的Container称为BitmapContainer
在这里插入图片描述

进行一个小小的总结,RBM的算法核心就是把数据表示成2进制共32位,分为高16和低16.分别存储,所以最大就是2的16次方65536。
如果用short存储,需要65536个short数据类型就是65536x2byte再除以1024就是128KB。
如果用BitMapContainer存储,需要的是65536个比特位,当前索引有数据就是1没有就是0,还是用二进制01表示,65536比特位除以8是8192个比特再除以1024就是8KB。
用数组short最大是128KB,bitmap最大是8KB且固定
再算一下,8KB可以存储多少个short类型的数据,8x1024=8192个byte,8192除以2是4096表示可以存储4096个short数组,所以低于4096用short存储比较节省空间,高于4096用bitmap比较好节省空间。

在这里插入图片描述

RBM算法的核心步骤如下:
(1)数组中每个数除以2^16,以商,余数的形式表示出来
(2)将相同商的归在一个Container,如果Contaniner中数值容量超过4096使用bitmap的形式来存储一个Container中的数,如果没有超过那就使用short[]来存储,如果是连续数组那就使用RunContainer来存储
 

其中container分为 ArrayContainerBitmapContainerRunContainer三钟

  • ArrayContainer ArrayContainer采用简单的short数组存储低16位数据,content始终有序且不重复,方便二分查找
    可以看出,ArrayContainer并没有采用任何的压缩算法,只是简单的将低16存储在short[]中 short为2字节,因此n个数据为2n字节
    随着数据量的增大,ArrayContainer占用的内存空间逐渐增多,且由于是二分查找,时间复杂度为O(logn),查找效率也会大打折扣,因此ArrayContainer规定最大数据量是4096,即8kb,
    超过则使用BitmapContainer
  • BitmapContainer BitmapContainer采用long数组存储低16位数据,这就是一个未压缩的普通位图,每一个bit位置代表一个数字。我们上面说过每一个Container最多可以处理65536(2的16次方)个数字,基于位图的原理,我们就需要65536个bit,每个long是8字节64bit,所以需要65536/64=1024个long。BitmapContainer构造方法会初始化一个长度为1024的long数组,因此BitmapContainer无论是存1个数据,10个数据还是最大65536个数据,都始终占据着8kb的内存空间。
  • RunContainer 在RBM创立初期只有以上两种容器,RunContainer其实是在后期加入的,RunContainer主要解决了大量连续数据的问题。举例说明:3,4,5,10,20,21,22,23这样一组数据会被优化成3,2,10,0,20,3,原理很简单,就是记录初始数字以及连续的数量,并把压缩后的数据记录在short数组中显而易见,这种压缩方式对于数据的疏密程度非常敏感,举两个最极端的例子:如果这个Container中所有数据都是连续的,也就是[0,1,2.....65535],压缩后为0,65535,即2个short,4字节。若这个Container中所有数据都是间断的(都是偶数或奇数),也就是[0,2,4,6....65532,65534],压缩后为0,0,2,0.....65534,0,这不仅没有压缩反而膨胀了一倍,65536个short,即128kb 因此是否选择RunContainer是需要判断的,RBM提供了一个转化方法runOptimize()用于对比和其他两种Container的空间大小,若占据优势则会进行转化

4.3 FOR压缩算法与RBM算法的适用场景

For算法是一种增量编码压缩算法 所以它适合处理的是数据分布比较致密的数组(也就是说数组相邻元素之间的差值不能太大 如果值太大则压缩效果不好) 而RBM算法则没有这种要求(redis的bitMap没有内存压缩,偏移量大的时候占内存高,类似于 FOR中的相邻元素差值巨大,适合采用RBM算法优化)。如果在数据量比较大的时候采用bitmapContainer则会极大的减少存储空间

FOR压缩算法:适合紧密的数组

FOR压缩算法:适合稀疏的数组,其中RunContainer适合连续数组

4.4 词项索引的检索原理:FST(Finite State Transducer)

      使用lucene进行查询不可避免都会使用到其提供的字典功能,即根据给定的term找到该term所对应的倒排文档id列表等信息。实际上lucene索引文件后缀名为tim和tip的文件实现的就是lucene的字典功能。

      怎么实现一个字典呢?我们马上想到排序数组,即term字典是一个已经按字母顺序排序好的数组,数组每一项存放着term和对应的倒排文档id列表。每次载入索引的时候只要将term数组载入内存,通过二分查找即可。这种方法查询时间复杂度为Log(N),N指的是term数目,占用的空间大小是O(N*str(term))。排序数组的缺点是消耗内存,即需要完整存储每一个term,当term数目多达上千万时,占用的内存将不可接受。

常用字典数据结构:

数据结构优缺点
排序列表Array/List使用二分法查找,不平衡
HashMap/TreeMap性能高,内存消耗大,几乎是原始数据的三倍
Skip List跳跃表,可快速查找词语,在lucene、redis、Hbase等均有实现。相对于TreeMap等结构,特别适合高并发场景(Skip List介绍
Trie适合英文词典,如果系统中存在大量字符串且这些字符串基本没有公共前缀,则相应的trie树将非常消耗内存(数据结构之trie树
Double Array Trie适合做中文词典,内存占用小,很多分词工具均采用此种算法(深入双数组Trie
Ternary Search Tree三叉树,每一个node有3个节点,兼具省空间和查询快的优点(Ternary Search Tree
Finite State Transducers (FST)一种有限状态转移机,Lucene 4有开源实现,并大量使用

 

 词项索引数据结构为Trie树,即字典树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种(基于FST实现)。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:最大限度地减少无谓的字符串比较。Trie的核心思想是空间换时间,利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。

前缀树的3个基本性质

1、根节点不包含字符,除根节点外每一个节点都只包含一个字符
2、从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串
3、每个节点的所有子节点包含的字符都不相同

 lucene从4开始大量使用的数据结构是FST(Finite State Transducer)。FST有两个优点:

  1. 空间占用小。通过对词典中单词前缀和后缀的重复利用,压缩了存储空间;
  2. 查询速度快。O(len(str))的查询时间复杂度。

 下面简单描述下FST的构造过程(工具演示:)。我们对“cat”、 “deep”、 “do”、 “dog” 、“dogs”这5个单词进行插入构建FST(注:必须已排序)。

1)插入“cat”

     插入cat,每个字母形成一条边,其中t边指向终点。

2)插入“deep”

    与前一个单词“cat”进行最大前缀匹配,发现没有匹配则直接插入,P边指向终点。

3)插入“do”

    与前一个单词“deep”进行最大前缀匹配,发现是d,则在d边后增加新边o,o边指向终点。

4)插入“dog”

与前一个单词“do”进行最大前缀匹配,发现是do,则在o边后增加新边g,g边指向终点。

5)插入“dogs”

     与前一个单词“dog”进行最大前缀匹配,发现是dog,则在g后增加新边s,s边指向终点。

 最终我们得到了如上一个有向无环图。利用该结构可以很方便的进行查询,如给定一个term “dog”,我们可以通过上述结构很方便的查询存不存在,甚至我们在构建过程中可以将单词与某一数字、单词进行关联,从而实现key-value的映射。



作者:liaijuyyer
链接:https://www.jianshu.com/p/2470acd41d7c
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

相关文章:

倒排索引的两种压缩算法:FOR算法和RBM算法

Elasticsearch中的倒排索引结构是什么

RoaringBitmap位图数据结构及源码分析 

lucene字典实现原理

 

  • 10
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值