【主流技术】详解 Spring Boot 2.7.x 集成 ElasticSearch7.x 全过程(二)
前言
ElasticSearch 简称 es,是一个开源的高扩展的分布式全文检索引擎,目前最新版本已经到了8.11.x了。
它可以近乎实时的存储、检索数据,且其扩展性很好,是企业级应用中较为常见的检索技术。
下面主要记录学习 ElasticSearch7.x 的一些基本结构、在Spring Boot 项目里基本应用的过程,在这里与大家作分享交流。
一、添加依赖
这里引用的依赖是 starter-data-elasticsearch,版本应与 Spring Boot(我是2.7.2)的版本一致,并不是 Elasticsearch 的版本。
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-elasticsearch -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
<version>2.7.2</version>
</dependency>
二、 yml 配置
spring:
elasticsearch:
uris: http://远程主机的公网IP:9200
username: 自己的用户名
password: 自己的密码
使用 Docker 安装的 Elasticsearch 设置账号/密码教程:https://blog.csdn.net/qq_38669698/article/details/130529829
因为 ES 设置了密码,所以 Kibana 的配置也需要修改:https://blog.csdn.net/weixin_45956631/article/details/130636880
三、注入依赖
(推荐)ElasticsearchRestTemplate 类来源于 org.springframework.data.elasticsearch.core 包,封装了 Elasticsearch 的 RESTful API,使用起来很便捷。
//直接引入即可,无需额外的 Bean 配置和序列化配置
@Resource
private ElasticsearchRestTemplate elasticTemplate;
(推荐)ElasticsearchRepository 接口来源于 org.springframework.data.elasticsearch.repository 包, 该接口用于简化对 Elasticsearch 中数据的操作。
public interface ArticleRepository extends ElasticsearchRepository<ESArticle, String>{}
注:ESArticle 为实体类,String 表示唯一 Id 的数据类型。
(不推荐)在 Elasticsearch 7.15版本之后,官方已将它的高级客户端 RestHighLevelClient 标记为弃用状态,之后的版本会推荐新的 RestClient。
经过笔者对比实践,无论是新/旧客户端,在 Spring Boot 项目中都没有上面前两个使用起来便捷。但值得注意的是,很多企业以前的项目都会使用旧的 RestHighLevelClient 来写业务。
@Resource
private RestHighLevelClient highLevelClient; @Resource
private RestClient restClient;
四、CRUD 常用 API
ES 实体类
和 MySQL、MongoDB 在 Spring 中的实体类一样,需要将字段和类属性进行映射,同样还可以使用注解进行简单配置。
以下是文章 ESArticle 的实体类,属性包含标题、内容、标签、点赞数/收藏数等:
@Data
@Document(indexName = "article")
@EqualsAndHashCode(callSuper = true)
public class ESArticle extends BaseEntity implements Serializable { private static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss"; /**
* 唯一标识 id
*/
@Id
@Field(type = FieldType.Text)
private String id; /**
* 标题,字段类型为 Text,没有 String 类型;分词类型为 ik 分词器的最细颗粒度划分法。
*/
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String title; /**
* 内容
*/
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String content; /**
* 标签列表
*/
private List<String> tags; /**
* 点赞数
*/
private Integer thumbNum; /**
* 收藏数
*/
private Integer favourNum; /**
* 创建用户 id
*/
@Field(type = FieldType.Text)
private String userId; /**
* 创建时间,单独存储,字段类型为 Date ,自定义格式
*/
@Field(store = true, type = FieldType.Date, format = {}, pattern = DATE_TIME_PATTERN)
private Date createTime; /**
* 更新时间,单独存储,字段类型为 Date ,自定义格式
*/
@Field(store = true, type = FieldType.Date, format = {}, pattern = DATE_TIME_PATTERN)
private Date updateTime; /**
* 是否删除
*/
private Integer isDelete;
}
documents 操作
documents 的概念和 MySQL 中的行类似,指的是一条条的记录,但是 ES 里所有的数据都是 JSON 格式的,所以看起来就像是一个个文档了。
以下简单的 CRUD 都由 ArticleRepository 来完成,下一小节复杂的查询交给 ElasticsearchRestTemplate 来完成。
新增(批量)
@Resource
private ArticleMapper articleMapper; @Resource
private ArticleRepository articleRepository; //todo: ES里的数据来源于数据库,需要做迁移,业务数据不会直接写进数据库
//todo: 有全量和增量两种方式做数据迁移,或者引入第三方框架处理
//todo: 此处暂不做数据迁移展示,就直接往 ES 里写,然后就当 ES 里已经有数据了,再做 CRUD 以及查询
@Override
public Boolean addDocuments(){
LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>();
List<Article> articleList = articleMapper.selectList(wrapper);
if (CollectionUtils.isNotEmpty(articleList)){
// 这里是两个实体的属性转换,这里不过多展开讲
List<ESArticle> esArticleList = articleList.stream().map(ESArticle::dbToEs).collect(Collectors.toList());
articleRepository.saveAll(esArticleList);
return Boolean.TRUE;
}
return Boolean.FALSE;
}
修改(更新)
//todo: 还可以使用 elasticTemplate 的 update() 来进行更新,不过一般没有单独针对 es 的数据更新需求
@Override
public Boolean updateDocuments(){
ESArticle esArticle = articleRepository.findById("18094375634670546").orElse(null);
if (Objects.nonNull(esArticle)){
esArticle.setTitle("测试修改标题更新操作");
articleRepository.save(esArticle);
return Boolean.TRUE;
}
return Boolean.FALSE;
}
获取
@Override
public List<ESArticle> getESDocuments(){
List<ESArticle> list = Lists.newArrayList();
Iterable<ESArticle> esArticleList = this.articleRepository.findAll(Sort.by(Sort.Order.desc("id")));
esArticleList.forEach(list::add);
return list;
}
删除
@Override
public Boolean deleteESDocuments(){
//如果存在该条 document 则继续删除
if (this.articleRepository.existsById("18094375634670546")){
this.articleRepository.deleteById("18094375634670546");
return Boolean.TRUE;
}
return Boolean.FALSE;
}
常见条件查询(重点)
以下会详细地演示一下 BoolQueryBuilder 条件构造、常见 QueryBuilders 的方法等多条件复杂查询场景:
//todo: 企业项目中真正的复杂条件查询
@Override
public PageInfo<ESArticle> testSearchFromES(ArticleSearchDTO articleSearchDTO){
//完整的合法 id
String id = articleSearchDTO.getId();
//非法 id
String notId = articleSearchDTO.getNotId();
//搜索框输入的内容(实际会从标签/内容/标题中查找)
String searchText = articleSearchDTO.getSearchWord();
//单独在标题中查找
String title = articleSearchDTO.getTitle();
//单独在内容中查找
String content = articleSearchDTO.getContent();
//单独在标签中查找(全部标签)
List<String> tagList = articleSearchDTO.getTags();
//任意标签
List<String> orTagList = articleSearchDTO.getOrTags();
//按照创建者的 userId 查找
String userId = articleSearchDTO.getUserId();
// 布尔查询初始化
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 过滤,首先被删除的就不要了
boolQueryBuilder.filter(QueryBuilders.termQuery(this.fn.fnToFieldName(ESArticle::getIsDelete), NumberUtils.INTEGER_ZERO));
//如果输入的是 id 那么就不对 id 分词,然后过滤掉不符合该 id 的其它文档
if (StringUtils.isNotBlank(id)) {
boolQueryBuilder.filter(QueryBuilders.termQuery("id", id));
}
//如果输入的是非法 id 那么什么也查不到,取反(也就是所有)返回
if (StringUtils.isNotBlank(notId)) {
boolQueryBuilder.mustNot(QueryBuilders.termQuery("id", notId));
}
//创建者 userId 也不分词,过滤掉不匹配的
if (StringUtils.isNotBlank(userId)) {
boolQueryBuilder.filter(QueryBuilders.termQuery("createId", userId));
}
// 必须包含所有标签
if (CollectionUtils.isNotEmpty(tagList)) {
for (String tag : tagList) {
boolQueryBuilder.filter(QueryBuilders.termQuery("tags", tag));
}
}
// 包含任何一个标签即可
if (CollectionUtils.isNotEmpty(orTagList)) {
BoolQueryBuilder orTagBoolQueryBuilder = QueryBuilders.boolQuery();
// DB 实体中 tag 字段为 String,而 ES 实体该字段的类型为 List,所以做循环遍历
for (String tag : orTagList) {
orTagBoolQueryBuilder.should(QueryBuilders.termQuery("tags", tag)).minimumShouldMatch(1);
}
//filter 可以结合 bool 做更复杂的过滤
boolQueryBuilder.filter(orTagBoolQueryBuilder);
}
// 按关键词检索(主要的搜索框,关键词会在两个字段里匹配)
if (StringUtils.isNotBlank(searchText)) {
boolQueryBuilder.should(QueryBuilders.matchQuery("title", searchText));
boolQueryBuilder.should(QueryBuilders.matchQuery("content", searchText));
boolQueryBuilder.minimumShouldMatch(1);
}
// 单独按标题检索
if (StringUtils.isNotBlank(title)) {
boolQueryBuilder.should(QueryBuilders.matchQuery("title", title));
}
// 单独按内容检索
if (StringUtils.isNotBlank(content)) {
boolQueryBuilder.should(QueryBuilders.matchQuery("content", content));
}
}
分页查询
Spring Data 自带的分页方案,即 PageRequest 对象:
// 分页参数:起始页为 0
long current = articleSearchDTO.getCurrent() - 1;
long pageSize = articleSearchDTO.getPageSize();
PageRequest pageRequest = PageRequest.of((int) current, (int) pageSize);
排序
设置了按条件排序则以排序字段为准来返回,没设置排序则默认按照分数,即匹配度返回:
// 排序字段,可以支持多个
String sortField = articleSearchDTO.getSortField();
SortBuilder<?> sortBuilder = SortBuilders.scoreSort();
if (StringUtils.isNotBlank(sortField)) {
sortBuilder = SortBuilders.fieldSort(sortField).order(SortOrder.DESC);
}
构造查询
将所有的条件放进 NativeSearchQueryBuilder 对象,并调用elasticTemplate.search()方法,最后放入PageInfo(这里引入的是com.github.pagehelper)对象返回:
// 构造查询
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(boolQueryBuilder)
.withSorts(sortBuilder)
.withPageable(pageRequest).build();
// 获取查询对象的结果:放入所有条件,指定索引实体
SearchHits<ESArticle> searchHits = elasticTemplate.search(searchQuery, ESArticle.class);
//todo: 先以 ES 的数据为准,后期数据迁移再考虑使用 MySQL 的数据源
//初始化 page 对象
PageInfo<ESArticle> pageInfo = new PageInfo<>();
pageInfo.setList(searchHits.getSearchHits().stream().map(SearchHit::getContent).collect(Collectors.toList()));
pageInfo.setTotal(searchHits.getTotalHits());
System.out.println(pageInfo);
return pageInfo;
测试调用
@Test
public void testSearchFromES(){
ArticleSearchDTO articleSearchDTO = new ArticleSearchDTO();
articleSearchDTO.setId("18094375634670546");
//articleSearchDTO.setSearchWord("是");
//articleSearchDTO.setTitle("标题");
//articleSearchDTO.setTags(Collections.singletonList("es"));
//articleSearchDTO.setSortField("createTime");
esTestService.testSearchFromES(articleSearchDTO);
}
测试数据如下图所示:

五、文章小结
使用 ElasticSearch 实现全文检索的过程并不复杂,只要在业务需要的地方创建 ElasticSearch 索引,将数据放入索引中,就可以使用 ElasticSearch 集成在 Spring Boot 中对搜索对象进行查询操作了。
无论是创建索引、精准匹配、还是字段高亮等操作,其本质上还是一个面向对象的过程。和 Java 中的其它“对象”一样,只要灵活运用这些“对象”的使用规则和特性,就可以满足业务上的需求。
关于 ElasticSearch7.x 的基本结构和在 Spring Boot 项目中的集成应用就和大家分享到这里。如有错误和不足,还期待大家的指正与交流。
参考文档:
- ElasticSearch 官方查询 API 文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/search.html
- Spring Data ElasticSearch 官方:https://docs.spring.io/spring-data/redis/docs/2.6.10/api/
【主流技术】详解 Spring Boot 2.7.x 集成 ElasticSearch7.x 全过程(二)的更多相关文章
- 详解Spring Boot集成MyBatis的开发流程
MyBatis是支持定制化SQL.存储过程以及高级映射的优秀的持久层框架,避免了几乎所有的JDBC代码和手动设置参数以及获取结果集. spring Boot是能支持快速创建Spring应用的Java框 ...
- 详解spring boot mybatis全注解化
本文重点介绍spring boot mybatis 注解化的实例代码 1.pom.xml //引入mybatis <dependency> <groupId>org.mybat ...
- 详解spring boot实现多数据源代码实战
之前在介绍使用JdbcTemplate和Spring-data-jpa时,都使用了单数据源.在单数据源的情况下,Spring Boot的配置非常简单,只需要在application.propertie ...
- 详解Spring Boot配置文件之多环境配置
一. 多环境配置的好处: 1.不同环境配置可以配置不同的参数~ 2.便于部署,提高效率,减少出错~ 二. properties多环境配置 1. 配置激活选项 spring.profiles.activ ...
- 架构设计:远程调用服务架构设计及zookeeper技术详解(下篇)
一.下篇开头的废话 终于开写下篇了,这也是我写远程调用框架的第三篇文章,前两篇都被博客园作为[编辑推荐]的文章,很兴奋哦,嘿嘿~~~~,本人是个很臭美的人,一定得要截图为证: 今天是2014年的第一天 ...
- Comet技术详解:基于HTTP长连接的Web端实时通信技术
前言 一般来说,Web端即时通讯技术因受限于浏览器的设计限制,一直以来实现起来并不容易,主流的Web端即时通讯方案大致有4种:传统Ajax短轮询.Comet技术.WebSocket技术.SSE(Ser ...
- SSE技术详解:一种全新的HTML5服务器推送事件技术
前言 一般来说,Web端即时通讯技术因受限于浏览器的设计限制,一直以来实现起来并不容易,主流的Web端即时通讯方案大致有4种:传统Ajax短轮询.Comet技术.WebSocket技术.SSE(Ser ...
- CDN学习笔记二(技术详解)
一本好的入门书是带你进入陌生领域的明灯,<CDN技术详解>绝对是带你进入CDN行业的那盏最亮的明灯.因此,虽然只是纯粹的重点抄录,我也要把<CDN技术详解>的精华放上网.公诸同 ...
- CDN技术详解及实现原理
CDN技术详解 一本好的入门书是带你进入陌生领域的明灯,<CDN技术详解>绝对是带你进入CDN行业的那盏最亮的明灯.因此,虽然只是纯粹的重点抄录,我也要把<CDN技术详解>的精 ...
- P2P技术详解(二):P2P中的NAT穿越(打洞)方案详解
1.内容概述 P2P即点对点通信,或称为对等联网,与传统的服务器客户端模式(如下图"P2P结构模型"所示)有着明显的区别,在即时通讯方案中应用广泛(比如IM应用中的实时音视频通信. ...
随机推荐
- 原来TypeScript中的接口和泛型这么好理解
"接口"和"泛型"是 TypeScript 相比于 JavaScript 新增的内容,都用于定义数据类型 前面两篇文章总结了TypeScript中的 类型注解. ...
- RocketMQ Linux单机测试:简易快速部署指南及Dashboard控制台部署
目录 简介 开始 下载 增加环境变量 修改启动文件jvm大小 修改rocketmq配置文件 启动 快速测试 关闭 Dashboard 下载Dashboard 已编译jar包网盘下载 启动命令 可能遇到 ...
- 个人GAN训练的性能迭代
使用GAN进行生成图片 损失函数的迭代 DCGAN->Wasserstein GAN-> Wasserstein GAN + Gradient Penalty Discriminator训 ...
- (2023.7.24)软件加密与解密-2-1-程序分析方法[XDbg].md
body { font-size: 15px; color: rgba(51, 51, 51, 1); background: rgba(255, 255, 255, 1); font-family: ...
- Web服务器部署上线的踩坑流程回顾与知新
5月份时曾部署上线了C++的Web服务器,温故而知新,本篇文章梳理总结一下部署流程知识: 最初的解决方案:https://blog.csdn.net/BinBinCome/article/detail ...
- docker搭建CMS靶场
项目地址:https://github.com/Betsy0/CMSVulSource 该项目是为了方便在对CMS漏洞进行复现的时候花费大量的时间在网上搜索漏洞源码,从而有了此项目.此项目仅为安全研究 ...
- 搭建 QT6+OpenCv4.7+CMake的环境
本文主要介绍如何搭建QT6+OpenCv的开发环境,基本流程如下 先安装CMake3.27.3,用来编译适用用QT的OpenCv的源码,安装完成后要配置系统的环境变量 安装Qt6的开发环境,并配置环境 ...
- 使用docker搭建seafile服务器
工作需要在单位和家里的不同电脑上同步指定文件夹及其内容.对比了一些解决方案,最终还是选择熟悉的seafile来做. 需要按照官方文档进行seafile的安装,选择官方推荐的docker方式快速部署. ...
- JavaAgent寄生在目标进程中引起的ClassNotFoundException
今天有解决方案部的小伙伴反映,我公司XWind产品在分析客户应用程序的潜在性能问题时,总是显现诊断任务异常,为了定位问题的根因,我们马上要求解决方案部的小伙伴提供XWind相关的日志,从日志中找到了如 ...
- firewalld规则配置
firewalld规则配置 一.概念 动态防火墙 启动新规则时,不会像iptables一样,先清空规则,再启动所有规则,如此会对现在程序有影响,哪怕只是一条规则.而firewalld 规则变更不需要对 ...