前言
此前虽然有尝试过集成elasticsearch,不过技术栈并非spring boot。本次尝试在springboot项目中集成elasticsearch,不过由于spring boot、es、rest的版本问题,折腾了好久,写这篇文章的目的一是分享一下,也是为了纪念当时的摸爬滚打。
注:集成过程中有参考部分网友的文章,在此表示感谢。
版本和环境
JDK: 1.8
spring boot: 2.3.2
elasticsearch: 7.6.2
IDE: 宇宙第一的IDEA
Elasticsearch、IK分词器的安装和配置
我安装的elasticsearch版本为7.6.2,网上教程很多,这里不再赘述
分词器:IK
测试:
http://127.0.0.1:9200/alarm_reason_index/_search
{
"query": {
"match": {
"planTitle": "那时候"
}
},
"sort": [
"_score",
{
"timestamp": {
"order": "desc",
"unmapped_type": "long"
}
}
],
"from": 0,
"size": 4
}
结果
{
"took": 5,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 5,
"relation": "eq"
},
"max_score": null,
"hits": [
{
"_index": "alarm_reason_index",
"_type": "_doc",
"_id": "1315844356900499451",
"_score": 4.377079,
"_source": {
"_class": "com.knowswift.stnl.bean.plan.po.EmergencyPlanES",
"emergencyPlanId": "1315844356900499451",
"planCode": "37090476851",
"planTitle": "那时的卡卡是",
"planLevel": "一级",
"createTime": "2020-10-24 10:20:18",
"timestamp": 1603506018
},
"sort": [
4.377079,
1603506018
]
}
...
]
}
}
集成过程
1. pom文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>7.6.2</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.6.2</version>
<exclusions>
<exclusion>
<artifactId>elasticsearch-rest-client</artifactId>
<groupId>org.elasticsearch.client</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>7.6.2</version>
</dependency>
其中
- spring-boot-starter-data-elasticsearch 需要2.3以上;
- org.elasticsearch和org.elasticsearch.client的版本需要和elasticsearch一致。
2. application.yml
spring:
elasticsearch:
rest:
uris: 127.0.0.1:9200
3. 实体类
@Data
@Document(indexName = "pl_index")
public class PlES {
@Id
private String id;
@Field(analyzer = "ik_smart")
private String code;
@Field(analyzer = "ik_max_word")
private String title;
@Field(type = FieldType.Date, format = DateFormat.custom, pattern = "uuuu-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Field
private Long timestamp;
public void setCreateTime(LocalDateTime createTime) {
this.createTime = createTime;
timestamp = createTime.toEpochSecond(ZoneOffset.of("+8"));
}
}
4.ElasticRepository
用于调用ElasticsearchRepository的方法
public interface PlElasticRepository extends ElasticsearchRepository<PlES, String> {
}
5.业务层接口IElasticService
基础的接口方法
public interface IElasticService<T, ID> {
boolean createIndex(Class<T> tClass);
boolean deleteIndex(String index);
void save(T entity);
void saveBatch(List<T> list);
List<T> findAll();
Page<T> query(String key);
void deleteById(ID id);
@Resource
ElasticsearchRepository getRepository();
}
6.业务层实现类
public class ElasticServiceImpl<M extends ElasticsearchRepository<T, ID>, T, ID> implements IElasticService<T, ID> {
@Resource
private ElasticsearchRestTemplate elasticsearchRestTemplate;
@Resource
RestHighLevelClient restHighLevelClient;
@Resource
private M getRepository;
@SneakyThrows
@Override
public boolean createIndex(Class<T> clazz) {
Document document = clazz.getAnnotation(Document.class);
GetIndexRequest request = new GetIndexRequest(document.indexName());
boolean exists = restHighLevelClient.indices().exists(request, RequestOptions.DEFAULT);
if (exists){
return true;
}
CreateIndexRequest createIndexRequest = new CreateIndexRequest(document.indexName());
restHighLevelClient.indices().create(createIndexRequest, RequestOptions.DEFAULT);
return true;
}
@Override
public boolean deleteIndex(String index) {
return elasticsearchRestTemplate.deleteIndex(index);
}
@Override
public void save(T entity) {
T save = getRepository.save(entity);
}
@Override
public void saveBatch(List<T> list) {
Iterable<T> ts = getRepository.saveAll(list);
}
@Override
public List<T> findAll() {
// FieldSortBuilder timestamp = SortBuilders.fieldSort("timestamp").order(SortOrder.DESC);
return (List<T>) getRepository.findAll(Sort.by("timestamp").descending());
}
@Override
public Page<T> query(String key) {
return null;
}
@Override
public M getRepository() {
return this.getRepository;
}
public void deleteById(ID id) {
getRepository.deleteById(id);
}
}
6. 业务实现方法
@Service
public class PlElasticService extends ElasticServiceImpl<PlElasticRepository, PlES, String> {
@SneakyThrows
public SearchHits list(String key, int pageNo, int pageSize) {
SearchRequest request = new SearchRequest();
SearchSourceBuilder builder = new SearchSourceBuilder();
if (StringUtils.isNotBlank(key)) {
builder.query(QueryBuilders.boolQuery().must(QueryBuilders.matchQuery("title", key)))
.sort(SortBuilders.scoreSort());
} else {
builder.query(QueryBuilders.boolQuery().must(QueryBuilders.matchAllQuery())).sort(SortBuilders.scoreSort());;
}
// 排序
FieldSortBuilder order = SortBuilders.fieldSort("timestamp").unmappedType("long").order(SortOrder.DESC);
// 分页 应注意,pageNo在此处并非是页码,而是当前页的第一行,也就是SQL语句的 offset
builder.from(pageNo).size(pageSize).sort(order);
builder.timeout(new TimeValue(2, TimeUnit.SECONDS));
// 高亮
builder.highlighter(new HighlightBuilder().field("title").preTags("<span style=\"color:red\">").postTags("</span>"));
request.source(builder);
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
// long value = hits.getTotalHits().value;
SearchHits searchHits = response.getHits();
SearchHit[] hits1 = searchHits.getHits();
for (SearchHit documentFields : hits1) {
Map<String, HighlightField> map = documentFields.getHighlightFields();
HighlightField title = map.get("title");
if (title == null) {
continue;
}
Text[] fragments = title.fragments();
if (fragments.length == 0) {
continue;
}
// 将es结果集转成实体类
String sourceAsString = documentFields.getSourceAsString();
PlES plES = JSONObject.parseObject(sourceAsString, PlES.class);
// 高亮替换
plES.setTitle(fragments[0].toString());
}
return searchHits;
}
}
至此,一个简单的分页查询方法就已经完成。
存在问题
所使用的elasticsearch客户端是 restHighLevelClient,在长时间不请求之后,再次请求会出现异常:远程主机强迫关闭了一个现有的连接,再次请求又正常。
暂时了解到的,这可能是elasticsearch存在——会杀死长时间空闲连接——的问题,或者是我不知道如何解决的问题。
目前我使用的解决方法是添加一个定时器,持续请求,保证连接是活跃的
@Scheduled(cron = "0 0/2 * * * *")
public void keepESAlive() {
try {
restHighLevelClient.info(RequestOptions.DEFAULT);
} catch (IOException ignored) {
}
}
如果有更好的解决办法,请在评论分享。