目录
背景
Springboot项目集成ElasticSearch(以下简称“ES”),实现项目中高级/复杂搜索需求,解决传统数据库检索时大SQL语句编写复杂,速度慢,以及难维护等问题。
摘要
本文将介绍如何在SpringBoot项目中集成ElasticSearch,实现高效的全文搜索功能。全文搜索在现代Web应用中越来越重要,ElasticSearch以其强大的搜索能力、高扩展性和易用性成为首选的搜索引擎。
maven依赖
在项目中加入es所需maven依赖包
<!-- es搜索 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.10.2</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
</dependency>
pom文件
连接es服务器
spring.elasticsearch.rest.uris=http://ip:端口
spring.elasticsearch.rest.username=账号
spring.elasticsearch.rest.password=密码
代码实现
以下部分为涉及到ES的JAVA主要代码逻辑,请结合自身项目情况参考使用。
创建实体
创建es实体,项目启动会自动创建索引
@Data
@Document(indexName = "testes",createIndex = true)
public class TestEsVo implements Serializable {
/**
* 主键
*/
@Id
@Field(type = FieldType.Integer,store = true,index = true)
Integer id;
/**
* 名字
*/
@Field(type = FieldType.Text,store = true,index = true)
String name;
/**
* 时间
*/
@Field(type = FieldType.Date, pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
Date create_time;
/**
* 集合
*/
@Field(type = FieldType.Text,store = true,index = true)
List<String> strList;
}
继承 Repository
@Repository
public interface TestEsRepository extends ElasticsearchRepository<TestEsVo, Integer> {
}
添加ES数据
方式一:
调用ElasticsearchRepository自带方法进行保存
// 批量添加
testEsRepository.saveAll(testEsVoList);
// 单个添加
testEsRepository.save(testEsVo);
方式二:
使用RestHighLevelClient+BulkRequest进行保存
@Autowired
private RestHighLevelClient client;
private void save(List<Testcase> caseAll, List<TestCaseFipLink> caseFipLinkList, List<TestCaseEeaLink> caseEeaLinkList, List<TestCaseVirtual> test_cases_virtual, List<UseCaseVirtual> useCaseVirtuals) {
try {
BulkRequest bulkRequest = new BulkRequest();
for(TestEsVo test : caseAll){
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> map = objectMapper.convertValue(test, Map.class);
bulkRequest.add(
new IndexRequest("testes")
.id(test.getId().toString())
.source(map));
}
BulkResponse bulkResponse = client.bulk(bulkRequest, RequestOptions.DEFAULT);
//System.out.println("====" + JSON.toJSONString(bulkResponse));
List<String> errList = new ArrayList<>();
if (bulkResponse.hasFailures()) {
// 有失败的操作
for (BulkItemResponse itemResponse : bulkResponse) {
if (itemResponse.isFailed()) {
// 处理失败的操作
errList.add(itemResponse.getFailure().getId());
}
}
} else {
// 所有操作都成功
System.out.println("All items were processed successfully");
}
}catch (Exception e){
e.printStackTrace();
}
}
注意:
大数据量,批量插入使用RestHighLevelClient+BulkRequest实现。实测效率比ElasticsearchRepository高很多。
修改ES数据
1、先从ES查询出文档信息
2、更新文档内容
3、通过save方法重新保存
// 先查询出ES中该文档全部数据
Optional<TestEsVo> test = testCaseEsRepository.findById(2011253);
TestEsVo testEsVo = test.get();
// 修改内容
testEsVo.setName("张三-ES修改");
testEsVo.setDemo1("新增的字段");
// 保存至ES
testEsRepository.save(testEsVo);
删除ES数据
只会删除文档内容,并不会删除索引结构
// 删除该索引下全部文档
testEsRepository.deleteAll();
// 根据id删除文档
testEsRepository.deleteById(123);
查询ES数据
1、keyword不分词 (普通查询),text分词 (模糊查询)
2、若查询条件values为数组,则使用 termsQuery;若查询条件value为字符串,则使用 termQuery
3、“包含(or)” values 中任意一个匹配即可,则使用 termsQuery;
“包含(and)” values 中所有都匹配,则循环使用 termQuery,即先过滤出values中包含元素1的数据,进一步再过滤values中包含元素2的数据
4、mustNot 类似与not in,values中存在的元素都过滤掉
5、“是” 多选字段若想完全匹配,则可将其数组排序后转为String类型进行存储,再使用 termsQuery 进行匹配
6、模糊查询使用text类型,使用matchPhrasePrefixQuery() 构造短语匹配,可避免text类型分词后查询结果不准确
7、es与mysql等存在时区问题,相差八小时(es慢8小时),所以查询是需要先转换时区,或者存入es前先转换时区再查询,但date类型无法设置时区
// 确定查询方式
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
List<String> values = Arrays.asList("A","B","C");
// 包含(or) 查找values中任一元素 例:name = "A"、name = ["A"、"D"]
boolQueryBuilder.filter(QueryBuilders.termsQuery("name.keyword", values));
// 不包含 只要是在values中存在的,则过滤掉 例:name = ["A"、"D"] 既数据中存在A或D都会被过滤掉
boolQueryBuilder.mustNot(QueryBuilders.termsQuery("name.keyword", values));
// 包含(and) 查找values中的所有元素在fip中都存在 values <= fip 例:fip=["A","B","C"]、fip=["A","B","C","D"]
for(String value : values){
boolQueryBuilder.filter(QueryBuilders.termQuery("fip.keyword", value));
}
// 是 完全匹配 例:name = ["A","B","C"] 显示,其余全不显示
values.sort(Comparator.naturalOrder());
values_str = String.join(",", values);
boolQueryBuilder.filter(QueryBuilders.termQuery("fip.keyword", values_str));
//模糊查询 类似与sql中的like。注意使用text类型,非keyword类型 例:name = "中华人民共和国" ,value = "中华" 或 "中华人民" 或 "中华人民共和国"
MatchPhrasePrefixQueryBuilder matchPhrasePrefixQueryBuilder = QueryBuilders.matchPhrasePrefixQuery("name", value);
boolQueryBuilder.filter(matchPhrasePrefixQueryBuilder);
//时间类型 注意es与mysql等存在时区问题,相差八小时(es慢8小时),所以查询是需要先转换时区
Integer filterType = testEsDto.getFilterType();
String columnName = testEsDto.getColumnName();
//columnName = columnName + ".keyword";
List<String> valueList = (List<String>) testEsDto.getValue();
//传入es的时间参数
String start_time_str = null;
String ent_time_str = null;
//获取前端的时间参数
Date start_time = null;
Date ent_time = null;
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
if(!CollectionUtils.isEmpty(valueList)){
try {
start_time = sdf.parse(valueList.get(0));
start_time_str = dateFormat.format(start_time);
if(valueList.size() > 1){
ent_time = sdf.parse(valueList.get(1));
ent_time_str = dateFormat.format(ent_time);
}
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery(columnName);
rangeQueryBuilder.gte(start_time_str);
rangeQueryBuilder.lte(start_time_str);
boolQueryBuilder.filter(rangeQueryBuilder);
//分页
Pageable pageable = PageRequest.of(pageNum-1, pageSize);
//排序 text不可排序
FieldSortBuilder sortBuilder = SortBuilders.fieldSort("name.keyword").order(SortOrder.DESC);
//构建查询条件
NativeSearchQuery request = new NativeSearchQueryBuilder()
.withQuery(boolQueryBuilder)
.withPageable(pageable)
.withSort(sortBuilder)
.build();
//执行,获得检索结果 可使用 ElasticsearchRestTemplate template;
SearchHits<TestEsVo> search = template.search(request, TestEsVo.class);
//遍历结果,进行处理
for (SearchHit<TestEsVo> hit : search){
TestEsVo content = hit.getContent();
TestEsVos.add(content);
}
Page<TestEsVo> page = new PageImpl<>(TestEsVos, pageable, search.getTotalHits());
Kibana DSL语句
创建索引
建议提前维护索引,springboot自动创建的索引格式可能与需求不符。
注意:维护数据前一定要检查ES生成的文档结构是否与以下相符
PUT / testes {
"mappings": {
"properties": {
"_class": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"id": {
"type": "long"
},
"name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"create_time": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss"
},
"ad_str": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"strList": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}
查看索引结构
GET testes
删除索引
注意:删除用例索引后,索引下的数据也全部被删除。谨慎使用
delete testes
增加索引下字段
两种方法,推荐使用DTL语句提前维护,可避免springboot自动创建的类型不适应需求
1、使用DTL语句添加
PUT /testes/_mapping
{
"properties": {
"new_field": {
"type": "text"
}
}
}
2、在Java代码对象中定义字段springboot自动创建
@Field(type = FieldType.Text,store = true,index = true)
String demo1;
查看用例数据
ES限制最多只能查看10000条,不加分页的话最多返回10条
GET testes/_search
{
"from": 0,
"size": 10000,
"query": {
"match_all": {}
},
"track_total_hits": true
}
查看索引下总数据量
GET /_cat/indices/testes?v&s=docs.count:desc&h=index,docs.count,store.size
查看某一用例
GET /{索引名称}/_doc/{用例ID}
GET /testes/_doc/3010168
更改索引最大返回数量
ES限制最多只能查看10000条,可根据实际情况调整,调整过大可能会影响性能
PUT /testes/_settings
{
"index": {
"max_result_window": "50000"
}
}