Elasticsearch 深分页问题

——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/

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值