哈哈最近终于用ElasticSearch+Logstash把社区的文章高亮搜索功能实现啦(●'◡'●)!开森噢
不过,这一路上真的踩了好多坑啊/(ㄒoㄒ)/~~(虽然踩坑才是进步最快的办法哈哈。)
我们先来看一下实现效果(gif图好像有点模糊欸,不过看起来效果还凑合)。
![326399f80b77f389ca2c4b4b31f90099.gif](https://i-blog.csdnimg.cn/blog_migrate/6ba164fedb00aa5c4d2bcd15323b99c9.gif)
从图中可以看到,我们通过关键词去搜索文章,文章中的标题和内容相应的关键词都会进行高亮显示。
那么,话不多说,我们直接看看这个效果究竟是怎么完成的吧(●'◡'●)。
前序准备
我们需要安装ElasticSearch(包括ik分词器插件) + Logstash + ElasticSearch-Header(这个主要是为了方便查看ES的数据)
(安装过程这里就不赘述啦,网上随便搜下就行喔,不过具体细节我还是会挑出来滴)
本文使用的ES和Logstash都是7.6.2版本的(主要配合SpringData-ES使用(最新版的SpringData-ES支持 ES 7.6.2))
引入相关依赖
后端项目使用的是 SpringBoot(2.3.0) ,需要导入一些核心的依赖
······其他必须依赖 org.springframework.data spring-data-elasticsearch
文章实体类
这个文章实体类就是我们搜索出来的具体数据喔。
实体类的字段如下(搜索主要用到的是 title和detail和createdTime)
- id 序号
- title 标题
- detail 内容
- createdTime 创建时间
- updatedTime 最近一次更新时间
- ……比如作者ID、浏览量、点赞量、逻辑删除字段等等
(其中,id、createdTime、isDeleted都在继承的BaseEntity里面)
这里先介绍下实体类代码中用到的SpringData-ES的注解:
@Document(indexName就是我们创建索引的名字,type已经不需要写了)
@Id 标记主键(放在id上面)
@Field(type就是这个字段的类型,analyzer和searchAnalyzer是分词规则,format是时间格式)
因为搜索关键词需要从title和detail进行搜索,所以type就写成text,这样可以进行分词。
关于analyzer和searchAnalyzer,我在官方文档中看到是这样解释的。
![b58fd1cb4e72d372ac530f5f3c530473.png](https://i-blog.csdnimg.cn/blog_migrate/bdf40ebfa512ab50b50ec4aa3224e7ae.jpeg)
所以,我猜测这两个东西都是指向同一个东西,这里就姑且都写上叭(任性哈哈(●'◡'●)
这里重点需要说下字段updatedTime和createdTime
因为mysql数据同步到ES时,时间数据的格式是类似yyyy-MM-dd'T'HH:mm:ss.SSSZ。
所以,我们在@Field需要声明下时间格式
@Field(type = FieldType.Date, format = DateFormat.date_optional_time)
![e2de196d2156d17e3fc7d3de35c6402a.png](https://i-blog.csdnimg.cn/blog_migrate/29eb21730126a1874af2cfd48b742328.jpeg)
同时使用JsonFormat注解,可以使前端调用接口获取的时间数据变成我们想要的2020-06-10 08:08:08这样的格式,同时声明下时区即可。
@JsonFormat(shape=JsonFormat.Shape.STRING,pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
实体类代码:
@Lombok的注解......@Document(indexName = "article",type = "_doc")public class Article extends BaseEntity { // 使用ik分词器,采用最大程度分词 @Field(type = FieldType.Text, analyzer = "ik_max_word" ,searchAnalyzer="ik_max_word") private String title; @Field(type = FieldType.Text,analyzer = "ik_max_word" ,searchAnalyzer="ik_max_word") private String detail; // 作者id // 点赞量、浏览量等等 /** * 修改时间 */ @JsonFormat(shape=JsonFormat.Shape.STRING,pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8") @Field(type = FieldType.Date, format = DateFormat.date_optional_time) public Date updatedTime; // id、createdTime、isDeleted在继承的BaseEntity里面}
创建索引及映射
先开启ElasticSearch
然后在测试类引入ElasticsearchRestTemplate,后续我们就使用这个类进行ES的高亮查询。
ElasticsearchRestTemplate是spring-data-elasticsearch项目中的一个类,和其他spring项目中的template类似。基于RestHighLevelClient,如果不手动配置RestHighLevelClient,ip+端口就默认为localhost:9200
![23155d6b2ee35cf0c38273cccd326336.png](https://i-blog.csdnimg.cn/blog_migrate/51d249946fde21607d27b370b548998b.jpeg)
@AutowiredElasticsearchRestTemplate ESRestTemplate;
写一个测试方法,运行下面这两行代码即可。
// 根据我们Article中的注解,创建对应的index// 根据我们Article中的注解,创建对应的mappingESRestTemplate.indexOps(Article.class);// 如果是删除index的话// ESRestTemplate.indexOps(Article.class).delete();即可
然后打开我们的ElasticSearch-Header,我们可以看到对应的索引及映射以及创建完毕啦~
![a196af10d58d02313c75443ca2b70675.png](https://i-blog.csdnimg.cn/blog_migrate/8ca11785050ceb2587cfaa5ed6c53ec4.jpeg)
同时,我们可以看一下具体的映射是否是我们注解中写的那样呢?
![1cf5e3d02eb1483a73a5eea1eddf1415.png](https://i-blog.csdnimg.cn/blog_migrate/dbb0fc742b36c48ed77d9fd2ce27daa6.jpeg)
哈哈,发现完全一样。
OK,No problems (●'◡'●) ,Let's go next !
接下来就到我们很关键的使用Logstash同步啦!
使用 `Logstash` 同步Mysql数据
把索引和映射设置完毕后,我们接下来需要将Mysql的数据同步到ElasticSearch中
(呜呜说实话,就是因为Logstash同步这里出现了很多问题,导致我这块卡了很久,真的有点小难受qaq)
我们需要使用Logstash的插件logstash-input-jdbc完成数据同步。
(Tips:我在网上看到有说法:logstash7.x版本本身不带logstash-input-jdbc插件,需要手动安装,但是我好像直接运行就可以0.0…..)
首先我们打开Logstash的bin目录,然后写一个配置文件Mysql.conf(建议直接就写在bin目录下,这样方便启动)
![8fce36ac1c88b1d2506c74a3494b2002.png](https://i-blog.csdnimg.cn/blog_migrate/b4e9980c7c8da73584e711f004e850d1.jpeg)
(这个配置文件非常关键,它是用来同来同步数据的)
基本配置的意思我都用注解写出来了。
需要自己修改的地方我在注解开头写了*DIY
数据同步的规则就是我们自己规定的sql语句,我这里使用了updated_time作为同步的判断依据,只同步在 最后一次同步的记录值
input { jdbc { # *DIY mysql连接驱动地址,这个随意,填写正确就行 jdbc_driver_library => "C:甥敳獲MasicsDesktoplogstash-7.6.2libmysql-connector-java-8.0.19.jar" # *DIY 驱动类名 jdbc_driver_class => "com.mysql.cj.jdbc.Driver" # *DIY 8.0以上版本:一定要把serverTimezone=UTC天加上 jdbc_connection_string => "jdbc:mysql://localhost:3306/lemonc?useSSL=false&&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&useUnicode=true" # *DIY 用户名和密码 jdbc_user => "root" jdbc_password => "123456" # *DIY 设置监听间隔 各字段含义(由左至右)分、时、天、月、年,全部为*默认含义为每分钟都更新 schedule => "* * * * *" # *DIY sql执行语句(记住查出来的字段大小写需要和映射里面的一致!!!) # 因为ES采用 UTC 时区,比北京时间早了8小时,所以ES读取数据时需要让最后更新时间 +8小时 statement => "SELECT id ,title,detail,created_time as createdTime,updated_time as updatedTime FROM article where updated_time > date_add(:sql_last_value,INTERVAL 8 HOUR) AND updated_time "_doc" # 字段名是否小写(如果为true的话,那么createdTime就会变成createdtime,就会报错) lowercase_column_names => false #是否记录最后一次运行内容 record_last_run => true # 是否使用列元素 use_column_value => true # 追踪的元素名,对应保存到es上面的字段名而不是数据库字段名 tracking_column => "updatedTime" # 默认为number,如果为日期必须声明为timestamp tracking_column_type => "timestamp" # *DIY设置记录的路径 last_run_metadata_path => "C:甥敳獲MasicsDesktoplogstash-7.6.2configlast_metadata" # 每次运行是否清除上次的同步点 clean_run => "false" }}output { elasticsearch { # *DIY ES的IP地址及端口 hosts => ["localhost:9200"] # *DIY 索引名称 index => "article" # 需要关联的数据库中有有一个id字段,对应类型中的id document_id => "%{id}" # 索引类型 document_type => "_doc" } stdout { codec => rubydebug }}
接下来我们就可以运行Logstash进行同步数据啦
在bin目录下打开命令行输入logstash -f yourconfig,就可以运行了。
但是呢,因为我是windows系统,我如果直接使用命令行就会出现下图这样的状况(很是迷惑Orz,我Java环境明明都没问题的说)
![79dee355ecbc2cc801e290231aa1443d.png](https://i-blog.csdnimg.cn/blog_migrate/52917ea5816f6e804360cf0380394853.jpeg)
于是,我查了好久,一度还直接用我的Linux服务器进行测试- -
后来我发现了另外一种正确的打开方式~
使用git的Git Bash Here
![4fc23dafcaa77e79f503de108ebcddd7.png](https://i-blog.csdnimg.cn/blog_migrate/06fb129f9078c1723a2ec6c828eea3a7.jpeg)
然后输入下图的命令
![f4d267802e308d2f8a723ba3b1f9b19a.png](https://i-blog.csdnimg.cn/blog_migrate/bbaabe3edbb298201e9defa75f278cab.jpeg)
然后我们可以看到下图中的sql语句,说明它正在进行数据同步
![ea4fca1ce65339ef8426ba10afbb427f.png](https://i-blog.csdnimg.cn/blog_migrate/fe4ba83e077f1357ed0fcaa396c67646.jpeg)
我们打开Header看看数据是否发生了变化呢?
![f80127875e273d36030e88e36e65d33e.png](https://i-blog.csdnimg.cn/blog_migrate/d7eaee4115bf1befcdde1952fb7234de.jpeg)
当当当!!!我们发现数据已经变成20条啦(第一次同步是全量更新,后续就是增量更新啦(●'◡'●))
为了验证后续都是增量更新,我就直接随便新写一篇文章,让大家看看效果OwO
![c01aeb8733f91a4bc3f6378a743577e0.png](https://i-blog.csdnimg.cn/blog_migrate/69d143db331f531c1458f52fb07dc7ab.jpeg)
因为我们刚刚配置文件设置了同步时间是每一分钟同步一次,所以我们稍等会嘿嘿
(One minute later······)
![dafb3b07fbdd61d654ad50a8635fe0b8.png](https://i-blog.csdnimg.cn/blog_migrate/c7ed2566daf0e26ecf5064933922d1c1.jpeg)
哈哈,我们发现sql语句中最后记录时间已经是我们第一次全量同步的时间喔(不是创建这篇文章的时间!)
又过了一分钟,我们发现再次同步的话,最后一次记录时间,就是上一次同步时间(也就是刚刚创建文章的时间),但是因为没有新数据,所以就没有进行数据同步)
![d3800b1d363ea7b8d02e0b0fb244d10f.png](https://i-blog.csdnimg.cn/blog_migrate/f26c3891d08b4c7d42c33a7c6e2815b1.jpeg)
至此,我们已经完成数据同步啦(包括全量和增量(●'◡'●))
不过这里有个小小的遗憾喔,就是使用Logstash进行同步的话,删除是没办法同步的,所以如果涉及到删除操作,需要自己手动进行删除一下喔。
实现高亮搜索
完成数据同步,接下来就要实现本篇文章的核心功能——高亮搜索啦
其实,这个功能我一开始使用SpringDataES 3.2完成的,但是我写文章查阅资料的时候发现,官网居然升级到4.0了……于是呜呜发现好多API都换了,就自己啃文档用4.0版本实现了下。
Controller
这里解释下前端需要传递的参数:
- curPage—— 当前页数,默认第一页
- size—— 每页数据量,默认每页⑦条
- type——查询的时间范围(我自己定义的是 -1表示全部,1表示一天内,7表示一周内,90表示三个月内)
- keyword—— 搜索的关键字
/** * 搜索文章 */@GetMapping("/search")public MyJsonResult searchArticles( @RequestParam(value = "curPage", defaultValue = "1") int curPage, @RequestParam(value = "size", defaultValue = "7") int size, @RequestParam(value = "type",defaultValue = "-1") int type, @RequestParam(value = "keyword") String keyword) { List articles = articleService.searchMulWithHighLight(keyword,type, curPage, size); return MyJsonResult.success(articles);}
Service
我们在Service层进行业务的操作。
首先根据前端传递过来的参数,我们需要完成 分页、时间范围、关键词高亮、关键词搜索
哈哈不过别担心!这些功能ElasticSearch全都有!!!
我这边就全部罗列在一个方法啦,感觉这样看起来会舒服点。如果需要封装下的话,也可以自己动手喔,基本注释写得很全啦
public List searchMulWithHighLight(String keyword, int type, int curPage, int pageSize) { // 高亮颜色设置(高亮其实就是用含有color的span标签把keyword包裹住) String preTags = ""; String postTags = ""; // 时间范围 // ES中对时间处理很方便 // now就是指当前时间 // now-1d/d 就是前一天的00:00:00 String from; String to = "now"; switch (type) { case 1: from = "now-1d/d"; break; case 7: from = "now-7d/d"; break; case 90: from = "now-90d/d"; break; default: from = "2020-01-01"; break; } // 构建查询条件(这些API都可以在官网找到喔,这里就不赘述了,链接:) // 1. 在title和detail查找相关的关键字 // 2. 时间范围查找 // 3. 分页查找 // 4. 高亮,设置高亮字段title和detail NativeSearchQuery searchQuery = new NativeSearchQueryBuilder() .withQuery(QueryBuilders.boolQuery()// ES的bool查询 // must就相当于我们mysql的and .must(QueryBuilders.multiMatchQuery(keyword, "title", "detail")) // 在title和detail里面查找关键词 .must(QueryBuilders.rangeQuery("createdTime").from(from).to(to))) // 根据创建时间,进行范围查询 .withHighlightBuilder(new HighlightBuilder().field("title").field("detail").preTags(preTags).postTags(postTags)) // 高亮 .withPageable(PageRequest.of(curPage - 1, pageSize)) // 设置分页参数,默认从0开始 .build(); // 执行搜索,获取结果 // SearchHits是SpringDataES 4.0版本新增加的类,里面除了包含高亮信息,还包含了其他信息比如score等等 // 4.0之前想要实现高亮需要自己手动写一个实体映射类,需要用到反射去实现,看起来4.0这方面方便了不少。 SearchHits contents = ESRestTemplate.search(searchQuery, Article.class); List articles = contents.getSearchHits(); // 如果list的长度为0,直接return if (articles.size() == 0) { return new ArrayList<>(); } // 完成真正的映射,拿到展示的文章数据。 List result = articles.stream().map(article -> { // 获取高亮数据 Map> highlightFields = article.getHighlightFields(); //如果集合不为空,说明包含高亮字段,则进行设置 // 这里比较迷的是,高亮的结果集居然是一个List,可能官方觉得没有必要全部变成一坨? // 不过正常想也是,我们不需要把整个文章的detail发给前端,只需要发一小部分就可以了,毕竟我们只需要部分高亮就行,这样也可以减少服务器的负担(嗯,说服自己了哈哈) // article.getContent()这个API就是返回查询到的article实体类 if (!CollectionUtils.isEmpty(highlightFields.get("title"))) { article.getContent().setTitle(highlightFields.get("title").get(0)); } if (!CollectionUtils.isEmpty(highlightFields.get("detail"))) { article.getContent().setDetail(highlightFields.get("detail").get(0)); } // 业务逻辑操作 // ······ // 最后完成数据封装 return articleDTO; }).collect(Collectors.toList()); return result;}
到这里我们就把后端接口实现啦!!!
接下来就到了令人激动的测试环节嘿嘿(●'◡'●)(应该不会翻车吧ヽ(*。>Д
接口测试
我们直接使用IDEA进行测试,输入keyword为java
![4550bd5b6e023b5fa07ccc00c76e2268.png](https://i-blog.csdnimg.cn/blog_migrate/8940dd7a1f08d10da0c0bc1a056927aa.jpeg)
结果,我们可以看到,在title和detail中java这个关键字已经被span包裹起来了。这样子,前端拿到数据就可以正常高亮展示啦!!
作者:柠檬味的咸鱼
链接:https://juejin.im/post/5edf13d16fb9a04797068bc7