vlambda博客
学习文章列表

让面试官不停点头的Elasticsearch排序进阶优化方案

请求按某个字段对结果进行排序是 Elasticsearch 极为常用的操作。为用户实现更快的排序查询,官方投入了大量时间和精力来对优化排序查询,本文将介绍对数值和日期字段进行的一些排序优化。

1

排序查询的工作原理

当想要找到匹配筛选条件的文档,并请求按某个字段对结果进行排序时,Elasticsearch 会检查该字段的文档值,找出所有匹配筛选条件的文档,并根据排在最前面的值选出前 K 个文档。最复杂的情况莫过于筛选条件极其宽泛(例如 match_all query),此时,我们必须检查并比较某个索引中所有文档的文档值。对于大的索引来说,这一过程可能需要相当长的时间。


优化特定字段排序查询的方法之一是使用索引排序,并对该字段中的整个索引进行排序。如果索引按字段进行了排序,那么其文档值也会被排序。因此,要获得按某个字段排序的前 K 个文档,我们只需获取前 K 个文档,甚至不必检查其余的文档,如此一来,排序查询自然就非常快了。

某字段的文档值 (左)和按该字段排序的索引中该字段的相同文档值(右)。


索引排序是一个不错的解决方案,但是只能以单一方式进行索引排序。索引排序并不适用于具有下列特征的排序查询:采用不同的排序标准,例如降序与升序;采用不同的字段;或所用组合不同于索引排序定义中指定的组合。因此需要其他更灵活的方法来提高排序查询的速度。

2

使用 distance_feature 查询优化数值排序查询

之前,通过为每个文档块存储最大影响因素(词频与文档长度的组合)的方式,可以显著提高基于词且按 _score 排序的查询的速度。在查询期间,可以通过查看文档块的最大影响因素来快速判断其是否具有竞争力。如果某个块不具有竞争力,可以跳过整个文档块,这将极大地加快查询速度。


用 distance_feature 查询代替排序是一种可行方法。distance_feature,它会返回最接近给定原点的前 K 个文档。如果使用字段的最小值作为原点,可以得到按升序排序的前 K 个文档。使用最大值作为原点则会按降序为我们提供前 K 个文档。


对我们来说,distance_feature 查询最具吸引力的特点在于,它可以有效跳过无竞争力的文档块。为实现这一点,它需要依赖于BKD树的属性。BKD 树在 Elasticsearch 中用于索引数值和日期字段。类似于将文本字段的倒排索引划分为文档块的方法,BKD 索引也被划分为单元格,并且每个单元格的最小值和最大值是已知的。因此,只需检查单元格的最小值和最大值,distance_feature 查询就可以有效跳过文档无竞争力的单元格。要运行这种排序优化方法,数值或日期字段既要有索引又要有文档值。


用 distance_feature 查询代替对文档值进行排序,可以实现大幅提速(在某些数据集上高达 35 倍增益)。官方已在 Elasticsearch 7.6 之后的版本引入了这种对日期和长字段的排序优化。

3

使用 search_after 优化排序查询

在使用 search_after 参数进行排序的问题上,到目前为止还没有找到一个完美的解决方案。利用 search_after 进行排序十分普遍,因为用户通常不仅会关注搜索结果的首页,也会对后续页面感兴趣。不过,相较于当前使用的在 Elasticsearch 中重写排序查询的方法,更好的解决方案是让 Lucene 中的比较器和收集器进行这种排序优化,并跳过无竞争力的文档。Lucene 中的比较器和收集器已支持 search_after,因此,这个问题也迎刃而解了。distance_feature 查询用于跳过无竞争力文档块的同一个 Elasticsearch 代码添加到 Lucene 的数值比较器中。


在 Elasticsearch 7.16 中,官方已利用 search_after 参数引入了这种排序优化。我们发现,在经过PMC测试后,发现查询速度大幅提升(高达 10 倍),效果立竿见影:

让面试官不停点头的Elasticsearch排序进阶优化方案

使用 search_after 对具有多个段的索引进行排序优化(左),和强制合并到单个段的索引(右)。优化前,使用 search_after 的降序排序耗时 1400-1800 ms,优化后为 200-300 ms。


4

使用 search_after 优化排序查询

一个分片由多个段组成。Elasticsearch 在搜索时会按顺序检查段,因此确定具有前 K 个值的文档,先从最有可能包含此类文档的段开始处理会事半功倍。一旦收集了具有前 K 个值的文档,就可以快速跳过仅含有排位靠后的值的其他段。


先从哪些段开始处理在很大程度上取决于用例。对于时间序列索引,最常见的请求是对时间戳字段的结果进行降序排序,因为用户最关心的是查看最新事件。为了优化时间序列索引的这种排序,首先应对 @timestamp 字段进行降序排序,以便可以最先处理包含最新数据的段,理想情况下,可以按时间戳文档在第一个段中收集最新的数据并跳过所有其他段。这一方法强有力地提高了时间戳字段的降序排序查询速度。


当较小的段合并成更大的段时,一般不希望出现最新的段排在最后,而其中最新的文档也位列末尾的情况。为了更加均衡地合并段,ES引入了新旧段交叉的全新合并策略,即新旧段的文档在全新的合并段中以混合顺序排列。这样可以高效查找最新的文档。

5

优化跨多个分片的排序

Elasticsearch 的强大之处在于它的分布式搜索,如果忽略了分布式的特点,任何优化都无法尽如人意。一些搜索可以跨越数百个分片(例如对时间序列索引的搜索),对此,从“正确”的分片集和不包含具有竞争力命中结果的快捷分片集开始操作将大有助益。该方法已得到全面落实。从 Elasticsearch 7.6 开始,ES根据主排序字段的最大值/最小值对分片进行预排序,以便可以从这样一个分片集开始分布式搜索,即其中的分片最有可能包含排在最前面的值。从 Elasticsearch 7.7 开始,ES利用其他分片的结果实现快捷查询阶段,即一旦我们从第一个分片集收集了排在最前面的值,就完全可以跳过其他分片,因为相较于前面分片中计算得到的最后面的排序值,这些分片中所有候选的值还要更加靠后。在许多机器生成的时间序列索引中,文档遵循索引生命周期策略,从性能优化的硬件开始,到成本优化的硬件结束,然后才是删除文档。这种分片跳过机制通常意味着用户可以发送广泛的查询,并获得由其性能优化的硬件所定义的查询性能,因为低配置的硬件上的分片会被跳过。

让面试官不停点头的Elasticsearch排序进阶优化方案

球分享

球点赞

球在看