SpringBoot 如何整合 ES 实现 CRUD 操作
作者:空夜 发布时间:2022-03-24 03:10:31
本文介绍 Spring Boot 项目中整合 ElasticSearch 并实现 CRUD 操作,包括分页、滚动等功能。
之前在公司使用 ES,一直用的是前辈封装好的包,最近希望能够从原生的 Spring Boot/ES 语法角度来学习 ES 的相关技术。希望对大家有所帮助。
本文为 spring-boot-examples 系列文章节选,示例代码已上传至 https://github.com/laolunsi/spring-boot-examples
安装 ES 与可视化工具
前往 ES 官方 https://www.elastic.co/cn/downloads/elasticsearch 进行,如 windows 版本只需要下载安装包,启动 elasticsearch.bat 文件,浏览器访问 http://localhost:9200
如此,表示 ES 安装完毕。
为更好地查看 ES 数据,再安装一下 elasticsearch-head 可视化插件。前往下载地址:https://github.com/mobz/elasticsearch-head
主要步骤:
git clone git://github.com/mobz/elasticsearch-head.git
cd elasticsearch-head
npm install
npm run start
open http://localhost:9100/
可能会出现如下情况:
发现是跨域的问题。
解决办法是在 elasticsearch 的 config 文件夹中的 elasticsearch.yml 中添加如下两行配置:
http.cors.enabled: true
http.cors.allow-origin: "*"
刷新页面:
这里的 article 索引就是我通过 spring boot 项目自动创建的索引。
下面我们进入正题。
Spring Boot 引入 ES
创建一个 spring-boot 项目,引入 es 的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
配置 application.yml:
server:
port: 8060
spring:
elasticsearch:
rest:
uris: http://localhost:9200
创建一个测试的对象,article:
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import java.util.Date;
@Document(indexName = "article")
public class Article {
@Id
private String id;
private String title;
private String content;
private Integer userId;
private Date createTime;
// ... igonre getters and setters
}
下面介绍 Spring Boot 中操作 ES 数据的三种方式:
实现 ElasticsearchRepository 接口
引入 ElasticsearchRestTemplate
引入 ElasticsearchOperations
实现对应的 Repository:
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
public interface ArticleRepository extends ElasticsearchRepository<Article, String> {
}
下面可以使用这个 ArticleRepository 来操作 ES 中的 Article 数据。
我们这里没有手动创建这个 Article 对应的索引,由 elasticsearch 默认生成。
下面的接口,实现了 spring boot 中对 es 数据进行插入、更新、分页查询、滚动查询、删除等操作。可以作为一个参考。其中,使用了 Repository 来获取、保存、删除 ES 数据,使用 ElasticsearchRestTemplate 或 ElasticsearchOperations 来进行分页/滚动查询。
根据 id 获取/删除数据
@Autowired
private ArticleRepository articleRepository;
@GetMapping("{id}")
public JsonResult findById(@PathVariable String id) {
Optional<Article> article = articleRepository.findById(id);
JsonResult jsonResult = new JsonResult(true);
jsonResult.put("article", article.orElse(null));
return jsonResult;
}
@DeleteMapping("{id}")
public JsonResult delete(@PathVariable String id) {
// 根据 id 删除
articleRepository.deleteById(id);
return new JsonResult(true, "删除成功");
}
保存数据
@PostMapping("")
public JsonResult save(Article article) {
// 新增或更新
String verifyRes = verifySaveForm(article);
if (!StringUtils.isEmpty(verifyRes)) {
return new JsonResult(false, verifyRes);
}
if (StringUtils.isEmpty(article.getId())) {
article.setCreateTime(new Date());
}
Article a = articleRepository.save(article);
boolean res = a.getId() != null;
return new JsonResult(res, res ? "保存成功" : "");
}
private String verifySaveForm(Article article) {
if (article == null || StringUtils.isEmpty(article.getTitle())) {
return "标题不能为空";
} else if (StringUtils.isEmpty(article.getContent())) {
return "内容不能为空";
}
return null;
}
分页查询数据
@Autowired
private ElasticsearchRestTemplate elasticsearchRestTemplate;
@Autowired
ElasticsearchOperations elasticsearchOperations;
@GetMapping("list")
public JsonResult list(Integer currentPage, Integer limit) {
if (currentPage == null || currentPage < 0 || limit == null || limit <= 0) {
return new JsonResult(false, "请输入合法的分页参数");
}
// 分页列表查询
// 旧版本的 Repository 中的 search 方法被废弃了。
// 这里采用 ElasticSearchRestTemplate 或 ElasticsearchOperations 来进行分页查询
JsonResult jsonResult = new JsonResult(true);
NativeSearchQuery query = new NativeSearchQuery(new BoolQueryBuilder());
query.setPageable(PageRequest.of(currentPage, limit));
// 方法1:
SearchHits<Article> searchHits = elasticsearchRestTemplate.search(query, Article.class);
// 方法2:
// SearchHits<Article> searchHits = elasticsearchOperations.search(query, Article.class);
List<Article> articles = searchHits.getSearchHits().stream().map(SearchHit::getContent).collect(Collectors.toList());
jsonResult.put("count", searchHits.getTotalHits());
jsonResult.put("articles", articles);
return jsonResult;
}
滚动查询数据
@GetMapping("scroll")
public JsonResult scroll(String scrollId, Integer size) {
// 滚动查询 scroll api
if (size == null || size <= 0) {
return new JsonResult(false, "请输入每页查询数");
}
NativeSearchQuery query = new NativeSearchQuery(new BoolQueryBuilder());
query.setPageable(PageRequest.of(0, size));
SearchHits<Article> searchHits = null;
if (StringUtils.isEmpty(scrollId)) {
// 开启一个滚动查询,设置该 scroll 上下文存在 60s
// 同一个 scroll 上下文,只需要设置一次 query(查询条件)
searchHits = elasticsearchRestTemplate.searchScrollStart(60000, query, Article.class, IndexCoordinates.of("article"));
if (searchHits instanceof SearchHitsImpl) {
scrollId = ((SearchHitsImpl) searchHits).getScrollId();
}
} else {
// 继续滚动
searchHits = elasticsearchRestTemplate.searchScrollContinue(scrollId, 60000, Article.class, IndexCoordinates.of("article"));
}
List<Article> articles = searchHits.getSearchHits().stream().map(SearchHit::getContent).collect(Collectors.toList());
if (articles.size() == 0) {
// 结束滚动
elasticsearchRestTemplate.searchScrollClear(Collections.singletonList(scrollId));
scrollId = null;
}
if (scrollId == null) {
return new JsonResult(false, "已到末尾");
} else {
JsonResult jsonResult = new JsonResult(true);
jsonResult.put("count", searchHits.getTotalHits());
jsonResult.put("size", articles.size());
jsonResult.put("articles", articles);
jsonResult.put("scrollId", scrollId);
return jsonResult;
}
}
ES 深度分页 vs 滚动查询
上次遇到一个问题,同事跟我说日志检索的接口太慢了,问我能不能优化一下。开始使用的是深度分页,即 1,2,3..10, 这样的分页查询,查询条件较多(十多个参数)、查询数据量较大(单个日志索引约 2 亿条数据)。
分页查询速度慢的原因在于:ES 的分页查询,如查询第 100 页数据,每页 10 条,是先从每个分区 (shard,一个索引默认是 5 个 shard) 中把命中的前 100 * 10 条数据查出来,然后由协调节点进行合并等操作,最后给出第 100 页的数据。也就是说,实际被加载到内存中的数据远超过理想情况。
这样,索引的 shard 越大,查询页数越多,查询速度就越慢。
ES 默认的 max_result_window 是 10000 条,也就是正常情况下,用分页查询到 10000 条数据时,就不会再返回下一页数据了。
如果不需要进行跳页,比如直接查询第 100 页数据,或者数据量非常大,那么可以考虑用 scroll 查询。
在 scroll 查询下,第一次需要根据查询参数开启一个 scroll 上下文,设置上下文缓存时间。以后的滚动只需要根据第一次返回的 scrollId 来进行即可。
scroll 只支持往下滚动,如果想要往回滚动,还可以根据 scrollId 缓存查询结果,这样就可以实现上下滚动查询了 —— 就像大家经常使用的淘宝商品检索时上下滚动一样。
来源:http://www.eknown.cn/index.php/spring-boot/elasticsearch.html
猜你喜欢
- 目录前言asyncawait从以往知识推导创建异步任务创建异步任务并返回Task异步改同步说说 await Task说说 async Tas
- Kotlin 基础教程之异常概述在Kotlin-null的处理里提到的NPE,它就是一个异常。而,异常是程序运行过程中出现的错误。在Kotl
- 一、DataGridView不显示下面的新行通常DataGridView的最下面一行是用户新追加的行(行头显示*)。如果不想让用户新追加行即
- MyBatis 通过包含的jdbcType类型BIT FLOAT CHAR &nbs
- 前言:由于公司的业务,硬生生的把ios开发的我,掰成了android!关于上传文件的需求处理,做了一个Java的简单封装 DocumentM
- 本文先为大家介绍如何利用缓存Cache方便地实现此功能。 Cache与Session这二个状态对像的其中有一个不同之处,Cache是一个全局
- 算法概述/思路归并排序是基于一种被称为“分治”(divide and conquer)的策略。其基本思路是这样的:1.对于两个有序的数组,要
- 使用JAVA工程管理越来越多的jar包,担心导错了,多导了,漏导了怎么办?换一个IDE项目后项目会不会出一堆BUG,看的头皮发麻?自己写的代
- 前言Queue 也是 Java 集合框架中定义的一种接口,直接继承自 Collection 接口。除了基本的 Collection 接口规定
- 前言我们在使用spring security的时候可以通过好几种方法获取用户信息, 但是今天这篇文章介绍的是一个笔者觉得最优雅的实现; 借鉴
- 1. Spring 是什么我们通常所说的 Spring 指的是 Spring Framework(Spring 框架),它是一个开源框架,有
- 今天在做数据分页显示的时候遇到了一个问题,经过测试,证实是Tomcat 6的一个bug,我所用的版本为:apache-tomcat-6.0.
- 概述本文介绍通过java程序向PDF文档添加图片,以及替换和删除PDF中已有的图片。另外,关于图片的操作还可参考设置PDF 图片背景、设置P
- Maven本地jar引用的实现方法有的时候需要在maven工程项目中引用本地的jar,pom.xml配置如下:<dependency&
- maven使用exec插件运行java main方法,以下是3种不同的操作方式。一、从命令行运行1、运行前先编译代码,exec:java不会
- 作为工厂方法模式的孪生兄弟,相信大家对工厂方法模式和抽象工厂模式傻傻分不清楚吧。那么,就让我来拯救大家吧!抽象工厂模式定义:所谓抽象工厂模式
- hello,今天给大家带来一道算法题。这道算法题,是我目前为止,见过最难的一道题。那么到底是怎样的一道算法题呢?如下:题目:给定一个数组,
- 介绍大家都知道微信支付的回调链接要求不能跟参数,但又要接收返回的xml数据。我开始使用@RequestBody注解在参数上,希望能获取xml
- 只需要调用该类的一个方法createNewFile(),但是在实际操作中需要注意一些事项,如判断文件是否存在,以及如何向新建文件中写入数据等
- 跨域跨域请求是指浏览器脚本文件在发送请求时,脚本所在的服务器和请求的服务器地址不一样。跨域是有浏览器的同源策略造成的,是浏览器对JavaSc