ElasticSearch在做搜索应用时,难免会遇到深分页的问题,上一篇文章介绍了ElasticSearch分页的客户端基本用法,本篇文章将结合具体代码示例来说明ElasticSearch分页的具体用法以及深分页的用法。
温馨提示:本博客已经发布小程序,可在微信小程序中搜索”百变码农”,手机上也能看!
1、ElasticSearch分页客户端代码及Java代码示例
现有索引:假设现在需要在student_index索引下的studentInfo类型中搜索学生列表,每页查询2条,分别去查询前3页
(1)客户端命令行实现
a、查询第一页:
GET /student_index/studentInfo/_search?from=0&size=2 或者: GET /student_index/studentInfo/_search { "from":0, "size":2 }
查询结果:
{ "took": 1, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 6, "max_score": 1, "hits": [ { "_index": "student_index", "_type": "studentInfo", "_id": "5", "_score": 1, "_source": { "name": "xiaozhang", "age": 25, "gender": "male" } }, { "_index": "student_index", "_type": "studentInfo", "_id": "2", "_score": 1, "_source": { "name": "wb3", "age": 24, "gender": "male" } } ] } }
b、查询第二页:
GET /student_index/studentInfo/_search?from=2&size=2 或者: GET /student_index/studentInfo/_search { "from":2, "size":2 }
搜索结果:和上述第一页结构类似,此处略。
c、查询第三页:
GET /student_index/studentInfo/_search?from=4&size=2 或者: GET /student_index/studentInfo/_search { "from":4, "size":2 }
搜索结果:和上述结构类似,此处略。
注意:在分页搜索中,结果集中通常需要的数据是total和hits中的列表。返回给客户端的是一个总数量及当前页中的数据列表。
(2)使用JavaAPI实现分页
// 获取查询对象SearchRequestBuilder,使用不同的客户端框架此处不同 SearchRequestBuilder srq = SearchUtils.getSearchRequestBuilder(); // 获取分页的当前页码,通过入参的query对象中传入 int pageNo = query.getPageNo() <= 0 ? 0 : query.getPageNo() - 1; // 设置其他查询条件,例如:名称,年龄,性别等 srq.setQuery(buildCommonQuery(query)); // 设置分页参数中的起始位置from srq.setFrom(pageNo * query.getPageSize()); // 设置分页参数中的size,即:每页数据条数 srq.setSize(activityQuery.getPageSize()); // 设置查询的排序规则,没有排序需求可以忽略,此处表示按照年龄倒叙 srq.addSort("age", SortOrder.DESC); SearchResponse response = srq.execute().actionGet(); // 创建返回结果集 List<ResultInfo> list = new ArrayList<>(); // 从查询的返回结果中获取命中的结果列表,即:上述结果中的hits数组中的内容 SearchHits hits = response.getHits(); // 获取总条数,即分页时需要返回的总数。MySQL中可能需要单独查询该数量 long total = hits.getTotalHits(); // 用来封装查询结果。此处示例使用Map,真正使用时,建议使用一个Info对象, // 通过对象中的total字段和list字段来封装总数量及数据列表,否则map的key // 写错之后,将会无法获取到数据。 Map<String,Object> resultMap = new HashMap<>(); // 如果未查到数据,直接返回空列表 if (null == hits || total <= 0) { resultMap.put("total", total); resultMap.put("list", list); return resultMap; } // ES返回的hits为一个字符串数组,数组中的每个值为一个文档对应的json字符串 for (SearchHit hit : hits) { String source = hit.getSourceAsString(); // 通过JSON序列化工具类将json传反序列化为对应的对象,ResultInfo中的字段 // 可以根据需要来确定 ResultInfo resultInfo = JsonUtils.fromJson(source, ResultInfo.class); list.add(resultInfo); } // 封装结果总数 resultMap.put("total", total); // 封装结果列表 resultMap.put("list", list); // 返回结果 return resultMap;
2、ElasticSearch的深分页解决方案代码实现
ES默认查询时,当from+size的值大于10000时,将会直接抛出查询异常,如下:
GET student_index/studentInfo/_search { "from":9999, "size":10 }
返回结果:
{ "error": { "root_cause": [ { "type": "query_phase_execution_exception", "reason": "Result window is too large, from + size must be less than or equal to: [10000] but was [10009]. 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." } ], "type": "search_phase_execution_exception", "reason": "all shards failed", "phase": "query", "grouped": true, "failed_shards": [ { "shard": 0, "index": "student_index", "node": "WkuQMAI5T0uCY-EaJr2jEg", "reason": { "type": "query_phase_execution_exception", "reason": "Result window is too large, from + size must be less than or equal to: [10000] but was [10009]. 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." } } ] }, "status": 500 }
此时,如果业务上无法做限制。则解决方案有如下的三种:
(1)临时针对某个索引修改index.max_result_window参数的值
修改方式如下:
PUT student_index/_settings
{
"max_result_window":20000
}
// 表示将student_index索引的查询结果集改到20000
// 即:from+size可以大于10000,但是不能大于20000
// 设置完成之后,可以通过如下方式查询是否设置成功
GET student_index/_settings
// 返回结果如下
{
"student_index": {
"settings": {
"index": {
"number_of_shards": "5",
"provided_name": "student_index",
"max_result_window": "20000", // from+size最大值已经被修改
"creation_date": "1546011977987",
"number_of_replicas": "1",
"uuid": "Qsp-Pd7HS_y7-BbR2hgr5g",
"version": {
"created": "6030299"
}
}
}
}
}
注意:该方式可以在紧急情况下解决查询问题,但是不建议使用,因为修改了最大大小之后,按照传统的分页方式向后查询的时候,会越来越慢,调用量如果太大,可能会导致ES查询线程被大量阻塞,最终无法响应客户端的查询请求。
(2)使用scroll实现深分页查询
a、ES客户端查询代码:
每次查询时指定scroll的过期时间,然后在返回结果中会有一个_scroll_id的字符串,在下次查询的时候带着该_scroll_id即可,示例如下:
查询第一页时,指定scroll的过期时间及分页参数:
GET student_index/studentInfo/_search?from=0&size=2&scroll=3m
返回结果:
{ // 返回的scroll_id "_scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAKE4FldrdVFNQUk1VDB1Q1ktRWFKcjJqRWcAAAAAAAChOhZXa3VRTUFJNVQwdUNZLUVhSnIyakVnAAAAAAAAoTwWV2t1UU1BSTVUMHVDWS1FYUpyMmpFZwAAAAAAAKE5FldrdVFNQUk1VDB1Q1ktRWFKcjJqRWcAAAAAAAChOxZXa3VRTUFJNVQwdUNZLUVhSnIyakVn", "took": 1, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 6, "max_score": 1, "hits": [ { "_index": "student_index", "_type": "studentInfo", "_id": "5", "_score": 1, "_source": { "name": "xiaozhang", "age": 25, "gender": "male" } }, { "_index": "student_index", "_type": "studentInfo", "_id": "2", "_score": 1, "_source": { "name": "wb3", "age": 24, "gender": "male" } } ] } }
查询第二页:
GET _search/scroll?scroll=10m&scroll_id=DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAKE4FldrdVFNQUk1VDB1Q1ktRWFKcjJqRWcAAAAAAAChOhZXa3VRTUFJNVQwdUNZLUVhSnIyakVnAAAAAAAAoTwWV2t1UU1BSTVUMHVDWS1FYUpyMmpFZwAAAAAAAKE5FldrdVFNQUk1VDB1Q1ktRWFKcjJqRWcAAAAAAAChOxZXa3VRTUFJNVQwdUNZLUVhSnIyakVn
查询结果:和上述查询第一页时返回的结果类似,此处略。
注意:
① 第一次生成了scroll_id之后,后续的查询都会基于这个scroll_id进行查询,只要是在这个scroll_id的有效期内。过期之后,才会返回一个新的scroll_id,所以在有效期内,使用scroll_id去查询数据时,不需要待index和type,直接根据scroll_id查询即可;
② 没读取一页,都会重新设置scroll_id的有效时间,所以这个scroll_id只需要保证读能够读取完当前页内的数据即可,不需要设置太长时间,大多数情况下1m就够用;
③ 切记,最好不要在大的循环内去重复开启游标,这样会造成内存大量浪费,如果循环次数过大,将会导致ES节点内存占用过大,发生Full GC,进而导致ES节点CPU短时间内迅速飙升;
b、Java代码中使用scroll_id代码示例:
// 获取查询对象SearchRequestBuilder,使用不同的客户端框架此处不同 SearchRequestBuilder srq = SearchUtils.getSearchRequestBuilder(); // 设置普通查询条件 BoolQueryBuilder qb = boolQuery(); // 设置查询条件 qb.must(QueryBuilders.termQuery("age", query.getAge())); SearchResponse response = srq .setQuery(qb) .setSize(size) .setScroll(new TimeValue(SCROLL_TIMEOUT)) // 设置游标有效期 .execute() .actionGet(); ResultInfo resultInfo = new ResultInfo(); // 获取查询结果 SearchHits hits = response.getHits(); // 查询到的总数 resultInfo.setTotal(hits.getTotalHits()); // 将查询结果取出来,封装为结果集,放入到resultInfo中 resultInfo.setResult(getResultFromHits(hits)); // 将scrollId放入到返回结果中,在下次调用时传入方法即可. resultInfo.setScrollId(response.getScrollId()); return resultInfo;
(3)使用search_after实现深分页查询
分页查询商品列表,每页查询2条商品数据:
// 查询第一页,按照ID排序 GET product_index/productInfo/_search // 返回结果 { "size": 2, "sort": [ { "id": { "order": "asc" } } ] } // 查询第二页,需要传入第一页最后一个数据的唯一标识,此处使用的是主键ID // 这个唯一标识需要确保全局唯一 GET product_index/productInfo/_search // 返回结果 { "size": 2, "sort": [ { "id": { "order": "asc" } } ], "search_after":[2] }
注意要点:
① search_after的原理就是通过上一页的位置直接查询下一页,所以需要使用一个唯一标识来保证这个位置;
② 在使用searc_after查询时,如果手动传入from参数,from参数的值必须为0或者-1,否则会抛出如下的异常:
{ "error": { "root_cause": [ { "type": "search_context_exception", "reason": "`from` parameter must be set to 0 when `search_after` is used." } ], "type": "search_phase_execution_exception", "reason": "all shards failed", "phase": "query", "grouped": true, "failed_shards": [ { "shard": 0, "index": "product_index", "node": "WkuQMAI5T0uCY-EaJr2jEg", "reason": { "type": "search_context_exception", "reason": "`from` parameter must be set to 0 when `search_after` is used." } } ] }, "status": 500 }
至此,ElasticSearch的深分页问题及对应的解决方案代码示例介绍完毕,欢迎转发!
温馨提示:如果小程序端代码显示混乱,是因为移动端兼容性导致,可移步至PC端站点查看!
文章属于原创,如果转发请标注文章来源:个人小站【www.jinnianshizhunian.vip】