索引压缩

ⅰ亾dé卋堺 提交于 2019-12-08 18:35:06

当待搜索的数据量极为庞大时,数据所对应的索引的数据量也会非常大。就拿最常见的倒排索引来说,特别是当用户查询的关键词是常用词时,这些词所对应的倒排列表可以达到几百兆,而将这样庞大的索引由磁盘读入内存,势必会严重增加检索响应时间,影响用户的搜索体验。为了解决这样的问题,学者们提出了一系列的索引压缩技术。

实际上,我们所要处理的数据类型多如牛毛,根据不同的要求,为这些数据设计的索引更是千变万化,最常见的有倒排索引,复杂一点的还有各种树形索引等等。要想总结出一种万能的索引压缩技术,实在是很难。但是压缩方法的基本原理却是相通的。本文,我将以倒排索引为例,介绍几种简单的索引压缩技术。之所以选择倒排索引,除了它通用性强之外,也是由于其具有普遍性:倒排索引由以下两部分构成:

  • 词典,其实就是由字符串构成的列表;
  • 倒排列表,其实就是由一系列数字;

其他类型索引,不过都是由字符以及数字构成的,所以说从倒排索引的压缩也就能延伸出对于其他索引的压缩方法。

词典压缩

下表是一个典型的倒排索引,由单词;文档频率(DF);倒排列表指针;3个部分组成


其中,词典部分的存储会浪费空间的根本原因在于分配给单词的空间是统一的,也就是说不论你的单词是像”we”这样短的,还是像”confidentiality”这样长的,都必须分配能够容纳最长单词的空间。所以比较直接的压缩方法是将这些单词连续地存储在一个区域中,而倒排列表中只是存储这些单词出现的起始位置。如下图所示:


显然,这种做法使得词典中不存在冗余的空间了,所有的单词相当于被合成为一个整的字符串。当然,当单词数量极大的时候,还可以通过存储单词位置之间的差值来替代真实的单词位置,以降低存储单词位置的空间消耗。比如原本单词位置应该是”1,5,10,13,20…”,可以存储为”1,4,5,3,7…”。

更进一步,我们可以将上述词典压缩技术改进。还是把单词连成一个整体存储,只不过存储前,对单词分组,比如两两一组。只在倒排索引中,为每两个单词存储他们的起始位置,如下:


实际操作时,可以根据单词的长度动态对单词分组。查找倒排索引时,先根据位置信息读取这个单词分组,再进一步读取每个单词。从而实现对单词索引的进一步压缩。这里面存在的问题是:如何区分一个单词分组中的所有单词,一般的做法是在每个单词结尾处标记一个终止符,比如上图中,我用’$’号分割这些单词。

倒排列表压缩

倒排列表一般的内容包括:文档编号、词频、词位置。一个3部分信息。而这3部分信息基本都是(或者说可以转化为)整数。所以对于这部分数据的压缩,基本上是根据以下2个原则进行:

  • 对于递增整数序列,我们一般存储其数值之间的差值,而不是数值本身(这一点在上面词典压缩时已经用到了)。比如倒排列表中,文档编号和词位置一般都是递增的整数序列。

  • 对于整数,采用合适的压缩算法,编码数据。

第一点不再赘述,我在这里主要谈一下第二点。首先介绍一下在压缩算法中常用的编码技术,一般是两种:一元编码和二进制编码,这两类编码是压缩算法的基础构件,因为本文中我们涉及的压缩算法,无论其内部工作原理如何,都最终将数字表示成这两类编码的混合。

  • 一元编码(Unary Code):一般是对于大于0的整数使用。对于整数X>1,编码结果由X1个二进制数字1和最末尾的二进制数字0构成。例如对于3,编码为110;对于5,编码为11110。显然这种编码方式适用于小整数,对于大整数,实在是相当不经济。

  • 二进制编码(Binary Code):这个大家很熟悉了,就是将整数转化为二进制字符串。

了解这两种编码之后,可以看一些经典的压缩算法了。由于压缩算法实在很多,其基本思想又都比较相似。所以我只介绍下面两类。

1. Elias-γ算法 和 Elias-δ算法

Elias提出了两种压缩算法,通过分解函数将待压缩的数字分解成2个因子,之后分别利用一元编码和二进制编码表示这2个因子。

(1) Elias-γ算法

Elias-γ压缩算法的分解函数为:

x=2e+d

其中,x为待压缩数字,e,d为分解因子。分解之后,对于数字e+1用一元编码表示,而对于数字d采用比特宽度为e的二进制编码表示。比如数字9,分解为 9=23+1,那么e+1=4,一元编码后为1110d=1的宽度为3的二进制编码为001,两个编码之间,以:分隔,最后得到数字9的Elias-γ压缩结果为1110:001

(2) Elias-δ算法

Elias-δ算法与Elias-γ相似,可以看做是Elias-γ进一步的延伸。第一步,对于待压缩整数按照与Elias-γ相同的分解函数分解成e,d两个因子,对于因子d同样采用长度为e的二进制编码表示。不同之处在于,对于e+1采用Elias-γ算法压缩,将压缩后的编码形式与d的二进制编码再用:结合起来。比如,还是以数字9为例,对于e+1=4通过Elias-γ压缩后得到110:00d=1宽度为3的二进制编码为001,结合起来,得到最终结果110:00:001

根据这两种算法的计算过程,其实不用通过论证我们也能看出Elias-δ算法比Elias-γ算法在对于大整数的压缩上更具优势。

2. Golomb算法 和 Rice算法

Golomb和Rice算法在原理上与Elias提出的两个算法一致,都是通过分解函数将大整数分解成2个因子,再对这两个因子编码,区别在于分解函数的不同。对于待压缩整数x,Golomb和Rice算法的分解函数如下:

f1=(x1)/bf2=(x1)modb

其中b为一个小于x的整数。f1,f2为分解后的因子。说白了,就是(x1)b的商和余数。经过这样的分解之后,对f1+1进行一元编码,对f2进行二进制编码。需要注意的是此处对于f2的宽度控制:因为f2的取值一定在 0~b-1 之间,所以,宽度为log(b)的二进制编码足够表示f2。下面具体看一下这两个算法的细节。

(1) Golomb算法

  1. 参数b的选择:Golomb和Rice算法的不同在于对参数b的选择。Golomb算法对于b的选择为:b=ln(2)×Avg,其中Avg为待压缩数值序列的平均数,也就是说Golomb和Rice算法不仅仅考虑压缩数值本身,而是考虑整个待压缩的数值序列;ln(2)是一个经验参数(一般取0.69),当然计算得到的b应该取整数。举个例子,一个待压缩序列{14,144,113,182}平均值为113(取整之后),此时,计算得到b=0.69×113=77.

  2. 二进制编码长度的选择:这是Golomb算法中1相对复杂的部分了。因为我们要对f20,1,,b1进行二进制编码,所以f2的编码长度一定不会超过log(b). 因此,Golomb算法按如下规则编码f2

    • If f2<2log(b)1,设定f2的编码长度为log(b),不足位补0;

    • If f22log(b)1,设定f2的编码长度为log(b),其中,第一位置为1,其他位正常二进制编码成log(b)长;

  3. 压缩实例:还是上面的待压缩序列{14,144,113,182},其平均值为113,b=77,根据分解函数,得到:

f1=(141)/77=0f2=(141)mod77=14

根据b=77,可以得到对于f2的二进制编码长度为6或7.

f1+1=0+1=1进行一元编码,得到0;对f2=14进行二进制编码,因为f2<261=32,所以f2=14的编码长度为6,编码为001110。最后压缩得到的14的编码结果为0:001110.

同理,编码这个序列中的144的步骤如下:

f1=(1441)/77=1f2=(1441)mod77=66

因为f2261=32,所以f2=66的编码长度为7,编码为1100010。综上,对144压缩的结果为10:1100010

注:此处,对Golomb算法的介绍,我参考了张俊林《这就是搜索引擎》,以及Ricardo《Modern Information Retrieval》,他们两者对Golomb算法的具体细节方面有些出入,我以后者为准。其实具体细节没有必要太过于纠结,重点还是在于理解这种通过分解后编码压缩数据的思路。

(2) Rice算法

Rice算法与Golomb算法操作基本一致,唯一一点不同在于对b值的选取。Rice算法中,对参数b的选取原则如下:

  • b为2的整数次幂

  • b为小于Avg的数中最大的

还是上面Golomb算法算法中的例子,此时选取b=64=26。之所以要这样取值,原因在于可以在具体运算中采用掩码操作或者比特位操作等快速运算,从而提高计算效率。

Golomb算法(Rice算法)的缺点在于无法通过对文档的一次遍历就获得压缩之后的索引。因为我们在压缩前必须提前知道待压缩序列的平均值。而其优点也是很明显的,动态处理编码长度的方式使得压缩效果更好。

压缩算法评估

最后,看看对压缩算法的优劣我们根据什么样的指标评估。一般来讲,有以下3方面:

  • 压缩率:压缩前大小与压缩后大小的比例关系,这个意义很明显。

  • 压缩速度:压缩算法的运行时间。这个指标其实不算很重要了,原因很明显,因为压缩索引是一次性的预计算,它不影响对用户查询的即时响应。即使时间较长,也不要紧。

  • 解压速度:将压缩后的数据恢复成原始数据所消耗的时间。这是3个指标中最重要的。因为在实际查询发生时,需要从磁盘将压缩数据读入内存,解压,搜索,最后返回结果,它直接关乎查询响应时间。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!