深入文档值编辑

在上一节一开头我们就说文档值(doc values) 是 "更快、更高效并且内存友好" 。 听起来好像是不错的营销术语,不过话说回来文档值到底是如何工作的呢?

文档值是在索引时与倒排索引同时产生的。也就是说文档值是按段来产生的并且是不可变的,正如用于搜索的倒排索引一样。 同样,和倒排索引一样,文档值也序列化到磁盘。这些对于性能和伸缩性很重要。

通过序列化一个持久化的数据结构到磁盘,我们可以依赖于操作系统的缓存来管理内存,而不是在 JVM 堆栈里驻留数据。 当 “工作集(working set)” 数据要小于系统可用内存的情况下,操作系统会自然的将文档值驻留在内存,这将会带来和直接使用 JVM 堆栈数据结构相同的性能。

不过,如果你的工作集远大于可用内存,操作系统会开始根据需要对文档值进行分页开/关。这会显著慢于纯内存驻留的数据结构,当然,它也拥有使用远大于服务器内存容量的伸缩性的好处。 如果这些数据结构是纯粹的存储于 JVM 堆内存,那么唯一的选项只能是随着内存溢出(OutOfMemory)而崩溃(或是实现一个分页模式,正如操作系统的那样)。

注意

因为文档值不是由 JVM 来管理,所以 Elasticsearch 服务器可以配置一个很小的 JVM 堆栈。 这会给操作系统带来更多的内存来做缓存。同时也带来一个好处就是让 JVM 的垃圾回收器工作在一个很小的堆栈,结果就是更快更高效的回收周期。

传统上,我们会建议分配机器内存的 50% 来给 JVM 堆栈。随着文档值的引入,这个建议开始不再适用。 在 64gb 内存的机器上,也许可以考虑给堆栈分配 4-16gb 的内存,而不是之前建议的 32gb。

有关更详细的讨论,查看 堆内存:大小和交换.

列式存储的压缩编辑

从广义来说,文档值本质上是一个序列化的 列式存储 。 正如我们上一节所讨论的,列式存储 擅长某些操作,因为这些数据的存储天然适合这些查询。

而且,他们也同样擅长数据压缩,特别是数字。 这对于节省磁盘空间和快速访问很重要。现代 CPU 的处理速度要比磁盘快几个数量级(尽管即将到来的 NVMe 驱动器正在迅速缩小差距)。 这意味着减少必须从磁盘读取的数据量总是有益的,尽管需要额外的 CPU 运算来进行解压。

要了解它如何帮助压缩数据,来看一组数字类型的文档值:

Doc      Terms
-----------------------------------------------------------------
Doc_1 | 100
Doc_2 | 1000
Doc_3 | 1500
Doc_4 | 1200
Doc_5 | 300
Doc_6 | 1900
Doc_7 | 4200
-----------------------------------------------------------------

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

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

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

文档值正是使用了像这样的一些技巧。它会按依次检测以下压缩模式:

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

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

注意

你也许会想 "好吧,貌似对数字很好,不知道字符串怎么样?" 通过借助顺序表(ordinal table),字符类型也是类似进行编码的。字符类型是去重之后存放到顺序表的,通过分配一个 ID,然后这些 ID 和数值类型的文档值一样使用。 也就是说,字符类型和数值类型一样拥有相同的压缩特性。

顺序表本身也有很多压缩技巧,比如固定长度、变长或是前缀字符编码等等。

禁用文档值编辑

文档值默认对所有字段启用,除了分析字符类型字段。也就是说所有的数字、地理坐标、日期、IP 和不分析( not_analyzed )字符类型。

分析字符类型暂时还不使用文档值。分析流程会产生很多新的 token,这会让文档值不能高效的工作。我们将在 聚合与分析 讨论如何使用分析字符类型来做聚合。

因为文档值默认启用,你可以选择对你数据集里面的大多数字段进行聚合和排序操作。但是如果你知道你永远也不会对某些字段进行聚合、排序或是使用脚本操作?

尽管罕见,但当这些情况出现时,你还是希望有办法来为特定的字段禁用文档值。这回为你节省磁盘空间(因为文档值再也没有序列化到磁盘),也许还能提升些许索引速度(因为不需要生成文档值)。

要禁用文档值,在字段的映射(mapping)设置 doc_values: false 即可。例如,这里我们创建了一个新的索引,字段 "session_id" 禁用了文档值:

PUT my_index
{
  "mappings": {
    "my_type": {
      "properties": {
        "session_id": {
          "type":       "string",
          "index":      "not_analyzed",
          "doc_values": false 
        }
      }
    }
  }
}

通过设置 doc_values: false ,这个字段将不能被用于聚合、排序以及脚本操作

反过来也是可以进行配置的:让一个字段可以被聚合,通过禁用倒排索引,使它不能被正常搜索,例如:

PUT my_index
{
  "mappings": {
    "my_type": {
      "properties": {
        "customer_token": {
          "type":       "string",
          "index":      "not_analyzed",
          "doc_values": true, 
          "index": "no" 
        }
      }
    }
  }
}

文档值被启用来允许聚合

索引被禁用了,这让该字段不能被查询/搜索

通过设置 doc_values: trueindex: no ,我们得到一个只能被用于聚合/排序/脚本的字段。无可否认,这是一个非常罕见的需求,但有时很有用。