Elasticsearch 6.x索引预排序分析

本文翻译自https://www.elastic.co/blog/index-sorting-elasticsearch-6-0,侵删

Elasticsearch 从6.0版本开始,引入了一个索引预排序(index sorting)的功能。使用这个功能,用户可以在文档写入的阶段,按指定的字段规则对文档进行排序。这是一个令人激动的新功能,它将极大的提高Elasticsearch在某些场景下的性能!

本文内容涉及如下几个方面:

  • Lucene 索引预排序功能的实现
  • 几个索引预排序功能提升查询性能的例子
  • 在时序数据中开启索引预排序的注意事项
  • 性能考量

索引预排序在 Lucene 中的实现

Lucene 离线排序工具 IndexSorter

初期,Lucene 曾引入了一个离线的排序工具——IndexSorter。IndexSorter 把需要排序的索引完全复制了一份,将新的复制索引中的文档按用户指定的顺序重新排序。因为排序后的索引是一个新的索引,每次源索引中有新的数据更新,不得不重新执行一遍这个工具。IndexSorter 工具是第一次在索引写入阶段而不是查询阶段对文档进行排序的尝试。

针对索引预排序,社区提出了一个新的概念“early termination”。假设你要遍历出前N个文档,并且文档是按 date 字段排序的。如果索引存储在磁盘上时已经是有序的了,那么我们遍历出前N个文档就可以直接返回,而不需要遍历所有的文档。这就是我们所说的“early termination”。提早的返回查询结果,可以明显的缩短查询响应时间,特别是含有排序的查询。刚才介绍的离线排序的方案不能满足有大量文档更新的场景,这也是为什么最终离线排序方案会被其他方案取代。为了替换离线排序的方案,我们提出了一个新的解决方案,在文档的 merge 阶段进行排序。

Lucene 所做的改进

正常情况下,Lucene 按文档的接收顺序写入,并且分配一个自增的文档id。在segment中的第一个文档的文档ID为0,依次递增。在查询阶段,segment中的文档是按文档id的顺序遍历的。如果某个查询需要遍历符合条件文档的 TOP N,Lucene 需要访问所有符合条件的文档,并建立最大(小)堆进行过滤。在文档数量为百万级别的场景中,这样的排序取前N的场景是非常耗时间的。

每当刷新(refresh)操作被触发,Lucene 会为索引创建一个新的段。新的段包含上一次刷新后的所有新加入的文档。刷新操作之后,新加入的文档才能被搜索。因为刷新操作发生的频率是恒定的,所以 segment 的数量会爆炸式的增长。segment 合并操作会在后台触发以限制 segment 的数量。merge 操作基于某种合适的策略被触发,几个小的 segment 会合并为一个更大的 segment。segment 合并的时候默认还是以文档id为序的。为了取代静态的离线合并工具(如上面提到的 IndexSorter ),引入了一种新的segment合并策略,允许在 segment 合并的时候,按用户指定的字段对文档重新排序。这个新的设计方案,在正确的方向上前进了一大步,允许索引在写入的过程中排序并且只用了 segment 的一些基本信息。如果一些 segment 已经被排序,另外一些新创建的 segment 还没有被排序。所以在合并的阶段,未排序的 segment 会首先进行排序,然后再与其它已经排序的segment进行合并。

这个新的 segment 合并策略已经出现在了 IndexWriterConfig 这个模块配置中的最外层的位置,成为了最重要的合并策略。

然而,一些 benchmark 测试显示,在合并阶段进行排序的性能会以指数递减:

es1.png

https://home.apache.org/~mikemccand/lucenebench/sparseResults.html#index_throughput

造成索引写入性能衰减的原因很简单:重新排序 segment 中的文档,将导致合并操作时间和jvm占用大幅增加。

如上所述,重新排序多个 segment 的耗时很长,我们决定将排序提前到生成索引的阶段。我们把排序的操作提前到新 segment 刷盘的阶段,而不是等到 merge 阶段才排序多个 segment :LUCENE-7579。显然,如果所有 segment 已经是排好序的了,那么 merge 阶段只需要执行一次快速的 merge sort 排序。这个新的算法首次在 Lucene 6.5 被引入,将压测的吞吐指标提升了65%左右。

索引预排序在 Lucene 中有那么长的历史,然后直到最近才被引入到 Elasticsearch 中。 感谢开源社区在这个功能上做的大量的优化和努力,我们终于在 Elasticsearch 6.x 开始解锁了这个功能, 并且期待这个新功能的发布能极大的优化你的使用!

索引预排序实践

尽早返回查询语句的结果

在日常应用中,返回按某个字段排序的 TOP N 是非常常见的。 大多数的情况下,除非对整个数据集遍历并排序,否则 Elasticsearch 不能快速的获得 TOP N 的值。尽管 Doc values 的列式存储可以加快遍历的速度,但是在数据集量级非常大的场景下,效果就不是特别的好了。

有了索引预排序的功能之后,我们现在能指定磁盘上存储文档的顺序,允许 Elasticsearch 尽快的返回查询结果。这里举一个例子,如果我们创建了一个电脑游戏的排行榜,返回成绩最好的前三个玩家。我们可以使用 Elasticsearch 来存储玩家的分数,并且保证数据以分数的维度排序。

es2.jpg
// Get the top 3 player scores (based on the number of points)
GET scores/score/_search
{
  "size": 3,
  "sort": [
      { "points": "desc" }
  ]
}

使用Elasticsearch 6.x 版本中的索引预排序,我们能更高效的存储上面场景中的数据:

es3.png

上面的查询依旧需要返回所有符合条件的文档个数,这会多做很多操作。我们可以让 track_total_hits 这个参数的值为 false 来去掉这个操作:

// Get the top 3 player scores (based on the number of points)
GET scores/score/_search
{
  "size": 3,
  "track_total_hits" : false,
  "sort": [
      { "points": "desc" }
  ]
}

现在,我们应用索引预排序构造了一个非常高效的玩家分数积分榜的查询。

指定索引与排序的字段顺序

继续我们上面玩家积分榜的例子,我们需要在索引写入的时候告诉 Elasticsearch 如果对文档进行排序。我们可以在索引的 settings 里面进行设置:

PUT scores
{
    "settings" : {
        "index" : {
            "sort.field" : "points", 
            "sort.order" : "desc" 
        }
    },
    "mappings": {
        "score": {
            "properties": {
                "points": {
                  "type": "long"
                },
                "playerid": {
                  "type": "keyword"
                },
                "game" : {
                  "type" : "keyword"
                }
            }
        }
    }
}

如上面的例子,文档在写入磁盘时会按照 points 字段的递减序进行排序。

聚合相似结构的文档存储

对相似类型的文档进行排序有很多好处。举例来说,一个名字为“scores”的索引,某些分数来自于游戏“Joust”,这个有些有一些自己特殊的字段,如“top-speed”和“farthest-jump”。另外一个游戏“Dragon's Lair”,含有字段“sword-fight-score”和“goblins-killed”:

// Score for the game "Joust"
{
  "game" : "joust",
  "playerid" : "1234",
  "top-speed" : 212,
  "farthest-jump" : 49
}
// Score for the game "Joust"
{
  "game" : "joust",
  "playerid" : "1234",
  "top-speed" : 212,
  "farthest-jump" : 49
}

将文档按 game 字段排序可以使相似的文档存在一个 segment 。这样做的好处可以加速查询和压缩的比率。 将相似结构的文档存储在一起确实有助于提高压缩的比例,并且 Lucene 可以更高效的存储偏移量信息:

PUT scores
{
    "settings" : {
        "index" : {
            "sort.field" : "game", 
            "sort.order" : "desc" 
        }
    }
}

更高效的 AND 连接查询

使用索引预排序可以提高 AND 连接查询的效率。

还是上面游戏的例子,当一个新玩家加入了游戏后,他应该能够和相同地区,相似等级的其他玩家配对,以便可以开始一局新的游戏。这里有一个简单的查询例子,可以帮助查找相似的玩家,然后让他们开始一局新的游戏:

GET players/player/_search
{
  "size": 3,
  "track_total_hits" : false,
  "query" : { 
    "bool" : {
      "filter" : [
        { "term" : { "region" : "eu" } },
        { "term" : { "game" : "dragons-lair" } },
        { "term" : { "skill-rating" : 9 } },
        { "term" : { "map" : "castle" } } 
      ]
    }
  }
}

让我们来展示下Elasticsearch是如何获取结果的。

es4.png

然后我们配置一下这个索引的排序策略,看能否提高查询效率:

PUT players
{
    "settings" : {
        "index" : {
            "sort.field" : ["region", "game", "skill-rating", "map"], 
            "sort.order" : ["asc", "asc", "asc", "asc"] 
        }
    },
    "mappings": {
        "player": {
            "properties": {
                "playerid": {
                  "type": "keyword"
                },
                "region": {
                  "type": "keyword"
                },
                "skill-rating" : {
                  "type" : "integer"
                },
                "game" : {
                  "type" : "keyword"
                },
                "map" : {
                  "type" : "keyword"
                }
            }
        }
    }
}

现在我们可以看到,所有相似条件的文档都被被存储到了一起:

es5.png

通过使用索引预排序的功能,我们能快速的定位到相似字段条件的文档,是我们的玩家配对查询能更快的得到结果。

索引预排序不适用的场景

开启索引预排序功能后,会比不开启这个功能耗费更多的索引生成时间。在某些用户适用场景下,开启索引预排序会有大约40%-50%的性能下降。基于这个问题,我们需要考虑好我们的业务更关注查询的性能还是写入的性能,这点是非常重要的。如果更关注写性能的业务,开启索引预排序不是一个很好的选择。

下图是一个是否开启索引预排序时写入吞吐的一个对比图。这个压测结果完全基于你的用户使用场景。比如,“geonames”的压测显示索引预排序对写入性能的影响是比较低的(深蓝色的线):

es6.png

https://elasticsearch-benchmarks.elastic.co/index.html#tracks/geonames/nightly/30d

另外一个场景,"NYC Taxis"的压测结果显示写入性能有大幅度的下降:

es7.png

https://elasticsearch-benchmarks.elastic.co/index.html#tracks/nyc-taxis/nightly/30d

在系统设计层面,我们必须仔细的考虑业务使用场景的方方面面,对是否开启索引预排序这个功能进行慎重的权衡。

原文链接:https://www.elastic.co/blog/index-sorting-elasticsearch-6-0

原文作者:Jason Zucchetto & Jim Ferenczi

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java架构师学习

一个今日头条的面试题——LRU原理和Redis实现

很久前参加过今日头条的面试,遇到一个题,目前半部分是如何实现 LRU,后半部分是 Redis 中如何实现 LRU。

5192
来自专栏salesforce零基础学习

salesforce 零基础学习(四十一)Group

 salesforce中,有的时候我们需要将一组用户放进一个Group,用来实现以下主要功能: 1.通过sharing rule设置默认的共享访问; 2.将记录...

2138
来自专栏ImportSource

设计模式-搞个接口,留有余地,让你我不再尴尬

设计模式,Design Patterns,Pattern,翻译为“模式”总感觉不够接地气,用今天的话来说可以叫“套路”。设计模式就是写代码的过程中一些常规打法和...

37312
来自专栏牛客网

腾讯视频C++后台

然后开始面试,面试过程比较凌乱,感觉面试官在想问题问,中间比较尴尬。下面是记得的一下题目:

1382
来自专栏aCloudDeveloper

防御性编程

Author:bakari       Date:2012.8.25 本篇是我根据网上的一些陈述经过整理和总结而得。其中详细的内容我会标注出处。看不懂的可以查看...

2678
来自专栏机器之心

资源 | 这是一份收藏量超过2万6的计算机科学学习笔记

项目地址:https://github.com/CyC2018/Interview-Notebook

1143
来自专栏ThoughtWorks

在项目中透明地引入特性开关

之前曾经推荐过崔立强的《使用功能开关更好地实现持续部署》,介绍Feature Toggle的实践。北京办公室的孟宇现在对这个问题有了新的思考,当我们抛却Spri...

4156
来自专栏郭霖

Android数据库高手秘籍(四)——使用LitePal建立表关联

目前我们已经对LitePal的用法有了一定了解,学会了使用LitePal来创建表和升级表的方式,那么今天就让我们一起继续进阶,探究一下如何使用LitePal来建...

3159
来自专栏玩转全栈

flutter全局数据共享通知方案

让我们先抛开Flutter这个平台说话,如果让你实现数据共享,你能想到的基础方案有哪些。

2.9K18
来自专栏liulun

Nim教程【一】

这应该是国内第一个关于Nim入门的系列教程 什么是Nim 我们先来引述网友 Luikore的一段话: Nim 不是函数式的, 但 ...

4069

扫码关注云+社区

领取腾讯云代金券