es(二)-lucene倒排索引

1.概念

elasticsearch使用一种称为倒排索引的数据结构,用于快速的全文搜索。一个倒排索引有文档中所有不重复词的列表构成,每个词都有一个包含它的文档列表。

2.b-tree 和 倒排索引的比较

  • btree是针对写入优化的索引结构

  • luence倒排索引采用预先排序的方式换取更小的存储空、更快的检索速度,但是更新速度慢

3.Term idnex,term dictionary,posting list

img

有一下一组数据:

docIdagegender
118
218
320

Age倒排索引:

181,2
203

gender倒排索引

1,3
2

可以看到,每个term都有一个倒排索引。18、20这些叫做term,1、2叫做posting list。posting list就是一组int数据,里面的值是文档Id。那么什么是term index和term dictionary呢?

假如我们有很多term,比如:

Carla,Sara,Elin,Ada,Patty,Kate,Selena

那么我们找出某一特定term时需要进行遍历,速度很慢。

如果对term进行排序的话:

Ada,Carla,Elin,Kate,Patty,Sara,Selena

就可以通过二分查找更快找到指定term,这个有序的term列表就是term dictionary。查找的时间复杂度为logN,logN次的磁盘读写仍然是非常昂贵的(一次random access大概需要10ms)。所以需要尽量少的读写磁盘,有必要把一些数据缓存到内存里,但是整个term dictionary又非常大,无法完整地放到内存里。于是就有了term index:

term idnex有点像书的章节:

A开头的term *********XX页
B开头的term *********XX页
C开头的term *********XX页

但是term的开头不只是26个英文字母,而且数据分布上也不是均匀的,term index的数据结构实际上是tier树:

img

这棵树不会包含所有的term,它包含的是term的一些前缀。通过term index可以快速地定位到term dictionary中的某个offset,然后从这个位置再往后顺序查找。**再加上一些压缩技术(lucene finite state transducer),使得使用内存缓存整个term index变成可能。 ** 整体效果:

img

由此可以得出为什么es检索比mysql快:mysql只有term dictionary这一层,是以btree的方式存储的。检索一个term需要若干次的random access的磁盘操作。而lucene在term dictionary的基础上添加了term index来加快检索,term index以树的形式缓存在内存中。从term index找到term dictionary所在block后,再去磁盘查找term,大大减少了磁盘的random access次数。

另外,term index是以FST(finite state transducers)的形式保存的,其特点是非常节省内存。term dictionary在磁盘上是以分block的方式保存的,一个block内部利用公共前缀压缩,比如都是AB开头的单词就可以把ab去掉,这样term dictionary就可以比btree更节省空间。

4.如何使用联合索引

给定的查询条件是:age=18时,首先会从term index中找到age=18在term dictionary的大概位置,然后从term dictionary里精确查找到这个18的term,然后得到一个指向posting list位置的指针。然后再查gender=女的过程也是类似的。最后得出一个满足age=18和gender=女的结果就是汇总两个posting list。

对于mysql来说,如果在age和gender上都建立索引,那么在联合查询时只会选择最selective的索引来使用,另外的条件会在之后对内存进行遍历时使用。那么如何才能联合使用两个索引呢?有两种方法:

  • 使用skip list数据结构,同时遍历age和gender的posting list,互相skip
  • 使用bitset数据结构,对gender和age两个filter分别求出bitset,对两个bitset做AND操作

PostgreSQL从8.4版本开始就是利用bitset数据结构联合使用两个索引的。es同时支持以上两种联合索引方式,如果查询的filter缓存到了内存中,那么合并就是两个bitset的and。如果查询的filtre没有缓存,那么就用skip list的方式去遍历两个 on disk的posting list。

  • 利用skip list 进行合并

    img

    首先选择最短的posting list,然后从小到大遍历。遍历的过程可以跳过一些元素。遍历过程如下:

    next : 2
    advanced(2): 13
    advanced(13): 13
    already on
    advanced(13)(13) MATCH!!!
    next: 17
    advanced(17): 22
    advanced(22): 98
    advanced(98): 98
    advanced(98): 98
    advanced(98): 98 MATCH!!!
    

    ​ 前提是什么样的list支持快速移动到指定位置?skip list

    img

    对于一个很长的posting list,比如:[1,3,13,101,105,108,255,256,257],我们可以对其进行划分:

    [1,3,13] [101,105,108] [255,256,257],然后可以构建出skip list的第二层:[1,101,255]

    1,101,255分别指向各自的block。这样就可以很快地跨block移动到指定位置了。

    lucene会对这个block进行压缩,压缩方式为Frame Of Reference编码,示例:

    img

    考虑到频繁出现的term(所谓low cardinality的值),比如gender里的男或者女。如果有1百万个文档,那么性别为男的posting list里就会有50万个int值。用Frame of Reference编码进行压缩可以极大减少磁盘占用。这个优化对于减少索引尺寸有非常重要的意义。当然mysql b-tree里也有一个类似的posting list的东西,是未经过这样压缩的。

    因为这个Frame of Reference的编码是有解压缩成本的。利用skip list,除了跳过了遍历的成本,也跳过了解压缩这些压缩过的block的过程,从而节省了cpu。

  • 利用bitset合并

    bitset是一种很直观的数据结构,对应posting list,比如[1,3,4,7,10]对应的就是[1,0,1,1,0,0,1,0,0,1]

    每个文档按照文档id顺序进行排序对应其中的一个bit。bitset本身就有压缩的特点,一个byte就可以保存8个文档,所以100万个文档只需要12.5万个bit,但是考虑到文档可能有数十亿之多,在内存里保存bitset仍然是很奢侈的事情,而且对于每一个filter都要消耗一个bitset,比如age=18缓存起来的话是一个bitset,18<=age<=25是另外一个filter缓存起来也要一个bitset

    所以就需要有一个数据结构:

    • 可以很压缩地保存数亿个文档代表的对应的文档是否匹配filter;

    • 这个压缩的bitset可以很快地进行AND和OR的逻辑操作

    lucene使用的这个数据结构叫做roaring bitmap:

    img

    其压缩的思路其实很简单。与其保存100个0,占用100个bit。还不如保存0一次,然后声明这个0重复了100遍。

  • 比较结果:因为Frame of Reference编码是如此 高效,对于简单相等条件的过滤缓存成内存的bitset还不如需要访问磁盘的skip list的方式快

5.如何减少文档数?

一种常见的压缩存储时间序列的方式是把多个数据合并成一行。Opentsdb支持海量数据的一个绝招就是把很多行数据合并成一行,这个过程叫做compaction。

过程如下:

img

elasticsearch有一个功能可以实现类似的优化效果,那就是nested document,可以把一段时间的很多数据点打包存储到一个父文档里,变成其嵌套的子文档。

{timestamp:12:05:01, idc:sz, value1:10,value2:11}
{timestamp:12:05:02, idc:sz, value1:9,value2:9}
{timestamp:12:05:02, idc:sz, value1:18,value:17}

变成:

{
max_timestamp:12:05:02, min_timestamp: 1205:01, idc:sz,
records: [
		{timestamp:12:05:01, value1:10,value2:11}
{timestamp:12:05:02, value1:9,value2:9}
{timestamp:12:05:02, value1:18,value:17}
]
}

可以把数据点公共的维度移到父文档里,而不用在每个子文档里重复存储,从而减少索引的尺寸。

img

参考文档:
https://zhuanlan.zhihu.com/p/33671444

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值