【仿掘金系列】使用ES+Logstash实现文章高亮搜索及Mysql数据同步

本文社区项目源码:前端 | 后端php

柠檬C社区项目是参考掘金社区设计和开发的,若是以为不错,欢迎Star支持,感谢。html

最近有空就会把实现的功能进行Review,欢迎关注后续文章~前端

哈哈最近终于用ElasticSearch+Logstash把社区的文章高亮搜索功能实现啦(●'◡'●)!开森噢vue

不过,这一路上真的踩了好多坑啊/(ㄒoㄒ)/~~(虽然踩坑才是进步最快的办法哈哈。)java

咱们先来看一下实现效果(gif图好像有点模糊欸,不过看起来效果还凑合)。mysql

从图中能够看到,咱们经过关键词去搜索文章,文章中的标题和内容相应的关键词都会进行高亮显示git

那么,话很少说,咱们直接看看这个效果到底是怎么完成的叭(●'◡'●)。github


0. 前序准备

咱们须要安装ElasticSearch(包括ik分词器插件) + Logstash + ElasticSearch-Header(这个主要是为了方便查看ES的数据)web

(安装过程这里就不赘述啦,网上随便搜下就行喔,不过具体细节我仍是会挑出来滴)spring

本文使用的ES和Logstash都是7.6.2版本的(主要配合SpringData-ES使用(最新版的SpringData-ES支持 ES 7.6.2)


1. 后端

1.1 引入相关依赖

后端项目使用的是 SpringBoot(2.3.0) ,须要导入一些核心的依赖

<dependencies>
    ······其余必须依赖
    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-elasticsearch</artifactId>
    </dependency>
</dependencies>
复制代码


1.2 文章实体类

这个文章实体类就是咱们搜索出来的具体数据喔。

实体类的字段以下(搜索主要用到的是 titledetailcreatedTime

  • id 序号
  • title 标题
  • detail 内容
  • createdTime 建立时间
  • updatedTime 最近一次更新时间
  • ……好比做者ID、浏览量、点赞量、逻辑删除字段等等

(其中,idcreatedTimeisDeleted都在继承的BaseEntity里面)



这里先介绍下实体类代码中用到的SpringData-ES的注解:

@Document(indexName就是咱们建立索引的名字,type已经不须要写了)

@Id 标记主键(放在id上面)

@Field(type就是这个字段的类型,analyzersearchAnalyzer是分词规则,format是时间格式)


由于搜索关键词须要从titledetail进行搜索,因此type就写成text,这样能够进行分词。

关于analyzersearchAnalyzer,我在官方文档中看到是这样解释的。

因此,我猜想这两个东西都是指向同一个东西,这里就姑且都写上叭(任性哈哈(●'◡'●)


这里重点须要说下字段updatedTimecreatedTime

由于mysql数据同步到ES时,时间数据的格式是相似yyyy-MM-dd'T'HH:mm:ss.SSSZ

因此,咱们在@Field须要声明下时间格式

@Field(type = FieldType.Date, format = DateFormat.date_optional_time)

同时使用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里面
}
复制代码


1.3 建立索引及映射

先开启ElasticSearch

而后在测试类引入ElasticsearchRestTemplate,后续咱们就使用这个类进行ES的高亮查询。

ElasticsearchRestTemplatespring-data-elasticsearch项目中的一个类,和其余spring项目中的template相似。
基于RestHighLevelClient,若是不手动配置RestHighLevelClient,ip+端口就默认为localhost:9200

@Autowired
ElasticsearchRestTemplate ESRestTemplate;
复制代码

写一个测试方法,运行下面这两行代码便可。

// 根据咱们Article中的注解,建立对应的index
// 根据咱们Article中的注解,建立对应的mapping
ESRestTemplate.indexOps(Article.class);
// 若是是删除index的话
// ESRestTemplate.indexOps(Article.class).delete();便可
复制代码


而后打开咱们的ElasticSearch-Header,咱们能够看到对应的索引及映射以及建立完毕啦~

同时,咱们能够看一下具体的映射是不是咱们注解中写的那样呢?

哈哈,发现彻底同样。

OK,No problems (●'◡'●) ,Let's go next !

接下来就到咱们很关键的使用Logstash同步啦!


1.4 使用 `Logstash` 同步Mysql数据

把索引和映射设置完毕后,咱们接下来须要将Mysql的数据同步到ElasticSearch

(呜呜说实话,就是由于Logstash同步这里出现了不少问题,致使我这块卡了好久,真的有点小难受qaq)


咱们须要使用Logstash的插件logstash-input-jdbc完成数据同步。

(Tips:我在网上看到有说法:logstash7.x版本自己不带logstash-input-jdbc插件,须要手动安装,可是我好像直接运行就能够0.0…..)

首先咱们打开Logstashbin目录,而后写一个配置文件Mysql.conf(建议直接就写在bin目录下,这样方便启动)

(这个配置文件很是关键,它是用来同来同步数据的)

基本配置的意思我都用注解写出来了。

须要本身修改的地方我在注解开头写了*DIY

数据同步的规则就是咱们本身规定的sql语句,我这里使用了updated_time做为同步的判断依据,只同步在 最后一次同步的记录值 <updated_time< 如今时间 这个范围的数据。

input {
  jdbc {
    # *DIY mysql链接驱动地址,这个随意,填写正确就行  
    jdbc_driver_library => "C:\Users\Masics\Desktop\logstash-7.6.2\lib\mysql-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 < NOW()"


    # 索引类型
    type => "_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:\Users\Masics\Desktop\logstash-7.6.2\config\last_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环境明明都没问题的说)

因而,我查了很久,一度还直接用个人Linux服务器进行测试- -

后来我发现了另一种正确的打开方式~

使用gitGit Bash Here

而后输入下图的命令

而后咱们能够看到下图中的sql语句,说明它正在进行数据同步

咱们打开Header看看数据是否发生了变化呢?

当当当!!!咱们发现数据已经变成20条啦(第一次同步是全量更新,后续就是增量更新啦(●'◡'●))

为了验证后续都是增量更新,我就直接随便新写一篇文章,让你们看看效果OwO

由于咱们刚刚配置文件设置了同步时间是每一分钟同步一次,因此咱们稍等会嘿嘿

(One minute later······)

哈哈,咱们发现sql语句中最后记录时间已是咱们第一次全量同步的时间喔(不是建立这篇文章的时间!)

又过了一分钟,咱们发现再次同步的话,最后一次记录时间,就是上一次同步时间(也就是刚刚建立文章的时间),可是由于没有新数据,因此就没有进行数据同步)

至此,咱们已经完成数据同步啦(包括全量和增量(●'◡'●))

不过这里有个小小的遗憾喔,就是使用Logstash进行同步的话,删除是没办法同步的,因此若是涉及到删除操做,须要本身手动进行删除一下喔。


1.5 实现高亮搜索

完成数据同步,接下来就要实现本篇文章的核心功能——高亮搜索

其实,这个功能我一开始使用SpringDataES 3.2完成的,可是我写文章查阅资料的时候发现,官网竟然升级到4.0了……因而呜呜发现好多API都换了,就本身啃文档用4.0版本实现了下。

1.5.1 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<Article> articles = articleService.searchMulWithHighLight(keyword,type, curPage, size);

    return MyJsonResult.success(articles);
}
复制代码

1.5.2 Service

咱们在Service层进行业务的操做。

首先根据前端传递过来的参数,咱们须要完成 分页时间范围关键词高亮关键词搜索

哈哈不过别担忧!这些功能ElasticSearch全都有!!!

我这边就所有罗列在一个方法啦,感受这样看起来会舒服点。若是须要封装下的话,也能够本身动手喔,基本注释写得很全啦

public List<Article> searchMulWithHighLight(String keyword, int type, int curPage, int pageSize) {

    // 高亮颜色设置(高亮其实就是用含有color的span标签把keyword包裹住)
    String preTags = "<span style=\"color:#F56C6C\">";
    String postTags = "</span>";


    // 时间范围
    // 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<Article> contents = ESRestTemplate.search(searchQuery, Article.class);
        List<SearchHit<Article>> articles = contents.getSearchHits();
        // 若是list的长度为0,直接return
        if (articles.size() == 0) {
            return new ArrayList<>();
        }


        // 完成真正的映射,拿到展现的文章数据。
        List<Article> result = articles.stream().map(article -> {
            // 获取高亮数据
            Map<String, List<String>> highlightFields = article.getHighlightFields();

            //若是集合不为空,说明包含高亮字段,则进行设置
            // 这里比较迷的是,高亮的结果集竟然是一个List<String>,可能官方以为没有必要所有变成一坨?
            // 不过正常想也是,咱们不须要把整个文章的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;
}
复制代码

到这里咱们就把后端接口实现啦!!!

接下来就到了使人激动的测试环节嘿嘿(●'◡'●)(应该不会翻车叭ヽ(*。>Д<)o゜)


1.6 接口测试

咱们直接使用IDEA进行测试,输入keyword为java

结果,咱们能够看到,在titledetailjava这个关键字已经被span包裹起来啦。

这样子,前端拿到数据就能够正常高亮展现啦!!


2. 前端

前端我是用的是vue

前端我就不花大量篇幅去介绍啦,由于实现很简单。

只须要使用v-forv-html就能完成页面展现啦(●'◡'●)

目前想法是经过下滑到底而后显示下一页数据,暂时还没实现(不过若是想用分页条的话却是就很简单)

具体代码欢迎前往👉 Github 查看喔。


3. 项目源码

前端:Github

后端:Github

欢迎你们⭐star支持,十分感谢(●'◡'●)

4. 写在最后

哈哈看完上面文章的介绍,是否是你早已火烧眉毛地去实现了呢hhh(●'◡'●)

在本身review代码的同时,也但愿能帮助到你们。

若是上面有哪里写得很差的地方,欢迎在评论区指正

若是你以为文章还不错,欢迎点赞o( ̄▽ ̄)d支持下哈哈

点赞👍不白嫖,从我作起哈哈

相关文章
相关标签/搜索