Elasticsearch是通过Lucene的倒排索引技术实现比关系型数据库更快的过滤。特别是它对多条件的过滤支持非常好。下面介绍一下为什么es在多条件查询下的性能如此出众,首先要从倒排索引开始介绍,首先看如下数据集合,每一行是一个document。每个document都有一个docid,年龄和性别属于term。
那么给这些document建立的倒排索引如下,每一个term都会有一个倒排索引:
其中[1,3]这种docid顺序排序的列表我们称为Posting list。
那当我们进行查找时,会首先基于二分查找找到对应的term,我们称这个查找树为 term dictionary。由于这个 term dictionary存储了所有的term和posting list,因此保存在磁盘上,即使二分查找加快速度,但是磁盘IO的性能远远不及内存查找,这样就引出了可以常驻在内存中的term index。
比如一本英文字典,term index只保存26个字母开头的章节信息。类似于:
term index这个树可以看成是所有term的部分前缀。通过term index可以快速地定位到term dictionary的某个offset,然后从这个位置再往后顺序查找。再加上一些压缩技术(搜索 Lucene Finite State Transducers) term index 的尺寸可以只有所有term的尺寸的几十分之一,使得用内存缓存整个term index变成可能。整体上来说就是这样的效果。
那接下来是联合查询的处理,给定查询过滤条件 age=18 的过程就是先从term index找到18在term dictionary的大概位置,然后再从term dictionary里精确地找到18这个term,然后得到一个posting list。然后再查询 gender=女 的过程也是类似的。最后得出 age=18 AND gender=女 就是把两个 posting list 做一个“与”的合并。合并有两种算法:
- 使用skip list数据结构。同时遍历gender和age的posting list,互相skip;
- 使用bitset数据结构,对gender和age两个filter分别求出bitset,对两个bitset做AN操作。
如果查询的filter缓存到了内存中(以bitset的形式),那么合并就是两个bitset的AND。如果查询的filter没有缓存,那么就用skip list的方式去遍历两个on disk的posting list。
利用 Skip List 合并
代码如下
public static void main(String args[]) {//初始化两个数组a,b,相当于两个倒排表 int a[]={2,4,8,16,19,23,28,43}; int b[]={1,2,3,5,8,41,51,60,71}; int c[]=new int [20];//数组用于存放相同的位置 int alen=a.length; int blen=b.length; int i=0,j=0,k=0,sk=4;//一次性跳四个 while(i=b[j+sk])//如果跳了sk个b位置仍然小于a位置 j=j+sk;//b跳sk个数字 else j++; } } for(i=0;i
使用bitset数据结构
Bitset是一种很直观的数据结构,对应posting list如:
[1,3,4,7,10]
对应的bitset就是:
[1,0,1,1,0,0,1,0,0,1]
每个文档按照文档id排序对应其中的一个bit。Bitset自身就有压缩的特点,其用一个byte就可以代表8个文档。所以100万个文档只需要12.5万个byte。但是考虑到文档可能有数十亿之多,在内存里保存bitset仍然是很奢侈的事情。而且对于个每一个filter都要消耗一个bitset,比如age=18缓存起来的话是一个bitset,18<=age<25是另外一个filter缓存起来也要一个bitset。
所以秘诀就在于需要有一个数据结构:
- 可以很压缩地保存上亿个bit代表对应的文档是否匹配filter;
- 这个压缩的bitset仍然可以很快地进行AND和 OR的逻辑操作。
Lucene使用的这个数据结构叫做 Roaring Bitmap。
其压缩的思路其实很简单。与其保存100个0,占用100个bit。还不如保存0一次,然后声明这个0重复了100遍。