ElasticSearch 深度分页解决方案

袁志蒙 1945次浏览

摘要: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_afterscroll 非常相似,同样适用于深度分页 + 排序,因为每一页的数据依赖于上一页最后一条数据,所以无法跳页请求(但可以通过循环来实现),且返回的始终是最新的数据,在分页过程中数据的位置可能会有变更,这种分页方式更加符合moa的业务场景,常用于数据导出等场景。


随机内容

表情

共0条评论
  • 这篇文章还没有收到评论,赶紧来抢沙发吧~