问题描述
查询全表数据也是日常工作中常见的一种查询场景。
在ES如果我们使用match_all查询索引的全量数据时,默认只会返回10条数据。
那么在ES如何查询索引的全量数据呢?
小实验
1、索引和数据准备
PUT book { "mappings": { "properties": { "name": { "type": "text", "analyzer": "ik_smart" }, "price": { "type": "double" } } } } PUT /book/_bulk { "create": { } } {"name": "java编程思想","price": 100} { "create": { } } {"name": "ES实战","price": 120} { "create": { } } {"name": "ES从入门到精通","price": 60} { "create": { } } {"name": "微服务架构 设计模式","price": 160} { "create": { } } {"name": "架构真经","price": 90} { "create": { } } {"name": "spring boot实战","price": 50} { "create": { } } {"name": "高性能mysql","price": 80} { "create": { } } {"name": "java进阶1","price": 10} { "create": { } } {"name": "java进阶2","price": 20} { "create": { } } {"name": "java进阶3","price": 30} { "create": { } } {"name": "java进阶4","price": 40} { "create": { } } {"name": "java进阶5","price": 50}
2、match_all全匹配查询
GET book/_search
等同于
GET book/_search { "query": { "match_all": {} } }
发现只返回了10条记录。
这样因为_search查询默认采用的是分页查询,每页记录数size的默认值为10.
3、添加size参数
GET book/_search { "query": { "match_all": {} }, "size": 100 }
将size值设置为100,而我们只添加了13条记录,所以成功返回索引的全量记录。
4、size大于10000
GET book/_search { "query": { "match_all": {} }, "size": 20000 }
返回结果:
{ "error" : { "root_cause" : [ { "type" : "illegal_argument_exception", "reason" : "Result window is too large, from + size must be less than or equal to: [10000] but was [20000]. 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 setting." } ],
异常说明:
1、查询结果的窗口太大,from + size的结果必须小于或等于10000,而当前查询结果的窗口为20000。
2、可以采用scroll api更高效的请求大量数据集。
3、查询结果的窗口的限制可以通过参数index.max_result_window进行设置。
index.max_result_window
The maximum value of from + size for searches to this index. Defaults to 10000. Search requests take heap memory and time proportional to from + size and this limits that memory. See Scroll or Search After for a more efficient alternative to raising this.
说明:
参数index.max_result_window主要用来限制单次查询满足查询条件的结果窗口的大小,窗口大小由from + size共同决定。不能简单理解成查询返回给调用方的数据量。
这样做主要是为了限制内存的消耗。
请求大数据集推荐采用Scroll or Search After 。
比如:from为1000000,size为10,逻辑意义是从满足条件的数据中取1000000到(1000000 + 10)的记录。这时ES一定要先将(1000000 + 10)的记录(即result_window)加载到内存中,再进行分页取值的操作。
尽管最后我们只取了10条数据返回给客户端,但ES进程执行查询操作的过程中确需要将(1000000 + 10)的记录都加载到内存中,可想而知对内存的消耗有多大。
这也是ES中不推荐采用(from + size)方式进行深度分页的原因。
同理,from为0,size为1000000时,ES进程执行查询操作的过程中确需要将1000000 条记录都加载到内存中再返回给调用方,也会对ES内存造成很大压力。
1.参数设置
PUT book/_settings { "index.max_result_window" :"5" }
注意:
1、此方法是设置单索引,如果需要更改索引需要将book换成_all
2、即使换成_all,对于新增的索引,还是默认的10000
2.查看参数
查看所有索引中的index.max_result_window值:
GET _all/_settings/index.max_result_window
查看book索引的_settings配置:
GET book/_settings
查看_settings配置中的参数index.max_result_window的值:
GET book/_settings/index.max_result_window
Scroll api实践
改动index.max_result_window参数值的大小,只能解决一时的问题,当索引的数据量持续增长时,在查询全量数据时还是会出现问题。而且会增加ES服务器内存大结果集消耗完的风险。
最佳实践还是根据异常提示中的采用scroll api更高效的请求大量数据集。
1.DSL命令查询
1、查询命令中新增scroll=1m,说明采用游标查询,保持游标查询窗口一分钟。
2、这里由于测试数据量不够,所以size值设置为2。实际使用中为了减少游标查询的次数,可以将值适当增大,比如设置为1000。
GET /book/_search?scroll=1m { "query": { "match_all": {}}, "size": 2 }
查询结果:
除了返回前2条记录,还返回了一个游标ID值_scroll_id。
{ "_scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFm1ab2pDVEpiVGh5ZWthYnRQanB5YlEAAAAAABRP-xZHYWJiZzJGNFJYQ1RPS0dZb1VwejRR", "took" : 0, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, …… }
采用游标id查询:
GET /_search/scroll { "scroll": "1m", "scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFm1ab2pDVEpiVGh5ZWthYnRQanB5YlEAAAAAABRP5BZHYWJiZzJGNFJYQ1RPS0dZb1VwejRR" }
说明:
多次根据scroll_id游标查询,直到没有数据返回则结果查询。。
采用游标查询索引全量数据,更安全高效,限制了单次对内存的消耗。
2.java高级API实现
/** * 通过游标查询所有数据 * @return */ public <T> List<T> searchAllData(SearchRequest searchRequest, Class<T> clazz) { List<T> tList = new ArrayList<>(); final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L)); searchRequest.scroll(scroll); try{ SearchResponse searchResponse = highLevelClient.search(searchRequest); String scrollId = searchResponse.getScrollId(); log.info("ES查询DSL语句:\nGET {}\n{}", String.format("/%s/_search", searchRequest.indices()[0]), searchRequest.source()); //打印命中数量 log.info("命中总数量:{}", searchResponse.getHits().getTotalHits()); SearchHit[] searchHits = searchResponse.getHits().getHits(); while (searchHits != null && searchHits.length > 0) { SearchHits hits = searchResponse.getHits(); List<T> resultList = Arrays.stream(hits.getHits()).map(e -> { String jsonStr = e.getSourceAsString(); return JSON.parseObject(jsonStr, clazz); }).collect(Collectors.toList()); if(!CollectionUtils.isEmpty(resultList)){ tList.addAll(resultList); } SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId); scrollRequest.scroll(scroll); searchResponse = highLevelClient.searchScroll(scrollRequest); searchHits = searchResponse.getHits().getHits(); } ClearScrollRequest clearScrollRequest = new ClearScrollRequest(); clearScrollRequest.addScrollId(scrollId); ClearScrollResponse clearScrollResponse = highLevelClient.clearScroll(clearScrollRequest); boolean succeeded = clearScrollResponse.isSucceeded(); }catch (Exception e){ log.error("执行EsService的searchAllData方法出现异常", e); } return tList; }
注意:
1、尽管通过采用Scroll API提高的查询全量数据的性能,减少了ES服务器的内存消耗。但是当返回结果集过大的时候,也会出现将调用方应用的内存撑爆的风险。
2、推荐的做法是:在调用searchAllData方法查询索引全量数据时,在方法内部添加返回记录条数的限制。避免出现返回几百万、甚至上千万的结果集,导致调用方程序由于内存吃完而宕机。
总结
1、本文主要介绍了如何通过Scroll API查询索引的全量数据。
2、介绍了index.max_result_window参数和相关配置方法。
3、尽管通过采用Scroll API查询索引全量数据提高了查询效率并减少了ES服务器的内存消耗。当同时要注意不要返回太大的结果集撑爆调用方应用的内存。
4、针对ES大结果集的查询,要同时考虑ES服务提供方和请求调用方的内存消耗。