Elasticsearch之数据压缩算法

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Elasticsearch之数据压缩算法相关的知识,希望对你有一定的参考价值。

参考技术A

Lucene使用FST算法以字节的方式来存储所有的Term,重复利用Term Index的前缀和后缀,使Term Index小到可以放进内存,减少存储空间,不过相对的也会占用更多的cpu资源。FST在Lucene4.0以后的版本中用于快速定位所查单词在字典中的位置。

Finite StateTransducers,简称 FST,通常中文译作 有穷状态转换器 ,在语音识别和自然语言搜索、处理等方向被广泛应用。

FST的功能类似于字典,可以表示成FST<Key, Value>的形式。其最大的特点是,可以用 O(length(key)) 的复杂度来找到key对应的value,也就是说查找复杂度仅取决于所查找的key长度。

假设我们现在要将以下term index映射到term dictionary的block序号:

最简单的做法就是定义个Map<string, integer="">,大家找到自己的位置对应入座就好了,但从内存占用少的角度想想,有没有更优的办法呢?答案就是FST。

对于经典FST算法来说,要求 Key必须按字典序从小到大加入到FST 中。上面的例子中key已经排好序了。

按照以下步骤建立FST:

最后加入 dogs ,得到最后的结果:

从上图可以看出,每条边有两条属性,一个表示label(key的元素),另一个表示Value(out)。注意Value不一定是数字,还可一是另一个字符串,但要求Value必须满足叠加性,如这里的正整数2 + 8 = 10。字符串的叠加行为: aa + b = aab。

建完这个图之后,我们就可以很容易的查找出任意一个key的Value了。例如:查找dog,我们查找的路径为:0 → 4 → 8 → 9。 其权值和为: 2 + 0 + 0 + 0 = 2。其中最后一个零表示 node[9].finalOut = 0。所以“dog”的Value为2。

Lucene除了上面说到用FST压缩term index外,对posting list也会进行压缩。

有人可能会有疑问:“posting list不是已经只存储文档id了吗?还需要压缩吗?”。设想这样一种情况,Lucene需要对一千万个同学的性别进行索引,而世界上只有男/女这样两个性别,每个posting list都会有数百万个文档id,这里显然有很大的压缩空间与价值,对于减少索引尺寸有非常重要的意义。

Lucene使用 Frame Of Reference 编码来实现对posting list压缩,其思路简单来说就是: 增量编码压缩,将大数变小数,按字节存储

示意图如下:

除此之外,Lucene在执行filter操作还会使用一种叫做 Roaring bitmaps 的数据结构来存储文档ID,同样可以达到压缩存储空间的目的。

说到Roaring bitmaps,就必须先从bitmap说起。bitmap是一种很直观的数据结构,假设有某个posting list:

对应的bitmap就是:

非常直观,用0/1表示某个值是否存在,比如10这个值就对应第10位,对应的bit值是1,这样用一个字节就可以代表8个文档id,旧版本(5.0之前)的Lucene就是用这样的方式来压缩的,但这样的压缩方式仍然不够高效,如果有1亿个文档,那么需要12.5MB的存储空间,这仅仅是对应一个索引字段(我们往往会有很多个索引字段)。于是有人想出了Roaring bitmaps这样更高效的数据结构。Roaring bitmaps压缩的原理可以理解为,与其保存100个0,占用100个bit,还不如保存0一次,然后声明这个0有100个。

Bitmap的缺点是存储空间随着文档个数线性增长,Roaring bitmaps需要打破这个魔咒就一定要用到某些指数特性:

将posting list按照65535为界限分块,比如第一块所包含的文档id范围在0 65535之间,第二块的id范围65536 131071,以此类推。再用<商,余数>的组合表示每一组id,这样每组里的id范围都在0~65535内了,剩下的就好办了,既然每组id不会变得无限大,那么我们就可以通过最有效的方式对这里的id存储。

为什么是以65535为界限呢?

程序员的世界里除了1024外,65535也是一个经典值,因为它=2^16-1,正好是用2个字节能表示的最大数,一个short的存储单位,注意到上图里的最后一行“If a block has more than 4096 values, encode as a bit set, and otherwise as a simple array using 2 bytes per value”,如果是大块,用节省点用bitset存,小块就豪爽点,2个字节我也不计较了,用一个short[]存着方便。

那为什么用4096来区分大块还是小块呢?

都说程序员的世界是二进制的,4096*2bytes = 8192bytes < 1KB, 磁盘一次寻道可以顺序把一个小块的内容都读出来,再大一位就超过1KB了,需要两次读。

一种常见的压缩存储时间序列的方式是把多个数据点合并成一行。Opentsdb支持海量数据的一个绝招就是定期把很多行数据合并成一行,这个过程叫compaction。类似的vivdcortext使用mysql存储的时候,也把一分钟的很多数据点合并存储到mysql的一行里以减少行数。

这个过程可以示例如下:

合并之后就变成了:

可以看到,行变成了列了。每一列可以代表这一分钟内一秒的数据。

Elasticsearch有一个功能可以实现类似的优化效果,那就是Nested Document。我们可以把一段时间的很多个数据点打包存储到一个父文档里,变成其嵌套的子文档。示例如下:

可以打包成:

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

在存储的时候,无论父文档还是子文档,对于 Lucene 来说都是文档,都会有文档 Id。但是对于嵌套文档来说,可以保存起子文档和父文档的文档 id 是连续的,而且父文档总是最后一个。有这样一个排序性作为保障,那么有一个所有父文档的 posting list 就可以跟踪所有的父子关系。也可以很容易地在父子文档 id 之间做转换。把父子关系也理解为一个 filter,那么查询时检索的时候不过是又 AND 了另外一个 filter 而已。前面我们已经看到了 Elasticsearch 可以非常高效地处理多 filter 的情况,充分利用底层的索引。

使用了嵌套文档之后,对于 term 的 posting list 只需要保存父文档的 doc id 就可以了,可以比保存所有的数据点的 doc id 要少很多。如果我们可以在一个父文档里塞入 50 个嵌套文档,那么 posting list 可以变成之前的 1/50。

以上是关于Elasticsearch之数据压缩算法的主要内容,如果未能解决你的问题,请参考以下文章

ElasticSearches 压缩 你的 数据

ElasticSearch之CURL操作

elasticSearch-mappings(映射,分析)

ElasticSearch实战-FST有限状态转换算法(索引数据压缩算法)

ElasticSearch实战-FST有限状态转换算法(索引数据压缩算法)

Elasticsearch使用指南之初始环境搭建