ElasticSearch深度分页并可以小幅度跳页的实现

这篇具有很好参考价值的文章主要介绍了ElasticSearch深度分页并可以小幅度跳页的实现。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

背景

最近项目上有个日志采集,我作为接收端接收udp发送过来的报文数据缓存到es上,然后查询es上的数据分页展示。但是之后我发现es对分页支持很不友好,它分为深分页与浅分页,浅分页就是MySQL里的limit,但是他最大展示长度只能到10000,也就是说当每页100条数据的话,只能翻100页,超过会报错。 所以你要么做限制,尽可能的把数据控制在10000条以内,要么对前端翻页进行限制。

下面我们针对es提供的search after深分页来完成小幅跳页的操作, 所谓的小幅跳页就是虽然我不能直接从第一页到最后一页,但是我也可以通过缓存游标的方式实现几页几页的跳,search after深分页的方式只能一直往后翻,scroll我不太了解,但是应该原理差不多。

环境

jdk8, es7.6.1, maven3.3.9, springboot2.3.2

代码

添加依赖

<dependencies>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.20</version>
		</dependency>

		<dependency>
			<groupId>cn.hutool</groupId>
			<artifactId>hutool-all</artifactId>
			<version>5.8.5</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

配置

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author 朝花不迟暮
 * @version 1.0
 * @date 2020/9/26 9:08
 */
@Configuration
public class ElasticSearchClientConfig
{
    @Bean
    public RestHighLevelClient restHighLevelClient()
    {
        RestHighLevelClient client = new RestHighLevelClient(
                RestClient.builder(new HttpHost("119.29.10.76", 9200, "http"))
        );
        return client;
    }
}

创建实体

索引类

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.util.Date;

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Document {
    /**
     * es中的唯一id
     */
    private Long id;
    /**
     * 文档标题
     */
    private String title;
    /**
     * 文档内容
     */
    private String content;
    /**
     * 创建时间
     */
    private Date createTime;
    /**
     * 当前时间
     */
    private Long currentTime;
}

传输层,当然有冗余设计,各位取其精华去其糟粕吧

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.util.Date;

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class DocumentDTO {

    private Integer pageNum = 1;

    private Integer pageSize = 10;

    /**
     * es中的唯一id
     */
    private Long id;

    /**
     * 文档标题
     */
    private String title;

    /**
     * 文档内容
     */
    private String content;

    /**
     * 创建时间
     */
    private Date createTime;

    /**
     * 当前时间
     */
    private Long currentTime;

    /**
     * 开始时间
     */
    private String startTime;

    /**
     * 结束时间
     */
    private String endTime;

    /**
     * 最后一页的游标页码
     */
    private Object[] lastPageSort;
}

返回对象,可根据需求自定义

import com.study.sample.entity.Document;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class DocumentVO {

    private Integer pageNum;

    private Integer pageSize;

    private long total;

    private List<Document> data;
}

服务层

import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import com.study.sample.entity.Document;
import com.study.sample.entity.dto.DocumentDTO;
import com.study.sample.entity.vo.DocumentVO;
import com.study.sample.service.DocumentService;
import com.study.sample.utils.DateParseUtil;
import com.study.sample.utils.EsClientUtil;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.*;

@Service
@Slf4j
public class DocumentServiceImpl implements DocumentService {

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    //存储游标的集合
    private static final Map<String, Map<Integer, Object[]>> sortMap = new HashMap<>(256);

    @Override
    public DocumentVO deepSearchPage(DocumentDTO documentDTO, HttpServletRequest req) {
        String id = req.getSession().getId();
        //条件构造器
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        //返回的数据
        List<Document> documents = new ArrayList<>();
        //当前页
        int currentPageNum;
        //总数
        long total = 0;

        DocumentVO documentVO = new DocumentVO();

        if (StrUtil.isEmpty(id)) throw new RuntimeException("id不能为空");

        //页码和游标对应的集合
        Map<Integer, Object[]> pageMap = sortMap.get(id);

        //---------------设置查询条件start--------------
        //范围查询
        if (documentDTO.getStartTime() != null) {
            Date startDate = DateParseUtil.parseString2Date(documentDTO.getStartTime());
            boolQueryBuilder.filter(QueryBuilders.rangeQuery("createTime").gte(startDate));
        }
        if (documentDTO.getEndTime() != null) {
            Date endDate = DateParseUtil.parseString2Date(documentDTO.getEndTime());
            boolQueryBuilder.filter(QueryBuilders.rangeQuery("createTime").lte(endDate));
        }
        // 模糊查询
        if (documentDTO.getContent() != null) {
            // 同一字段在多个field里查询
            // boolQueryBuilder.filter(QueryBuilders.multiMatchQuery(documentDTO.getContent(), fields));

            boolQueryBuilder.must((QueryBuilders.wildcardQuery("content", documentDTO.getContent())));
        }
        if (documentDTO.getTitle() != null) {
            boolQueryBuilder.should((QueryBuilders.wildcardQuery("title", documentDTO.getTitle())));
        }
        //---------------设置查询条件end----------------

        /*首先不能是第一页,其次页码集合不能是空的,对应的页码也得在这个集合里,最后是当前页要小于此集合。
         * 我觉得重点在于最后一条,为什么一定要小于呢?因为当前页数=集合的容量,可以视为已经翻到了最后一页,那么我们要继续向后查询5页
         * 索引所以我们把这个边界处理放到了最后一层,本层只处理缓存有的游标,存在就放search after里查*/
        if (documentDTO.getPageNum() != 1 && MapUtil.isNotEmpty(pageMap)
                && pageMap.containsKey(documentDTO.getPageNum())
                && pageMap.size() > documentDTO.getPageNum()) {
            try {
                //构造查询条件
                searchSourceBuilder.query(boolQueryBuilder)
                        .sort("_id", SortOrder.DESC) //拿什么排序,游标就是什么
                        .size(documentDTO.getPageSize());
                //从缓存里拿到了当前页的游标---> 存放的时候就已经做了对应处理!!
                searchSourceBuilder.searchAfter(pageMap.get(documentDTO.getPageNum()));
                SearchRequest searchRequest2 = new SearchRequest("document")
                        .source(searchSourceBuilder);
                SearchResponse searchResponse2 = restHighLevelClient.search(searchRequest2, RequestOptions.DEFAULT);
                SearchHits searchHits = searchResponse2.getHits();
                if (searchHits.getTotalHits().value > 0) {
                    SearchHit[] hits = searchHits.getHits();
                    EsClientUtil.convertResult(documents, Document.class, hits);
                    total = searchHits.getTotalHits().value;
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        /*当pageNum=1的时候我就默认他刚接在或者已经刷新当然也有可能是从第2页回去之类的情况,但这里均不予考虑,只要是1就
         * 重新构造页标和游标的对应关系*/
        else if (documentDTO.getPageNum() == 1) {
            // 先移除
            sortMap.remove(id);
            // 上面被移除,pageMap更不可能获取到,这里必须自己初始化
            pageMap = new HashMap<>();
            //游标
            Object[] sortValues;
            //当前页
            currentPageNum = 1;
            //下一页
            int nextPageNum = currentPageNum + 1;

            try {
                searchSourceBuilder.query(boolQueryBuilder)
                        .sort("_id", SortOrder.DESC)
                        .from(0) //必须是0,不熟悉的朋友可能会觉得这里就可以循环,from从1开始就可以拿第二页,其实不行
                        //这样拿到的数据会有一点点错位,而easy-es这个框架是直接不允许深分页查询from > 0的
                        .size(documentDTO.getPageSize());
                SearchRequest searchRequest = new SearchRequest("document").source(searchSourceBuilder);
                SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
                SearchHit[] hits = searchResponse.getHits().getHits();
                if (hits.length != 0) {
                    //查询最后一个数据
                    SearchHit result = hits[hits.length - 1];
                    sortValues = result.getSortValues();
                    pageMap.put(1, new Object[]{}); // 第一页没有游标
                    pageMap.put(2, sortValues); //第一页的游标是去拿第二页的数据的,所以是2
                    EsClientUtil.convertResult(documents, Document.class, hits);
                    total = searchResponse.getHits().getTotalHits().value;
                }

                //向后获取5页的游标数据 所以你要品nextPageNum和currentPageNum的作用,就是处理游标和页码的对应关系的
                for (int i = nextPageNum; i < nextPageNum + 5; i++) {
                    //取出上一页的游标
                    searchSourceBuilder.searchAfter(pageMap.get(i));
                    SearchRequest searchRequest2 = new SearchRequest("document")
                            .source(searchSourceBuilder);
                    SearchResponse searchResponse2 = restHighLevelClient.search(searchRequest2, RequestOptions.DEFAULT);
                    SearchHits searchHits = searchResponse2.getHits();
                    if (searchHits.getTotalHits().value > 0) {
                        SearchHit[] nextHits = searchHits.getHits();
                        //当数据量不大的情况下且每页pageSize很大的话,他可能都没有5页,所以每次循环要判断,一旦
                        //不足就要终止,因为总数据已经不足分页了,在遍历就越界了
                        if (nextHits.length < documentDTO.getPageSize()) break;
                        SearchHit nextHit = nextHits[nextHits.length - 1];
                        sortValues = nextHit.getSortValues();
                        //从3开始 3/4/5/6/7
                        pageMap.put(i + 1, sortValues);
                    }
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }

        }
        /*这里是边界,也就是当前端页面显示当前展示的最大页数到第7页了,而你的页码正好是7,那么就该继续向后拿后面的游标并且要和页码对应*/
        else if (pageMap.containsKey(documentDTO.getPageNum()) && pageMap.size() == documentDTO.getPageNum()) {
            searchSourceBuilder.query(boolQueryBuilder)
                    .sort("_id", SortOrder.DESC)
                    .size(documentDTO.getPageSize());
            currentPageNum = documentDTO.getPageNum();
            try {
                for (int i = currentPageNum; i < currentPageNum + 5; i++) {
                    //这里要知道当前页的游标在上面的集合里已经有了
                    searchSourceBuilder.searchAfter(pageMap.get(i));
                    SearchRequest searchRequest2 = new SearchRequest("document")
                            .source(searchSourceBuilder);
                    SearchResponse searchResponse2 = restHighLevelClient.search(searchRequest2, RequestOptions.DEFAULT);
                    SearchHits searchHits = searchResponse2.getHits();
                    total = searchHits.getTotalHits().value;

                    if (searchHits.getTotalHits().value > 0) {
                        SearchHit[] hits = searchHits.getHits();
                        //这里是数据边界的终止,上面已说
                        if (hits.length < documentDTO.getPageSize()) {
                            EsClientUtil.convertResult(documents, Document.class, hits);
                            break;
                        }
                        SearchHit result = hits[hits.length - 1];
                        Object[] sortValues = result.getSortValues();
                        //存放游标
                        pageMap.put(i + 1, sortValues);
                        //这里是拿出当前页的数据
                        if (i == documentDTO.getPageNum()) {
                            EsClientUtil.convertResult(documents, Document.class, hits);
                        }
                    }
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        documentVO.setPageNum(documentDTO.getPageNum());
        documentVO.setPageSize(documentDTO.getPageSize());
        documentVO.setTotal(total);
        documentVO.setData(documents);
        sortMap.put(id, pageMap);
        return documentVO;
    }
}

思路简述

其实从上面的代码加注释,我觉得你应该就可以理解了,思路只有一个那就是缓存游标,这里我有个三个判断,第一个判断是判断当前页数是不是已经在缓存里了,进了第一个说明是有的,就直接拿出游标查询并返给前端。
第二个判断是判断是不是初次加载,如果是就清掉之前缓存的游标集合,因为你要考虑数据增量的情况,如果你没有数据增量的情况甚至都不用按标记分,直接建立个游标缓存,什么时候有增量数据(比如那种一天一增),就什么时候删缓存。然后还要获取后五页的游标数据。
第三个判断是边界判断,主要任务有三个,第一个任务是获取后5页的游标,第二个任务是判断总数据是不是没得分了,第三个任务是拿到当前边界的数据

后续

首先是关于这个缓存的维护,比如session已经不再有效,怎么移除,其实我的项目里是还有个map的,他就是来实时更新这个session的最后查询时间的,可以通过定时任务,一旦超过一个时间点,就从sortMap移除。

其次关于时间的建议,我个人建议你在存时间的时候字段设置成Long型,就算不方便你也要一个Date类型一个Long型,Es读取出来的那个时间你不好转Date,所以建议用Long比较,建议你采纳我的建议!!

第三是谈浅分页,其实我们一开始也不是用深分这个方案的,而是通过限制数据的首次加载条数,我们后台逻辑处理好,尽量避免超出那个1w的限制。跟前端也说好,比如我每页50条数据,那么前端那边翻页的总页数就不能大于200,也不要展示总页数,也不要让前端弄那个尾页最大页的那个按钮,就让用户5页5页往后跳。如果你不能的客户不允许这样,那我这边建议你放弃es拥抱MySQL,两难自解!

第四条如果你可以随意选型的话,且你对传统的api不熟悉的话,建议你考虑easy-es,让你操作es如同操作关型数据库,且封装好了深分页查询。

第五是关于上面的代码逻辑,可能还会有漏洞,就是关于跳页的问题,我也许还没有处理的太成熟,但是前端那边老老实实jump的话应该不会出什么问题!

有问题可以联系707409741文章来源地址https://www.toymoban.com/news/detail-406346.html

到了这里,关于ElasticSearch深度分页并可以小幅度跳页的实现的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 什么是ElasticSearch的深度分页问题?如何解决?

    在ElasticSearch中进行分页查询通常使用from和size参数。当我们对ElasticSearch发起一个带有分页参数的查询(如使用from和size参数)时,ElasticSearch需要遍历所以匹配的文档直到达到指定的起始点(from),然后返回从这一点开始的size个文档 在这个例子中: 1.from 参数定义了要跳过的

    2024年03月16日
    浏览(30)
  • Mybatis (3)-----分页的运用

    目录 一、分页查询 二,特殊的字符处理 三、总结 前言:在我们上篇已经学的动态sql的基础上,今天继续讲解关于maybatis的分页,特殊的字符处理。希望这篇博客可以帮助到大家哦! 为什么要重写mybatis的分页? 重写MyBatis的分页功能有几个原因。 首先,MyBatis默认的分页功能

    2024年02月11日
    浏览(30)
  • Redis滚动分页的使用

    关注推送也叫Feed流。通过无限下拉刷新获取新的信息。 Feed流产品常见有两种模式: Timeline: 不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈 优点:信息全面,不会有缺失。并且实现也相对简单 缺点:信息噪音较多,用户不一定感兴趣,内容获

    2024年02月04日
    浏览(30)
  • 基于vue+element 分页的封装

    分页也是我们在实际应用当中非常常见的存在,其实分页本身在element中做的就挺好的了,但是使用确实非常的多,所以还是有必要封装一下,主要是为了减少代码的冗余,以及提升开发的效率和降低后续维护的成本。 这是一段普通分页的示例 效果是这样的 在这当中用到了我

    2024年02月15日
    浏览(32)
  • elasticsearch 深度分页查询 Search_after(图文教程)

    前言 这是我在这个网站整理的笔记,有错误的地方请指出,关注我,接下来还会持续更新。 作者:神的孩子都在歌唱 search_after 是 Elasticsearch 提供的一种分页查询方式,它可以用来在已经排序的结果集中进行分页查询。 search_after查询步骤如下(下面有具体的例子帮助理解):

    2024年04月11日
    浏览(41)
  • ElasticSearch - DSL查询文档语法,以及深度分页问题、解决方案

    目录 一、DSL 查询文档语法 前言 1.1、DSL Query 基本语法 1.2、全文检索查询 1.2.1、match 查询 1.2.2、multi_match 1.3、精确查询 1.3.1、term 查询 1.3.2、range 查询 1.4、地理查询 1.4.1、geo_bounding_box 1.4.2、geo_distance 1.5、复合查询 1.5.1、相关性算分 1.5.2、function_score 1.5.3、boolean query 1.6、搜索

    2024年02月07日
    浏览(40)
  • SpringBoot实现分页的三种方式

    一 自己封装Page对象实现 博客链接 二 使用sql实现分页 2.1 场景分析 前段传递给给后台什么参数? 当前页码 currentPage 每页显示条数 pageSize 后台给前端返回什么数据? 当前页数据 List 总记录数 totalCount 2.2 前段代码 2.3 后端代码 PageBean mapper service impl controller 三 使用PageHelper插件

    2024年02月10日
    浏览(36)
  • es中3种分页的介绍以及对比

    类型 原理 优点 缺点 使用场景 from + size 类似 msql的 limit 0,100;  limit  from,size 灵活性好,实现简单,适合浅分页 无法实现深度分页问题 ,当查询数量超过10000就会报错 top10000以内的查询 Scroll 首次查询会在内存中保存一个历史快照以及游标(scroll_id),记录当前消息查询的终

    2024年02月03日
    浏览(38)
  • 学会Mybatis框架:一文掌握MyBatis与GitHub插件分页的完美结合【三.分页】

    接下来看看由辉辉所写的关于Mybatis的相关操作吧 目录 🥳🥳Welcome Huihui\\\'s Code World ! !🥳🥳 一.Mybatis分页 1. Mybatis自带分页 2.插件分页 二.特殊字符的操作 1.使用CDATA区间 2.实体引用 三.结果集的处理   其实Mybatis本身是有分页的功能的,但是我们还是需要重写其中的分页,这

    2024年02月11日
    浏览(36)
  • java中实现分页的常见几种方式

    无论是自我学习中,还是在工作中,固然会遇到 与前端搭配实现分页的功能 ,发现有几种方式,特此记录一下。 分页功能直接交给前端实现 (根据业务场景且仅仅只能用于 数据量少 的情况)。即后端不做任何数据的限制,直接把全部数据返回给前端,前端通过组件实现分页

    2023年04月10日
    浏览(28)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包