背景
为了保存倒排索引(docid列表)ES发明了一些有意思的编码方法,包括Frame of Reference 和 Roaring Bitmaps
检索的时候,在做一些过滤操作的时候(与或非),实际上是对倒排索引的一个拉链归并操作。
为了做高效地实现这个功能,ES采用了roaring bitmap的数据结构,参见文章
比如
拉链1:[1,2,3]
&
拉链2:[2,3,4]
&
拉链3:[3,4]
Frame of Reference
为了有效地计算交集和并集,我们需要这些倒排索引是排序的。排好序的数组还可以使用delta-encoding进一步压缩
举个例子,拉链是[73,300,302,332,343,372],那delta list就是[73,227,2,30,11,29]
有以下特点
- 所有delta都是0-255(只需要一个byte)
- 这是Lucene使用的在硬盘上保存倒排索引的方法:将拉链切分成blocks,每个block是256个docID,每个block单独使用上面的方式编码:Lucene计算一个block里要存储这些delta需要的空间,将这个信息加到block header,然后编码所有deltas。
搜索的时候也是用的相同的抽象:queries和filters返回一个排序迭代器,表示他们匹配的文档列表。
注:这儿有个点挺难理解的。到底block是怎么切分的呢?我们来细品一下。
直接看上面这个图。整个算法流程是这样的。
step 1、将排序的整数列表转换成delta列表
step 2、切分成blocks。具体是怎么做的呢?Lucene是规定每个block是256个delta,这里为了简化一下,搞成3个delta。
step 3、看下每个block最大的delta是多少。上图的第一个block,最大的delta是227,最接近的2次幂是256(8bits),于是规定这个block里都用8bits来编码(看绿色的header就是8),第二个block,最大的delta是30,最接近的2次幂是32(5bits),于是规定这个block里都用5bit来编码(看绿色的header就是5)
Roaring bitmaps
在filter cache中,Lucene需要编码一个int列表。这是一个很受欢迎的技术,可以加速频繁使用的过滤器。这是一个简单的cache。xxx。但是这个的约束跟倒排索引是不一样的:
- 因为我们只需要cache那些常用的filter,压缩率就显得没那么重要了(xxx)
- 然而我们需要这些filter是比那些重复执行的filter要快,所以使用好的数据结构很重要
- cached filters是保存在内存的,倒排索引是典型的保存在磁盘的
因为上述原因,对倒排索引和cached filter而言最好的编码技术是不一样的
那么我们在这里应该用啥?很清晰的是最重要的要求是要有一些很快的东西:如果你的cached filter比起重新执行一遍filter还要慢,那就不仅在浪费内存,而且还使得的你的query变得更慢了。编码越复杂,越可能拖慢编解码,因为增加了CPU使用,所以让我们看一下一些我们有的编码排序整数的方法:
选项1:Integer array
最简单的方法:直接保存成array。这样遍历很简单,然而压缩很糟糕。这个编码技术每个entry需要4个bytes,使得稠密的filters变得很消耗内存。如果你有一个segment包含100M的文档,而且有一个filter匹配了大多数文档(这儿有点抽象了,举个例子吧,拉链是“的”这个词对应的倒排索引,100M的文档全匹配上了)那么单纯地在这个segment缓存这一个filter就需要大约400MB的内存。我们还是希望有一些更好的方法来缓存这个稠密的sets
选项2:bitmap
当数字列表很稠密的时候,bitmaps就很好使了。还是刚才这个例子,100M的文档,用bitmap那就是100M/8=12.5MB。
选项3:roaring bitmaps
这家伙想要融合上面两种选项的优势。首先根据数字的高16位把拉链切分成不同的块(block)。
这就意味着,第一个block我们会编码0-65535的值,第二个block是65536-131071,以此类推。
然后在每个block我们分别编码低16位:如果列表数量小于4096,我们会使用选项一(Integer array),否则使用bitmap。
需要注意的是,使用Integer array的时候每个值实际上我们一般需要4个byte。但是在这里我们只需要使用2个byte因为block ID已经说明了高16位是啥
为什么使用4096作为阈值呢?其实看个图就懂
因为每个block的大小是65536(2^16),如果是bitmap的话,需要65536/8 byte=8192 byte(不论实际的文档数有多少都需要这么多)
而如果是用Integer array的话,则真正包含的文档数越多的话,需要的内存越大,是一个线性增长的过程。
那么看看上面的图,我们就可以得出这个阈值就是 8192byte / 2byte = 4096(个)文档。也就是说,当文档数少于4096的时候,用Integer array划算,否则用bitmap划算。
roaring bitmap有很多feature,但是只有两个是在Lucene中使用的:
- 遍历所有匹配文档。如果你在一个cached filter上运行constant_score query就会用到
- 在集合中找到第一个大于等于某个数字的doc id。典型的应用:filter取交集。