请求按某个字段对结果进行排序是Elasticsearch极为常用的操作。我们投入了大量 时间和精力来优化排序查询,以便为用户提供更快的排序查询体验。本篇博文将介 绍我们对数值和日期字段进行的一些排序优化。
1. 排序查询的工作原理
当您想要找到匹配筛选条件的文档,并请求按某个字段对结果进行排序时, Elasticsear-ch 会检查该字段的文档值,找出所有匹配筛选条件的文档,并根据排在 最前面的值选出前K个文档。最复杂的情况莫过于筛选条件极其宽泛(例如 match_all query),此时,我们必须检查并比较某个索引中所有文档的文档值。对 于大的索引来说,这一过程可能需要相当长的时间。
优化特定字段排序查询的方法之一是使用索引排序,并对该字段中的整个索引进行 排序。如果索引按字段进行了排序,那么其文档值也会被排序。因此,要获得按某 个字段排序的前K个文档,我们只需获取前K个文档,甚至不必检查其余的文档, 如此一来,排序查询自然就非常快了。
某字段的文档值(左)和按该字段排序的索引中该字段的相同文档值(右)
索引排序是一个不错的解决方案,但是您只能以单一方式进行索引排序。索引排序 并不适用于具有下列特征的排序查询:采用不同的排序标准,例如降序与升序;采 用不同的字段;或所用组合不同于索引排序定义中指定的组合。因此需要其他更灵 活的方法来提高排序查询的速度。
2. 使用 distance_feature 查询优化数值排序查询
以前,我们通过为每个文档块存储最大影响因素(词频与文档长度的组合)的方式, 显著提高了基于词且按—score排序的查询的速度。在查询期间,我们可以通过查看 文档块的最大影响因素来快速判断其是否具有竞争力。如果某个块不具有竞争力, 我们可以跳过整个文档块,这将极大地加快查询速度。
我们当时觉得有可能利用类似的方法来提高对数值或日期字段的排序查询速度。结 果证明,用distance_feature查询代替排序是可行的。distance_feature查询是一 种值得关注的查询方式,它会返回最接近给定原点的前K个文档。如果我们使用字 段的最小值作为原点,我们将得到按升序排序的前K个文档。使用最大值作为原点 则会按降序为我们提供前K个文档。
对我们来说,distance_feature查询最具吸引力的特点在于,它可以有效跳过无竞 争力的文档块。为实现这一点,它需要依赖于BKD树的属性OBKD树在Elasticsearch 中用于索引数值和日期字段。类似于将文本字段的倒排索引划分为文档块的方法, BKD索引也被划分为单元格,并且每个单元格的最小值和最大值是已知的。因此, 只需检查单元格的最小值和最大值,distance_feature查询就可以有效跳过文档无 竞争力的单元格。要运行这种排序优化方法,数值或日期字段既要有索引又要有文档值。
用distance_feature查询代替对文档值进行排序,使我们可以实现大幅提速(在某 些数据集上高达35倍增益)。我们已在Elasticsearch 76中引入了这种对日期和长 字段的排序优化。
3. 使用 search_after 优化排序查询
我们为所取得的这些提速成果感到欣慰,不过在使用search_after参数进行排序的 问题上,我们还没有找到一个令人满意的解决方案。利用search_after进行排序十 分普遍,因为用户通常不仅会关注搜索结果的首页,也会对后续页面感兴趣。可以 确定的是,相较于当前使用的在Elasticsearch中重写排序查询的方法,更好的解决 方案是让Lucene中的比较器和收集器进行这种排序优化,并跳过无竞争力的文档。 Lucene中的比较器和收集器已支持search_after,得益于此,我们苦于寻找解决方 案的难题也迎刃而解了。我们将distance_feature查询用于跳过无竞争力文档块的 同一个Elasticsearch代码添加到Lucene的数值比较器中。
在Elasticsearch716中,我们已利用search_after参数引入了这种排序优化。我们 发现,在一些夜间性能基准测试中,查询速度大幅提升(高达10倍)效果立竿见 影:
使用search_after 对具有多个段的索引进行排序优化(左)
强制合并到单个段的索引(右)
优化前(2021-09-13),使用 search_after 的降序排序耗时 1400-1800ms,
优化后为200-300ms。
4. 优化跨多个段的排序
—个分片由多个段组成。Elasticsearch在搜索时会按顺序检查段,因此确定具有前 K个值的文档,先从最有可能包含此类文档的段开始处理会事半功倍。一旦收集了 具有前K个值的文档,我们就可以快速跳过仅含有排位靠后的值的其他段。
先从哪些段开始处理在很大程度上取决于用例。对于时间序列索引,最常见的请求 是对时间戳字段的结果进行降序排序,因为人们最关心的是查看最新事件。为了优 化时间序列索引的这种排序,我们首先对@timestamp字段进行降序排序,以便可 以最先处理包含最新数据的段,理想情况下,我们可以按时间戳文档在第一个段中 收集最新的数据并跳过所有其他段。这一方法强有力地提高了时间戳字段的降序排 序查询速度。
当较小的段合并成更大的段时,我们不希望出现最新的段排在最后,而其中最新的 文档也位列末尾的情况。为了更加均衡地合并段,我们引入了新旧段交叉的全新合 并策略,即新旧段的文档在全新的合并段中以混合顺序排列。这也可以让我们高效 查找最新的文档。
5. 优化跨多个分片的排序
Elasticsearch的强大之处在于它的分布式搜索,如果忽略了分布式的特点,任何优 化都无法尽如人意。一些搜索可以跨越数百个分片(例如对时间序列索引的搜索) 对此,从"正确'‘的分片集和不包含具有竞争力命中结果的快捷分片集开始操作将 大有助益。该方法已得到全面落实。
从Elasticsearch7.6开始,我们根据主排序字段的最大值/最小值对分片进行预排序, 以便我们可以从这样一个分片集开始分布式搜索,即其中的分片最有可能包含排在 最前面的值。从Elasticsearch7.7开始,我们利用其他分片的结果实现快捷查询阶 段,即一旦我们从第一个分片集收集了排在最前面的值,就完全可以跳过其他分片, 因为相较于前面分片中计算得到的最后面的排序值,这些分片中所有候选的值还要 更加靠后。
在许多机器生成的时间序列索引中,文档遵循索引生命周期策略,从性能优化的硬 件开始,到成本优化的硬件结束,然后才是删除文档。这种分片跳过机制通常意味 着用户可以发送广泛的查询,并享受由其性能优化的硬件所定义的查询性能,因为 速度较慢和更经济的硬件上的分片会被跳过(使可搜索快照的应用更加高效)。
6. 对用户的影响
作为Elasticsearch的用户,您如何才能利用这些排序优化呢?只有在无需跟踪某个 请求确切的总命中数且该请求不包含聚合时,这些排序优化才会发挥作用。如果您 需要知道确切的总命中数,我们就不能执行任何跳过操作,因为我们需要计算所有 匹配筛选条件的文档。
track_total_hits的默认值设置为10,000,也就是说,排序优化只有在收集了 10,000 个文档时才能开始。如果将此值设置为一个较小的数或“false",那么 Elasticsearch就可以更早地开始排序优化,换言之,您的请求响应速度也会更快。
最近Kibana也开始在track_total_hits被禁用的情况下发送请求,因此Kibana中 的排序查询也应该会更快。