一、Frame Of Reference
搜索引擎一项很重要的工作就是高效的压缩和解压缩一系列整数,这些整数指的就是包含特定词的文档id;每个词term有对应包含该词的doc id列表, term->doc id1,doc id2。。。这种数据即为postings list,这里的doc id是段内文档标识,按照顺序编号,范围0-2^31-1; 针对文档id列表,Lucene采用一种增量编码的方式将一系列id进行压缩存储,即称为Frame Of Reference的压缩方式;
比如一个词对应的文档id列表为[73, 300, 302, 332,343, 372] ,id列表首先要从小到大排好序;第一步增量编码就是从第二个数开始每个数存储与前一个id的差值,即300-73=227,302-300=2。。。,一直到最后一个数;第二步就是将这些差值放到不同的区块,Lucene使用256个区块,下面示例为了方便展示使用了3个区块,即每3个数一组;第三步位压缩,计算每组3个数中最大的那个数需要占用bit位数,比如30、11、29中最大数30最小需要5个bit位存储,这样11、29也用5个bit位存储,这样才占用15个bit,不到2个字节,压缩效果很好,如下面原理图所示:
Frame Of Reference原理图
Frame Of Reference压缩算法对于倒排表来说效果很好,但对于需要存储在内存中的Filter缓存等不太合适,两者之间有很多不同之处:倒排表存储在磁盘,针对每个词都需要进行编码,而Filter等内存缓存只会存储那些经常使用的数据,而且针对Filter数据的缓存就是为了加速处理效率,对压缩算法要求更高;这就产生了下面针对内存缓存数据可以进行高效压缩解压和逻辑运算的roaring bitmaps算法。
二、Roaring bitmaps
缓存主要分三种:Request Cache、Filter/Query Cache、Fielddata Cache。在文档【elasticsearch 搜索模块之cache】中有详细的描述。
其中Filter/Query Cache、Fielddata Cache是Lucene级别缓存支持,Request Cache是elasticsearch在特定条件下协调节点缓存其它节点的分片结果
缓存之前执行结果的目的就是为了加速响应,本质上是对一系列doc id进行合理的压缩存储然后解码并进行与、或、亦或等逻辑运算
选择1:首先一个简单的方案是使用数组存储,这样每个doc id占用4个字节,如果有100M个文档,大约需要400M的内存,比较占用资源,不是一个很好的方式。
选择2:bitmap位图方式,用一个bit位(0或者1)来代表一个doc id的存在与否(JDK也有内置的位图类即BitSet);与数组方式类似,只不过这里只用1个bit代表一个文档,节约了存储(100M bits = 12.5MB),这是一种很好的方式,lucene使用了很长时间,但是有个缺点:对于一个比较稀疏的文档列表就浪费了很多的存储空间,极端情况段内doc id范围0~231-1,一个位图就占用(231-1)/8/1024/1024=256M的空间,有压缩改进的空间
选择3:从Lucene 5 开始采用了一种改进的位图方式,即roaring bitmaps(官网http://roaringbitmap.org/),它是一个压缩性能比bitmap更好的位图实现;
第一步 针对每个文档id,得到其对应的元组,即括号中第一个数为除以65535的值,第二个数时对65535取余的值
第二步 按照除以65536的结果将元组划分到特定的区块中,示例中有0、2、3三个区块
第三步 对于每个区块,如果元素个数大于4096个,采用bit set编码(选择2使用的方式),否则对于区块中每个元素使用2个字节编码(取余之后最大值65535使用2个字节即可表示,选择1方式)
为什么使用4096作为一个阈值,经验证超过4096个数后,使用bitmap方式要比使用数组方式要更高效,具体对比示例见最上面的官方链接,也可获取其中的示例代码。
roaring bitmaps原理图