您现在的位置是:首页 > 程序 > sticsearch网站首页sticsearch
ElasticSearch 深度分页解决方案
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的业务场景,常用于数据导出等场景。
相关文章
文章评论 (0)
- 这篇文章还没有收到评论,赶紧来抢沙发吧~

