一、前言
1.1、scroll
与from+size
区别
之前讲过from+size的分页,为何又有scroll+size的深分页呢?这里先对比一下两者的区别;
ES对于from+size的个数是有限制的,二者之和不能超过1w。当所请求的数据总量大于1w时,可用scroll来代替from+size。
from+size在ES查询数据的方式步骤如下:
- 1、先将用户指定的关键字进行分词;
- 2、将词汇去分词库中进行检索,得到多个文档的id;
- 3、去各个分片中拉取指定的数据,相对耗时较长;
- 4、将数据根据score进行排序,耗时相对较长;
- 5、根据from,size的值,截取满足条件的查询到的数据;
- 6、返回结果;
优点:每次都能获取到最新的记录;
缺点:同一个查询,展示另一页的from+size时,以上步骤需要再来一遍;
scoll+size在ES查询数据的方式:
- 1、先将用户指定的关键字进行分词;
- 2、将词汇去分词库中进行检索,得到多个文档的id;
- 3、将文档的id存放在内存的一个ES的上下文中;
- 4、根据你指定的size的个数去ES上下文中检索指定个数的数据,拿完了数据的文档id,会从上下文中移除;
- 5、如果需要下一页数据,直接去ES的上下文中,找后续内容;
- 6、循环第4步,第五步,直到数据都取完了;
优点:数据缓存进了内存,速度快,同一个查询,展示另一页的scoll+size时,只需要循环4,5步;
缺点:冷加载,不适合做实时,当数据更新时,内存中的上下文id数据不会更新;
1.2、Scroll原理
1.2.1、 ES搜索两阶段简介
ES的搜索是分2个阶段进行的,即Query阶段和Fetch阶段。
Query阶段比较轻量级,通过查询倒排索引,获取满足查询结果的文档ID列表。
Fetch阶段比较重,需要将每个shard的结果取回,在协调结点进行全局排序。 通过From+size这种方式分批获取数据的时候,随着from加大,需要全局排序并丢弃的结果数量随之上升,性能越来越差。
1.2.2、 scroll分析
Scroll查询,先做轻量级的Query阶段以后,免去了繁重的全局排序过程。 它只是将查询结果集,也就是doc id列表保留在一个上下文里, 之后每次分批取回的时候,只需根据设置的size,在每个shard内部按照一定顺序(默认doc_id续), 取回这个size数量的文档即可。
###1.2.3、 scroll使用场景
可以看出scroll不适合支持那种实时的和用户交互的前端分页工作,其主要用途用于从ES集群分批拉取大量结果集的情况,一般都是offline的应用场景。 比如需要将非常大的结果集拉取出来,存放到其他系统处理,或者需要做大索引的reindex等等。
具体原理分析可参考如下三篇文章:
https://elasticsearch.cn/question/2935 (ES中文社区讨论内容)
https://www.elastic.co/guide/cn/elasticsearch/guide/cn/_fetch_phase.html (query_then_fetch官方文档)
https://www.jianshu.com/p/91d03b16af77 (scroll源码分析)
二、Scroll实践
2.1、依据fee字段和moblie字段倒序按照每一页2条scroll查询公司信息
2.1.1、RESTful 代码
2.1.1.1、步骤1 scoll 查询,返回第一页数据,将ES的id存放在上下文中
参数scroll=2m表示scroll查询的上下文在内存中存放2分钟,不指定默认生存时间为0,当超时,会自动删除上下文,则下面的步骤2和3会查询报错
指定size为2
scroll可以指定字段排序,默认按照文档id排序
POST /sms-logs-index/_search?scroll=2m
{
"query": {
"match_all": {}
},
"size": 2,
"sort": [
{
"fee": {
"order": "desc"
},
"moblie": {
"order": "desc"
}
}
]
}
2.1.1.2、步骤2 根据scroll查询下一页数量,再下一页的话再执行下此语句,再下一页再再执行,直到结束或超时;
scroll_id: 指的是上面的查询结果
scroll: 还是要继续指定上下文在内存中缓存2分钟
POST /_search/scroll
{
"scroll_id": "FGluY2x1ZGVfY29udGV4dF91dWlkDnF1ZXJ5VGhlbkZldGNoBRZlY3FTZWZGZVNUR0VFQmVMUlVBUmVnAAAAAAAAthMWUl94NU1lek1TZUMzSFMtSzVHWS03URZlY3FTZWZGZVNUR0VFQmVMUlVBUmVnAAAAAAAAthQWUl94NU1lek1TZUMzSFMtSzVHWS03URZlY3FTZWZGZVNUR0VFQmVMUlVBUmVnAAAAAAAAthYWUl94NU1lek1TZUMzSFMtSzVHWS03URZlY3FTZWZGZVNUR0VFQmVMUlVBUmVnAAAAAAAAthcWUl94NU1lek1TZUMzSFMtSzVHWS03URZlY3FTZWZGZVNUR0VFQmVMUlVBUmVnAAAAAAAAthUWUl94NU1lek1TZUMzSFMtSzVHWS03UQ==",
"scroll": "2m"
}
2.1.1.3、 删除scroll在es上下文中的数量
可能我查到第一页就知道了结果,对后面的分页不感兴趣了,我想提前删除scroll中的上下文
DELETE /_search/scroll/FGluY2x1ZGVfY29udGV4dF91dWlkDnF1ZXJ5VGhlbkZldGNoBRZlY3FTZWZGZVNUR0VFQmVMUlVBUmVnAAAAAAAAthMWUl94NU1lek1TZUMzSFMtSzVHWS03URZlY3FTZWZGZVNUR0VFQmVMUlVBUmVnAAAAAAAAthQWUl94NU1lek1TZUMzSFMtSzVHWS03URZlY3FTZWZGZVNUR0VFQmVMUlVBUmVnAAAAAAAAthYWUl94NU1lek1TZUMzSFMtSzVHWS03URZlY3FTZWZGZVNUR0VFQmVMUlVBUmVnAAAAAAAAthcWUl94NU1lek1TZUMzSFMtSzVHWS03URZlY3FTZWZGZVNUR0VFQmVMUlVBUmVnAAAAAAAAthUWUl94NU1lek1TZUMzSFMtSzVHWS03UQ==
2.1.2、java 代码
package com.chb.test;
import com.chb.utils.ESClient;
import org.elasticsearch.action.search.*;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.junit.Test;
import java.io.IOException;
public class ScrollDemo {
static RestHighLevelClient myClient = ESClient.getClient(); //获取操作ES的
String index = "sms-logs-index";
@Test
public void scrollQuery() throws IOException {
//1. 创建SearchRequest
SearchRequest request = new SearchRequest(index);
//2.指定scroll鑫鑫
request.scroll(TimeValue.timeValueMinutes(2L));
//3.指定查询条件
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.size(4);
builder.sort("fee", SortOrder.DESC);
builder.query(QueryBuilders.matchAllQuery());
request.source(builder);
//4. 获取返回结果scrollId,source的首页信息
SearchResponse response = myClient.search(request, RequestOptions.DEFAULT);
String scrollId = response.getScrollId();
System.out.println("-----------------------首页----------------------------");
for (SearchHit hit : response.getHits().getHits()) {
System.out.println(hit.getSourceAsMap());
}
while (true) {
//5.循环-创建SearchSrollRequest
SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
//6.指定scrollId的生存时间
scrollRequest.scroll(TimeValue.timeValueMinutes(2L));
//7.执行查询获取返回结果
SearchResponse scrollResp = myClient.scroll(scrollRequest, RequestOptions.DEFAULT);
//8.判断这一页是否还有数据,有则输出,没有则跳出循环
SearchHit[] hits = scrollResp.getHits().getHits();
if (hits != null && hits.length > 0) {
System.out.println("-----------------------下一页----------------------------");
for (SearchHit hit : hits) {
System.out.println(hit.getSourceAsMap());
}
} else {
//9。判断没有查询到数据-退出循环
System.out.println("-----------------------结束----------------------------");
break;
}
}
//10.创建ClearScrollRequest
ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
//11.指定ScrollId
clearScrollRequest.addScrollId(scrollId);
//12.删除ScrollId
ClearScrollResponse clearScrollResponse = myClient.clearScroll(clearScrollRequest, RequestOptions.DEFAULT);
//13.输出结果
System.out.println("删除scroll:" + clearScrollResponse.isSucceeded());
}
}