本文介绍 Spring Boot 项目中整合 ElasticSearch 并实现 CRUD 操作,包括分页、滚动等功能。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-elasticsearchartifactId>
dependency>
spring:
elasticsearch:
rest:
uris: 192.168.1.81:9200
package com.practice.elkstudy.entity;
import cn.hutool.core.date.DateTime;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import java.util.Date;
/**
* @Description : 文档模型
* @Version : V1.0.0
* @Date : 2021/12/22 14:08
*/
@Document(indexName = "article")
@Data
public class ArticleEntity {
@Id
private String id;
private String title;
private String content;
private Integer userId;
private Date createTime = DateTime.now();
}
package com.practice.elkstudy.repository;
import com.practice.elkstudy.entity.ArticleEntity;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
/**
* @Description : article数据操作接口
* @Version : V1.0.0
* @Date : 2021/12/22 14:18
*/
public interface ArticleRepository extends ElasticsearchRepository<ArticleEntity,String> {
}
下面可以使用这个 ArticleRepository 来操作 ES 中的 Article 数据。
我们这里没有手动创建这个 Article 对应的索引,由 elasticsearch 默认生成。
下面的接口,实现了 spring boot 中对 es 数据进行插入、更新、分页查询、滚动查询、删除等操作。可以作为一个参考。
其中,使用了 Repository 来获取、保存、删除 ES 数据;使用 ElasticsearchRestTemplate 或 ElasticsearchOperations 来进行分页/滚动查询。
package com.practice.elkstudy.controller.controller;
import com.practice.elkstudy.entity.ArticleEntity;
import com.practice.elkstudy.repository.ArticleRepository;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.Optional;
/**
* @Description : article控制类
* @Version : V1.0.0
* @Date : 2021/12/22 14:11
*/
@RestController
@RequestMapping("/elk")
public class ArticleController {
@Resource
private ArticleRepository articleRepository;
/**
* 根据文档id查询数据
*
* @param id 文档id
* @return 文档详情
*/
@GetMapping("/byId")
public String findById(@RequestParam String id) {
Optional<ArticleEntity> record = articleRepository.findById(id);
return record.toString();
}
/**
* 保存文档信息
*
* @param article 文档详情
* @return 保存的文档信息
*/
@PostMapping("/saveArticle")
public String saveArticle(@RequestBody ArticleEntity article) {
ArticleEntity result = articleRepository.save(article);
return result.toString();
}
@DeleteMapping("/deleteById")
public String deleteArticle(@RequestParam String id) {
articleRepository.deleteById(id);
return "success";
}
}
package com.practice.elkstudy.controller.controller;
import com.practice.elkstudy.entity.ArticleEntity;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
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.SearchHitsImpl;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @Description : article高级查询
* @Version : V1.0.0
* @Date : 2021/12/22 15:10
*/
@RestController
@RequestMapping("/elk")
public class ArticleAdvanceController {
@Autowired
private ElasticsearchRestTemplate restTemplate;
@Autowired
private ElasticsearchOperations operations;
/**
* 分页查询
*
* @param pageNum 页码,从0开始
* @param pageSize 分页大小
* @return 查询结果
*/
@GetMapping("/queryPage")
public String queryPage(@RequestParam int pageNum, @RequestParam int pageSize) {
NativeSearchQuery query = new NativeSearchQuery(new BoolQueryBuilder());
query.setPageable(PageRequest.of(pageNum, pageSize));
// 方法1
SearchHits<ArticleEntity> search = restTemplate.search(query, ArticleEntity.class);
// 方法2
// SearchHits search = operations.search(query, ArticleEntity.class);
List<ArticleEntity> articles = search.getSearchHits().stream().map(SearchHit::getContent).collect(Collectors.toList());
return articles.toString();
}
/**
* 滚动查询
*
* @param scrollId 滚动id
* @param pageSize 分页大小
* @return 查询结果
*/
@GetMapping(value = "/scrollQuery")
public String scroll(String scrollId, Integer pageSize) {
if (pageSize == null || pageSize <= 0) {
return "please input query page num";
}
NativeSearchQuery query = new NativeSearchQuery(new BoolQueryBuilder());
query.setPageable(PageRequest.of(0, pageSize));
SearchHits<ArticleEntity> searchHits;
if (StringUtils.isEmpty(scrollId) || scrollId.equals("0")) {
// 开启一个滚动查询,设置该scroll上下文存在60s
// 同一个scroll上下文,只需要设置一次query(查询条件)
searchHits = restTemplate.searchScrollStart(60000, query, ArticleEntity.class, IndexCoordinates.of("article"));
if (searchHits instanceof SearchHitsImpl) {
scrollId = ((SearchHitsImpl) searchHits).getScrollId();
}
} else {
// 继续滚动
searchHits = restTemplate.searchScrollContinue(scrollId, 60000, ArticleEntity.class, IndexCoordinates.of("article"));
}
List<ArticleEntity> articles = searchHits.getSearchHits().stream().map(SearchHit::getContent).collect(Collectors.toList());
if (articles.size() == 0) {
// 结束滚动
restTemplate.searchScrollClear(Collections.singletonList(scrollId));
scrollId = null;
}
if (Objects.isNull(scrollId)) {
Map<String, String> result = new HashMap<>(2);
result.put("articles", articles.toString());
result.put("message", "已到末尾");
return result.toString();
} else {
Map<String, String> result = new HashMap<>();
result.put("count", String.valueOf(searchHits.getTotalHits()));
result.put("pageSize", String.valueOf(articles.size()));
result.put("articles", articles.toString());
result.put("scrollId", scrollId);
return result.toString();
}
}
}
之前遇到的一个问题,日志检索的接口太慢了。
开始使用的是深度分页,即1,2,3…10,这样的分页查询,查询条件较多(十多个参数)、查询数据量较大(单个日志索引约2亿条数据)。
分页查询速度慢的原因在于:ES的分页查询,如查询第100页数据,每页10条,是先从每个分区(shard,一个索引默认是5个shard)中把命中的前100*10条数据查出来,然后协调节点进行合并操作,最后给出100页的数据。也就是说,实际被加载到内存的数据远远超过理想情况。
这样,索引分片数越多,查询页数越多,查询速度就越慢。ES默认的max_result_window是10000条,也就是正常情况下,用分页查询到10000条数据时,就不会在返回下一页数据了。
如果不需要进行跳页,比如直接查询第100页数据,或者数据量非常大,那么可以考虑用scroll查询。在scroll查询下,第1次需要根据查询参数开启一个scroll上下文,设置上下文缓存时间。以后的滚动只需要根据第一次返回的scrollId来进行即可。
scroll只支持往下滚动,如果想要往前滚动,还可以根据scrollId缓存查询结果,这样就可以实现上下文滚动查询了一一就像大家经常使用的淘宝商品检索时上下滚动一样。
SpringBoot 整合 ES 实现 CRUD 操作