摘要:Elasticsearch分页方式有三种,分别是“from + size 浅分页”、 “scroll” 和 “search_after”方式,本文详细介绍一下这三种分页的使用场景,elasticsearch默认采用的分页方式是from+size的形式,但是在深度分页的情况下...
Elasticsearch分页方式有三种,分别是“from + size 浅分页”、 “scroll” 和 “search_after”方式。
一、from + size 浅分页
"浅"分页可以理解为简单意义上的分页,它的原理很简单,就是查询前20条数据,然后截断前10条,只返回10-20的数据,这样其实白白浪费了前10条的查询。
在这里有必要了解一下from/size的原理:
因为es是基于分片的,假设有5个分片,from=100,size=10。则会根据排序规则从5个分片中各取回100条数据数据,然后汇总成500条数据后选择最后面的10条数据,所以越往后的分页,执行的效率越低。总体上会随着from的增加,消耗时间也会增加。而且数据量越大,就越明显!
除了效率上的问题,还有一个无法解决的问题是,es 目前支持最大的 skip 值是 max_result_window ,默认为 10000 。也就是当 from + size > max_result_window 时,es 将返回错误,max_result_window 调大方式,治标不治本,不建议使用。
二、scroll 深分页
为了满足深度分页的场景,es 提供了 scroll 的方式进行分页读取。原理上是对某次查询生成一个游标 scroll_id , 后续的查询只需要根据这个游标去取数据,直到结果集中返回的 hits 字段为空,就表示遍历结束。scroll_id 的生成可以理解为建立了一个临时的历史快照,在此之后的增删改查等操作不会影响到这个快照的结果。
scroll 类似于sql中的cursor,使用scroll,每次只能获取一页的内容,然后会返回一个scroll_id。根据返回的这个scroll_id可以不断地获取下一页的内容,所以scroll并不适用于有跳页的情景。
scroll使用过程:
先获取第一个 scroll_id,url 参数包括 /index/_type/ 和 scroll,scroll 字段指定了scroll_id 的有效生存期,以分钟为单位,过期之后会被es自动清理。如果文档不需要特定排序,可以指定按照文档创建的时间返回会使迭代更高效。
$client = ClientBuilder::create()->build(); $params = array( 'index' => 'product-*', '_source' => 'shopname,number,price', "scroll" => "1m", "size" => 10 ); //scroll=1m 表示设置 scroll_id 保留1分钟可用。使用scroll必须要将from设置为0或者不写。 // 返回结果 array(5) { ["_scroll_id"] => string(64) "cXVlcnlBbmRGZXRjaDsxOzg3OTA4NDpTQzRmWWkwQ1Q1bUlwMjc0WmdIX2ZnOzA7" ["took"] => int(1) ["timed_out"] => bool(false) ["_shards"] => array(3) { ["total"] => int(1) ["successful"] => int(1) ["failed"] => int(0) } ["hits"] => array(10) { ... } }
然后我们可以通过数据返回的 _scroll_id 读取下一页内容,如果srcoll_id 的生存期很长,那么每次返回的 scroll_id 都是一样的,直到该 scroll_id 过期,才会返回一个新的 scroll_id。请求指定的 scroll_id 时就不需要 /index/_type 等信息了。每读取一页都会重新设置 scroll_id 的生存时间,所以这个时间只需要满足读取当前页就可以,不需要满足读取所有的数据的时间,1分钟足以。
$client = ClientBuilder::create()->build(); $res = $client->scroll(array('scroll_id' => $scroll_id, 'scroll' => '1m'));
一个完整的 es scroll深度翻页PHP代码:
public function lists(){ $page = $_POST['page'] ?? 1; $size = $_POST['size'] ?? 10; $params = array( 'index' => 'yzm_users', 'scroll' => '1m', 'size' => $size ); $params['body'] = array( //查询条件 ); $docs = $this->client->search($params); $scroll_id = $docs['_scroll_id']; if($page == 1 ){ return_json(array( 'status' => 1, 'data' => $docs['hits']['hits'] )); } $i = 1; while ($i < $page) { $response = $this->client->scroll( array( 'scroll_id' => $scroll_id, 'scroll' => '1m' ) ); if (count($response['hits']['hits']) > 0) { $scroll_id = $response['_scroll_id']; } else { break; } $i++; } return_json(array( 'status' => 1, 'data' => $response['hits']['hits'] )); }
三、search_after 深分页
上述的 scroll search 的方式,官方的建议并不是用于实时的请求,因为每一个 scroll_id 不仅会占用大量的资源(特别是排序的请求),而且是生成的历史快照,对于数据的变更不会反映到快照上。这种方式往往用于非实时处理大量数据的情况,比如要进行数据迁移或者索引变更之类的。那么在实时情况下如果处理深度分页的问题呢?es 给出了 search_after 的方式,这是在 >= 5.0 版本才提供的功能。
search_after 分页的方式和 scroll 有一些显著的区别,首先它是根据上一页的最后一条数据来确定下一页的位置,同时在分页请求的过程中,如果有索引数据的增删改查,这些变更也会实时的反映到游标上。
为了找到每一页最后一条数据,每个文档必须有一个全局唯一值,这种分页方式其实和目前 moa 内存中使用rbtree 分页的原理一样,官方推荐使用 _uid 作为全局唯一值,其实使用业务层的 id 也可以。
search_after使用过程:
第一页的请求和正常的请求一样。
$client = ClientBuilder::create()->build(); $params = array( 'index' => 'product-*', '_source' => 'shopname,number,price', 'body' => [ 'sort' => [ 'timestamp' =>['order'=>'asc'], '_id' =>['order'=>'desc'], ] ], "size" => 10 ); //search_after必须使用唯一值进行排序才可以 // 返回结果 array(4) { ["took"] => int(1753) ["timed_out"] => bool(false) ["_shards"] => array(4) { ["total"] => int(3) ["successful"] => int(3) ["skipped"] => int(0) ["failed"] => int(0) } ["hits"] => array(3) { ["total"] => array(2) { ["value"] => int(10000) ["relation"] => string(3) "gte" } ["max_score"] => NULL ["hits"] => array(10) { [0] => array(6) { ["_index"] => string(16) "product" ["_type"] => string(7) "_doc" ["_id"] => string(20) "FHsS6XwBEqA0wm2Y5INV" ["_score"] => NULL ["_source"] => array(7) { .... } ["sort"] => array(2) { [0] => int(1635997891112) [1] => string(20) "FHsS6XwBEqA0wm2Y5INV" } } } } }
第二页的请求,使用第一页返回结果的最后一个数据的 sort 值,加上 search_after 字段来取下一页。注意,使用 search_after 的时候要将 from参数必须被设置成 0 或 -1 (当然你也可以不设置这个from参数)。
$client = ClientBuilder::create()->build(); $params = array( 'index' => 'product-*', '_source' => 'shopname,number,price', 'body' => [ 'search_after' => [ '1635997891112', 'FHsS6XwBEqA0wm2Y5INV' ], 'sort' => [ 'timestamp' =>['order'=>'asc'], '_id' =>['order'=>'desc'], ] ], "size" => 10 ); $res = $client->search($params);
search_after 与 scroll 非常相似,同样适用于深度分页 + 排序,因为每一页的数据依赖于上一页最后一条数据,所以无法跳页请求(但可以通过循环来实现),且返回的始终是最新的数据,在分页过程中数据的位置可能会有变更,这种分页方式更加符合moa的业务场景,常用于数据导出等场景。