首先简单介绍下写这篇博文的背景,最近负责的一个聚合型的新项目要大量使用ES的检索功能,之前对es的了解还只是纯理论最多加个基于postman的索引创建操作,所以这次我得了解在java端如何编码实现;网上搜索是一搜一大堆,但是大部分都是些复制粘贴,毫无上下文可言,所以我是阵痛了好几天一点点的梳理整合,最后选择了基于springData封装的es操作,一点点编码测试最终满足了业务端的查询需求;下面就来从零开始介绍下如何在springboot项目中引入es并操控它;
ES简介
es是一个支持全文检索的开源的、高拓展的分布式搜索引擎,它基于Lucene而来,与solr这里也不比较了,这里更关注与java的整合,然后我们知道ES通过分词、倒排索引等技术能支持关系型数据库做不到或者说做不好的全文快速索引就行了,至于es的索引、文档、字段等关键属性这里也不介绍了,下面直接开始整合
项目搭建
一、整合springdata
需要注意的是springboot的版本不能太低,太低有些API不支持,我这里用的是2.6.8
;
1、引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
2、创建索引实体
package com.darling.po;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import java.util.Date;
@Data
@Document(indexName = "dll_index", createIndex=true)
public class EsTestInfo {
@Id
@Field(type = FieldType.Keyword)
private String id;
@Field(type = FieldType.Text)
private String name;
@Field(type = FieldType.Text)
private String desc;
@Field(type = FieldType.Date)
private Date publishDt;
}
上面的实体类就相当于索引的映射了,可以在里面根据业务需求设置索引名称、每个字段在es中的类型
3、创建DAO
package com.darling.repository;
import com.darling.po.EsTestInfo;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface EsReportInfoDao extends ElasticsearchRepository<EsTestInfo,Long> {
}
这里的写法类似于mybatisPlus,由springdata封装es的操作,后续针对单个索引的增删改查都会基于这个类
4、在项目的配置文件添加ES的配置信息
#配置ES的服务端地址和端口
elasticsearch.host=127.0.0.1
elasticsearch.port=9200
至此,配置基本完成,接下来启动项目就会自动根据实体的配置创建索引和映射了,非常方便;但是有两点需要注意:
- 一是第一次启动时会创建,第二次启动时如果索引被删除了也会创建,但是如果索引没删除,只是改了索引实体里的配置,比方说把Text改成了Keyword,或者新增了映射,那么按上面的配置索引是不会自动修改的
- 二是我自身理解的一个坑,刚开始我一直在纠结索引实体类应该放哪项目启动才会自动创建,或者说哪里有配置能指向索引实体所在的包,后来通过测试发现这里自动创建索引与实体类无关与DAO有关,只要你按要求创建了DAO并交给spring管理了,那么dao里泛型传入的实体类就会创建索引
二、增删改查操作
package com.darling.test;
import com.darling.model.PaginationModel;
import com.darling.model.TestQuery;
import com.darling.po.EsTestInfo;
import com.darling.repository.EsTestInfoDao;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.RangeQueryBuilder;
import org.junit.Test;
import org.junit.platform.commons.util.StringUtils;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class SpringDataESIndexTest {
@Autowired
private ElasticsearchRestTemplate elasticsearchRestTemplate;
@Autowired
private EsTestInfoDao reportInfoDao;
/**
* 新增
*/
@Test
public void save(){
EsTestInfo reportInfo = new EsTestInfo();
reportInfo.setId("123");
reportInfo.setName("张三01");
reportInfo.setDesc("描述01");
reportInfo.setCreateTime(new Date());
reportInfoDao.save(reportInfo);
}
/**
* 修改 ID不变 内容改变会自动修改
*/
@Test
public void update(){
EsTestInfo reportInfo = new EsTestInfo();
reportInfo.setId("123");
reportInfo.setName("张三02");
reportInfo.setDesc("描述02");
reportInfo.setCreateTime(new Date());
reportInfoDao.save(reportInfo);
}
/**
* 根据id查询
*/
@Test
public void findById(){
EsTestInfo reportInfo = reportInfoDao.findById(123L).get();
System.out.println(reportInfo);
}
/**
* 查询所有数据
*/
@Test
public void findAll(){
Iterable<EsTestInfo> products = reportInfoDao.findAll();
for (EsTestInfo reportInfo : products) {
System.out.println(reportInfo);
}
}
/**
* 根据ID删除文档
*/
@Test
public void delete(){
EsTestInfo reportInfo = new EsTestInfo();
reportInfo.setId("222");
reportInfoDao.delete(reportInfo);
}
/**
* 批量新增
*/
@Test
public void saveAll(){
List<EsTestInfo> productList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
EsTestInfo reportInfo = new EsTestInfo();
reportInfo.setId("123");
reportInfo.setName("张三01");
reportInfo.setDesc("描述01");
reportInfo.setCreateTime(new Date());
productList.add(reportInfo);
}
reportInfoDao.saveAll(productList);
}
/**
* 分页查询
* 这里我没找如何添加条件进行分页查询所有在下面通过elasticsearchRestTemplate查了
*/
@Test
public void findByPageable(){
//设置排序(排序方式,正序还是倒序,排序的id)
Sort sort = Sort.by(Sort.Direction.DESC,"id");
//当前期望查询的页码,第一页从0开始,1表示第二页
int currentPage=0;
int pageSize = 5;
//设置查询分页
PageRequest pageRequest = PageRequest.of(currentPage, pageSize,sort);
//分页查询
Page<EsTestInfo> productPage = reportInfoDao.findAll(pageRequest);
for (EsTestInfo EsTestInfo : productPage.getContent()) {
System.out.println(EsTestInfo);
}
}
@Test
public PaginationModel<EsTestInfo> queryTestDateQuery(TestQuery query){
PaginationModel<EsTestInfo> res = new PaginationModel<>();
int currentPage=query.getPageIndex()-1;
int pageSize = query.getPageSize();
PageRequest pageRequest = PageRequest.of(currentPage, pageSize);
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
BoolQueryBuilder textKeyBqb = new BoolQueryBuilder();
if (StringUtils.isNotBlank(query.getTextKey())) {
/**
* 由于下面会用到must查询,所以此处用textKeyBqb再封装一个builder出来,否则
* 和must同时查询此处会出现0匹配也返回结果的情况
* 如果不想封装textKeyBqb,加上boolQueryBuilder.minimumShouldMatch(1)强制使es
* 最少满足一个should子句才能返回结果也行
*/
textKeyBqb.should(QueryBuilders.matchQuery("id", query.getTextKey()))
.should(QueryBuilders.matchQuery("name", query.getTextKey()))
.should(QueryBuilders.matchQuery("desc", query.getTextKey()))
.should(QueryBuilders.matchQuery("createTime", query.getTextKey()));
}
if (Objects.nonNull(query.getStartDate()) && Objects.nonNull(query.getEndDate())) {
RangeQueryBuilder timeRangeQuery = QueryBuilders.rangeQuery("publishDt")
.gte(query.getStartDate().getTime())
.lte(query.getEndDate().getTime());
boolQueryBuilder.must(timeRangeQuery);
}
if (Objects.nonNull(query.getRptStatus())) {
boolQueryBuilder.must(QueryBuilders.matchQuery("rptStatus", query.getRptStatus()));
}
// 将上面封装的子句加入到主查询条件中
boolQueryBuilder.must(textKeyBqb);
log.info("<<<<<<<<<<<<<<<<<<boolQueryBuilder:{}",boolQueryBuilder);
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(boolQueryBuilder).withPageable(pageRequest).build();
SearchHits<EsTestInfo> search = elasticsearchRestTemplate.search(searchQuery, EsTestInfo.class);
List<EsTestInfo> list = new ArrayList<>();
for (SearchHit<EsTestInfo> productSearchHit : search) {
EsTestInfo pro = productSearchHit.getContent();
list.add(pro);
}
res.setList(list);
res.setTotal(search.getTotalHits());
res.setPageIndex(query.getPageIndex());
res.setPageSize(query.getPageSize());
return res;
}
}
提示:代码中的
PaginationModel
为分页出参封装类、TestQuery
为分页查询条件,这里就不贴出来了
三、总结
其实上面的基于dao的增删改查很简单,关键在于分页查询这块,特别是当BoolQueryBuilder 的should和must同时使用时把我一顿坑,下面简单介绍下
- must表示返回的结果必须满足must子句的条件,并且参与计算分值;
- should表示返回的结果可能满足should子句的条件.在一个bool查询中,如果没有must,有一个或者多个should子句,那么只要满足一个就可以返回.但是如果既有must,又有should,然后不做特殊设置的情况下,可能即使should字句一个都不匹配
也会有结果返回;所以需要进行相应设置,具体代码里的注释有写供参考;