Spring boot 2.5 集成 Elasticsearch 实现增删改查以及全文搜索

1 摘要

Elasticsearch 是一款基于 Apache Lucene 的优秀的搜索服务器。本文将介绍基于 SpringBoot 2.5 集成 Elasticsearch 7 实现基本增删改查以及全文搜索。

Elasticsearch 官网: https://www.elastic.co

Elasticsearch 安装教程:

centOS 7 Elasticsearch 7.16 安装使用教程

2 核心 Maven 依赖

./demo-elasticsearch/pom.xml
        <!-- elasticsearch -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-elasticsearch</artifactId>
            <version>${elastic.springdata.version}</version>
        </dependency>

其中 ${elastic.springdata.version} 的版本为 4.3.0

其他相关依赖

        <!-- web mvc -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>${springboot.version}</version>
        </dependency>
        <!-- validation -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
            <version>${springboot.version}</version>
        </dependency>
        <!-- hutool -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>${hutool.version}</version>
        </dependency>
        <!-- Swagger 3,openApi 3 -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-boot-starter</artifactId>
            <version>${springfox-swagger3.version}</version>
        </dependency>

版本信息

        <springboot.version>2.5.7</springboot.version>
        <hutool.version>5.7.17</hutool.version>
        <springfox-swagger3.version>3.0.0</springfox-swagger3.version>

注意事项:

Swagger 3.0 不支持 SpringBoot 2.6+

Springfox 3.0.0 is not working with Spring Boot 2.6.0

3 核心代码

3.1 application 配置
./demo-elasticsearch/src/main/resources/application.yml
## spring
spring:
  application:
    name: demo-elasticsearch
  elasticsearch:
    rest:
      uris: 192.168.1.110:9200
      username: elastic
      password: elastic666
      connection-timeout: 10S
      read-timeout: 30S
      sniffer:
        interval: 5m
        delay-after-failure: 1m
3.2 实体类
./demo-elasticsearch/src/main/java/com/ljq/springboot/elasticsearch/model/entity/BlogEntity.java
package com.ljq.springboot.elasticsearch.model.entity;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.ToString;
import org.springframework.data.elasticsearch.annotations.Document;

/**
 * @Description: 博客信息
 * @Author: junqiang.lu
 * @Date: 2021/11/13
 */
@Data
@ToString(callSuper = true)
@Document(indexName = "blog")
@ApiModel(value = "博客信息实体类", description = "博客信息实体类")
public class BlogEntity extends BaseEntity {

    private static final long serialVersionUID = -2124422309475024490L;

    /**
     * 标题
     */
    @ApiModelProperty(value = "标题", name = "title")
    private String title;
    /**
     * 作者
     */
    @ApiModelProperty(value = "作者", name = "author")
    private String author;
    /**
     * 内容
     */
    @ApiModelProperty(value = "内容", name = "content")
    private String content;
    /**
     * 阅读数量
     */
    @ApiModelProperty(value = "阅读数量", name = "countRead")
    private Integer countRead;
    /**
     * 点赞数量
     */
    @ApiModelProperty(value = "点赞数量", name = "countLike")
    private Integer countLike;
    /**
     * 客户端时间戳(精确到秒)
     */
    @ApiModelProperty(value = "客户端时间戳(精确到秒)", name = "clientTimestamp")
    private Integer clientTimestamp;

}
./demo-elasticsearch/src/main/java/com/ljq/springboot/elasticsearch/model/entity/BaseEntity.java
package com.ljq.springboot.elasticsearch.model.entity;

import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.LastModifiedDate;

import java.io.Serializable;
import java.util.Date;

/**
 * @Description: 基础实体类
 * @Author: junqiang.lu
 * @Date: 2021/9/24
 */
@Data
public class BaseEntity implements Serializable {

    private static final long serialVersionUID = -3003658740476069858L;

    /**
     * id,主键
     */
    @Id
    @ApiModelProperty(value = "id,主键", name = "id")
    private String id;
    /**
     * 创建时间
     */
    @CreatedDate
    @ApiModelProperty(value = "创建时间", name = "createTime")
    private Long createTime;
    /**
     * 修改时间
     */
    @LastModifiedDate
    @ApiModelProperty(value = "修改时间", name = "updateTime")
    private Date updateTime;
}

org.springframework.data.elasticsearch.annotations.Document 用于标注索引,类似于关系型数据库中的表名

org.springframework.data.annotation.Id 用于指定主键

org.springframework.data.elasticsearch.annotations.Field 用于指定字段,默认实体类中所有字段都会保存到 elasticsearch 中,所以可以不在字段上写这个注解

3.3 基础增删改查(CRUD)
./demo-elasticsearch/src/main/java/com/ljq/springboot/elasticsearch/service/impl/BlogServiceImpl.java
package com.ljq.springboot.elasticsearch.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.ljq.springboot.elasticsearch.common.api.ApiMsgEnum;
import com.ljq.springboot.elasticsearch.common.api.ApiResult;
import com.ljq.springboot.elasticsearch.model.entity.BlogEntity;
import com.ljq.springboot.elasticsearch.model.param.*;
import com.ljq.springboot.elasticsearch.service.BlogService;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.common.unit.Fuzziness;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.Operator;
import org.elasticsearch.index.query.QueryBuilders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
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.data.support.PageableExecutionUtils;
import org.springframework.stereotype.Service;

import java.util.Objects;
import java.util.stream.Collectors;

/**
 * @Description: 博客业务实现类
 * @Author: junqiang.lu
 * @Date: 2021/12/11
 */
@Slf4j
@Service("blogService")
public class BlogServiceImpl implements BlogService {

    @Autowired
    private ElasticsearchRestTemplate elasticTemplate;

    /**
     * 新增单条
     *
     * @param addParam
     * @return
     */
    @Override
    public ApiResult<BlogEntity> save(BlogAddParam addParam) {
        BlogEntity blogEntity = new BlogEntity();
        BeanUtil.copyProperties(addParam, blogEntity, CopyOptions.create().ignoreError().ignoreNullValue());
        elasticTemplate.save(blogEntity);
        return ApiResult.success(blogEntity);
    }

    /**
     * 查询单条
     *
     * @param queryOneParam
     * @return
     */
    @Override
    public ApiResult<BlogEntity> queryOne(BlogQueryOneParam queryOneParam) {
        return ApiResult.success(elasticTemplate.get(queryOneParam.getId(), BlogEntity.class));
    }

    /**
     * 更新单条
     *
     * @param updateParam
     * @return
     */
    @Override
    public ApiResult<BlogEntity> update(BlogUpdateParam updateParam) {
        BlogEntity blogEntity = elasticTemplate.get(updateParam.getId(), BlogEntity.class);
        if (Objects.isNull(blogEntity)) {
            return ApiResult.fail(ApiMsgEnum.BLOG_NOT_EXIST);
        }
        BeanUtil.copyProperties(updateParam, blogEntity, CopyOptions.create().ignoreError().ignoreNullValue());
        elasticTemplate.save(blogEntity);
        return ApiResult.success(blogEntity);
    }

    /**
     * 删除单条
     *
     * @param deleteOneParam
     * @return
     */
    @Override
    public ApiResult<Void> delete(BlogDeleteOneParam deleteOneParam) {
        BlogEntity blogEntity = elasticTemplate.get(deleteOneParam.getId(), BlogEntity.class);
        if (Objects.isNull(blogEntity)) {
            return ApiResult.fail(ApiMsgEnum.BLOG_NOT_EXIST);
        }
        elasticTemplate.delete(deleteOneParam.getId(), BlogEntity.class);
        return ApiResult.success();
    }


}

3.4 全文搜索(重点)
./demo-elasticsearch/src/main/java/com/ljq/springboot/elasticsearch/service/impl/BlogServiceImpl.java
    /**
     * 分页查询
     *
     * @param queryPageParam
     * @return
     */
    @Override
    public ApiResult<Page<BlogEntity>> queryPage(BlogQueryPageParam queryPageParam) {
        Pageable pageable = PageRequest.of(queryPageParam.getCurrentPage() - 1, queryPageParam.getPageSize());
        // 构建查询条件
        NativeSearchQueryBuilder searchQueryBuilder = new NativeSearchQueryBuilder();
        BoolQueryBuilder filter = QueryBuilders.boolQuery();
        // id-精确查询-idsQuery
        if (StrUtil.isNotBlank(queryPageParam.getId())) {
           filter.must(QueryBuilders.idsQuery().addIds(queryPageParam.getId()));
        }
        // 标题-模糊查询-matchQuery
        if (StrUtil.isNotBlank(queryPageParam.getTitle())) {
            filter.must(QueryBuilders.matchQuery("title",
                            queryPageParam.getTitle()).operator(Operator.OR).fuzziness(Fuzziness.AUTO));
        }
        // 作者-精确查询-queryStringQuery
        if (StrUtil.isNotBlank(queryPageParam.getAuthor())) {
            filter.must(QueryBuilders.queryStringQuery(
                    queryPageParam.getAuthor()).defaultField("author").fuzziness(Fuzziness.AUTO));
        }
        // 内容-模糊查询-fuzzyQuery
        if (StrUtil.isNotBlank(queryPageParam.getContent())) {
            filter.must(QueryBuilders.fuzzyQuery("content",
                            queryPageParam.getContent().toLowerCase()).fuzziness(Fuzziness.AUTO));
        }
        // 全文查询-模糊查询-multiMatchQuery
        if (StrUtil.isNotBlank(queryPageParam.getKeyword())) {
            filter.must(QueryBuilders.multiMatchQuery(
                    queryPageParam.getKeyword(),"title", "author", "content").fuzziness(Fuzziness.AUTO));
        }
        // 客户端时间戳-范围查询-rangeQuery
        if (Objects.nonNull(queryPageParam.getMinClientTimestamp())) {
            filter.must(QueryBuilders.rangeQuery("clientTimestamp")
                    .gte(queryPageParam.getMinClientTimestamp()));
        }
        if (Objects.nonNull(queryPageParam.getMaxClientTimestamp())) {
            filter.must(QueryBuilders.rangeQuery("clientTimestamp")
                    .lte(queryPageParam.getMaxClientTimestamp()));
        }

        searchQueryBuilder.withFilter(filter);
        // 分页信息
        searchQueryBuilder.withPageable(pageable);

        NativeSearchQuery query = searchQueryBuilder.build();
        SearchHits<BlogEntity> searchHits = elasticTemplate.search(query, BlogEntity.class);
        log.info("搜索结果: {}\n{}", searchHits, JSONUtil.toJsonStr(searchHits.getSearchHits()));
        Page<BlogEntity> page = PageableExecutionUtils.getPage(searchHits.getSearchHits().stream()
                .map(SearchHit::getContent).collect(Collectors.toList()),pageable, searchHits::getTotalHits);
        return ApiResult.success(page);
    }
3.5 SpringBoot 启动类
./demo-elasticsearch/src/main/java/com/ljq/springboot/elasticsearch/DemoElasticsearchApplication.java
package com.ljq.springboot.elasticsearch;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.elasticsearch.config.EnableElasticsearchAuditing;

/**
 * @author ls-ljq
 */
@EnableElasticsearchAuditing
@SpringBootApplication(scanBasePackages = "com.ljq.springboot.elasticsearch")
public class DemoElasticsearchApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoElasticsearchApplication.class, args);
    }

}

4 注意事项

4.1 SpringBoot properties/yml 配置

SpringBoot elasticsearch 自动化配置类

org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration

SpringBoot 2.5.7 :

支持的配置类配置前缀
org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientPropertiesspring.elasticsearch.rest

SpringBoot 2.6.1 :

支持的配置类是否过时配置前缀
org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchPropertiesspring.elasticsearch
org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientPropertiesspring.elasticsearch.rest
org.springframework.boot.autoconfigure.elasticsearch.DeprecatedElasticsearchRestClientPropertiesspring.elasticsearch.rest

兼容性配置示例:

## spring
spring:
  application:
    name: demo-elasticsearch
  elasticsearch:
    rest:
      uris: 192.168.1.110:9200
      username: elastic
      password: elastic666
      connection-timeout: 10S
      read-timeout: 30S
      sniffer:
        interval: 5m
        delay-after-failure: 1m

4.2 字母大小写敏感问题

termQuery fuzzyQuery 区分大小写,推荐操作方案: (1)在应用程序中将用户输入的搜索词统一转化为小写;(2) 在Elasticsearch 中添加转小写过滤器

spring-boot - 如何在 Elasticsearch 中搜索不匹配大小写的精确文本

4.3 分词问题

matchQuery 只能匹配一个完整的单词,如果数据库中有 「spring」 这个单词,搜索条件为 「sprin」 则无法搜索到,而 fuzzyQuery 可以

elasticsearch 默认的中文分词是按照一个字一个字进行分词,英文分词则是按照一个完整的单词进行分词,如"黄鹤楼" 可以分出 “黄”,“鹤”,"楼"三个词;“springboot” 则就是一个单词

5 推荐参考资料

Spring Data Elasticsearch - Reference Documentation

Elasticsearch实战篇——Spring Boot整合ElasticSearch

Introduction to Spring Data Elasticsearch – Baeldung

SpringBoot Elasticsearch 7.x 多条件分页查询

ElasticSearch集成SpringData史上最全查询教程

Elasticsearch Queries with Spring Data

SpringBoot + ElasticSearch系列: springboot中使用QueryBuilders、NativeSearchQuery ElasticsearchRestTemplate 实现复杂查询

6 Github 源码

Gtihub 源码地址 : https://github.com/Flying9001/springBootDemo

个人公众号:404Code,分享半个互联网人的技术与思考,感兴趣的可以关注.
404Code

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值