——from+size、search after、scroll
背景
订单查询从mysql迁移至Es,分页查询订单有超过1w条的需求
Es几种分页方式
from+size
1、coordinate node向index的其余的shards 发送同样的请求,请求查询from+size条记录
2、汇总(shards * (from + size))
条记录到coordinate node
3、coordinate node排序记录,最终抽取出真正的 from 后的 size 条结果
索引非常大时(千万级或亿级),无法用这个方法做深度分页,有OOM的风险
Java API:6.3
SearchResponse response = client.prepareSearch("index1", "index2") .setSearchType(SearchType.DFS_QUERY_THEN_FETCH) .setQuery(QueryBuilders.termQuery("multi", "test")) // Query .setPostFilter(QueryBuilders.rangeQuery("age").from(12).to(18)) // Filter .setFrom(0).setSize(60).setExplain(true) .get(); |
search after
1、search after需要指定排序
官方文档建议:
每个文档具有一个唯一值的字段应该用作排序。否则,具有相同排序值的文档的排序顺序将无法识别。建议的方法是使用每个文档唯一值字段_id
排序。
GET twitter/_search { "size": 10, "query": { "match" : { "title" : "elasticsearch" } }, "search_after": [1463538857, "654323"], "sort": [ {"date": "asc"}, {"_id": "desc"} ] } |
当时使用文档的_id
排序感觉很慢,后使用业务中的id
字段
2、必须从第一页开始搜起(你可以随便指定一个坐标让它返回结果,只是你不知道会在全量结果的何处)
3、从第一页开始以后每次都带上search_after
排序值,从而为无状态实现一个状态,把每次固定的from + size偏移变成一个确定值,而查询则从这个偏移量开始获取size个doc,每个shard 获取size个,coordinate node最后汇总 shards*size 个。search after是一个常量查询延迟和开销
Java API:6.3
SearchResponse response = client.prepareSearch("index1", "index2") .setSearchType(SearchType.DFS_QUERY_THEN_FETCH) .setQuery(QueryBuilders.termQuery("multi", "test")) // Query .searchAfter(sortValues) //sortValues .setPostFilter(QueryBuilders.rangeQuery("age").from(12).to(18)) // Filter .setFrom(0).setSize(60).setExplain(true) .get(); |
使用search after方式,参数from
必须设置为0(或-1)
Scroll
使用scroll分页主要分为2步
第1步,初始化查询,获取scrollId,初始化时将所有符合搜索条件的搜索结果缓存起来,可以想象成快照。
第2步,使用scrollId迭代查询,从这个快照里取数据,也就是说,在初始化后对索引插入、删除、更新数据都不会影响遍历结果。
import static org.elasticsearch.index.query.QueryBuilders.*; QueryBuilder qb = termQuery("multi", "test"); SearchResponse scrollResp = client.prepareSearch(test) .addSort(FieldSortBuilder.DOC_FIELD_NAME, SortOrder.ASC) .setScroll(new TimeValue(60000)) .setQuery(qb) .setSize(100).get(); //max of 100 hits will be returned for each scroll //Scroll until no hits are returned do { for (SearchHit hit : scrollResp.getHits().getHits()) { //Handle the hit... } scrollResp = client.prepareSearchScroll(scrollResp.getScrollId()).setScroll(new TimeValue(60000)).execute().actionGet(); } while(scrollResp.getHits().getHits().length != 0); // Zero hits mark the end of the scroll and the while loop. |
Scroll与search after对比
1、scroll与search after都不能解决跳页问题
2、scroll不适合用来做实时搜索,而更适用于后台批处理任务,短时间内不断重复同一查询
3、search after始终针对最新版本进行查询,可用于实时用户请求
折衷方案
由于scroll无法做到实时查询,也不适用前段用户请求,最后采用from size + seach after结合的方式
总体思路,隐式支持跳页:
sortValues不为空时,使用searh after方式
sortValues为空时,使用from size方式
1、用户第一次查询后,缓存返回结果最后一条记录的sortValue,作为下一页的search after参数
2、用户跳页时,如果缓存中没有sortValue值,将使用from size方式查询,最跳至10000条记录处
3、用户点击一页后,持续点击下一页,将使用search after方式,可持续进行查询,没有深分页限制
4、折衷方案,最好的方式是前端尽量使用下一页方式查询,不要支持跳页
class Service { public Response queryFromEs(Request request) { Response response = null; int pageNo = request.getPageNo(); int pageSize = request.getPageSize(); int from = (pageNo - 1) * pageSize; //从Es中获取当前pageNo对应的的sortValue Object[] sortValues = getSortValue(request); Result result = Seacher.deepQuery(sortValues, from, pageSize); List hits = result.getHits(); if(CollectionUtils.isNotEmpty(hits)){ DocValue docValue = hits.get(hits.size() -1); //Es结果最后一条记录的sortValue设置到redis中,作为请求下一页的参数 setSortValueTodRedis(request, docValue.getSortValue()); } response.setResult(hits); response.setTotal(result.getTotal()); return response; } /** * 获取当前请求对应的sortValue * @param request * @return */ private Object[] getSortValue(Request request) { Object[] sortValues = null; String sortValue = JedisUtil.get(getRequestMd5(request, request.getPageNo(), request.getPageSize())); if (StringUtils.isNotBlank(sortValue)) { String[] values = sortValue.split(","); sortValues = new Object[2]; sortValues[0] = Long.valueOf(values[0]);//date sortValues[1] = values[1];//id } return sortValues; } /** * 获取当前请求参数对应的md5值 */ private String getRequestMd5(Request request, int pageNo, int pageSize) { return Md5Util.EncoderByMd5(SORTVALUE_PREFIX + request.getParam1() + request.getParam2() + request.getParam3() + pageNo + pageSize); } /** * 在redis中缓存设置当前请求下一页的sortValues值 */ private void setSortValuesTodRedis(Request request, String sortValue) { String md5Key = getRequestMd5(request, request.getPageNo() + 1, request.getPageSize()); JedisUtil.setNx(md5Key, sortValue, TimePeriod.Seconds.minutes30); } } |
class Seacher { /** * sortValues不为空时,使用searh after方式 * sortValues为空时,使用from size方式 */ public Result deepQuery(Object[] sortValues, int from, int size) { Result result = new Result<>(); QueryBuilder qb = termQuery("multi", "test"); SearchRequestBuilder request = client.prepareSearch("index1") .setTypes("type1") .addSort(SortBuilders.fieldSort("date").order(SortOrder.DESC)) .addSort(SortBuilders.fieldSort("id").order(SortOrder.DESC)) .setQuery(qb); if(sortValues != null){ request.searchAfter(sortValues); from = 0;//search_after方式 from必须从0开始 } request.setFrom(from); request.setSize(size); //超出ES from + size 最大限制,ES_MAX_FROM_SIZE=10000 if (from + size > ES_MAX_FROM_SIZE){ result.setTotal(0); result.setHits(new ArrayList<>()); log.warn("from + size >= ES_MAX_FROM_SIZE, Query DSL:{}", request.toString()); return result; } log.info("Query DSL:{}", request.toString()); SearchResponse response = request.get(); SearchHits hits = response.getHits(); result.setTotal(hits.getTotalHits()); result.setHits(hits); return result; } } |
文章来源:https://www.oolongbox.com/box/8be2bca3/