当前位置: 首页 > 后端技术 > Java

京东面试题:ElasticSearch深度分页解决方案

时间:2023-04-02 10:23:46 Java

前言Elasticsearch是一个实时的分布式搜索和分析引擎。在使用过程中,有一些典型的使用场景,比如分页、遍历等。在关系型数据库的使用中,我们被告知要注意甚至明确禁止使用深度分页。同样,在Elasticsearch中,我们应该尽量避免使用深度分页。本文主要介绍Elasticsearch中的分页相关内容!From/Size参数在ES中,分页查询默认返回前10个匹配命中。如果需要分页,则需要使用from和size参数。from参数定义了需要跳过的命中数,默认为0;size参数定义需要返回的最大命中数。一个基本的ES查询语句是这样的:POST/my_index/my_type/_search{"query":{"match_all":{}},"from":100,"size":10}上面的query表示从搜索结果从第100条开始取10条数据。“那么,这条查询语句在ES集群内部是如何执行的呢?”在ES中,搜索一般包括两个阶段,query和fetch阶段,这个很容易理解。查询阶段确定要获取哪些文档,获取阶段获取特定文档。医生。?查询阶段如上图所示,它描述了一个搜索请求的查询阶段:Client发送一个搜索请求,node1接收到请求,然后node1创建一个大小为from+size的优先级队列来存储结果。我们管理的Node1称为协调节点。协调节点将请求广播到涉及的分片,每个分片在内部执行搜索请求,然后将结果存储在内部优先级队列中,大小与from+size相同。优先级队列可以理解为包含前N个结果的列表。每个分片将暂存在自己优先级队列中的数据返回给协调节点,协调节点获取各个分片返回的结果并合并一次结果生成一个全局优先级队列,存储在自己的优先级队列中。在上面的例子中,协调节点得到(from+size)*6条数据,然后合并排序,选择最先的from+size条数据存入优先级队列,供fetch阶段使用。另外,每个分片返回给协调节点的数据用于从+size条数据中选择第一个,所以只需要返回唯一标识doc的_id和用于排序的_score,这样也可以保证返回的数据量足够小。协调节点计算出自己的优先级队列后,查询阶段结束,进入获取阶段。?Fetchstagequerystage知道要取哪些数据,但不会取具体的数据,fetchstage就是干这个的。上图为fetch流程:协调节点向相关分片发送GET请求。分片根据doc的_id获取数据详情,然后返回给协调节点。协调节点将数据返回给客户端。协调节点的优先级队列中有from+size_doc_ids。然而,在获取阶段,没有必要检索所有数据。上面的例子中,前100条数据不需要取,只需要取优先级队列中的第101到110条数据。需要取的数据可能在不同的分片中,也可能在同一个分片中,协调节点使用“multi-get”避免多次从同一个分片中取数据,从而提高性能。“以这种方式请求深度分页是有问题的:”我们可以假设在具有5个主分片的索引中进行搜索。当我们请求第一页结果时(从1到10的结果),每个分片产生前10个结果返回给协调节点,协调节点对50个结果进行排序,得到所有结果中的前10个。现在假设我们请求第1000页——从10001到10010的结果。除了每个分片必须产生前10010个结果之外,所有的工作方式都相同。协调器节点然后对所有50050个结果进行排序,最后丢弃其中的50040个结果。“对结果进行排序的成本随着页面的深度呈指数增长。””注1:“size的大小不能超过index.max_result_window参数设置,默认为10000。如果搜索大小大于10000,则需要设置index.max_result_window参数PUT_settings{"index":{"max_result_window":"10000000"}}"注2:"_doc会在以后的版本中去掉,见:https://www.elastic.co/cn/blo...https://elasticsearch.cn/arti...深度分页问题Elasticsearch的From/Size方法提供了分页的功能,同时,也有相应的限制。比如一个索引有10亿条数据,分成10个分片,然后一个搜索请求,from=1000000,size=100,这时候,会带来严重的性能问题:CPU、内存、IO、网络带宽。在查询阶段,每个分片需要向协调节点返回1000100条数据,协调节点需要接收10*1000,100条数据。即使每条数据只有_doc_id和_score,数据量是不是太大了?“另一方面,我们意识到这种深度分页请求是不合理的,因为我们很少人为地看后面的请求。在很多业务场景中,分页是直接被限制的,比如只看前100页。”比如一个1000万粉丝的微信大V,要给所有粉丝群发群消息,或者给某个省份的粉丝发群消息,那么他需要获取所有符合条件的粉丝,最简单的想到的方法就是使用from+大小,但这是不现实的。这时候可以使用Elasticsearch提供的其他方法来实现遍历。深度分页问题大致可以分为两类:《随机深度分页:随机跳页》《滚动深度分页:一次只能查询一页》《下面介绍几种官方的深度分页方式》Scroll?Scroll来遍历data,我们可以把scroll理解为关系型数据库中的游标。所以scroll不适合实时搜索,更适合后台批量处理任务,比如群发。该页面的用途“不是为了实时查询数据”,而是为了“一次性查询大量数据(甚至所有数据”)。因为这个滚动相当于维护了一个当前索引段的快照信息,这个快照信息就是你执行这个滚动查询时的快照。在这个查询之后任何新索引的数据都不会在这个快照中被查询。但是相对于from和size,它不是查询所有的数据,然后剔除不需要的部分,而是记录一个读取的位置,保证下次继续快速读取。不考虑排序时,可与SearchType.SCAN结合使用。滚动可以分为初始化和遍历。初始化时,“缓存所有满足搜索条件的搜索结果(注意这里只是缓存了doc_id,并不是所有的文档数据都真正缓存了,数据在fetch阶段就已经完成了)”,可以这么认为作为快照。遍历时,从这个快照中取数据,即初始化后,向索引插入、删除、更新数据不会影响遍历结果。"基本使用"/twitter/tweet/_search?scroll=1m{"size":100,"query":{"match":{"title":"elasticsearch"}}}初始化并指定索引和类型,然后添加上面的参数scroll表示暂存搜索结果的时间,其他和普通的搜索请求一样。会返回一个_scroll_id,_scroll_id用于下次取数据。"遍历"POST/_search?scroll=1m{"scroll_id":"XXXXXXXXXXXXXXXXXXXXXX我是scrollidXXXXXXXXXXXXXXXXXX"}这里的scroll_id是之前遍历得到的_scroll_id或者初始化返回的_scroll_id。同样,scroll是必需的参数。重复此步骤,直到返回数据为空,即遍历完成。“注意每次都要传参数scroll,用于刷新搜索结果的缓存时间。”此外,“无需指定索引和类型”。设置scroll时,需要缓存搜索结果,直到下一次遍历完成,”同时,不能太长,毕竟空间有限。》《优缺点》缺点:《scroll_id会占用大量资源(尤其是排序请求)》同样,scroll后面跟着超时,频繁发起scroll请求,会导致一系列问题。》是生成的历史快照,数据的变化不会反映到快照上。”“优点:”适用于数据迁移或索引变化等大数据量的非实时处理。ScrollScanES提供了一个scrollscan方法进一步提升遍历性能,但是scrollscan不支持排序,所以scrollscan适用于不需要排序的场景,【基本使用】ScrollScan的遍历和普通Scroll一样,有初始化有点区别POST/my_index/my_type/_search?search_type=scan&scroll=1m&size=50{"query":{"match_all":{}}}需要指定参数:search_type:赋值给scan,即使用ScrollScan遍历,同时告诉Elasticsearch搜索结果不需要排序。scroll:同上,上传时间。size:不同于普通size,这个size表示每个分片返回的size个数,最终结果最多为number_of_shards*size。《ScrollScan与Scroll的区别》Scroll-Scan的结果是“未排序”的,不排序,按索引顺序返回,可以提高数据检索性能。初始化时只返回_scroll_id,没有具体命中。结果大小控制的是每个片段返回的数据量,而不是整个请求返回的数据量。SlicedScroll如果你的数据量很大,用Scroll来遍历数据确实是不能接受的。现在可以使用Scroll接口并发进行数据遍历。每个Scroll请求可以分成多个Slice请求,可以理解为切片。每个Slice独立并行,比使用Scroll遍历快很多倍。POST/index/type/_search?scroll=1m{"query":{"match_all":{}},"slice":{"id":0,"max":5}}POSTip:port/index/type/_search?scroll=1m{"query":{"match_all":{}},"slice":{"id":1,"max":5}}上面的例子可以分别请求两条数据,最后合并5条数据的结果和直接滚动扫描是一样的。其中,max为块数,id为块数。?官方文档建议max的值不要超过分片数,否则可能会导致内存爆炸。SearchAfterSearch_after是ES5引入的一种新的分页查询机制,它的原理和scroll差不多,所以代码也差不多。"基本使用:"第一步:POSTtwitter/_search{"size":10,"query":{"match":{"title":"es"}},"sort":[{"date":"asc"},{"_id":"desc"}]}返回结果信息:{"took":29,"timed_out":false,"_shards":{"total":1,"successful":1,"跳过”:0,“失败”:0},“命中”:{“总计”:{“价值”:5,“关系”:“eq”},“max_score”:空,“命中”:[{。..},“排序”:[...]},{...},“排序”:[124648691,"624812"]}]}}上面的请求会为每个文档返回一个排序值数组,这些排序值可以在search_after参数中用来获取下一页数据。比如我们可以使用最后一个文档的排序值,将其传递给search_after参数:GETtwitter/_search{"size":10,"query":{"match":{"title":"es"}},"search_after":[124648691,"624812"],"sort":[{"date":"asc"},{"_id":"desc"}]}如果我们要读取上次读取结果后的下一个结果Pagedata,第二个查询在第一个查询中的语句的基础上增加了search_after,指定读取哪些数据之后。《基本原理》es维护了一个实时游标,它使用上一个查询的最后一条记录作为游标,方便查询下一页,是无状态查询,所以每次查询都是最新的数据,由于是以记录为游标,"SearchAfter要求doc中至少有一个全局唯一的变量(一个字段,每个变量值唯一)document应该作为排序规范)》《优缺点》《优点:》无状态查询可以防止在查询过程中,数据的变化不能及时反映在查询中。不需要维护scroll_id,不需要维护snapshots,可以避免大量资源消耗。【缺点:】由于查询是无状态的,查询过程中的变化可能会导致跨页面的值不一致。排序顺序可能会在执行期间发生变化,具体取决于索引更新和删除。至少需要指定一个唯一的非重复字段进行排序。不适合大规模的页面跳转查询,或者全量导出。跳转到N页查询相当于在ones之后重复执行N次搜索,而fullexport则是在短时间内执行大量的重复查询。SEARCH_AFTER不是自由跳转到任意页面的解决方案,而是并行滚动多个查询的解决方案。总结分页方式的性能优缺点。+尺寸的场景很低。灵活性好。简单的deeppaging问题比较小,deeppaging的问题可以容忍。scroll解决了深分页的问题。,需要维护一个scroll_id海量数据导出需要查询海量结果集的数据search_after高性能最好没有深度分页问题可以反映数据的实时变化实现复杂,需要全局唯一字段实现连续分页会比较复杂,因为每次查询都需要上次查询的结果,不适合分页ES7版本大跳转查询海量数据:https://www.elastic.co/guide/...中version7.*,ES官方不再推荐使用Scroll方式进行深度分页,而是推荐使用search_afterwithPIT进行查询;从版本7.*开始,您可以使用SEARCH_AFTER参数在上一页和下一页命中中搜索一组已排序的值。使用SEARCH_AFTER需要具有相同查询和排序值的多个搜索请求。如果在这些请求之间发生刷新,则结果的顺序可能会发生变化,从而导致页面之间的结果不一致。为防止这种情况,您可以创建一个时间点(PIT)以在搜索期间保留当前索引状态。POST/my-index-000001/_pit?keep_alive=1m返回一个PITID:{"id":"46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA=="}在搜索请求中指定PIT:GET/_search{"size":10000,"query":{"match":{"user.id":"elkbee"}},"pit":{"id":"46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==","keep_alive":"1m"},"sort":[{"@timestamp":{"order":"asc","format":"strict_date_optional_time_nanos","numeric_type":"date_nanos"}}]}性能比较:1-10、49000-49010、99000-99010,各10个数据(前提10w条),性能大致是这样的:向前翻页对于向前翻页,在ES中没有对应的API,但是按照官方的说法(https://github.com/elastic/el...),ES中的向前翻页问题可以通过翻页来解决实现方式是:对于某个页面,正向search_after中页面的最后一个dataid为下一页,reversesearch_after中页面的第一个dataid为上一页。国内论坛上,有人用缓存解决问题上一页的问题:https://elasticsearch.cn/ques...总结如果数据量小(from+size在10000以内),或者你只关注结果集的TopN数据,可以使用from/size分页,简单对于数据量大、深度翻页、后台批处理任务(数据迁移)等任务,使用scroll大数据量、深度翻页、用户实时、高并发查询需求的方法,使用searchafter方法个人认为Scroll和search_after原理基本相同,都是使用游标方法进行深度分页。虽然这种方法可以在一定程度上解决深度分页的问题。然而,它们并不是深度分页问题的最终解决方案,“必须避免!!”。对于Scroll来说,维护scroll_id和历史快照是不可避免的,还必须保证scroll_id的存活时间,这对服务器来说是一个巨大的负载。对于Search_After,如果允许用户大幅度跳转页面,会导致短时间内频繁的搜索动作,效率很低,而且会增加服务器的负载。数据不一致或排序改变,导致结果不准确。Search_After本身就是一种业务折中方案,不允许指定跳转页面,只提供下一页的功能。scroll默认你会在后面fetch所有符合条件的数据,所以它只是搜索所有符合条件的doc_id(这也是为什么官方推荐使用doc_id进行排序的原因,因为doc_id本身是有缓存的,如果使用其他字段排序会增加查询),并将它们排序并保存在坐标节点(coordinatenode)中,但是不是每次scroll读取所有数据,而是每次滚动读取size个文档,并返回最后读取的一个文档和上下文状态,用于告知哪个文档是哪个下次需要读取碎片。这也是为什么官方不推荐scroll用于用户实时分页查询,而是适合大批量拉取数据的原因,因为它不是为实时读取数据而设计的。