天天看点

ElasticSearch学习笔记之八 Doc Values and FielddataDoc Values 简介Doc values 的原理深入理解 Doc ValuesAnalyzed strings and Fielddata

ElasticSearch学习笔记之八 Doc Values and Fielddata

  • Doc Values 简介
  • Doc values 的原理
  • 深入理解 Doc Values
    • 列式存储的压缩
    • 禁用 Doc Values
  • Analyzed strings and Fielddata

Doc Values 简介

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

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

    转置

    倒排索引。

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

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

Doc Values

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

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

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

排序发生在索引时建立的平行数据结构(Doc Values )中。

doc values 的数据结构 可以使聚合更快、更高效并且对内更存友好,所以理解它的工作方式十分有益。

Doc values 的原理

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

对于以下倒排索引:

Term Doc_1 Doc_2 Doc_3
brown X X
dog X X
dogs X X
fox X X
foxes X
in X
jumped X X
lazy X X
leap X
over X X X
quick X X X
summer X
the X X

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

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

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

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

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

Doc Terms
Doc_1 brown, dog, fox, jumped, lazy, over, quick, the
Doc_2 brown, dogs, foxes, in, lazy, leap, over, quick, summer
Doc_3 dog, dogs, fox, jumped, over, quick, the

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

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

注意:

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

深入理解 Doc Values

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

Doc Values 通过序列化把数据结构持久化到磁盘,我们可以充分利用操作系统的内存,而不是 JVM 的 Heap 。 当 working set 远小于系统的可用内存,系统会自动将 Doc Values 驻留在内存中,使得其读写十分快速;不过,当其远大于可用内存时,系统会根据需要从磁盘读取 Doc Values,然后选择性放到分页缓存中。很显然,这样性能会比在内存中差很多,但是它的大小就不再局限于服务器的内存了。如果是使用 JVM 的 Heap 来实现那么只能是因为 OutOfMemory 导致程序崩溃了。

注意:

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

列式存储的压缩

从广义来说,Doc Values 本质上是一个序列化的 列式存储 。列式存储 适用于聚合、排序、脚本等操作。

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

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

Doc Values

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 的倍数,Doc Values 会检测一个段里面的所有数值,并使用一个 最大公约数 ,方便做进一步的数据压缩。

如果我们保存 100 作为此段的除数,我们可以对每个数字都除以 100,然后得到:

[1,10,15,12,3,19,42]

。现在这些数字变小了,只需要很少的位就可以存储下,也减少了磁盘存放的大小。

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

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

你会发现这些压缩模式不是传统的通用的压缩方式,比如 DEFLATE 或是

LZ4

。 因为列式存储的结构是严格且良好定义的,我们可以通过使用专门的模式来达到比通用压缩算法(如 LZ4 )更高的压缩效果。

注意:

通过借助顺序表(ordinal table),String 类型也是类似进行编码的。String 类型是去重之后存放到顺序表的,通过分配一个 ID,然后通过数字类型的 ID 构建 Doc Values。这样 String 类型和数值类型可以达到同样的压缩效果。

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

禁用 Doc Values

Doc Values 默认对所有字段启用,除了 analyzed strings。也就是说所有的数字、地理坐标、日期、IP 和不分析( not_analyzed )字符类型都会默认开启。

analyzed strings 暂时还不能使用 Doc Values。文本经过分析流程生成很多

Token

,使得 Doc Values 不能高效运行。我们将在 聚合与分析 讨论如何使用分析字符类型来做聚合。

因为 Doc Values 默认启用,你可以选择对你数据集里面的大多数字段进行聚合和排序操作。但是如果你知道你永远也不会对某些字段进行聚合、排序或是使用脚本操作? 尽管这并不常见,但是你可以通过禁用特定字段的 Doc Values 。这样不仅节省磁盘空间,也许会提升索引的速度。

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

Doc Values

PUT my_index
{
  "mappings": {
    "my_type": {
      "properties": {
        "session_id": {
          "type":       "text",
          "index":      "flase",
          "doc_values": false 
        }
      }
    }
  }
}
           

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

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

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

Doc Values 被启用来允许聚合。

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

通过设置 doc_values: true 和 index: false ,我们得到一个只能被用于聚合/排序/脚本的字段。无可否认,这是一个非常少见的情况,但有时很有用。

Analyzed strings and Fielddata

Doc values 不支持 analyzed 字符串字段,因为它们不能很有效的表示多值字符串。 Doc values 最有效的是,当每个文档都有一个或几个 tokens 时, 但不是无数的,分析字符串(想象一个 PDF ,可能有几兆字节并有数以千计的独特 tokens)。

出于这个原因,doc values 不生成分析的字符串,然而,这些字段仍然可以使用聚合,那怎么可能呢?

答案是一种被称为 fielddata 的数据结构。与 doc values 不同,fielddata 构建和管理 100% 在内存中,常驻于 JVM 内存堆。这意味着它本质上是不可扩展的,有很多边缘情况下要提防。 本章的其余部分是解决在分析字符串上下文中 fielddata 的挑战。

注意:

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

继续阅读