深度剖析Elasticsearch核心倒排索引数据结构

Elasticsearch 简介

Elasticsearch 是一个分布式、高扩展、高实时的搜索与数据分析引擎。Elasticsearch 建立在全文搜索引擎 Apache Lucene™ 基础上,通过 Lucene 的倒排索引技术实现比关系型数据库更快的过滤,从而很方便的使大量数据具有搜索、分析和探索的能力。

毫无疑问,Elasticsearch的底层核心是倒排索引。 Elasticsearch通过扩展服务器集群的方式,将数据以文档的形式,FST压缩的方式,分布式实时存储;同时为文件每一个字段添加倒排索引,通过倒排索引以及skip list 和 bitset 三种数据结构实现实时分布式分析和快速搜索的功能。

Elasticsearch 数据存储

先说Elasticsearch的文件存储,Elasticsearch是面向文档型数据库,一条数据在这里就是一个文档,用JSON作为文档序列化的格式,比如下面这条学生数据:

{
	"stuName":"Rose",
	"age":18,
	"gender":"Male",
	"resume":"I am gooding at studying",
	"tuition":26800.00,
	"hobbies":["sleep","games"],
	"address":{
		"province":"JiangSu",
		"city":"NanJing",
		"district":"YuHua"
	}
}

如果用传统的关系型数据库比如说mysql来存储上述这条数据时,我们能够想到的是去建立一张student表,表中有stuName,age,tuition,hobbies,address字段等,而在Elasticsearch里这就是一个记录student类型的文档,所有的字段类型都存在于这个文档的索引里。这里有一份简易的将Elasticsearch和关系型数据术语对照表:

在这里插入图片描述

Elasticsearch和传统的数据库一样,索引、类型、文档、字段都是一对多的关系。Elasticsearch数据交互可以通过HTTP的Restful请求方式直接请求,也可以通过java API请求。值得注意的是,Elasticsearch主要用来查询,因为其每一个字段都有倒排索引这种需要大量的储存空间,我刚开始接触Elasticsearch的事实增删改,如果不实用shell脚本的话,要一条一条的执行,增删改的效率很低,不如传统的数据库。事实上,现在市场的主流就是Elasticsearch与传统数据库公用,Elasticsearch用来查询,传统的用来增删改,Elasticsearch连接数据库和客户端搜索引擎的桥梁。

倒排索引

Elasticsearch倒排索引的精髓:

倒排索引压缩储存空间,减少磁盘读取次数;严格储存结构,节省搜索时间。

简单的来说,Elasticsearch将磁盘里的东西尽量搬进内存,减少磁盘随机读取次数(同时也利用磁盘顺序读特性),结合各种奇技淫巧的压缩算法,用极其苛刻的态度使用内存。

Elasticsearch能够通过倒排索引来达到实时、高效的搜索是怎么实现的呢?下面我从时间和空间的概念来谈谈倒排索引的原理。

倒排索引的空间结构是什么样的?

首先根据上述例子拿出stuName,age,gender字段的来说:

student类型的文档上层school对应着一个索引的index为1:

PUT http://192.168.152.129:9200/school

student类型的文档对应着一个index:

| ID | stuName| age| gender |
| -- |--------|----| -------| 
| 1  | Rose   | 24 | Male   |
| 2  | John   | 24 | Female |
| 3  | Bill   | 29 | Female |

stuName:

| stuName| Posting List|
|--------|-------------| 
| Rose   |     1       | 
| John   |     2       |  
| Bill   |     3       | 

age:

| Term | Posting List |
| ---- |--------------|
| 24   | 	[1,2] 	  |
| 29   | 	  3 	  |

gender:

|  Term  | Posting List |
| ------ |--------------|
| Female |      1       |
|  Male  |    [2,3]     |

如上所述,student所在的school索引index,student每个文档dictionary,student的每个字段Posting List都建立了索引。用一张图表示如下:
在这里插入图片描述

Posting List

Posting List是Elasticsearch中为每个字段field自动提供的索引集合,比方说24,29 这些叫做 term,而 [1,2] 就是 posting list。Posting list 就是一个 int 的数组,存储了所有符合某个 term 的文档 id。

Term Dictionary

Elasticsearch中为了能够快速的找到某个term,也就是我们经常用某个字段来快速查询,为了实现这一功能,Term Dictionary就产生了。Term Dictionary的实现底层就是B+Tree,使用二分法进行查询term, logN 次磁盘查找效率,就像字典查询一样,首字母是什么,就先检索什么,然后再看第二个字母是什么,检索第二个字母,…,一直到检索到这个term为止。

Term Index

由于磁盘随机读的存在,就必须将一部分数据存在缓存内存中,但是Term Dictionary磁盘存储空间的巨大,又不能将Term Dictionary完整的放到内存里。因此就有了Term Index,它就像字典里一个更大的章节一样,每个大的章节再对应着多个小的章节Term Dictionary,这样就能实现速的找到某个term。

Term Index、Term Dictionary和Posting List个关系

如下图 所示:“A”, “to”, “tea”, “ted”, “ten”, “i”, “in”, 和 “inn” 这些单词是如何储存在Elasticsearch里的呢?Term Index就像一棵倒挂的树一样,它就是这棵树的根节点,也就是这本字典;Term Dicitionary是根节点的子节点,存放着“t”、“A”、“i”,也就是所储存单词的前缀;然后Posting List就是相同前缀的单词(term)集合,里面装着我们要检索的单词(term)。因此通过 term index能够快速精确的检索到我们所需要的term

在这里插入图片描述
如下图所示,关系型数据库如Mysql 只有 term dictionary 这一层,是以 b-tree 排序的方式存储在磁盘上的。检索一个 term 需要若干次的 random access 的磁盘操作。而 Elasticsearch 在 term dictionary 的基础上添加了 term index 来加速检索,term index 以树的形式缓存在内存中。从 term index 查到对应的 term dictionary 的 block 位置之后,再去磁盘上找 term,大大减少了磁盘的 random access 次数。
在这里插入图片描述

倒排索引的时间概念是怎么节省的?

B-Tree+整体分区快速查找

上面说到Elasticsearch中的 term dictionary不同于关系型数据库的term dicitionary的结构B-Tree那样,将所存储的数据按照某种规则进行排序储存于磁盘里,然后通过二分法去查找某个term,这样能够达到log N的查询效率;而Elasticsearch中先把 term dictionary分为相同大小的块,然后递归去把每个块分成相同大小的块,进行快速查找。

举个例子,对于1-16这组数据进行快速查找其中的某个值:

B-Tree二分法
在这里插入图片描述
如图所示B-Tree二分法查找7这个数,需要4次方能查出来。同样的查找1和16也需要4次才能查找出来。

B-Tree+整体分区法
在这里插入图片描述
如图所示B-Tree+整体分区法查找7的时候,只需3次就能找到,相当于“三分法”一样比二分法更加的有效率,但是如果数据每次“三分”时都处于中间,那就无形的增加了判断次数(这种做法,拿要检索的值7和中间块的两头6和11比较),但是这只是极少的数据而已,在海量的数据面前,这数据更是微不足道,所以根据二八定律,它基本上能满足搜索更快的需求。这种是将块或者区域作为一个整体的思想来实现快速搜索,有一点像希尔排序一样,虽然检索效率不稳定,但是能够解决大部分的数据效率问题,就等于实现了整个数据的效率问题。
更为重要的是,Elasticsearch中不仅term dictionary实现了倒排索引,而且term index也采用了这种倒排索引,这就相当于又套了一层B-Tree+整体分区法,效率提高了一个档次。

FST增量压缩技术

上面说过,term index 在内存中是以 FST(finite state transducers)的形式保存的,其特点是非常节省内存。Term dictionary 在磁盘上是以分 block 的方式保存的,一个 block 内部利用公共前缀压缩,比如都是 Ab 开头的单词就可以把 Ab 省去。这样 term dictionary 可以比 b-tree 更节约磁盘空间。那么什么是FTS呢?

FST的含义:

FSTs are finite-state machines that map a term (byte sequence) to an arbitrary output.

简单的来说,就是用更小的内存空间来储存更多的数据;这就是FST增量压缩技术的核心思想。那么Elasticsearch是如何使用FST来节省空间,达到快速检索的功能了?

字节bitmaps存储

之前说过,使用我们要储存term的前缀放入term dictionary似乎就是更小的空间;然而并不是,FST的所用的更小空间实际上是一个byte类型的数组,也就是所谓的字节数组,为了实现这种储存方式,Elasticsearch的倒排索引,将每一个term对应的posting list和其对应的term dictionary都采用short类型的数组建立索引,比如说mop, moth, pop, star, stop and top(term index里的term前缀)映射到序号:0,1,2,3,4,5(term dictionary的block位置)。这就相当于Map<string, integer>的键值对的方式,来实现更小的空间储存。

简单的posting list 和bitmaps对应关系如下:
在这里插入图片描述

字节数组分区bitset存储

似乎按字节数组那样储存就已经是最小了,答案显然是错误的;倒排索引提供了一种针对特殊field,比如性别、状态这种只有极少的值,如果按照字节数组来存储,当有海量数据时,那么每一种值对应的term,bitmaps所储存的字节数组也会非常的大,那么该如何储存呢?Elasticsearch posting list倒排索引采用了字节数组分区存储;以0-65535为一个区,65536-131071为下一个区,以此类推,再用再用<商,余数>的组合表示每一组索引id,这样每组里的id范围都在0~65535内了。65535是short类型储存的最大值,正好是用2个字节能表示的最大数。

简单的posting list 和bitset对应关系如下:
在这里插入图片描述
与其保存 100 个 0,占用 100 个 bit。还不如保存 0 一次,然后声明这个 0 重复了 100 遍。这就是bitset的核心所在。

倒排索引规定:

If a block has more than 4096 values, encode as a bit set, and otherwise as a simple array using 2 bytes per value

上述规定posting list的数组长度超过4kb时采用字节数组分区bitset存储,低于4kb时采用字节bitmaps存储。这里的4kb 刚好是磁盘每次随机读取的一个块block的字节大小,更是linux系统规定内存每次读取最小的内存单位。

联合索引查询

传统关系型数据库的做法:
在这里插入图片描述
如上图所示,mysql怎么做到联合查询的呢?实际上是通过mysql的连接查询来实现的,那么连接查询是怎么查询的,首先拿出第一张表里的每一条数据,逐一到第二张表一条一条数据逐一的对照,不论对照是否匹配,都要进行匹配,上述是mysql连接查询的4种方式:内连接、左连接、右连接和全连接的结果,可以看出它们每一种都要匹配6*16=96次,才能得出结果。

此外,对于关系型数据库 mysql 来说,如果你给 age 和 gender 两个字段都建立了索引,查询的时候只会选择其中最为严格的索引来使用,然后另外一个条件是在遍历行的过程中在内存中计算之后过滤掉,并且很多种情况比如包含NULL值的列、组合索引组合不当和like 前缀出现通配符等会导致索引失效,这样就大大的降低了检索效率。那么要如何才能联合使用两个索引呢?有两种办法:

  1. 使用 skip list 数据结构。同时遍历 gender 和 age 的 posting list,互相 skip;
  2. 使用 bitset 数据结构,对 gender 和 age 两个 filter 分别求出 bitset,对两个 bitset 做 “位与” 操作。

skip list

skip list数据结构核心是将一个有序链表level0,挑出其中几个元素到level1及level2,每个level越往上,选出来的指针元素越少,查找时依次从高level往低查找。也就是说每一个链表都有一个指针,每个指针都只要移动一次完整的遍历链表即可,这样比传统的数据库mysql 联表查询一条一条的遍历另一张表的效率要快很多。
比如以下3个posting list,互相 skip过程:
在这里插入图片描述
执行整个过程如下:

Next -> 2
Advance(2) -> 13
Advance(13) -> 13
Already on 13
Advance(13) -> 13 MATCH!!!
Next -> 17
Advance(17) -> 22
Advance(22) -> 98
Advance(98) -> 98
Advance(98) -> 98 MATCH!!!

最后得出的交集是 [13,98],所需的时间比完整遍历三个 posting list 要快得多。但是前提是每个 list 需要指出 Advance 这个操作,快速移动指向的位置。

此外当得出这个结果交集的时候,Elasticsearch 还进行了以下方式对内存数据进行压缩,减少读取磁盘速度,提高效率。
在这里插入图片描述
对我们要查询的结果进行增量压缩,也就是posting list每一个结果都对应一个int类型的数值,那么第一个采用原值去储存,第二个采用第二个值与第一个值的差来储存,这样就能将posting list中大的值变的相对小一点,然后就可以用更小的空间去存储。

bitset 按位与

如果查询的数据是用bitset方式存储,那么就太过简单了,只要按位与,就能查询到两个结果的交集。

参考文章

时间序列数据库的秘密 (2)——索引

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值