es中的Doc Values与FieldData

1.Doc Values

  在es搜索中使用的是倒排索引的数据结构,而在聚合中使用的是一个叫Doc Values的数据结构,该结构可以使得聚合更快、更高效对内存更友好。
  当你需要在索引中检索一个值时,此时倒排索引的结构非常适合索引,但是当你对该值进行排序时,该结构就不太适合了,因为我们需要获取该文档中该字段对应的值,此时我们就需要正排索引。
  在es中,Doc Values 就是一种列式存储结构,默认情况下,在索引一个文档时,也会创建对应的Doc Values 。
  es中的 Doc Values 常被应用到以下场景:
  (1)对一个字段进行排序。
  (2)对一个字段进行聚合。
  (3)某些过滤,比如地理位置过滤。
  (4)某些与字段相关的脚本计算。
  Doc Values和倒排索引一样,基于段生成且不可变,同时都是序列化到磁盘,所以对性能和扩展有很大帮助。
  因为文档值被序列化到磁盘,所以我们可以充分利用操作系统的内存。当 working set 远小于节点的可用内存,系统会所有的Doc Values值保存在操作系统的内存中,使得其读写十分高速; 当其远大于可用内存,操作系统会根据需要从磁盘读取Doc Values然后选择性的放到分页缓存中。
  因为Doc Values不是由JVM进行管理,所以es可以配置一个更小的JVM Heap,这样可以给系统留下更多的内存。
  由于Doc Values是列式存储,所以这种方式也非常便于压缩,尤其是数字类型,在压缩过程中会使用以下技巧:
  (1)如果所有的数值各不相同(或缺失),设置一个标记并记录这些值。
  (2)如果这些值小于 256,将使用一个简单的编码表。
  (3)如果这些值大于 256,检测是否存在一个最大公约数。
  (4)如果没有存在最大公约数,从最小的数值开始,统一计算偏移量进行编码。
  比如多个doc中的 num 字段分别为[100, 200, 300, 400, 500],构建Doc Values时,会按照以上规则进行构建,它会发现这些字段值的公约数为100, 则它会构建[1, 2, 3, 4, 5]然后保存100作为此段的除数。
  Doc Values默认会对所有不分词的字段开启,需要分词的字段并不适合Doc Values,因为会产生大量的Token, 会大大影响聚合的操作速率。而且会影响聚合的结果,如果你的索引不需要进行排序、聚合等操作你可以通过在设置该索引的mapping时,通过doc_values参数禁用该字段Doc Values,反过来也是可以的,如果你只想该字段参与聚合操作,不想它被搜索到, 可以设置 doc_valuestrue, indexfalse

PUT my_index
{
  "mappings": {
    "my_type": {
      "properties": {
        "session_id": {
          "type":       "string",
          "index":      "not_analyzed",
         "doc_values": false
          "index": "true"    //默认就为true
        }
      }
    }
  }
}

2. FieldData

  我们之前说过,分词的字段不适合分词,如果对分词的字段进行聚合结果很可能会出乎意料。比如下边这个例子。

POST /agg_analysis/data/_bulk
{ "index": {}}
{ "state" : "New York" }
{ "index": {}}
{ "state" : "New Jersey" }
{ "index": {}}
{ "state" : "New Mexico" }
{ "index": {}}
{ "state" : "New York" }
{ "index": {}}
{ "state" : "New York" }
然后对该字段进行分组操作
GET /agg_analysis/data/_search
{
    "size" : 0,
    "aggs" : {
        "states" : {
            "terms" : {
                "field" : "state"
            }
        }
    }
}
然后会得到如下结果
{
...
   "aggregations": {
      "states": {
         "buckets": [
            {
               "key": "new",
               "doc_count": 5
            },
            {
               "key": "york",
               "doc_count": 3
            },
            {
               "key": "jersey",
               "doc_count": 1
            },
            {
               "key": "mexico",
               "doc_count": 1
            }
         ]
      }
   }
}

  我们可以看到,结果中显示的不是某个字段值对应的文档数量,而是某个 token 对应的文档数量。在本文中,我们说过Doc Values不支持分词的字段,因为它不能有效的表示多值字符串,但在上例中,分词的字段仍然可以使用聚合操作,这是为什么呢?
  因为一种叫 FiledData 的数据结构,与Doc Values不同,它由JVM管理,常驻于JVM Heap中。由于FiledData是存储在内存中,所以一些高基数字段在生成 FiledData 是会消耗大量的内存,比如 n-gram的分析过程。
  一旦分析字符串被加载到 fielddata ,他们会一直在那里,直到被驱逐(或者节点崩溃)。
  Fielddata 是延迟加载。只有使用该字段进行聚合操作,才会加载该字段的 filedData 到内存中。此外,fielddata 是基于字段加载的,事实上,filedData 会加载针对该字段的所有文档。
  与Doc Values不同,在索引文档时,不会创建 filedData, 而是在查询运行时,动态填充。因为filedData 是由JVM管理,所以资源有限,所以应该限制 FiledData 所占资源的大小,否则会导致节点不稳定(垃圾回收机制),甚至抛出异常(OOM)。
  可以通过设置indices.fielddata.cache.size控制JVM为 filedData 分配的度空间大小,当一个没有加载过的字段,被分析聚合时,将会加载该字段的 filedData, 如果超过指定的空间,则会通过LRU算法剔除其他的值。默认es永远都不会剔除 filedData 。
  我们都知道当一个字段的 filedData 加载之后,才会知道他所占用的内存大小,如果超过了可用内存, 将会抛出OOM异常。
  es包括一个 fielddata 断熔器 ,该设计就是为了处理上述情况。 断熔器通过内部检查(字段的类型、基数、大小等等)来估算一个查询需要的内存。它然后检查要求加载的 fielddata 是否会超过配置的可用内存大小,如果超过,则将会结束该次查询,抛出异常。该断路器的配置请参考 断路器
  es加载 FiledData 是延迟加载,对于一些小的 filedData 加载时间会非常短,但是对于一些非常大的 filedData 所需时间有可能数十秒,这个过程对用户来说是不可能接受的,所以有以下三种解决这种问题:
(1) 预加载 FiledData
  启动预加载,可以使得在新分段创建过程,就将该字段的filedData 加载到内存中。

PUT /music/_mapping/_song
{
  "tags": {
    "type": "string",
    "fielddata": {
     "loading" : "eager"  // lazy
    }
  }
}

(2)全局序号
  当该文档中的某个字段有很多重复的值时,比如 status1 , status2, status3 等等,它将会给这些状态保存一份状态到序号的映射,该全局序号是一个构建在 fielddata 之上的数据结构, 而且跨所有分段识别的,在进行聚合的过程中,将使用这些全局序号进行操作,在聚合完成之后,才会将这些序号替换成真实的值将结果返回。

PUT /music/_mapping/_song
{
  "song_title": {
    "type": "string",
    "fielddata": {
      "loading" : "eager_global_ordinals"
    }
  }
}

  可以通过上述命令进行开启某个字段的全局序号,注意:序号的加载只适用于字符串,数值、地理、日期等不需要,因为本质上自己本身就是序号映射。
  当然天下没有免费的午餐, 全局序号分布在索引的所有段中,所以如果新增或删除一个分段时,需要对全局序号进行重建。 重建需要读取每个分段的每个唯一项,基数越高(即存在更多的唯一项)这个过程会越长。
(3)索引预热器
  该方式早于前两种方式出现,主要是使我们的一个查询或者聚合需要在新分片被搜索之前执行,

PUT /music/_warmer/warmer_id
{
  "query" : {
    "bool" : {
      "filter" : {
        "bool": {
          "should": [
            { "term": { "tag": "rock"        }},
            { "term": { "tag": "hiphop"      }},
            { "term": { "tag": "electronics" }}
          ]
        }
      }
    }
  },
  "aggs" : {
    "price" : {
      "histogram" : {
        "field" : "price",
        "interval" : 10
      }
    }
  }
}

  如上例所示,预热器基于索引进行创建的,每个预热器有唯一的id, 每个索引包括多个预热器。
  当新建一个分段时,es会首先执行注册在该索引中的所有预热器,目的使用来手动加载 filedData ,和进行过滤器缓存, 用来提高搜索和聚合效率。

3.优化聚合查询

  es允许我们改变聚合的集合模式默认的的策略叫做 深度优先 , 先构建完整的树,然后修剪无用节点。另一种集合策略叫做广度优先 。这种策略的工作方式有些不同,它先执行第一层聚合, 再继续下一层聚合之前会先做修剪。
  比如接下来的这个例子,我们想找到今年最受欢迎的3个演员,和与他们合作最多的3个演员。

{
  "aggs" : {
    "actors" : {
      "terms" : {
         "field" : "actors",
         "size" :  3,
         "collect_mode" : "depth_first"
      },
      "aggs" : {
        "cooperationActors" : {
          "terms" : {
            "field" : "cooperations",
            "size" :  3
          }
        }
      }
    }
  }
}

  es会按照默认的集合模式深度优先进行构建结果,执行流程如下:
  (1)现获取所有的演员,然后根据doc_count进行排序。
  (2)获取该演员的合作过的所有演员,然后根据doc_count进行排序。
  (3)裁剪结果,首先裁剪出第一步中前三个结果,然后在分别裁剪出这三个结果中的前三个结果。然后将结果返回。
  这样的工作方式对大多数聚合操作都能正常工作,但是对于该例不太适合,应为我们只需要知道前三个结果就可以了,不需要再为其他的演员构建结果树了。所以我们可以将collect_mode参数设置为breadth_first。这样该流程的第一步就会变为如下:
  获取所有的演员,然后根据doc_count进行排序,然后裁剪出前三条结果......。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容