首先簡單介紹下寫這篇博文的背景,最近負責的一個聚合型的新項目要大量使用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字句一個都不匹配
也會有結果返回;所以需要進行相應設置,具體代碼裏的註釋有寫供參考;