今日目标
es实现分页查询,在ES中有三种方式可以实现分页:from+size、scroll、search_after
1.from+size 分页
public function list(Request $request)
{
$params = [
'size' => $request->limit,
'from' => ($request->page - 1) * $request->limit,
'index' => $this->index,
'type' => $this->type,
];
$response = app('es')->search($params);
return $response['hits']['hits'];
}
- 在使用过程中,有一些典型的使用场景,比如分页、遍历等。在使用关系型数据库中,我们被告知要注意甚至被明确禁止使用深度分页,同理,在 Elasticsearch 中,也应该尽量避免使用深度分页。es为了性能,限制了我们分页的深度,es目前支持的最大的 max_result_window = 10000;from+size二者之和不能超过1w,也就是说我们不能分页到1w条数据以上。
- from+size分页原理很简单,比如需要查询10条数据,es则需要执行from+size条数据然后根据偏移量截断前N条处理后返回。随着偏移量的增大这个时间会呈几何式增长。
如何解决from+size 分页带来的性能问题
- 1.在业务逻辑上禁止深度分页,比如不允许查询100页以后的数据
- 2.更换分页方式,采用游标 scroll的方式
2.游标 scroll 分页
- Scroll往往是应用于后台批处理任务中,不能用于实时搜索,因为这个scroll相当于维护了一份当前索引段的快照信息,这个快照信息是你执行这个scroll查询时的快照。在这个查询后的任何新索引进来的数据,都不会在这个快照中查询到。但是它相对于from和size,不是查询所有数据然后剔除不要的部分,而是记录一个读取的位置,保证下一次快速继续读取。查询时会自动返回一个_scroll_id,通过这个id可以继续查询
public function list(Request $request)
{
if (!isset($request->scroll_id)) {
$params = [
'scroll' => '30s', //快照存活的时间 1m ->一分钟
'size' => $request->limit,
'index' => $this->index,
'type' => $this->type,
];
$response = app('es')->search($params);
} else {
$response = app('es')->scroll([
'scroll_id' => $request->scroll_id, //...using our previously obtained _scroll_id
'scroll' => '30s', // and the same timeout window
]
);
}
return [
'scroll_id' => $response['_scroll_id'],
'data' => $response['hits']['hits'],
];
}
游标 scroll 带来的问题
这种分页方式虽然查询变快了,但滚动上下文代价很高,每一个 scroll_id 不仅会占用大量的资源(特别是排序的请求),而且是生成的历史快照,对于数据的变更不会反映到快照上,那么在实时情况下如果处理深度分页的问题呢?es 给出了 search_after 的方式,这是在 >= 5.0 版本才提供的功能。
3.search_after分页
searchAfter的方式通过维护一个实时游标来避免scroll的缺点,它可以用于实时请求和高并发场景。
search_after的理念是,=在不同分片上(假设有5个分片),先按照指定顺序排好,根据我们传的search_after值 ,然后仅取这个值之后的size个文档。这 5*size 个文档拿到Es内存中排序后,返回前size个文档即可。避免了浅分页导致的内存爆炸情况,经实际使用性能良好,ES空闲状态下查询耗时稳定在50ms以内,平均10~20ms。
public function list(Request $request)
{
if (!isset($request->search_after)) {
$params = [
'size' => $request->limit,
'index' => $this->index,
'type' => $this->type,
'body' => [
'sort' => [
[
'_id' => 'desc',
],
],
],
];
} else {
$params = [
'size' => $request->limit,
'index' => $this->index,
'type' => $this->type,
'body' => [
'sort' => [
[
'_id' => 'desc',
],
],
'search_after' => [$request->search_after],
],
];
}
$response = app('es')->search($params);
return [
//项目中需优化数组溢出,此处仅为简单演示
'search_after' => $response['hits']['hits'][count($response['hits']['hits']) - 1]['sort'][0],
'data' => $response['hits']['hits'],
];
- 注意:
- 当我们使用search_after时,from值必须设置为0或者-1(当然你也可以不设置这个from参数)。
- 当存在search_after参数时,不允许出现scroll参数
search_after 带来的问题
ElasticSearch之Search_After的注意事项
1.搜索时,需要指定sort,并且保证值是唯一的(可以通过加入_id或者文档body中的业务唯一值来保证);
2.再次查询时,使用上一次最后一个文档的sort值作为search_after的值来进行查询;
3.不能使用随机跳页,只能是下一页或者小范围的跳页(一次查询出小范围内各个页数,利用缓存等技术,来实现小范围分页,比较麻烦,比如从第一页调到第五页,则依次查询出2,3,4页的数据,利用每一次最后一个文档的sort值进行下一轮查询,客户端或服务端都可以进行,如果跳的比较多,则可能该方法并不适用)
它与滚动API非常相似,但与它不同,search_after参数是无状态的,它始终针对最新版本的搜索器进行解析。因此,排序顺序可能会在步行期间发生变化,具体取决于索引的更新和删除
总结
from+ size 分页,如果数据量不大或者from、size不大的情况下,效率还是蛮高的。但是在深度分页的情况下,这种使用方式效率是非常低的,并发一旦过大,还有可能直接拖垮整个ElasticSearch的集群。
scroll 分页通常不会用在客户端,因为每一个 scroll_id 都会占用大量的资源,一般是后台用于全量读取数据使用
search_after通过维护一个实时游标来避免scroll的缺点,它可以用于实时请求和高并发场景,一般用于客户端的分页查询
大体而言就是在这三种分页方式中,from + size不适合数据量很大的场景,scroll不适合实时场景,而search after在es5.x版本之后应运而生,较好的解决了这个问题。