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

背景

以 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);//获取滚动查询idscrollId = 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);}// 更新滚动查询idscrollId = 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){ // 最后一页且总长不足 sizeint 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.mzph.cn/news/166227.shtml

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

相关文章

css图片缩放属性object-fit说明

object-fit 属性可以设置以下值&#xff1a; 属性值说明例子fill填充容器&#xff0c;可能会改变图片的比例。object-fit: fill;contain保持图片的原始比例&#xff0c;确保图片完全包含在容器内。object-fit: contain;cover保持图片的原始比例&#xff0c;确保图片覆盖整个容…

性能优化中使用Profiler进行页面卡顿的排查及解决方式

文章目录 一、前言二、页面卡顿的排查方式1、耗时操作的监控2、页面卡顿的监控 三、参考链接 一、前言 程序的优化在做过线上bug处理&#xff0c;布局层级优化&#xff0c;项目依赖库版本更新&#xff0c;重复库合并&#xff0c;删除未使用的资源&#xff0c;删除冗余的库&…

机器学习【01】相关环境的安装

学习实例 参考资料&#xff1a;联邦学习实战{杨强}https://book.douban.com/subject/35436587/ 项目地址&#xff1a;https://github.com/FederatedAI/Practicing-Federated-Learning/tree/main/chapter03_Python_image_classification 一、环境准备 GPU安装CUDA、cuDNN pytho…

PHP 语法||PHP 变量

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

nvm-切换node版本工具安装-方便好用

去官网下载&#xff1a; https://github.com/coreybutler/nvm-windows#installation--upgrades 网站进去后点击下载&#xff0c;点击那个exe文件就下载本地&#xff0c;然后双击安装 安装nvm 就直接按照窗口提示的下一步就行&#xff0c;如果改了某些地方会不成功&#xf…

数字孪生技术:提升UI交互性与个性化设计

随着数字化时代的到来&#xff0c;数字孪生技术正在逐渐改变我们的生活和工作方式。数字孪生是一种复制现实世界系统或实体的技术&#xff0c;通过创建数字模型来模拟现实世界中的各种行为和事件。这种技术不仅为人们提供了一个全新的视角来看待和解决问题&#xff0c;同时也为…

内衣专用洗衣机怎么样?口碑最好的小型洗衣机

随着人们的生活水平的提升&#xff0c;越来越多小伙伴来开始追求更高的生活水平&#xff0c;一些智能化的小家电就被发明出来&#xff0c;而且内衣洗衣机是其中一个。现在通过内衣裤感染到细菌真的是越来越多&#xff0c;所以我们对内衣裤的清洗频次会高于普通衣服&#xff0c;…

Spring Boot 3.2发布:大量Java 21的支持上线,改进可观测性

就在今天凌晨&#xff0c;Spring Boot 3.2正式发布了&#xff01;该版本是在Java 21正式发布之后的重要支持版本&#xff0c;所以在该版本中包含大量对Java 21支持的优化。 下面&#xff0c;我们分别通过Spring官方发布的博文和Josh Long长达80分钟的介绍视频&#xff0c;一起…

飞翔的鸟游戏

一.准备工作 首先创建一个新的Java项目命名为“飞翔的鸟”&#xff0c;并在src中创建一个包命名为“com.qiku.bird"&#xff0c;在这个包内分别创建4个类命名为“Bird”、“BirdGame”、“Column”、“Ground”&#xff0c;并向需要的图片素材导入到包内。 二.代码呈现 pa…

【医学图像处理】超详细!PET图像批量预处理

目录 一、单个PET图像预处理1、使用[MRIConvert](https://pan.baidu.com/s/1cn3kgeVRir8HvP6HHm0M0Q?pwd5rt5)处理DCM2、MRI和PET数据预处理过程1&#xff09; 打开matlab命令行输入spm pet&#xff0c;打开SMP12&#xff0c;界面如下2&#xff09; Realign&#xff0c;只需要…

【Vue】插值表达式

作用&#xff1a;利用表达式进行插值渲染 语法&#xff1a;{ { 表达式 } } 目录 案例一&#xff1a; 案例二&#xff1a; 案例三&#xff1a; ​编辑 注意&#xff1a; 案例一&#xff1a; <!DOCTYPE html> <html lang"en"> <head><me…

项目中如何配置数据可视化展现

在现今数据驱动的时代&#xff0c;可视化已逐渐成为数据分析的主要途径&#xff0c;可视化大屏的广泛使用便应运而生。很多公司及政务机构&#xff0c;常利用大屏的手段展现其实力或演示业务&#xff0c;可视化的效果能让观者更快速的理解结果并直观的看到数据展现。因此&#…

加速软件开发:自动化测试在持续集成中的重要作用!

持续集成的自动化测试 如今互联网软件的开发、测试和发布&#xff0c;已经形成了一套非常标准的流程&#xff0c;最重要的组成部分就是持续集成&#xff08;Continuous integration&#xff0c;简称CI&#xff0c;目前主要的持续集成系统是Jenkins&#xff09;。 那么什么是持…

教育+AIGC开局之年:教育派作业帮、科技派科大讯飞同路不同道

配图来自Canva可画 与往年相比&#xff0c;今年的双11显得格外冷清&#xff0c;GMV&#xff08;商品交易总额&#xff09;数据和增长数据无人提及&#xff0c;京东、淘宝天猫、抖音、快手等平台的火药味都淡了。一片祥和有序的双11氛围中&#xff0c;昔日的K12教育企业与科技企…

清华大学提出全新加速训练大模型方法SoT

近日&#xff0c;微软研究和清华大学的研究人员共同提出了一种名为“Skeleton-of-Thought&#xff08;SoT&#xff09;”的全新人工智能方法&#xff0c;旨在解决大型语言模型(LLMs)生成速度较慢的问题。 尽管像GPT-4和LLaMA等LLMs在技术领域产生了深远影响&#xff0c;但其处…

提供电商数据|带你简单认识天猫API接口相关参数文档调用说明

什么是API接口 API接口(Application Programming Interface Interface)是应用程序与开发人员或其他程序互相通信的方式。它允许开发者访问应用程序的数据和功能。 API接口,软件的“握手”与“交流”之道,软件世界的“好基友”。想让软件聊得来?想开发App却无从下手?API来相救…

【腾讯云云上实验室-向量数据库】Tencent Cloud VectorDB为非结构化数据查询插上飞翔的翅膀——以企业知识库为例

前言 以前我曾疑惑&#xff0c;对于非结构化的内容&#xff0c;如一张图片或一段视频&#xff0c;如何实现搜索呢&#xff1f;图片或视频作为二进制文件&#xff0c;我们如何将其转化为可搜索的数据并存储起来&#xff0c;然后在搜索时将其还原呢&#xff1f; 后来我发现&…

Tomcat 配置

1&#xff1a; 打开 2&#xff1a;选择版本号&#xff0c;我这边是 1.7 3&#xff1a;添加 web 4: 添加jar包 5&#xff1a;添加 6&#xff1a;添加 Tomcat

【每日一题】1410. HTML实体解析器-2023.11.23

题目&#xff1a; 1410. HTML 实体解析器 「HTML 实体解析器」 是一种特殊的解析器&#xff0c;它将 HTML 代码作为输入&#xff0c;并用字符本身替换掉所有这些特殊的字符实体。 HTML 里这些特殊字符和它们对应的字符实体包括&#xff1a; 双引号&#xff1a;字符实体为 &…