ES为了避免深分页,不允许使用分页(from&size)查询10000条以后的数据,因此如果要查询第10000条以后的数据,要使用ES提供的 scroll(游标) 来查询
假设取的页数较大时(深分页),如请求第20页,Elasticsearch不得不取出所有分片上的第1页到第20页的所有文档,并做排序,最终再取出from后的size条结果作爲最终的返回值
假设你有16个分片,则需要在coordinate node彙总到 shards* (from+size)条记录,即需要16*(20+10)记录后做一次全局排序
所以,当索引非常非常大(千万或亿),是无法使用from + size 做深分页的,分页越深则越容易OOM,即便不OOM,也很消耗CPU和内存资源
因此ES使用index.max_result_window:10000作爲保护措施 ,即默认 from + size 不能超过10000,虽然这个参数可以动态修改,也可以在配置文件配置,但是最好不要这麽做,应该改用ES游标来取得数据
可以把 scroll 理解爲关系型数据库里的 cursor,因此,scroll 并不适合用来做实时搜索,而更适用于后台批处理任务,比如群发
scroll 具体分爲初始化和遍历两步
初始化时将所有符合搜索条件的搜索结果缓存起来,可以想象成快照
在遍历时,从这个快照里取数据
也就是说,在初始化后对索引插入、删除、更新数据都不会影响遍历结果
游标可以增加性能的原因,是因为如果做深分页,每次搜索都必须重新排序,非常浪费,使用scroll就是一次把要用的数据都排完了,分批取出,因此比使用from+size还好
初始化
请求
注意要在URL中的search后加上scroll=1m,不能写在request body中,其中1m表示这个游标要保持开启1分钟
可以指定size大小,就是每次回传几笔数据,当回传到没有数据时,仍会返回200成功,只是hits裡的hits会是空list
在初始化时除了回传_scroll_id,也会回传前100笔(假设size=100)的数据
request body和一般搜索一样,因此可以说在初始化的过程中,除了加上scroll设置游标开启时间之外,其他的都跟一般的搜寻没有两样 (要设置查询条件,也会回传前size笔的数据)
总结:
在分页处理时,我们要确定两个参数,start & size,如果一个分页查询start值很大,那么这就是一个深度分页查询。
深度分页是很有问题的,用sql举例:select * from user order by id limit 10000,10 ,表面上看起来只取10条数据,而实际上它是个大查询,因为查询过程中,数据库要确定前10010条数据,然后才能拿出最后10条。
显而易见,一方面人为深度分页是个伪需求,没有谁会一直狂翻,或者直接跳第100页看数据。另一方面,深度分页对系统的稳定性有潜在威胁。
mysql并没有限制深度分页,而Es专门搞了一个 max_result_window 的东西 – 最大结果窗口,默认值是10000,它不仅限制了用户在一次查询中最多数据条数是1w条,并且限制了start+size 必须小于1w,也就是说,你想取第9999条,往后的2条数据是不可以的,因为 9999+2 > 10000。如此一来,一石二鸟,同时防止了一次取太多和深度分页两个问题。
好,那么问题就来了,那怎么取第1万条以后的数据?要导数据怎么办?为此,es 提供了一种数据遍历的接口 — scroll,如果对数据不要求排序,可以用scroll+scan,速度更快。当使用scroll提取数据时,es 会为这个查询做快照,然后给用户提供一个游标来顺序访问快照。
1. 普通请求
假设我们想一次返回大量数据,下面代码中一次请求58000条数据:
/**
* 普通搜索
* @param client
*/
public static void search(Client client) {
String index = "simple-index";
String type = "simple-type";
// 搜索条件
SearchRequestBuilder searchRequestBuilder = client.prepareSearch();
searchRequestBuilder.setIndices(index);
searchRequestBuilder.setTypes(type);
searchRequestBuilder.setSize(58000);
// 执行
SearchResponse searchResponse = searchRequestBuilder.get();
// 搜索结果
SearchHit[] searchHits = searchResponse.getHits().getHits();
for (SearchHit searchHit : searchHits) {
String source = searchHit.getSource().toString();
logger.info("--------- searchByScroll source {}", source);
} // for
}
返回如下报错:
Caused by: QueryPhaseExecutionException[Result window is too large, from + size must be less than or equal to: [10000] but was [58000]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level parameter.]
at org.elasticsearch.search.internal.DefaultSearchContext.preProcess(DefaultSearchContext.java:212)
at org.elasticsearch.search.query.QueryPhase.preProcess(QueryPhase.java:103)
at org.elasticsearch.search.SearchService.createContext(SearchService.java:676)
at org.elasticsearch.search.SearchService.createAndPutContext(SearchService.java:620)
at org.elasticsearch.search.SearchService.executeQueryPhase(SearchService.java:371)
at org.elasticsearch.search.action.SearchServiceTransportAction$SearchQueryTransportHandler.messageReceived(SearchServiceTransportAction.java:368)
at org.elasticsearch.search.action.SearchServiceTransportAction$SearchQueryTransportHandler.messageReceived(SearchServiceTransportAction.java:365)
at org.elasticsearch.transport.TransportRequestHandler.messageReceived(TransportRequestHandler.java:33)
at org.elasticsearch.transport.RequestHandlerRegistry.processMessageReceived(RequestHandlerRegistry.java:75)
at org.elasticsearch.transport.TransportService$4.doRun(TransportService.java:376)
at org.elasticsearch.common.util.concurrent.AbstractRunnable.run(AbstractRunnable.java:37)
... 3 more
2. 使用scroll方式:
package com.smk.es.servicce;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.transport.TransportClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.TransportAddress;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.transport.client.PreBuiltTransportClient;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.Map;
public class TestEs {
private String clusterName ="es-smk-sit";
private String clusterNode = "192.168.23.10";
private String clusterPort ="9301";
private String poolSize = "10";
private boolean snf = true;
private String index = "smk-label";
private String type = "label";
public TransportClient transportClient() {
TransportClient transportClient = null;
try {
Settings esSetting = Settings.builder()
.put("cluster.name", clusterName) //集群名字
.put("client.transport.sniff", snf)//增加嗅探机制,找到ES集群
.put("thread_pool.search.size", Integer.parseInt(poolSize))//增加线程池个数,暂时设为5
.build();
//配置信息Settings自定义
transportClient = new PreBuiltTransportClient(esSetting);
TransportAddress transportAddress = new TransportAddress(InetAddress.getByName(clusterNode), Integer.valueOf(clusterPort));
transportClient.addTransportAddresses(transportAddress);
} catch (Exception e) {
e.printStackTrace();
System.out.println("elasticsearch TransportClient create error!!");
}
System.out.println("es客户端创建成功");
return transportClient;
}
public static String scrollId = "";
/**
* 第一次查询的方式
* @param client
* @return
*/
private Map<String,Object> my(TransportClient client){
BoolQueryBuilder mustQuery = QueryBuilders.boolQuery();
//设置查询条件
mustQuery.must(QueryBuilders.matchQuery("sex","男"));
mustQuery.must(QueryBuilders.matchQuery("city","杭州市"));
SearchResponse rep = client.prepareSearch()
.setIndices(index) // 索引
.setTypes(type) //类型
.setQuery(mustQuery)
.setScroll(TimeValue.timeValueMinutes(2)) //设置游标有效期
.setSize(100) //每页的大小
.execute()
.actionGet();
Map<String,Object> m = new HashMap<String,Object>();
m.put("scrollId",rep.getScrollId());//获取返回的 游标值
m.put("id", (rep.getHits().getHits())[0].getId());
return m;
}
private Map<String,Object> my2(String scrollId,TransportClient client){
SearchResponse rep1 = client.prepareSearchScroll(scrollId) //设置游标
.setScroll(TimeValue.timeValueMinutes(2)) //设置游标有效期
.execute()
.actionGet();
Map<String,Object> m = new HashMap<String,Object>();
m.put("scrollId",rep1.getScrollId());
SearchHit[] s = rep1.getHits().getHits();
if(s == null || s.length == 0){
return null;
}
m.put("id", (rep1.getHits().getHits())[0].getId());
return m;
}
public static void main(String[] args) {
TestEs t = new TestEs();
TransportClient client = t.transportClient();
Map<String,Object> m1 = t.my(client);
System.out.println("first:"+m1.get("id"));
String s = m1.get("scrollId").toString();
System.out.println("first:"+s);
int i = 0;
while (true){
i++;
Map<String,Object> m2 = t.my2(s,client);
// 查询不到数据了,就表示查询完了
if(m2 == null){
break;
}
System.out.println("insert to mysql");
}
System.out.println("总次数:"+i);
System.out.println("end");
}
}