Elasticsearch from+size与scroll混合使用实现深度分页搜索

一. 需求

环境准备: JDK1.8 Elasticsearch7.3.1 RestHighLevelClient客户端
对Elasticsearch做深度分页,比如第1500页,每页20条记录,且需要支持前后翻页。

二. 思考

由于index.max_result_window的限制,直接使用from+size无法搜索满足条件10000条以上的记录。如果贸然增大index.max_result_window值,那么你怎么知道系统未来会在索引内存多少条数据?

就算这一次设置值暂时解决了问题,那么未来又陷入瓶颈了怎么办?重新设值吗?调大后会增大内存压力的问题难道就不需要考虑吗?

这时就需要使用scroll了,但scroll不能盲目的使用,它虽然支持深度分页,纯粹的使用scroll只能不断地向后翻页,我们还需要考虑如何向前翻页。

三. 实现方案

不改变index.max_result_window的默认值,但搜索手段根据搜索数量划分为以下两种:

  1. 搜索数量<=10000
    使用from+size的方式分页和搜索数据。
  2. 搜索数量>10000
    使用scroll的方式搜索数据。针对对每次分页查询请求,我都会创建游标,接着手动滚动到包含请求数据的那一屏,最后取出请求页面中的目标数据。

比如现在准备查询第1413页,页面容量为10条数据,游标每次移动1000条记录,总记录数为1000000(这个值不重要了)。如果以1作为第一条数据的下标,则有以下规律:

滚屏次数数据的下标范围
11~1000
21001~2000
1514001 ~ 15000
n(n-1) * 1000 + 1 ~ n*1000

第1413页的第一条数据的下标=(1413-1)*10+1=14121
第1413页的最后一条数据的下标=14121+10-1=14130
只需要移动15次游标,则在第15次游标查询返回的1000条数据中,一定包含了第1413页的所有数据。

但我们还需要考虑另一种情况,比如现在准备查询第934页,页面容量为15条数据,游标仍然保持每次移动1000条记录。
第934页的第一条数据的下标=(934-1)*15+1=13996
第934页的最后一条数据的下标=13996+15-1=14010
注意,我们的游标只能获取13001~14000和14001~15000范围内的数据,第934页会横跨两次游标执行结果,针对这种情况,我在代码中做了特殊处理。

接下来是代码:

  • 定义搜索条件
// 自定义搜索条件
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
boolQueryBuilder.must(QueryBuilders.matchQuery("name", "麦当劳"));

SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(boolQueryBuilder);
// 设置请求超时时间
sourceBuilder.timeout(new TimeValue(20, TimeUnit.SECONDS));
// 排序
sourceBuilder.sort("salary", SortOrder.ASC);
  • 与ES客户端交互的底层逻辑
    esClient就是RestHighLevelClient的对象
protected SearchResponse search(String requestIndexName, SearchSourceBuilder sourceBuilder) throws Exception {
    SearchRequest searchRequest = new SearchRequest(requestIndexName);
    searchRequest.source(sourceBuilder);
    return esClient.search(searchRequest, RequestOptions.DEFAULT);
}

protected SearchResponse search(String requestIndexName,SearchSourceBuilder searchSourceBuilder,
                                        TimeValue timeValue) throws IOException {
    SearchRequest searchRequest = new SearchRequest(requestIndexName);
    searchSourceBuilder.size(ElasticsearchConstant.MAX_SCROLL_NUM);
    searchRequest.source(searchSourceBuilder);
    searchRequest.scroll(timeValue);
    return esClient.search(searchRequest, RequestOptions.DEFAULT);
}

protected SearchResponse searchScroll(String scrollId, TimeValue timeValue) throws IOException {
    SearchScrollRequest searchScrollRequest = new SearchScrollRequest(scrollId);
    searchScrollRequest.scroll(timeValue);
    return esClient.scroll(searchScrollRequest, RequestOptions.DEFAULT);
}
  • 搜索逻辑(核心代码)
// 本次搜索满足条件的数据总数
long total = 0;
// 精度
int accuracy = 1;
// 希望被忽略的记录条数
int ignoreLogNum = (pageNum - 1) * pageSize;
// 待查询页面内第一条记录的下标
int firstSelectLogNum = 1;
// 待查询页面内最后一条记录的下标
int lastSelectLogNum = -1;
// 当前游标查询返回结果中最后一条记录的下标
int lastAllowLogNum = -1;
// 游标Id
String scrollId = null;
// Elasticsearch 搜索返回结果对象
SearchResponse response = null;

try {
    firstSelectLogNum = ignoreLogNum + 1;
    lastSelectLogNum = firstSelectLogNum + pageSize - 1;
    String indexName = ElasticsearchConstant.SUB_INDEX_NAME_PREFIX + bizSubLogQuery.getProductNum().toLowerCase();
    if(firstSelectLogNum > ElasticsearchConstant.MAX_RESULT_WINDOW) {
        // 构建游标查询 此时游标已经移动了1次
        response = search(indexName, sourceBuilder, TimeValue.timeValueMinutes(1));
        if(response != null && response.getHits().getHits().length > 0) {
            // 游标总共需要移动的次数
            int scrollNum = firstSelectLogNum / ElasticsearchConstant.MAX_SCROLL_NUM + 1;
            lastAllowLogNum = scrollNum * ElasticsearchConstant.MAX_SCROLL_NUM;
            accuracy = firstSelectLogNum - (firstSelectLogNum / ElasticsearchConstant.MAX_SCROLL_NUM) * ElasticsearchConstant.MAX_SCROLL_NUM;
            // 游标Id
            scrollId = response.getScrollId();
            // 游标还需移动scrollNum-1次
            while(--scrollNum > 0 && scrollId != null) {
                response = searchScroll(scrollId, TimeValue.timeValueMinutes(1));
                scrollId = response.getScrollId();
            }
        }
    } else {
        // 分页参数
        sourceBuilder.from((pageNum - 1) * pageSize);
        sourceBuilder.size(pageSize);

        // 获取满足记录的总条数
        response = search(indexName, sourceBuilder);
    }

    // 查询总数
    sourceBuilder.size(0);
    sourceBuilder.trackTotalHits(true);
    SearchResponse sumResponse = search(indexName, sourceBuilder);
    if(sumResponse != null) {
        total = sumResponse.getHits().getTotalHits().value;
    }
} catch (ElasticsearchStatusException ese) {
    if (RestStatus.NOT_FOUND == ese.status()) {
        log.error("待搜索的产品不存在");
    } else {
        log.error(ese.getMessage());
    }
} catch (IOException ioe) {
    log.error("搜索失败,网络连接出现异常", ioe);
} catch (Exception e) {
    log.error("搜索失败,未知异常", e);
}

if (response == null) {
    return new PageInfo<>();
}

// 搜索结果,使用集合来存放
List<Map<String, String>> list = new ArrayList<>();

// 游标一次性最高可能返回1000条数据,需要通过页面容量来约束
int maxPageSize = pageSize;

for (int i = 0; i < response.getHits().getHits().length; i++) {
    if(i+1 >= accuracy) {
        SearchHit hit = response.getHits().getAt(i);
        if(--maxPageSize < 0) {
            break;
        }
        try {
            list.add(JacksonUtils.jsonStrToMap(hit.getSourceAsString(), String.class, String.class));
        } catch (JsonProcessingException e) {
            log.error("jackson转换异常", e);
        }
    }
}

if(scrollId != null && maxPageSize>0 && lastAllowLogNum!=-1 && lastSelectLogNum>lastAllowLogNum) {
    // 存在目标数据不在本次游标查询的结果范围内
    // 需要再次移动游标 (务必保证游标移动的步长大于页面容量)
    try {
        response = searchScroll(scrollId, TimeValue.timeValueMinutes(1));
        for(int i = 0; i < maxPageSize && i < response.getHits().getHits().length; i++) {
            SearchHit hit = response.getHits().getAt(i);
            try {
                list.add(JacksonUtils.jsonStrToMap(hit.getSourceAsString(), String.class, String.class));
            } catch (JsonProcessingException e) {
                log.error("jackson转换异常", e);
            }
        }
    } catch (IOException ioe) {
        log.error("搜索失败,网络连接出现异常", ioe);
    }
}
  • 2
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Elasticsearch使用深度分页功能需要注意以下几点: 1. 尽量避免使用深度分页功能,因为它会增加网络和计算开销,可能导致性能问题。 2. 深度分页功能是通过设置 from 和 size 参数来实现的。from 参数表示从哪个位置开始查询,size 参数表示每页返回的文档数量。 3. Elasticsearch 默认最多只能返回 10000 条记录,如果需要查询更多的记录,需要设置 index.max_result_window 参数。但是设置太大会占用过多的内存,影响性能。 下面是一个 Java 实现 Elasticsearch 分页查询的示例代码: ``` import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.SearchType; import org.elasticsearch.client.Client; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHits; import org.elasticsearch.search.sort.SortBuilders; import org.elasticsearch.search.sort.SortOrder; public class ESQuery { private Client client; public ESQuery(Client client) { this.client = client; } public void search(String index, String type, int from, int size) { SearchResponse response = client.prepareSearch(index) .setTypes(type) .setQuery(QueryBuilders.matchAllQuery()) .addSort(SortBuilders.fieldSort("_id").order(SortOrder.DESC)) .setSearchType(SearchType.DFS_QUERY_THEN_FETCH) .setFrom(from) .setSize(size) .execute() .actionGet(); SearchHits hits = response.getHits(); for (SearchHit hit : hits) { System.out.println(hit.getSourceAsString()); } } } ``` 调用示例: ``` ESQuery esQuery = new ESQuery(client); esQuery.search("my_index", "my_type", 0, 10); // 查询第一页,每页10条记录 esQuery.search("my_index", "my_type", 10, 10); // 查询第二页,每页10条记录,从第11条记录开始 ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值