文章目录

  • 倒排索引
  • Term dictionary与Term index
  • 列式存储——Doc Values
  • Fielddata
  • 索引压缩
    • FOR编码
    • Roaring Bitmaps
  • 联合索引

倒排索引

例如,假设我们有两个文档,每个文档的 content 域包含如下内容:

  1. The quick brown fox jumped over the lazy dog
  2. Quick brown foxes leap over lazy dogs in summer

为了创建倒排索引,首先我们需要借助分词器,将每个文档的 content 域拆分成单独的词(我们称它为 词条tokens、term ),创建一个包含所有不重复词条的排序列表,然后列出每个词条出现在哪个文档。

Term      Doc_1  Doc_2
-------------------------
Quick   |       |  X
The     |   X   |
brown   |   X   |  X
dog     |   X   |
dogs    |       |  X
fox     |   X   |
foxes   |       |  X
in      |       |  X
jumped  |   X   |
lazy    |   X   |  X
leap    |       |  X
over    |   X   |  X
quick   |   X   |
summer  |       |  X
the     |   X   |
------------------------

如果我们想搜索 quick brown ,我们只需要查找包含每个词条的文档:

Term      Doc_1  Doc_2
-------------------------
brown   |   X   |  X
quick   |   X   |
------------------------
Total   |   2   |  1

这里我们匹配到了两个文档(为了节省空间,这里返回的只是文档ID,最后再通过文档id去查询到具体文档。)。对于搜索引擎来说,用户总希望能够先看到相关度更高的结果,因此实际使用时我们通过一些算法来进行权重计算,将查询的结果按照权重降序返回。

Term dictionary与Term index

Elasticsearch为了能够快速的在倒排索引中找到某个term,他会按照字典序对所有的term进行排序,再通过二分查找来找到term,这就是Term Dictionary,但即使有了Term Dictionary,O(logN)的磁盘读写仍然是影响性能的一大问题。

B-Tree通过减少磁盘寻道次数来提高查询性能,Elasticsearch也是采用同样的思路,直接通过内存查找term,不读磁盘,但是如果term太多,term dictionary也会很大,放内存不现实,于是有了Term Index,从下图可以看出,Term Index其实就是一个Trie树(前缀树)

Term index

这棵树不会包含所有的term,它包含的是term的一些前缀。通过term index可以快速地定位到term dictionary的某个offset,然后从这个位置再往后顺序查找。

查找流程

为什么Elasticsearch/Lucene检索比mysql快呢?

Mysql只有term dictionary这一层,是以b-tree排序的方式存储在磁盘上的。检索一个term需要若干次的random access的磁盘操作。而Lucene在term dictionary的基础上添加了term index来加速检索,term index以树的形式缓存在内存中。从term index查到对应的term dictionary的block位置之后,再去磁盘上找term,大大减少了磁盘的random access次数。

列式存储——Doc Values

Doc values的存在是因为倒排索引只对某些操作是高效的。 倒排索引的优势在于查找包含某个项的文档,而对于从另外一个方向的相反操作并不高效,即:确定哪些项是否存在单个文档里,聚合需要这种次级的访问模式。

以排序来举例——虽然倒排索引的检索性能非常快,但是在字段值排序时却不是理想的结构。

  • 在搜索的时候,我们能通过搜索关键词快速得到结果集。
  • 当排序的时候,我们需要倒排索引里面某个字段值的集合。换句话说,我们需要转置倒排索引。

转置 结构经常被称作 列式存储 。它将所有单字段的值存储在单数据列中,这使得对其进行操作是十分高效的,例如排序、聚合等操作。

在Elasticsearch中,Doc Values就是一种列式存储结构,在索引时与倒排索引同时生成。也就是说Doc Values和倒排索引一样,基于 Segement生成并且是不可变的。同时Doc Values和倒排索引一样序列化到磁盘。

Doc Values常被应用到以下场景:

  • 对一个字段进行排序
  • 对一个字段进行聚合
  • 某些过滤,比如地理位置过滤
  • 某些与字段相关的脚本计算

下面举一个例子,来讲讲它是如何运作的

假设存在以下倒排索引

Term      Doc_1   Doc_2   Doc_3
------------------------------------
brown   |   X   |   X   |
dog     |   X   |       |   X
dogs    |       |   X   |   X
------------------------------------

那么其生成的DocValues如下(实际存储时不会存储doc_id,值所在的顺位即为doc_id)

Doc_id    Values
------------------
Doc_1    | brown |
Doc_1    | dog   |
Doc_2    | brown |
Doc_2    | dogs  |
Doc_3    | dog   |
Doc_3    | dogs  |
------------------

假设我们需要计算出brown出现的次数

GET /my_index/_search
{"query":{"match":{"body":"brown"}},"aggs":{"popular_terms":{"terms":{"field":"body"}}},"size" : 0
}

下面来分析上述请求在ES中是如何来进行查询的:

  1. 定位数据范围。通过倒排索引,来找到所有包含brown的doc_id。
  2. 进行聚合计算。借助doc_id在doc_values中定位到为brown的字段,此时进行聚合累加得到计算结果。browm的count=2。

但是doc_values仍存在一些问题,其不支持analyzed类型的字段,因为这些字段在进行文本分析时可能会被分词处理,从而导致doc_values将其存储为多行记录。但是在我们实际使用时,为什么仍然能对analyzed的字段进行聚合操作呢?这时就需要介绍一下Fielddata

Fielddata

doc values不生成分析的字符串,那为什么这些字段仍然可以使用聚合呢?是因为使用了fielddata的数据结构。与doc values不同,fielddata构建和管理100%在内存中,常驻于JVM内存堆。

从历史上看,fielddata 是所有字段的默认设置。但是Elasticsearch已迁移到doc values以减少 OOM 的几率。分析的字符串是仍然使用fielddata的最后一块阵地。 最终目标是建立一个序列化的数据结构类似于doc values ,可以处理高维度的分析字符串,逐步淘汰 fielddata。

它的一些特性如下

  • 延迟加载。如果你从来没有聚合一个分析字符串,就不会加载fielddata到内存中,其是在查询时候构建的。
  • 基于字段加载。 只有很活跃地使用字段才会增加fielddata的负担。
  • 会加载索引中(针对该特定字段的) 所有的文档,而不管查询是否命中。逻辑是这样:如果查询会访问文档 X、Y 和 Z,那很有可能会在下一个查询中访问其他文档。
  • 如果空间不足,使用最久未使用(LRU)算法移除fielddata。

因此,在聚合字符串字段之前,请评估情况:

  • 这是一个not_analyzed字段吗?如果是,可以通过doc values节省内存 。
  • 否则,这是一个analyzed字段,它将使用fielddata并加载到内存中。这个字段因为N-grams有一个非常大的基数?如果是,这对于内存来说极度不友好。

索引压缩

FOR编码

在Elasticsearch中,为了能够更方便的计算交集和并集,它要求倒排索引是有序的,而这个特点同时也带来了一个额外的好处,我们可以使用增量编码来压缩倒排索引,也就是FOR(Frame of Reference)编码

增量编码压缩,将大数变小数,按字节存储

FOR编码分为三个步骤

  • 增量编码
  • 增量分区
  • 位压缩

如下图所示,如果我们的倒排索引中存储的文档id为[73, 300, 302, 332, 343, 372],那么经过增量编码后的结果则为[73, 227, 2, 30, 11, 29]。这种压缩的好处在哪里呢?我们通过增量将原本的大数变成了小数,使得所有的增量都在0~255之间,因此每一个值就只需要使用一个字节就可以存储,而不会使用int或者bigint,大大的节约了空间。

接着,第二步我们将这些增量分到不同的区块中(Lucene底层用了256个区块,下面为了方便展示之用了两个)。

第三步,我们计算出每组数据中最大的那个数所占用的bit位数,例如下图中区块1最大的为227,所以只占用8个bit位,所以三个数总共占用3 * 8bits即3字节。而区块2最大为29,只占用5个bit位,因此这三个数总共占用3 * 5bits即差不多2字节。通过这种方法,将原本6个整数从24字节压缩到了5字节,效果十分出色。

ROF编码实例

Roaring Bitmaps

FOR编码对于倒排索引来说效果很好,但对于需要存储在内存中的过滤器缓存等不太合适,两者之间有很多不同之处:

  • 由于我们仅仅缓存那些经常使用的过滤器,因此它的压缩率并不需要像倒排索引那么高(倒排索引需要对每个词都进行编码)。
  • 缓存过滤器的目的就是为了加速处理效率,因此它必须要比重新执行过滤器要快,因此使用一个好的数据结构和算法非常重要。
  • 缓存的过滤器存储在内存之中,而倒排索引通常存储在磁盘中。

基于以上的不同,对于缓存来说FOR编码并不适用,因此我们还需要考虑其他的一些选择。

  • 整数数组:数组可能是我们马上能想到的最简单的实现方式,我们将文档id存储在数组中,这样就使得我们的迭代变得非常简单,但是这种方法的内存利用率又十分低下,因为每个文档都需要4个字节。
  • Bitmaps:在数据分布密集的下,位图是一个很好的选择。它本质上就是一个数组,其中每一个文档id占据一个位,用0和1来标记文档是否存在。这种方法大大节约了内存,将一个文档从4字节降低到了一个位,但是一旦数据分布稀疏,此时的位图性能将大打折扣,因为无论数据量多少,位图的大小都是由数据的上下区间来决定的。
  • Roaring Bitmaps:Roaring Bitmaps即是对位图的一种优化,它会根据16位最高位将倒排索引划分为多块,如第一个块将对0到65535之间的值进行编码,第二个块将在65536和131071之间进行编码。在每一个块中,我们再对低16位进行编码,如果它的值小于4096在使用数组,否则就使用位图。由于我们编码的时候只会对低16位进行编码,因此在这里数组每个元素只需要2个字节

Roaring Bitmaps原理

为什么要使用4096作为数组和位图选取的阈值呢?

下面是官方给出的数据报告,在一个块中只有文档数量超过4096,位图的内存优势才会凸显出来

位图与数组内存利用对比

这就是Roaring Bitmaps高效率的原因,它基于两种完全不同的方案来进行编码,并根据内存效率来动态决定使用哪一种方案。

官方也给出了几种方案的性能测试

迭代性能

跳过性能——稀疏集

跳过性能——密集集

内存占用

构造时间

从上述对比可以看出,没有一种方法是完美的,但是以下两种方法的巨大劣势使得它们不会被选择

  • 数组:性能很好,但是内存占用巨大。
  • Bitmaps:数据稀疏分布的时候内存和性能都会大打折扣。

因此在综合考量下,Elasticsearch还是选择使用Roaring Bitmaps,并且在很多大家了解的开源大数据框架中,也都使用了这一结构,如Hive、Spark、Kylin、Druid等。

联合索引

如果多个字段索引的联合查询,倒排索引如何满足快速查询的要求呢?

  • 跳表:同时遍历多个字段的倒排索引,互相skip。
  • 位图:对多个过滤器分别求出位图,对这几个位图做AND操作。

Elasticsearch支持以上两种的联合索引方式,如果查询的过滤器缓存到了内存中(以位图的形式),那么合并就是两个位图的AND。如果查询的过滤器没有缓存,那么就用跳表的方式去遍历两个硬盘中的倒排索引。

假设有下面三个倒排索引需要联合索引:

假设有三个倒排索引

  • 如果使用跳表,则对最短的倒排索引中的每一个id,逐个在另外两个倒排索引中查看是否存在,来判断是否存在交集。
  • 如果使用位图,则直接将几个位图按位与运算,最终得到的结果就是最后的交集。

ElasticSearch探索之路(四)索引原理:倒排索引、列式存储、Fielddata、索引压缩、联合索引相关推荐

  1. 深入解析:DB2 V10.5新特性列式存储表的优点与缺点

    关注"数据和云",精彩不容错过 李培杨 云和恩墨西区交付技术顾问,有多年数据库运维经验,长期服务移动运营商行业客户,熟悉 DB2 数据库故障诊断,数据库迁移升级. >> ...

  2. tablestore列式存储原理_10分钟搞透:技术人必会的MySQL体系结构与存储引擎!

    MySQL是目前使用最广的开源数据库,不管从装机量.使用人群.专职人员.社区发展,还是基于MySQL的其他分支,都是当之无愧的No.1. 本文将从以下4个方面,带你搞透MySQL体系结构与存储引擎. ...

  3. 用户关系表 存储_列式存储系列(一)CStore

    作者:辛庸,阿里巴巴计算平台事业部 EMR 技术专家.Apache Hadoop,Apache Spark commiter.对 Hadoop.Spark.Hive.Druid 等大数据组件有深入研究 ...

  4. oracle层级关系按列存储_几张图看懂列式存储

    最近看到一篇很好资料,里面三言两语配上几个图就把列式存储(Column-based Storage)讲明白了,牛啊!最喜欢的就是这种浅显易懂就把背景知识讲得明明白白,而不是长篇大论的讲概念. 1 为什 ...

  5. Mysql框架<二> 行式存储与列式存储

    文章目录 数据处理场景分类 OLTP 联机事务处理 OLAP 联机分析处理 行式存储和列式存储 概念 特点 对比 数据处理场景分类 OLTP 联机事务处理 OLTP : on-line transac ...

  6. 列式存储和行式存储它们真正的区别是什么

    本文是<Column-Stores vs. Row-Stores: How Different Are They Really?>论文的读后感,作者徐明明.原文链接:https://zhu ...

  7. 列式存储的分布式数据库——HBase Shell与SQL实战操作(HBase Master高可用实现)

    文章目录 一.前言 二.命令行操作(hbase shell) 1)连接HBase 2)创建表(create) 3)添加数据(put) 4)查询数据(scan ) 5)获取单行数据(get) 6)禁用/ ...

  8. 什么是列式存储和行式存储

    一.列式存储和行式存储 列式存储是指一列中的数据在存储介质中是连续存储的: 行式存储是指一行中的数据在存储介质中是连续存储的. 简单的说,可以把列式数据库认为是每一列都是一个表,这个表只有一列,如果只 ...

  9. mysql 列式存储_[转]几张图看懂列式存储

    最近看到一篇很好资料,里面三言两语配上几个图就把列式存储(Column-based Storage)讲明白了,牛啊!最喜欢的就是这种浅显易懂就把背景知识讲得明明白白,而不是长篇大论的讲概念. 1 为什 ...

最新文章

  1. JAVA编程TXT文件_java读写txt文件的方法
  2. 批处理之显示隐藏文件
  3. 使用Apache Flume抓取数据(1)
  4. 判断是否是完全二叉树_【数据结构】二叉树高频考试题目【代码模板】!
  5. POJ - 3257 Cow Roller Coaster (背包)
  6. wmode解决flash透明及层深问题
  7. 循序渐进之Maven(4) - 第一个SpringMVC项目
  8. 工作中常用知识点、工具总结
  9. 【物联网智能网关-08】TinyGUI和WPF汉字显示技术比较
  10. Linux操作系统中df和du命令常见用法
  11. 【SSM】手动SQL分页和PageHelper插件分页
  12. uds协议之can总线程序升级
  13. linux下安装java
  14. Autoencoder理解(2): 自动编码器的作用之稀疏编码
  15. 关系数据库标准语言SQL——详解版2
  16. 河北科技师范学院对口计算机分数线,河北科技师范学院对口分数线
  17. 数据库管理及常用语句
  18. 破解水卡教程 超详细
  19. ascll编码表图片_ASCll码 表
  20. Windows 优质软件

热门文章

  1. 封装案例-完成开火方法
  2. 【报错笔记】eclipse运行tomcat程序时报错
  3. 【报错笔记】在eclipse中做Spring项目时,创建Spring容器时老是出错
  4. Linux下输出不同颜色的字体详解
  5. 彻底搞清楚浏览器渲染过程
  6. python爬虫知识点总结(十九)Scrapy命令行详解
  7. 用VIPER构建iOS应用
  8. Intent中各种常见的Action
  9. 【原创】MySQL 返回更新值(RETURNING)
  10. 常见问题kernel调优