自从用了 Spring-Data-Elasticsearch ,后端ES开发效率提高了10倍!

发布时间:2026/7/2 5:08:16
自从用了 Spring-Data-Elasticsearch ,后端ES开发效率提高了10倍! 前言本篇文章讲的内容基于我的开源微服务项目【校园博客】进行分析和讲解本文中的代码是在项目的 /blog-service/blog-content-server 路径下感兴趣的同学欢迎随时查看觉得不错的话也欢迎点点star噢。GitHub地址https://github.com/stick-i/scblogs为了给项目添加一个好的搜索功能我去学习了一下elasticsearch。在学习elasticsearch-client的期间发现它提供的api不太优雅用起来也不太舒服而且我觉得有些操作完全是可以封装在内部的比如获取数据后对数据转化为bean的操作还有属性高亮不仅设置比较麻烦而且设置完成的高亮居然是单独在一个字段里的需要开发者去手动的替换才行这些操作我觉得其实都可以封装在内部的。然后我就去看了一下spring-data里面提供的 es 操作库发现有很多操作都封装的比较完善使用起来也比较优雅于是我便使用spring-data-elasticsearch完成了这个功能过程中查阅了很多资料、博客、官方文档有些地方我觉得官方文档讲的也不够详细导致走了很多弯路。为了方便大家学习和少走弯路故记录于此本文主要是一些使用上的记录和讲解对原理和基础知识并没有介绍。技术要点使用copyTo和ElasticsearchRepository完成的多字段搜索。使用注解Highlight和HighlightField完成的高亮显示。使用Pageable和SearchPage实现分页和高亮两不误的接口。使用RabbitMQ完成MySQL和elasticsearch的数据同步。依赖项我当前的环境springboot 2.6.6elasticsearch 7.12kibana 7.12这个不是必须的然后当前版本的spring默认是用的 7.15.2 的我担心和我的es不兼容就加了个标签给它改了一下版本elasticsearch.version7.12.1/elasticsearch.version核心依赖其实就这一个这里面已经依赖了elasticsearch需要的一些依赖例如elasticsearch-rest-highlevel-client。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-elasticsearch/artifactId /dependency然后如果跟我一样使用RabbitMQ做数据同步的话还需要引用mq的依赖!--AMQP依赖包含RabbitMQ-- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-amqp/artifactId /dependency !-- json序列化依赖需要手动配置bean -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId /dependency配置文件这里需要配置elasticsearch的账号密码spring: elasticsearch: uris: http://localhost:9200 username: 12345 password: 12345核心代码实体类BlogDoc下面是我代码当中跟 es 进行交互的实体类代码上有相关的注释我将一些多余的、意义不大的属性删掉了方便大家查看。package cn.sticki.blog.content.pojo; import com.fasterxml.jackson.annotation.JsonIgnore; 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; /** * Blog ES文档类型 * * author 阿杆 * version 1.0 * date 2022/7/8 15:24 */ Data Document(indexName blog) public class BlogDoc { /** * 博客id */ Id Integer id; /** * 封面图链接 */ Field(type FieldType.Keyword, index false) String coverImage; /** * 标题 */ Field(type FieldType.Text, analyzer ik_max_word, copyTo descriptiveContent) String title; /** * 描述 */ Field(type FieldType.Text, analyzer ik_max_word, copyTo descriptiveContent) String description; /** * 创建时间 */ Field(type FieldType.Date, pattern uuuu-MM-dd HH:mm:ss) Date createTime; /** * 发表状态1表示已发表、2表示未发表、3为仅自己可见、4为回收站、5为审核中 */ Field(type FieldType.Integer) Integer status; /** * 由其他属性copy而来主要用于搜索功能不需要储存数据 */ JsonIgnore Field(type FieldType.Text, analyzer ik_max_word, ignoreFields descriptiveContent, excludeFromSource true) String descriptiveContent; }注解说明Document(indexName blog)声明该实体类对应es中的哪个索引库。Id声明该字段对应索引库当中的id。JsonIgnore这个应该很熟悉吧就是在json序列化时将对象中的一些属性忽略掉使返回的json数据不包含该属性。Field(...) 这些其实都对应es的api调用时传入的字段有一点es基础会很容易看懂也可以看看我写的elasticsearch专栏下的其他文章前几篇是我学基础的时候记录的。type FieldType.Integer 声明字段属性如果不写默认为auto就是es会帮你自动匹配成最合适的字段类型建议还是写一下。index false 声明该字段不需要建立索引一般用于不会被拿来搜索、排序、统计的字段比如我这里写的封面图链接。analyzer ik_max_word 声明该text字段需要使用的分词器我这里是用的ik分词器需要开发者去手动安装但对中文分词比较友好。excludeFromSource true翻译出来意思是“从源中排除”应该是指这个字段的属性不会插入到es索引库当中吧这个字段是我用来copy_to的主要是搜索的时候使用本身并不会直接存入数据所以这个字段如果有数据我希望插入的时候把它忽略。copyTo descriptiveContent这个就是跟es的copy_to一样就是说把当前属性拷贝到“descriptiveContent”当中可以拷贝多个属性到同一个字段中便于搜索、查询。pattern uuuu-MM-dd HH:mm:ss 声明该自定义的格式字符串一般在type FieldType.Date时使用。format跟pattern差不多官方解释是用于定义至少一种预定义格式。如果未定义则使用默认值*_date_optional_time和epoch_millis*。也就是只能使用给定的枚举值不能自定义自定义的话得用pattern。下图是谷歌翻译的官方解释image-20221016134857918实体类属性copy_to大家都知道在es当中如果有多个字段需要被同时查询比如我的博客业务要搜索内容的时候我会把用户输入的关键字同时拿来匹配标题和文章描述那可以用multi_match、query_string进行多字段查询也可以用copy_to将多个字段复制到一个新属性上再去查新属性这几种方法都是可以的但是copy_to它的性能会高一些尤其是在同时要查的属性非常多的时候这属于是一种储存换取速度的方式。copy_to的属性在上面已经讲过了跟es的api用来起来差不多的但是我上面的代码还写了一个descriptiveContent/** * 由其他属性copy而来主要用于搜索功能不需要储存数据 */ JsonIgnore Field(type FieldType.Text, analyzer ik_max_word, ignoreFields descriptiveContent, excludeFromSource true) String descriptiveContent;这个属性就是被cope_to到的那个属性但实际上我们在写代码的时候并不会给它赋值或者取值或者别的怎么样总是就是希望他尽可能透明仅在对es时有效因为es里是已经提前定义好这个索引库了的es创建索引库的代码我会贴在文章最后。这是因为后面我们要使用ElasticsearchRepository的时候被查询的字段如果不存在于这个实体类idea会有一个很碍眼的提示作为强迫症患者这就引发了我的思考是不是我们在定义实体类的时候要和定义索引库的时候一样给出全部的字段呢尽管这个字段只是一个“隐身”的字段。为了把这个碍眼的提示去掉 为了让代码变得更可读一点所以我加上了这个字段并加了一些忽略的属性使它尽可能隐身。Mapper层Repository核心代码如下具体解释和分析在下面package cn.sticki.blog.content.mapper; import cn.sticki.blog.content.pojo.BlogDoc; import org.springframework.data.domain.Pageable; import org.springframework.data.elasticsearch.annotations.Highlight; import org.springframework.data.elasticsearch.annotations.HighlightField; import org.springframework.data.elasticsearch.annotations.HighlightParameters; import org.springframework.data.elasticsearch.core.SearchPage; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; /** * BlogRepository操作类 * 提供save、findById、findAll、count、delete、exists等接口 * * author 阿杆 * version 1.0 * date 2022/7/9 10:53 */ public interface BlogRepository extends ElasticsearchRepositoryBlogDoc, Long { /** * 通过描述内容来搜索博客 * * param descriptiveContent 描述语句 * param pageable 分页 * return 博客列表 */ SuppressWarnings(SpringDataRepositoryMethodReturnTypeInspection) Highlight(fields { HighlightField(name title, parameters HighlightParameters(requireFieldMatch false)), HighlightField(name description, parameters HighlightParameters(requireFieldMatch false)), }) SearchPageBlogDoc findByDescriptiveContent(String descriptiveContent, Pageable pageable); }继承ElasticsearchRepository这个其实就有点像继承BaseMapper它会给你提供一些基础的CRUD方法方便你直接使用比如save、delete、find之类的。image-20221016142548009它是个泛型类两个参数分别是**实体类id的类型**。在该接口下BlogRepository按照特殊的命名规则声明的方法可以直接调用不需要开发者实现接口且它返回的内容是已经封装好的你需要的数据会被封装在你提供的实体类里面不用手动解析数据。大概就是findByXxxAndXxxOrXxx()这个类型具体的可以参考官网https://docs.spring.io/spring-data/elasticsearch/docs/4.3.5/reference/html/#elasticsearch.query-methods.criterions这里也截一点给大家看看谷歌浏览器翻译的image-20221016142312365image-20221016142406280也可以使用Query注解写原生的 api 请求接口不太优雅个人不推荐使用。然后这里我只添加了一个方法SearchPageBlogDoc findByDescriptiveContent(String descriptiveContent, Pageable pageable);这个意思就是所通过DescriptiveContent属性来查询数据后面的两个参数一个是搜索的内容一个是分页的参数分页需要配合支持分页的返回值才行。这个findByXxx的Xxx属性必须是实体类里面存在的属性才可以不然会提示错误image-20221016142649418高亮显示SuppressWarnings(SpringDataRepositoryMethodReturnTypeInspection) Highlight(fields { HighlightField(name title, parameters HighlightParameters(requireFieldMatch false)), HighlightField(name description, parameters HighlightParameters(requireFieldMatch false)), })使用注解Highlight和HighlightField来设置高亮的字段使用HighlightParameters来添加高亮的参数。我这里设置了requireFieldMatch false这个参数是取消只有字段匹配才给高亮的规则这是因为我搜索的字段是由另外两个字符copyTo而来的高亮的内容肯定是在另外两个字段里面设置该参数可以让其他字段的高亮也展示出来。这里还有一篇高亮显示的教程文章我讲的比较粗糙他这个写的比较详细贴给大家学习https://blog.csdn.net/qq_45794678/article/details/111188548官方文档给的说明就这么点。。。怕我学会了然后教别人吗。。。image-20221016142838476分页功能通过Pageable做参数和SearchPage做返回值来完成了对分页的需求传参的时候使用PageRequest.of(page, size)来创建分页参数即可。得到结果后仅需将分页的内容替换掉实体类的内容即可并且数据里面包含有获取页码的信息的接口image-20221016143118800Service层核心代码如下Service public class BlogContentServiceImpl implements BlogContentService { Resource private BlogRepository blogRepository; /** * 搜索博客 * * param key 搜索内容 * param page 页码 * param size 页大小 * return 搜索到的结果列表 */ Override public ListBlogDoc searchBlog(String key, int page, int size) { // 1. 获取数据 SearchPageBlogDoc searchPage blogRepository.findByDescriptiveContent( // 1.1 设置key和分页这里是从第0页开始的所以要-1 key,PageRequest.of(page - 1, size)); // 2. 高亮数据替换 ListSearchHitBlogDoc searchHitList searchPage.getContent(); ArrayListBlogDoc blogDocList new ArrayList(searchHitList.size()); for (SearchHitBlogDoc blogHit : searchHitList) { // 2.1 获取博客数据 BlogDoc blogDoc blogHit.getContent(); // 2.2 获取高亮数据 MapString, ListString fields blogHit.getHighlightFields(); if (fields.size() 0) { // 2.3 通过反射将高亮数据替换到原来的博客数据中 BeanMap beanMap BeanMap.create(blogDoc); for (String name : fields.keySet()) { beanMap.put(name, fields.get(name).get(0)); } } // 2.4 博客数据插入列表 blogDocList.add(blogDoc); } return blogDocList; } }替换高亮数据到这里其实就只要做一件事了因为Repository返回的数据已经帮你封装好实体类了不需要再去json转bean了它唯一的缺点就是高亮数据还是得自己去做替换所以我上面这些代码也就是做了这一件事就是把高亮的数据替换掉原来的数据。这里我用到了BeanMap代码里不用写死属性名称相对来说更优雅一点如果有需要的话也可以把中间这一段分离成一个单独的方法可以提供给不同的类使用。数据同步数据同步指的是elasticsearch和MySQL的数据同步由于我的项目做的是微服务架构我的博客服务和博客内容服务是两个微服务本文讲的是博客内容服务博客服务提供文章的增删改查功能并连接MySQL博客内容服务提供搜索功能并连接ES故两者的数据需要同步。这里我使用的是RabbitMQ主要逻辑如下用户新建修改或删除博客时博客服务发送消息到MQ中发到自己的交换机里并指定key。内容服务提前创建队列并绑定到博客服务的交换机中。当内容服务接收到消息时做出对应的操作。核心代码如下/** * 内容服务对博客服务的消息队列监听器 * * author 阿杆 * version 1.0 * date 2022/7/10 9:32 */ Slf4j Component public class BlogServerListener { Resource private BlogRepository blogRepository; RabbitListener(bindings QueueBinding( exchange Exchange(name BLOG_EXCHANGE), value Queue(name BLOG_SAVE_QUEUE), key {BLOG_INSERT_KEY, BLOG_UPDATE_KEY} )) public void saveListener(BlogDoc blogDoc) { log.debug(save blogDoc{}, blogDoc); blogRepository.save(blogDoc); } RabbitListener(bindings QueueBinding( exchange Exchange(name BLOG_EXCHANGE), value Queue(name BLOG_DELETE_QUEUE), key BLOG_DELETE_KEY )) public void deleteListener(Long blogId) { log.debug(delete blog ,id-{}, blogId); blogRepository.deleteById(blogId); } }其实可以看出通过Repository来实现这些操作都是很简单的。需要注意的是这里的save操作是ES的全量更新所以发送过来的数据一定要是完整的数据否则会导致部分字段丢失。然后发送消息的大概就是代码是rabbitTemplate.convertAndSend(BLOG_EXCHANGE, BLOG_UPDATE_KEY, blog);MQ序列化配置这里RabbitMQ的序列化配置我也贴一下这个可以让MQ消息变成json格式的。package cn.sticki.common.amqp.autoconfig; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * author 阿杆 * version 1.0 * date 2022/6/25 18:01 */ Configuration public class AmqpMessageConverterConfig { Bean public MessageConverter messageConverter() { return new Jackson2JsonMessageConverter(); } }后记本篇文章主要使用了ElasticsearchRepository和相关注解来完成了一些常有的需求比较优雅个人认为的实现了查询分页和高亮的功能网上找到的教程都没有把分页和高亮一起适配的。但如果有更为复杂的需求可能还是需要使用ElasticsearchRestTemplate来完成。