【仿掘金系列】使用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支持下哈哈

點贊👍不白嫖,從我作起哈哈

相關文章
相關標籤/搜索