1 倒排索引
1.1 背景
当前的搜索引擎,为什么能通过某些关键字,就能快速搜出相关的文章出来?
答案就是:把不同网站上的文章先收录下来,然后对文章内容进行分段、分词 (term),最后构造一个这样的索引:
**[分词1] to [文档1, 文档2, 文档3]**
**[分词2] to [文档1, 文档34, 文档35]**
**[分词3] to [文档23]**
因为索引的key是一个分词数组,只要先遍历这些 key,就能快速找到对应的文档 id / 文档链接
1.2 定义
背景中的索引,其实就是倒排索引,它与传统索引的区别是,传统索引的 key 是id,value 是对应的某个或者某些字段值 (详情可以参考下基于 B-Tree 的 MySQL 下的索引) ,而倒排索引正好相反,为某个字段建倒排索引后 (mapping.properties.字段.index = true) , 索引的 key 就是对应字段的 value 中的分词, 然后指向一个 doc_id 的链表
1.3 优势
这样的索引有什么好处?
根据关键字找文档就找得快,只需要遍历这个倒排索引就能找出对应的文章
1.4 生成/更新时机
跟普通的索引一样,在文档被插入的时候,倒排索引就会被生成和更新
1.5 数据结构
找了半天,它的数据结构大概是这样的
(原图在某乎上面贴来的,它的源在哪我也不知道)
三个部分,分别是字典树构造的 term index,保存在内存中,所以可以快搜,term index 找到对应的 term,最后根据 term dictionary 找到对应的 posting list
1.5.1 分词索引(Term Index)
FST 结构,大概是长这样
特点就是省内存,FST压缩率一般在3倍~20倍之间,相对于TreeMap/HashMap的膨胀3倍,内存节省就有9倍到60倍
1.5.2 分词字典(Term Dictionary)
Term dictionary 在磁盘上是以分 block 的方式保存的,一个 block 内部利用公共前缀压缩,比如都是 Ab 开头的单词就可以把 Ab 省去。这样 Term dictionary 可以比 b-tree 更节约磁盘空间。
1.5.3 Posting List
这个东西在做联合索引查询的时候有学问,首先做联合索引涉及到两种数据结构,分别是 bitset 和 skip list
1 bitset (当查询命中内存中的 filter 时)
首先看看 bitset 转换
### bitset to posting list
[1,0,1,1,0,0,1,0,0,1] -> [1, 3, 4, 7, 10]
利用 bitset 进行条件筛选的时候十分简单,找到 Posting List,然后转换 bitset,接着进行 AND 操作就好了
而 Lucene 会用 Roaring Bitmap 对 bitset 进行压缩,压缩原理也很简单,就是记录每个 bit 前面有多少个 0 就行,具体的原理看下下图
2 skip list (查询没有命中,直接在磁盘中进行数据过滤)
跳表的话大家都知道,就是这样的:
由于 posting list 很长,所以 Lucene 会先对它进行分块,接着再压缩,分块后的压缩方式称为 *FOR (Frame of Reference) * ,压缩方式的算法见下图
压缩方式比较简单
- 将 [73, 300, 302, 332, 343, 372] 转为 [73, 227, 2, 30, 11, 29],就是记录每个 id 的增量,因为 posting list 是有序的,所以这个方案才可行
- posting list 分块,每一块不超过256,这样子的话,增量也不会超过256了,不超过256的话,每一个增量可以用一个 byte 表示 (2 ^ 8 - 1)
- 块压缩,每一个块找到最大值要用的 bit,然后放到块头,那每一块要用的 bit 就能算出来了,同时也对块进行了再压缩
(算法描述参照 Lucene 压缩算法)
2 正排索引
2.1 背景
好了,在有了倒排索引之后,通过 term 快速命中 doc的问题是解决了,但是,如果说我的文章中,还有 weight / boost 这种字段,然后我还得在查询的时候根据这些字段进行聚合操作的话,如果用倒排索引会咋样了,就以刚刚那个例子来说,假设一个 doc 里面包含了 tags 和 boost ,如果说我要这样查(sql like)
select * from test_docs where tags like '%elastic index%' order by boost;
在tags 字段建立了倒排的情况下,应该先从倒排找到对应的 posting list,然后找到对应的doc,接着再从doc里面找出所有的boost,对这个boost 排序,返回。这个流程乍看上去没问题,但是数据量大的时候,得遍历大量的doc,最后排序才能返回查询结果,性能比较差。
如果 boost 建立了倒排的话,首先呢,我得遍历整个 boost 的倒排,接着排序,然后再从 tags 查出来的 posting list 跟上面的排序结果进行过滤,才能得出结果,这个操作也是让人头疼的,性能不高不说,要是 boost 索引特别大的时候,效率还低。
所以这个时候就必须得用正排索引了。
2.2 定义
正排索引与传统索引有些像, key 是doc_id,value 是对应的某个字段值
**[doc_1] to [term1]**
**[doc_2] to [term2]**
**[doc_3] to [term3]**
在 es 中,要启用它比较简单,就是 mapping.properties.字段.doc_value= true,
其实 es 会默认创建正排索引的
2.3 优势
当然就是为了在聚合操作的时候能够快速聚合出结果而生的
2.4 生成/更新时机
跟倒排索引一样,在文档被插入的时候,倒排索引就会被生成和更新