继续创造,加速生长!这是我参加「日新方案 10 月更文应战」的第17天,点击检查活动概况

系列阐明

本系列文章基于我的开源微服务项目【校园博客】进行剖析和解说,所有源码均可在GitHub仓库上找到。 系列文章地址请见我的 校园博客专栏。

  • GitHub地址:github.com/stick-i/scb…

现在项目还有很大改进和完善的空间,欢迎各位有志愿的同学参加项目奉献(尤其前端),一同学习一同前进。

项目的技能栈首要是:
后端 Java + SpringBoot + SpringCloud + Nacos + Getaway + Fegin + MybatisPlus + MySQL + Redis + ES + RabbitMQ + Minio + 七牛云OSS + Jenkins + Docker
前端 Vue + ElementUI + Axios(说实话前端我不太清楚)

前言

本篇文章首要是一些对Spring-Data-Elasticsearch运用上的记载和解说,对原理和根底知识并没有介绍,合适有必定ES根底的朋友阅览。

为了给项目增加一个好的查找功用,我去学习了一下elasticsearch。

在学习elasticsearch-client的期间,发现它供给的api不太高雅,用起来也不太舒服,而且我觉得有些操作完全是能够封装在内部的,比方获取数据后,对数据转化为bean的操作;还有特点高亮,不仅设置比较麻烦,而且设置完结的高亮居然是独自在一个字段里的,需求开发者去手动的替换才行,这些操作我觉得其实都能够封装在内部的,害,个人慨叹,请勿介意。

然后我就去看了一下spring-data里边供给的 es 操作库,发现有许多操作都封装的比较完善,运用起来也比较高雅,于是我便运用spring-data-elasticsearch完结了这个功用,查阅了许多资料、博客、官方文档,有些地方我觉得官方文档讲的也不行具体,导致走了许多弯路,也可能是我没有找到具体的文档。

为了便利咱们学习和少走弯路,也便于自己日后回顾,故记载于此。

本篇文章讲的内容是在项目的 /blog-service/blog-content-server 途径下,感兴趣的同学欢迎随时检查,觉得不错的话也欢迎点点star噢。

技能要点

  1. 运用 copyToElasticsearchRepository 完结的多字段查找。
  2. 运用注解 @Highlight@HighlightField 完结的高亮显示。
  3. 运用 PageableSearchPage 完成分页和高亮两不误的接口。
  4. 运用 RabbitMQ 完结 MySQLelasticsearch 的数据同步。

依靠项

我当时的环境:

  • springboot 2.6.6

  • elasticsearch 7.12

  • kibana 7.12(这个不是有必要的)

  • 然后当时版其他spring默认是用的 7.15.2 的我担心和我的es不兼容,就加了个标签给它改了一下版别:

    <elasticsearch.version>7.12.1</elasticsearch.version>
    

中心依靠其实就这一个,这儿边现已依靠了elasticsearch需求的一些依靠,例如 elasticsearch-rest-high=level-client

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
		</dependency>

然后假如跟我相同运用 RabbitMQ 做数据同步的话,还需求引证mq的依靠:

		<!--AMQP依靠,包括RabbitMQ-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-amqp</artifactId>
		</dependency>
		<!-- json序列化依靠,需求手动配置bean -->
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-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_timeepoch_millis。也便是只能运用给定的枚举值,不能自界说,自界说的话得用pattern。下图是谷歌翻译的官方解说:

      ✨基于Spring-Data-Elasticsearch 优雅的实现 多字段搜索 + 高亮 + 分页 + 数据同步✨

实体类特点copy_to

咱们都知道,在es傍边假如有多个字段需求被一同查询(比方我的博客业务,要查找内容的时分,我会把用户输入的关键字一同拿来匹配标题和文章描绘),那能够用multi_matchquery_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 ElasticsearchRepository<BlogDoc, Long> {
	/**
	 * 经过描绘内容来查找博客
	 *
	 * @param descriptiveContent 描绘句子
	 * @param pageable           分页
	 * @return 博客列表
	 */
	@SuppressWarnings("SpringDataRepositoryMethodReturnTypeInspection")
	@Highlight(fields = {
			@HighlightField(name = "title", parameters = @HighlightParameters(requireFieldMatch = false)),
			@HighlightField(name = "description", parameters = @HighlightParameters(requireFieldMatch = false)),
	})
	SearchPage<BlogDoc> findByDescriptiveContent(String descriptiveContent, Pageable pageable);
}

继承ElasticsearchRepository

  1. 这个其实就有点像继承BaseMapper,它会给你供给一些根底的CRUD办法,便利你直接运用,比方save、delete、find之类的。

    ✨基于Spring-Data-Elasticsearch 优雅的实现 多字段搜索 + 高亮 + 分页 + 数据同步✨

  2. 它是个泛型类,两个参数分别是 <实体类,id的类型>

  3. 在该接口下(BlogRepository)依照特别的命名规矩声明的办法,能够直接调用,不需求开发者完成接口,且它回来的内容是现已封装好的,你需求的数据会被封装在你供给的实体类里边(不必手动解析数据)。

    大约便是 findByXxxAndXxxOrXxx() 这个类型,具体的能够参考官网:docs.spring.io/spring-data…,这儿也截一点给咱们看看(谷歌浏览器翻译的):

    ✨基于Spring-Data-Elasticsearch 优雅的实现 多字段搜索 + 高亮 + 分页 + 数据同步✨

    ✨基于Spring-Data-Elasticsearch 优雅的实现 多字段搜索 + 高亮 + 分页 + 数据同步✨

  4. 也能够运用 @Query 注解写原生的 api 恳求接口,不太高雅,个人不引荐运用。

然后这儿我只增加了一个办法:

SearchPage<BlogDoc> findByDescriptiveContent(String descriptiveContent, Pageable pageable);

这个意思便是所经过 DescriptiveContent 特点来查询数据,后面的两个参数一个是查找的内容,一个是分页的参数(分页需求合作支持分页的回来值才行)。

这个findByXxx的Xxx特点有必要是实体类里边存在的特点才能够,否则会提示错误:

✨基于Spring-Data-Elasticsearch 优雅的实现 多字段搜索 + 高亮 + 分页 + 数据同步✨

高亮显示

	@SuppressWarnings("SpringDataRepositoryMethodReturnTypeInspection")
	@Highlight(fields = {
			@HighlightField(name = "title", parameters = @HighlightParameters(requireFieldMatch = false)),
			@HighlightField(name = "description", parameters = @HighlightParameters(requireFieldMatch = false)),
	})

运用注解 @Highlight@HighlightField,来设置高亮的字段,运用 @HighlightParameters 来增加高亮的参数。

我这儿设置了requireFieldMatch = false,这个参数是撤销只有字段匹配才给高亮的规矩,这是由于我查找的字段是由别的两个字符copyTo而来的,高亮的内容肯定是在别的两个字段里边,设置该参数能够让其他字段的高亮也展现出来。

这儿还有一篇高亮显示的教程文章,我讲的比较粗糙,他这个写的比较具体,贴给咱们学习:blog.csdn.net/qq_45794678…

官方文档给的阐明就这么点。。。怕我学会了然后教别人吗。。。

✨基于Spring-Data-Elasticsearch 优雅的实现 多字段搜索 + 高亮 + 分页 + 数据同步✨

分页功用

经过 Pageable 做参数和 SearchPage 做回来值来完结了对分页的需求,传参的时分运用 PageRequest.of(page, size) 来创立分页参数即可。

得到成果后仅需将分页的内容替换掉实体类的内容即可,而且数据里边包括有获取页码的信息的接口:

✨基于Spring-Data-Elasticsearch 优雅的实现 多字段搜索 + 高亮 + 分页 + 数据同步✨

Service层

中心代码如下:

@Service
public class BlogContentServiceImpl implements BlogContentService {
	@Resource
	private BlogRepository blogRepository;
	/**
	 * 查找博客
	 *
	 * @param key 查找内容
	 * @param page 页码
	 * @param size 页大小
	 * @return 查找到的成果列表
	 */
	@Override
	public List<BlogDoc> searchBlog(String key, int page, int size) {
		// 1. 获取数据
		SearchPage<BlogDoc> searchPage = blogRepository.findByDescriptiveContent(
				// 1.1 设置key和分页,这儿是从第0页开端的,所以要-1
				key,PageRequest.of(page - 1, size));
		// 2. 高亮数据替换
		List<SearchHit<BlogDoc>> searchHitList = searchPage.getContent();
		ArrayList<BlogDoc> blogDocList = new ArrayList<>(searchHitList.size());
		for (SearchHit<BlogDoc> blogHit : searchHitList) {
			// 2.1 获取博客数据
			BlogDoc blogDoc = blogHit.getContent();
			// 2.2 获取高亮数据
			Map<String, List<String>> 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,代码里不必写死特点称号,相对来说更高雅一点,假如有需求的话,也能够把中间这一段别离成一个独自的办法,能够供给给不同的类运用。

数据同步

数据同步指的是 elasticsearchMySQL 的数据同步,由于我的项目做的是微服务架构,我的博客服务博客内容服务是两个微服务(本文讲的是博客内容服务),博客服务供给文章的增修正查功用,并衔接MySQL,博客内容服务供给查找功用,并衔接ES,故两者的数据需求同步。

这儿我运用的是RabbitMQ,首要逻辑如下:

  1. 用户新建修正或删除博客时,博客服务发送音讯到MQ中,发到自己的交换机里,并指定key。
  2. 内容服务提前创立行列并绑定到博客服务的交换机中。
  3. 当内容服务接收到音讯时,做出对应的操作。

中心代码如下:

/**
 * 内容服务对博客服务的音讯行列监听器
 *
 * @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来完结。 官网:docs.spring.io/spring-data…