doc_values

当你对一个字段进行排序时,Elasticsearch 需要访问每个匹配到的文档得到相关的值。倒排索引的检索性能是非常快的,但是在字段值排序时却不是理想的结构。

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

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

Elasticsearch 中,Doc Values 就是一种列式存储结构,默认情况下每个字段的 Doc Values 都是激活的,Doc Values 是在索引时创建的,当字段索引时,Elasticsearch 为了能够快速检索,会把字段的值加入倒排索引中,同时它也会存储该字段的 Doc Values

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

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

因为文档值被序列化到磁盘,我们可以依靠操作系统的帮助来快速访问。当 working set 远小于节点的可用内存,系统会自动将所有的文档值保存在内存中,使得其读写十分高速; 当其远大于可用内存,操作系统会自动把 Doc Values 加载到系统的页缓存中,从而避免了 jvm 堆内存溢出异常。

该平行数据结构是在文档索引时建立的。

倒排索引与 doc_values 区别

对于以下倒排索引:

  1. Term Doc_1 Doc_2 Doc_3
  2. ------------------------------------
  3. brown | X | X |
  4. dog | X | | X
  5. dogs | | X | X
  6. fox | X | | X
  7. foxes | | X |
  8. in | | X |
  9. jumped | X | | X
  10. lazy | X | X |
  11. leap | | X |
  12. over | X | X | X
  13. quick | X | X | X
  14. summer | | X |
  15. the | X | | X
  16. ------------------------------------

如果我们想要获得所有包含 brown 的文档的词的完整列表,我们会创建如下查询:

  1. GET /my_index/_search
  2. {
  3. "query" : {
  4. "match" : {
  5. "body" : "brown"
  6. }
  7. },
  8. "aggs" : {
  9. "popular_terms": {
  10. "terms" : {
  11. "field" : "body"
  12. }
  13. }
  14. }
  15. }

查询部分简单又高效。倒排索引是根据项来排序的,所以我们首先在词项列表中找到 brown ,然后扫描所有列,找到包含 brown 的文档。我们可以快速看到 Doc_1Doc_2 包含 brown 这个 token。

然后,对于聚合部分,我们需要找到 Doc_1Doc_2 里所有唯一的词项。 用倒排索引做这件事情代价很高: 我们会迭代索引里的每个词项并收集 Doc_1Doc_2 列里面 token。这很慢而且难以扩展:随着词项和文档的数量增加,执行时间也会增加。

Doc values 通过转置两者间的关系来解决这个问题。倒排索引将词项映射到包含它们的文档,doc values 将文档映射到它们包含的词项:

  1. Doc Terms
  2. -----------------------------------------------------------------
  3. Doc_1 | brown, dog, fox, jumped, lazy, over, quick, the
  4. Doc_2 | brown, dogs, foxes, in, lazy, leap, over, quick, summer
  5. Doc_3 | dog, dogs, fox, jumped, over, quick, the
  6. -----------------------------------------------------------------

当数据被转置之后,想要收集到 Doc_1Doc_2 的唯一 token 会非常容易。获得每个文档行,获取所有的词项,然后求两个集合的并集。

因此,搜索和聚合是相互紧密缠绕的。搜索使用倒排索引查找文档,聚合操作收集和聚合 doc values 里的数据。

Doc values 不仅可以用于聚合。 任何需要查找某个文档包含的值的操作都必须使用它。 除了聚合,还包括排序,访问字段值的脚本,父子关系处理

深入理解

默认情况下,大多数字段已被索引,这使得他们可以被搜索。通过倒排索引,查询请求可以在词条的唯一排序列表中查询词条,这样,可以立即获取到包含该词条的文档。

排序、聚合、访问脚本中字段的值,需要一种不同的数据访问方式。除了可以查询词条找到相应的文档,我们还需要能够查找文档,并且找到其字段中有的词条。

Doc值是在文档索引时构建的磁盘数据结构,这使这种数据访问模式成为可能。它们存储与 _source相同的值,但是以列的方式存储,这对于排序和聚合而言更为有效。除了分词的字段上(text 类型),doc_values 在所有的字段上都支持。

支持 doc_values 的所有字段都是默认开启的。如果你确定不需要在某个字段上做排序、聚合或者是通过脚本访问,你可以禁用它来节省磁盘空间。

  1. PUT my_index
  2. {
  3. "mappings": {
  4. "_doc": {
  5. "properties": {
  6. "status_code": {
  7. "type": "keyword"
  8. },
  9. "session_id": {
  10. "type": "keyword",
  11. "doc_values": false
  12. }
  13. }
  14. }
  15. }
  16. }

如上所示:

status_code 默认开起来 doc_values;

session_id 禁用了 doc_values,但是依然可以索引。

说明

Doc Values 是在索引时与 倒排索引 同时生成。也就是说 Doc Values倒排索引 一样,基于 Segement 生成并且是不可变的。同时 Doc Values倒排索引 一样序列化到磁盘,这样对性能和扩展性有很大帮助。

因为 Doc Values 不是由 JVM 来管理,所以 Elasticsearch 实例可以配置一个很小的 JVM Heap,这样给系统留出来更多的内存。同时更小的 Heap 可以让 JVM 更加快速和高效的回收。

之前,我们会建议分配机器内存的 50% 来给 JVM Heap。但是对于 Doc Values,这样可能不是最合适的方案了。 以 64gb 内存的机器为例,可能给 Heap 分配 4-16gb 的内存更合适,而不是 32gb

列式存储的压缩

这种存储方式也非常便于压缩,特别是数字类型。这样可以减少磁盘空间并且提高访问速度。现代 CPU 的处理速度要比磁盘快几个数量级(尽管即将到来的 NVMe 驱动器正在迅速缩小差距)。所以我们必须减少直接存磁盘读取数据的大小,尽管需要额外消耗 CPU 运算用来进行解压。

要了解它如何压缩数据的,来看一组数字类型的 Doc Values

  1. Doc Terms
  2. -----------------------------------------------------------------
  3. Doc_1 | 100
  4. Doc_2 | 1000
  5. Doc_3 | 1500
  6. Doc_4 | 1200
  7. Doc_5 | 300
  8. Doc_6 | 1900
  9. Doc_7 | 4200
  10. -----------------------------------------------------------------

按列布局意味着我们有一个连续的数据块: [100,1000,1500,1200,300,1900,4200] 。因为我们已经知道他们都是数字(而不是像文档或行中看到的异构集合),所以我们可以使用统一的偏移来将他们紧紧排列。

而且,针对这样的数字有很多种压缩技巧。你会注意到这里每个数字都是 100 的倍数,Doc Values 会检测一个段里面的所有数值,并使用一个 最大公约数 ,方便做进一步的数据压缩。

如果我们保存 100 作为此段的除数,我们可以对每个数字都除以 100,然后得到: [1,10,15,12,3,19,42] 。现在这些数字变小了,只需要很少的位就可以存储下,也减少了磁盘存放的大小。

Doc Values 在压缩过程中使用如下技巧。它会按依次检测以下压缩模式:

  1. 如果所有的数值各不相同(或缺失),设置一个标记并记录这些值
  2. 如果这些值小于 256,将使用一个简单的编码表
  3. 如果这些值大于 256,检测是否存在一个最大公约数
  4. 如果没有存在最大公约数,从最小的数值开始,统一计算偏移量进行编码

你会发现这些压缩模式不是传统的通用的压缩方式,比如 DEFLATE 或是 LZ4。 因为列式存储的结构是严格且良好定义的,我们可以通过使用专门的模式来达到比通用压缩算法(如 LZ4 )更高的压缩效果。

参看附录1。

附录

  1. https://www.elastic.co/guide/cn/elasticsearch/guide/current/_deep_dive_on_doc_values.html
  2. https://www.elastic.co/guide/en/elasticsearch/reference/6.3/doc-values.html