JD.COM面试问题:ElasticSearch深度分页解决方案
在关系数据库的使用中,我们被告知要注意甚至明确禁止使用深度分页。同样,在Elasticsearch中,我们应该尽量避免使用深度分页。
本文主要介绍Elasticsearch中分页相关的内容!
在ES中,缺省情况下,分页查询返回前10个匹配命中。
如果需要分页,需要使用from和size参数。
基本的ES查询语句是这样的:
上述查询指示从100条搜索结果开始的10条数据。
那么,这个查询是如何在ES集群中执行的呢?」
在ES中,搜索一般包括两个阶段,查询阶段和取货阶段,可以简单理解。查询阶段决定取哪个单据,取的阶段取出具体的单据。
如上图所示,描述了搜索请求的查询阶段:。
在上面的示例中,协调节点获得(from+size) * 6条数据,然后对它们进行合并和排序,然后选择前面的from+size条数据存储在优先级队列中,供提取阶段使用。
另外,每个切片返回给协调节点的数据用来选择第一个from+size数据,所以只需要返回唯一标记文档的_id和用于排序的_score,也可以保证返回的数据量足够小。
在协调节点计算出自己的优先级队列后,查询阶段结束并进入获取阶段。
查询阶段知道获取什么数据,但是它不获取特定的数据,这是获取阶段应该做的。
上图显示了获取过程:
协调节点的优先级队列中有from+size _ doc _ id,但是在获取阶段,不需要检索所有数据。在上面的例子中,不需要检索前100条数据,只需要检索优先级队列中的101到110条数据。
要取的数据可能在不同的片上,也可能在同一个片上,协调节点使用“multi-get”避免多次去同一个片上取数据,从而提高性能。
"以这种方式请求深度分页是有问题的:"
我们可以假设我们在一个有五个主要部分的索引中进行搜索。当我们请求结果的第一页(结果范围从1到10)时,每个片生成前10个结果,并将它们返回给协调节点,协调节点对50个结果进行排序,以获得所有结果的前10个结果。
现在假设我们请求页面1000——结果是从10001到10010。除了每个切片都必须产生前10010个结果之外,所有这些都以相同的方式工作。然后,协调节点对所有50050个结果进行排序,并最终丢弃这些结果中的50040个。
“排序结果的成本随着分页的深度呈指数增长。」
"注释1:"
大小的大小不能超过参数index.max_result_window的设置,默认值为10000。
如果搜索大小大于10000,则需要设置index.max_result_window参数。
"注2:"
_doc将在未来版本中删除。参见:
Elasticsearch的From/Size模式提供了分页的功能,同时也有相应的限制。
比如一个索引有1亿个数据,分成10个分片,然后一个搜索请求,from=100000,size=100,会带来严重的性能问题:CPU,内存,IO,网络带宽。
在查询阶段,每个shards需要向协调节点返回1000100条数据,协调节点需要接收10 * 1000,100条数据,即使每条数据只有_doc _id和_score,数据量也是非常大的。
“另一方面,我们意识到这种深度分页请求是不合理的,因为我们很少人工查看非常晚的请求。在很多业务场景下,分页是直接受限的,比如我们只能看第100页。」
比如拥有10万粉丝的微信大V,需要给所有粉丝发消息,或者给某个省份的粉丝发消息。这时候就要获取所有合格的粉丝,最容易想到的就是用from+size来实现。然而,这是不现实的。这时可以使用Elasticsearch提供的其他方式来实现遍历。
深度分页问题可以大致分为两类:
"下面是一些官方的深度分页方法."
我们可以将滚动理解为关系数据库中的光标。所以scroll不适合实时搜索,更适合后台批量处理任务,比如批量投放。
这种分页的用法是“不是实时查询数据”,而是一次性查询大量数据(甚至所有数据)。
因为此滚动相当于维护当前索引段的快照,所以当您执行此滚动查询时,此快照信息就是快照。在此快照中将找不到此查询后任何新索引的数据。
但与from和size相比,它并不是查询所有数据然后剔除不必要的部分,而是记录一个阅读位置,以保证下一次快速阅读。
不考虑排序时,可以和SearchType.SCAN结合使用。
滚动可以分为两部分:初始化和遍历。初始化时,“所有符合搜索条件的搜索结果都被缓存(注意这只是缓存的doc_id,并不是所有的文档数据都被实际缓存,数据检索是在fetch阶段完成的)”,可以想象成一个快照。
遍历时,数据是从这个快照中取出的,也就是初始化后,在索引中插入、删除、更新数据都不会影响遍历结果。
“基本用途”
初始化表示索引和类型,然后,添加参数scroll表示暂时存储搜索结果的时间,剩下的就像普通的搜索请求一样。
将为下一次数据检索返回_scroll_id,_scroll_id。
“遍历性”
这里的scroll_id是从上一次遍历中检索到的_scroll_id或者初始化返回的_scroll_id。同样,scroll参数也是必需的。
重复此步骤,直到返回的数据为空,即遍历完成。
注意,每次刷新搜索结果的缓存时间都要传递参数scroll。另外,“不需要指定索引和类型”。
设置scroll时,需要缓存搜索结果,直到下一次遍历完成。“同时,也不应该太久。毕竟空间有限。」
“优点和缺点”
缺点:
"优势:"
适用于大量数据的非实时处理,如数据迁移或索引更改。
ES提供了滚动扫描的方式来进一步提高遍历性能,但是滚动扫描不支持排序,所以滚动扫描适用于不需要排序的场景。
“基本用途”
滚动扫描的遍历和普通滚动是一样的,只是初始化有点区别。
需要指定参数:
“滚动扫描和滚动之间的区别”
如果你的数据量很大,用Scroll遍历数据真的是无法接受的。现在滚动界面可以并发地遍历数据。
每个滚动请求可以分为多个切片请求,可以理解为切片,每个切片都是独立并行的,比滚动快很多倍。
上面的例子可以分别请求两个数据,合并五个数据的最终结果和直接滚动扫描是一样的。
其中,max是块数,id是块数。
Search_after是ES 5引入的一种新的分页查询机制。它的原理和scroll差不多,所以代码也差不多。
"基本用途:"
第一步:
返回的结果信息:
上述请求将返回一个数组,其中包含每个文档的排序值。
可以在search_after参数中使用这些排序值来捕获下一页的数据。
例如,我们可以使用最后一个文档的排序值,并将其传递给search_after参数:
如果我们想要读取上一次读取结果之后的下一页数据,第二个查询将search_after添加到第一个查询的句子中,并指示从哪个数据开始读取。
“基本原则”
Es保持一个实时光标。上一次查询的最后一条记录是一个游标,方便查询下一页。它是一个无状态查询,所以每次都查询最新的数据。
因为它使用记录作为游标,“SearchAfter要求doc中至少有一个全局唯一的变量(每个文档具有唯一值的字段应该用作排序规范)”。
“优点和缺点”
"优势:"
"缺点:"
SEARCH_AFTER不是自由跳转任意页面的解决方案,而是并行滚动多个查询的解决方案。
分页模式的性能优缺点场景from+size低且灵活,实现简单深度分页问题的数据量小,可以容忍深度分页问题。scroll解决了深度分页问题,不能反映数据的实时性能(快照版)。维护成本高。对于海量数据的导出,需要维护一个scroll_id。需要查询海量结果集的数据。search_after性能很高。最好不要有深度分页问题。它能反映实时数据变化的复杂性。有一个全局唯一的字段进行连续分页比较复杂,因为每次查询都需要上一次查询的结果,不适合大跳页的海量数据分页。
参考:/elastic/elastic search/issues/29449),es中向前翻页的问题可以通过翻转排序来实现,即:
Scroll和search_after的原理基本相同,都是使用游标进行深度分页。
虽然这种方法可以在一定程度上解决深度分页的问题。但是,它们不是深度分页问题的最终解决方案,深度分页是必须避免的!!」 。
对于Scroll来说,维护scroll_id和历史快照是不可避免的,还需要保证scroll_id的存活时间,这对服务器来说是一个巨大的负载。
对于Search_After来说,如果允许用户大幅度跳转到页面,会导致短时间内频繁的搜索动作,效率非常低,也会增加服务器的负载。同时,在查询过程中,索引的添加、删除和修改会导致查询数据不一致或排名变化,导致结果不准确。
Search_After本身就是一个商业折中方案,不允许你指定跳转到一个页面,只提供下一页的功能。
Scroll默认以后会把所有符合条件的数据都拿出来,所以只是搜索所有符合条件的doc_id(这也是为什么官方推荐使用Doc _ ID进行排序,因为它本身缓存了Doc _ ID,用其他字段排序会增加查询量),然后保存在坐标节点。然而,我们不是获取所有的数据,而是一次滚动读取大小文档,并返回这次读取的最后一个文档和上下文状态,以告知下次需要读取哪个碎片文档。
这也是为什么scroll不被政府推荐给用户做实时分页查询,而适合大批量拉取数据的原因,因为它不是为实时读取数据而设计的。