ES 万条以外分页检索功能实现及注意事项

news2024/9/24 18:20:07

背景

以 ES 存储日志,且需要对日志进行分页检索,当数据量过大时,就面临 ES 万条以外的数据检索问题,如何利用滚动检索实现这个需求呢?本文介绍 ES 分页检索万条以外的数据实现方法及注意事项。

需求分析

在这里插入图片描述
用 ES 存储数据,分页检索,当 ES 数据量过大时,在页面上直接点击最后一页时,怎么保证请求能正常返回?

常规思路就是,超过万条以后,使用滚动检索,但需要注意:编写滚动检索的分页查询时,滚动请求的 size 一定不能用页面分页参数的 pageSize ,要能快速滚动到目标页所在的数据,最好以 ES 最大检索窗口值。

算法要点

第一,滚动检索的 Request 请求不能包含 from 属性, 且设置了 size 参数后,以后的每次滚动返回的数据量都以 size 为主。

第二,滚动获取数据的 size 选取。 滚动分页检索高效的关键是不能以页面分页参数 pageSize 作为滚动请求的 size ,而是以一个较大的数,或者直接以 ES 默认的滚动窗口最大值 10000 作为每批次获取的数据量。

第三,计算目标页的数据所在的位置。

  1. 根据分页参数计算出目标数据的位置是 [(pageSize-1)*pageSize, pageSize * pageNo] ,为了拿到目标页的数据,总共的数据量 total = pageNo * pageSize
  2. 目标数据在最终数据中的真正范围决定因素:mode = total % 10000
  3. 计算滚动请求几次能拿到目标数据。实际需要滚动请求的次数 scrollCount = mode == 0 ? total/ esWindowCount : (total/ esWindowCount + 1)
  4. 目标页的数据有没有分布在两次请求中。当 10000 % pageSize !=0 时,说明这一页的数据会横跨两次 ES 请求。例如 pageSize =15,pageNo = 2667,total = 40005,目标页的数据包含在最后两次请求中,倒数第二次请求中有 10 条数据,最后一次请求中有 5 条数据,合起来才是一整页的 15 条数据。
  5. 最后一页数据不足 pageSize 时,最后一页数据真正的长度。

第四,分页数据所在范围处理。 当最后一批次获取到数据后,从中摘出目标页的数据时,需要考虑的四种情况,主要是 mode 和最终获取的数据总长度直接的关系:

在这里插入图片描述
case 1:上图左,mode=0 时存在最后一页不足 size 的情况,realSize = size - (windowSize-length)

case 2:上图右,length < mode 时,最后一页不足 size 的情况,realSize = size - (mode -length)

最终的数据区间是 [from,to ] = [ length -realSize,length -1 ]
数据总长度 = end -start +1 = realSize
在这里插入图片描述
case 3 :上图左,分页数据在 mode 往前推 size 条。
case 4:上图右,分页数据横跨两次请求,两批数据组合成一页数据。

编码实现

编写 ES 滚动分页检索请求,处理超过万条之外的查询操作:

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.action.search.ClearScrollRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.search.SearchScrollRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.*;
import org.elasticsearch.search.Scroll;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.SearchModule;
import org.elasticsearch.search.builder.SearchSourceBuilder;

import java.io.IOException;
import java.util.*;

@Slf4j
public class EsPageUtil {
    /**
     * 真正的 ES 连接对象
     */
    private RestHighLevelClient client;

    public void initClient() {
        // TODO 初始化 client 对象
    }

    /**
     * 使用 DSL JSON 配置创建检索请求 Builder
     * @param queryJson
     * @return
     */
    public SearchSourceBuilder createSearchSource(String queryJson) {
        if (StringUtils.isEmpty(queryJson)) {
            log.error("ElasticSearch dsl config is empty.");
            return null;
        }

        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        try {
            SearchModule searchModule = new SearchModule(Settings.EMPTY, false, Collections.emptyList());
            NamedXContentRegistry registry = new NamedXContentRegistry(searchModule.getNamedXContents());
            XContentParser parser = XContentFactory.xContent(XContentType.JSON).createParser(registry, LoggingDeprecationHandler.INSTANCE, queryJson);
            searchSourceBuilder.parseXContent(parser);
            return searchSourceBuilder;
        } catch (Exception e) {
            log.error("Parse dsl error.", e);
            return null;
        }
    }

    /**
     * ES 分页查询:区分万条以内还是万条以外
     * @param pageSize  分页size
     * @param pageNo    查询页数
     * @param indices   目标索引
     * @param queryJson 查询 DSL JSON 格式字符串
     * @return
     */
    public Map<String, Object> queryByPage(int pageSize, int pageNo, String[] indices, String queryJson) {
        SearchSourceBuilder searchSourceBuilder = createSearchSource(queryJson);
        if (searchSourceBuilder == null) {
            return null;
        }

        // 创建请求对象
        SearchRequest searchRequest = new SearchRequest(indices).source(searchSourceBuilder);

        Map<String, Object> result = new HashMap<>();
        List<Map<String, Object>> data = null;
        int total = pageSize * pageNo ;
        int maxEsWindow = 10000;

        try {
            if (total <= 10000) {
                // 万条以内,直接查询:设置 from , size 属性
                searchSourceBuilder .from((pageNo - 1) * pageSize) .size(pageSize);

                SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
                data =  parseResponseToListData(response);
            } else {
                // 万条以外,以 ES 最大窗口值查询:只设置size 属性
                searchSourceBuilder.size(maxEsWindow);
                data = scrollQuery(maxEsWindow, pageSize, total, searchRequest);
            }
        } catch (IOException e) {
            log.error("ElasticSearch query error.", e);
        }

        result.put("total" , 0);
        result.put("data" , data);
        return result;
    }

    /**
     * 滚动查询
     *
     * @param esWindowCount
     * @param pageSize
     * @param total
     * @param searchRequest
     * @return
     */
    private List scrollQuery(int esWindowCount, int pageSize, int total , SearchRequest searchRequest) {
        List pageData = new ArrayList(pageSize);

        //创建滚动,指定滚动查询保持的时间
        final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(10L));

        //添加滚动
        searchRequest.scroll(scroll);

        //提交第一次请求
        SearchResponse searchResponse = null;
        String scrollId = null;
        try {
            searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
            //获取滚动查询id
            scrollId = searchResponse.getScrollId();
        } catch (IOException e) {
            log.error("Elasticsearch request error.", e);
            return pageData;
        }

        int counter = 2;
        int mode = total % esWindowCount;
        int realPageCount = mode == 0 ? total/ esWindowCount : (total/ esWindowCount + 1);

        while (counter <= realPageCount) {
            // 设置滚动查询id,从id开始继续向下查询
            SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);

            // 重置查询时间,若不进行重置,则在提交的第一次请求中设置的时间结束,滚动查询将失效
            scrollRequest.scroll(scroll);

            // 提交请求,获取结果
            try {
                searchResponse = client.scroll(scrollRequest, RequestOptions.DEFAULT);
            } catch (IOException e) {
                log.error("Elasticsearch scroll request error.", e);
            }

            // size 非 10 的整数,则当前页数据横跨两个 Scroll 请求
            if (mode != 0 && mode < pageSize && counter == (realPageCount -1)) {
                collectFirstPart(searchResponse, pageData, mode, pageSize);
            }

            // 更新滚动查询id
            scrollId = searchResponse.getScrollId();
            counter++;
        }

        // 收集最后一次响应结果中的数据
        collectPageData(searchResponse, pageData, mode, pageSize, esWindowCount);

        //  滚动查询结束时,清除滚动
        ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
        clearScrollRequest.addScrollId(scrollId);
        try {
            client.clearScroll(clearScrollRequest, RequestOptions.DEFAULT);
        } catch (IOException e) {
            log.error("Elasticsearch clear scroll info error.", e);
        }

        return pageData;
    }

    /**
     * @param searchResponse
     * @param mode
     * @param size
     * @return
     */
    public void collectFirstPart(SearchResponse searchResponse, List<Map<String, Object>> firstPartData, int mode, int size) {
        int firstPartCount = size - mode;

        // 只截取响应结果中的 结尾 size - mode 部分的内容
        SearchHits hits = searchResponse.getHits();
        SearchHit[] dataList = hits.getHits();

        int from = dataList.length - firstPartCount;
        for (int i = from; i < dataList.length; i++) {
            firstPartData.add(dataList[i].getSourceAsMap());
        }

        log.info("Mode less than size, first part data is here {} .", firstPartCount);
    }

    /**
     * 滚动到最后一组数据中包含目标页的数据,从中摘出来
     * @param searchResponse
     * @param mode
     * @param size
     * @param esWindowCount
     * @return
     */
    public void collectPageData(SearchResponse searchResponse, List<Map<String, Object>> pageData, int mode, int size, int esWindowCount) {
        SearchHits hits = searchResponse.getHits();
        SearchHit[] dataList = hits.getHits();
        int from = 0;
        int length = dataList.length;
        if (mode == 0) { // 刚好在万条结尾
            // 不够一页
            if (length < esWindowCount) {
                int realSize = size - (esWindowCount - length);
                from = (length - realSize ) >= 0 ? (length - realSize ) : 0;
            } else {// 总长够一页
                from = length == esWindowCount ? (length - size) : 0;
            }
        } else if (length < mode){ // 最后一页且总长不足 size
            int realSize = size - (mode - length);
            from = (length - realSize) >= 0 ? (length - realSize) : 0;
        } else if (mode > size){ // 中间部分
            from = (mode - size) >= 0 ? (mode -size) : 0;
        } else  { // mode < size ,说明是一页数据的下半部分
            from = 0;
            size = mode;
            log.info("Page data is across two request ,this response has {} .", mode);
        }

        // 收集目标数据
        for (int i = from; i< from + size && i < length; i++) {
            pageData.add(dataList[i].getSourceAsMap());
        }
    }

    /**
     * 解析 ES 响应结果为数据集合
     * @param response
     * @return
     */
    public static List<Map<String, Object>> parseResponseToListData(SearchResponse response){
        List<Map<String, Object>> listData = new ArrayList<>();
        if (response == null) {
            return listData;
        }

        // 遍历响应结果
        SearchHits hits = response.getHits();
        SearchHit[] hitArray = hits.getHits();
        listData = new ArrayList<>(hitArray.length);
        for (SearchHit hit : hitArray) {
            Map<String, Object> sourceAsMap = hit.getSourceAsMap();
            listData.add(sourceAsMap);
        }

        // 返回结果
        return listData;
    }
}

启示录

滚动查询时优化了 size 用一万,相比用页面的分页参数 pageSize ,可以解决数据量过大时,直接从页面点击最后一页导致页面卡死长时间无响应的问题。

页面分页参数最大不过 100,当总数量几百万、pageSize=10,分页跳转查询后面某页 如 3000 时,ES 的滚动请求次数 是 3000 次,而优化后滚动请求 3次,第三次中的一万条数据的最后10条即本页的数据。

话说回来,ES 数据量过大时,用分页查询靠后的数据时,也没多大的价值了,列表宽泛条件查询结果过大时,谁看得过来呢?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1243766.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

单个视频生成视频二维码,手把手图文教程

单个视频生成视频二维码帮助教程&#xff08;图文教程&#xff09;&#xff0c;手把手教程如下&#xff1a; STEP1 注册帐号 使用视频二维码&#xff0c;您需要注册酷播云用户帐号&#xff08;免费5G空间&#xff0c;普通用户够用&#xff09;。 参考如图1-1&#xff0c;按照…

【数据结构/C++】栈和队列_链栈

链头 栈顶。 #include<iostream> using namespace std; // 链栈 typedef int ElemType; typedef struct Linknode {ElemType data;struct Linknode *next; } *LiStack; // 初始化 void InitLiStack(LiStack &S) {S (LiStack)malloc(sizeof(struct Linknode));S->…

安全框架springSecurity+Jwt+Vue-2(后端开发)

一、创建项目及配置 ①&#xff1a;创建新的项目及常用包 ②&#xff1a;引入依赖和配置 devtools&#xff1a;项目的热加载重启插件 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId&…

RK3399平台开发系列讲解(内核入门篇)ConfigFS 的核心数据结构

🚀返回专栏总目录 文章目录 一、关键数据结构二、config_item 的结构体三、属性和方法沉淀、分享、成长,让自己和他人都能有所收获!😄 📢虚拟文件系统 ConfigFS 是一个特殊的文件系统,旨在提供一种动态配置 Linux 内核和设备的机制。 一、关键数据结构 ConfigFS 的核…

[23] 4K4D: Real-Time 4D View Synthesis at 4K Resolution

paper | proj | code 提出一种基于K-Planes的4D point cloud Representation&#xff1b;提出一种Hybrid appearance model&#xff0c;包含image blending model和SH model。其中&#xff0c;image blending model将3D点映射回原图中求得&#xff0c;SH model通过模型预测求得…

机器学习/sklearn笔记:MeanShift

1 算法介绍 一种基于质心的算法通过更新候选质心使其成为给定区域内点的均值候选质心的位置是通过一种称为“爬山”技术迭代调整的&#xff0c;该技术找到估计的概率密度的局部最大值 1.1 基本形式 给定d维空间的n个数据点集X&#xff0c;那么对于空间中的任意点x的均值漂移…

HCIP --- HCIA(部分汇总)--- 点对点网络

抽象语言 --- 电信号 抽象语言 --- 编码 编码 --- 二进制 二进制 --- 电信号 处理电信号 OSI/RM ---- 开放式系统互联参考模型 --- 1979 --- ISO --- 国际标准化组织 核心思想 --- 分层 应用层 --- 提供各种应用程序&#xff0c;抽象语言转换成编码&#xff0c;人机交互…

导数、方向导数、梯度方向、梯度

导数&#xff1a;自变量改变一定量时&#xff08;大于或小于0&#xff09;&#xff0c;因变量改变多少 方向导数&#xff1a;限定在某一个方向上&#xff0c;自变量改变一定量时&#xff08;大于0&#xff09;&#xff0c;因变量改变多少 梯度方向&#xff1a;方向导数最大的…

PHP 语法||PHP 变量

PHP 脚本在服务器上执行&#xff0c;然后将纯 HTML 结果发送回浏览器。 基本的 PHP 语法 PHP 脚本可以放在文档中的任何位置。 PHP 脚本以 <?php 开始&#xff0c;以 ?> 结束&#xff1a; <?php // PHP 代码 ?> 值得一提的是&#xff0c;通过设定php.ini的相…

【MATLAB源码-第87期】基于matlab的Q-learning算法栅格地图路径规划,自主选择起始点和障碍物。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 Q-learning是一种无模型的强化学习算法&#xff0c;适用于有限的马尔可夫决策过程&#xff08;MDP&#xff09;。它的核心是学习一个动作价值函数&#xff08;action-value function&#xff09;&#xff0c;即Q函数&#xf…

YOLO目标检测——泄露检测数据集下载分享【含对应voc、coco和yolo三种格式标签】

实际项目应用&#xff1a;泄露检测数据集说明&#xff1a;泄露检测数据集&#xff0c;真实场景的高质量图片数据&#xff0c;数据场景丰富&#xff0c;含多个类别标签说明&#xff1a;使用lableimg标注软件标注&#xff0c;标注框质量高&#xff0c;含voc(xml)、coco(json)和yo…

Visual Studio(VS) C++程序LNK2005错误,提示“error LNK2005: _XXX已经在xxx.obj中定义”解决方案

1.问题如图 2.出现原因 项目中有多个源文件或头文件&#xff0c;include后导致有些变量重复定义&#xff0c;加上Visual Studio新版版要求更严格 3.解决办法 查询到的解决办法很多不好用&#xff0c;此处记录解决自己问题的一个办法&#xff1a;直接让编译器忽略第二次定义的…

Linux CentOS7的主机名

主机名&#xff0c;也称为计算机名&#xff0c;是提供给网络连接的设备&#xff08;如系统、交换机、路由器等&#xff09;的识别名称。同一网络中不能有两个主机名相同的系统。Linux系统给当前主机命名的目的是能够容易记住&#xff0c;尤其是在部署集群的时候更加方便。 一般…

【C++】POCO学习总结(五):功能介绍

【C】郭老二博文之&#xff1a;C目录 1、POCO 简介 github&#xff1a;https://github.com/pocoproject/poco 官网&#xff1a;https://pocoproject.org/index.html POCO第一个版本于 2005 年 2 月发布 POCO完全免费&#xff1a;POCO C 库根据 Boost 软件许可证获得许可。非…

【C语法学习】26 - strcmp()函数

文章目录 1 函数原型2 参数3 返回值4 比较机制5 示例5.1 示例1 1 函数原型 strcmp()&#xff1a;比较str1指向的字符串和str2指向的字符串&#xff0c;函数原型如下&#xff1a; int strcmp(const char *str1, const char *str2);2 参数 strcmp()函数有两个参数str1和str2&a…

【python爬虫】scrapy在pycharm 调试

scrapy在pycharm 调试 1、使用scrapy创建一个项目 scrapy startproject tutorial 2、在朋友pycharm中调试scrapy 2.1 通过文件run.py调试 在根目录下新建一个文件run.py(与scrapy.cfg文件的同一目录下), debug ‘run’即可 # -*- coding:utf-8 -*- from scrapy import c…

excel单元格加背景颜色不生效?

如果在 Excel 中设置单元格背景颜色而发现不生效&#xff0c;可能有几个原因。以下是一些常见的解决方法&#xff1a; 1. **单元格锁定&#xff1a;** 检查所在单元格是否被锁定。如果单元格被锁定&#xff0c;并且工作表被保护&#xff0c;你可能无法更改其背景颜色。在工作表…

基于51单片机数字电流表数码管显示( proteus仿真+程序+设计报告+讲解视频)

电流表 1. 主要功能&#xff1a;2. 讲解视频&#xff1a;3. 仿真4. 程序代码5. 设计报告6. 设计资料内容清单&&下载链接资料下载链接&#xff1a; 基于51单片机数字电流表数码管显示( proteus仿真程序设计报告讲解视频&#xff09; 仿真图proteus7.8及以上 程序编译器…

Flutter学习(四)如何取消listview的越界效果

背景 在flutter的开发过程中&#xff0c;ListView是很常见的一个组件&#xff0c;但是&#xff0c;由于ListView的某些自带的体验&#xff0c;导致不太好的用户体验。例如ListView中&#xff0c;滑动到顶部或者底部的时候&#xff0c;再次滑动&#xff0c;会有越界的效果&…

(保姆级教程)Mysql中事务的概念,什么是事务,如何使用事务,以及事务的隔离级别,什么是脏读、幻读,代码演示

继续讲解 Mysql 数据库中最重要的一个概念&#xff1a;事务 文章目录 事务1.1 什么是事务1.2 执行原理1.3 如何操作事务1.4 事务的特点&#xff08;ACID原则&#xff09;1.5 事务并发1.6 事务隔离级别1.6.1 事务并发问题操作演示1.6.2 脏读演示1.6.3 不可重复读演示1.6.4 幻读演…