一、doc_values介绍
doc values是一个我们再三重复的重要话题了,你是否意识到一些东西呢?
- 搜索时,我们需要一个“词”到“文档”列表的映射
- 排序时,我们需要一个“文档”到“词“列表的映射,换句话说,我们需要一个在倒排索引的基础上建立的“正排索引”
这里的“正排索引”结构通常在其他系统中(如关系型数据库)被称为“列式存储”。本质上,它是在数据字段的一列上存储所有value,这种结构在某些操作上会表现得很高效,比如排序。
在ES里这种“列式存储”就是我们熟悉的“doc values”,默认情况下它是被启用的,doc values在index-time(索引期)被创建:当一个字段被索引时,ES会把“词”加入到倒排索引中,同时把这些词也加入到面向“列式存储”的doc values中(存储在硬盘上)。
doc values通常被应用在以下几个方面:
- 基于一个字段排序
- 基于一个字段聚合
- 执行某些filter上(如:geolocation filter)
- 在script(脚本)中引用了一个或多个字段
由于doc values在索引期被序列化到硬盘上,我们可以利用操作系统去快速的访问它们,关于doc values在磁盘上是如何被管理的,后面会讲到。
大多数的字段默认情况下都会被索引,这使得他们可以被搜索到,倒排索引允许一个查询基于一个词表排序,也可以快速访问包含某个词的文档列表。
排序、聚合,和在脚本中访问一些字段值时都需要另一种不同的访问方式,因为倒排索引不支持这种访问,所以我们需要一种结构能查询到文档到词的映射。
doc values是在索引期创建基于磁盘的数据结构,这种结构使得上述访问成为可能。doc values支持绝大部分字段类型,除了“analyzed”类型的string字段。
所有的字段都默认支持doc values,如果你确定你不需要在某个字段上排序或者聚合或者在脚本中访问,你可以disable掉:
- status_code字段默认开启doc_values
- session_id字段禁用了doc_values,虽然被禁用但是还是可以被查询
TIP:doc_values可以在同一个索引的同名字段上设置不同值,它也可以基于一个已存在的字段使用put mapping api来禁用它。
看如下的倒排索引结构:
如果我们想为每一个包含“brown”的文档编辑一份完整的词列表,我们可能会用如下查询:
看上面的查询部分。倒排索引通过词条排好了序,所以我们首先找到包含“brown”的词条列表,然后跨列扫描所有包含“brown”的文档,这里我们很幸运的找到了“Doc_1”和“Doc_2”。
然后在聚合部分,我们需要找Doc_1和Doc_2中找到所有的词,在倒排索引的去做这个操作很非常昂贵的:意味着我们不得不迭代索引中的每一个词,看它们是否包含在doc_1和doc_2中,这个过程是非常缓慢的,而且也是非常傻逼的:因为随着文档词量的增加,我们聚合的执行时间也会增加。
让我们看看下面的结构:
有了这个结构我们就会很容易得到doc_1和doc_2所包含的词条,我们只需要通过上面的结构把两个集合合并起来就行了。
因此,查询和聚合是非常复杂的,查询文档使用的是倒排索引,聚合文档使用的是正排索引(doc_values)
note:doc values不仅仅是用在聚合中,还被用在排序、脚本、子父文档关系(这里暂不做介绍)。
二、深入Doc Values
前面讲到的doc values给我们几个印象:快速访问、高效、基于硬盘。现在我们来看看doc values到底是如何工作的?
doc values是在“索引期“随着倒排索引一起生成的,也就是说 doc values是基于每个索引段生成且是不可改变的(immutable), 和倒排索引一样,doc values也会被序列化到磁盘上,这使得它具有了高效性和可扩展性。
通过序列化一个数据结构到磁盘上,我们可以依赖操作系统的 file system cache 替代JVM的堆内存,当我们的“工作集”小于OS可用内存时,操作系统会自然的加载这些doc values到内存。这时doc values的性能和在JVM堆内存中表现是一样的。
但是当工作集大于操作系统可用内存时,操作系统将会按需加载doc values,这种情况下的访问速度会明显的慢于全量加载doc values的时候。但这种操作使得我们的服务器内存利用率远超过服务器最大内存限制。试想一下,如果全量加载到doc values到内存中势必会造成ES OutOfMemery。
NOTE:由于doc values不受JVM堆内存管理,所以我们可以把ES对内存设置得小一点,把更多的内存留给操作系统来换出(doc values),同时这也可以使JVM的GC工作在更小的堆内存上,更快更高效的执行GC。
通常,我们配置JVM的堆内存基本和操作系统内存各占一半(50%),由于引进了doc values所以我们可以考虑把JVM的堆内存设置得小一些,比如我们可以在一个64G的服务器上设置JVM堆内存为4 – 16GB比设置堆内存为32G更加高效。
三、Column-store compression(列式存储压缩)
本质上doc values是一个被序列化的面向“列式储存”的结构,我们前面讨论过列式存储在某些查询操作上是有优势的,不仅如此它们也更擅长数据压缩,特别是数字,这对磁盘存储和快速访问来说是及其重要的。
为了了解它是如何压缩数据的,我们看下面简单的doc values结构
像上面这种每行一条数据的形式,我们可以得到连续的数字块,如:[100,1000,1500,1200,300,1900,4200]。因为我们知道它们都是数字值可以被排列在一起通过一个一致的偏移量。
跟深层次的,这里有几种压缩方法可以运用在这些数字上。你可能知道上面的数字都是100的倍数,如果索引段上所有的的数字都共享一个“最大公约数”,那么就可以用这个最大公约数去压缩数据。如上面的数字我们可以除以100,得到的数据是[1,10,15,12,3,19,42]。这样这些数字会变得小一些,存储时占用的比特数也会小一些。
doc values使用几种手段来压缩数字。
- 如果所有的数字值都相等(或者缺失),会设置一个标记来表示该值
- 如果所有数字值的个数小于256个,将会使用一个简单的编码表来压缩
- 如果大于了256个,看看是否存在最大公约数,存在则使用最小公倍数压缩
- 如果不存在最大公约数,则存储偏移量来压缩数字。
如你看到的,你可能会想“这样做对数值型字段做压缩确实很好,那么对字符串类型呢?”,其实字符串压缩也是和数字压缩一样采用同样的方法通过一个序数表来压缩,字符串被去重、排序后被赋予了一个ID,这些ID就是数字,这样就可以采用上面的方案进行压缩了。对于序数表本身也会采用压缩存储。