I-team 博客全文檢索 Elasticsearch 實戰

来源:https://www.cnblogs.com/haifeiWu/archive/2018/07/22/9352009.html
-Advertisement-
Play Games

一直覺得博客缺點東西,最近還是發現了,當博客慢慢多起來的時候想要找一篇之前寫的博客很是麻煩,於是作為後端開發的樓主覺得自己動手豐衣足食,也就有了這次博客全文檢索功能Elasticsearch實戰,這裡還要感謝一下‘輝哥’贊助的一臺伺服器。 <! more 全文檢索工具選型 眾所周知,支持全文檢索的工 ...


一直覺得博客缺點東西,最近還是發現了,當博客慢慢多起來的時候想要找一篇之前寫的博客很是麻煩,於是作為後端開發的樓主覺得自己動手豐衣足食,也就有了這次博客全文檢索功能Elasticsearch實戰,這裡還要感謝一下‘輝哥’贊助的一臺伺服器。

全文檢索工具選型

眾所周知,支持全文檢索的工具有很多,像 Lucene,solr, Elasticsearch 等,相比於其他的工具,顯然 Elasticsearch 社區更加活躍,遇到問題相對來說也比較好解決,另外 Elasticsearch 提供的restful介面操作起來還是比較方便的,這也是樓主選擇 Elasticsearch 的重要原因,當然 Elasticsearch 占據的記憶體相對來說比較大一點,樓主2G的雲伺服器跑起來也是捉襟見肘。

數據遷移,從 MySQL 到 Elasticsearch

這個功能相對來說比較簡單,就是定時從 MySQL 更新數據到 Elasticsearch 中,本來樓主打算自己寫一個數據遷移的工具,但是想起之前樓主做數據遷移時用到的DataX很是不錯,看了寫官方文檔還是支持的,但是樓主硬是沒有跑起來,原因就是樓主2G記憶體的雲伺服器不夠使啊,DataX光是跑起來就要1G多的記憶體,所以樓主只能另謀它法。對DataX感興趣的小伙伴可以看看樓主的另一篇文章阿裡離線數據同步工具 DataX 踩坑記錄

說起可以省記憶體的語言,小伙伴可能會想到最近比較火的golang,沒錯樓主也想到了。最後樓主使用的就是一個叫go-mysql-elasticsearch的工具,就是使用golang實現的從 MySQL 將數據遷移到 Elasticsearch 的工具。具體搭建過程樓主不在這裡細說,感興趣的小伙伴請移步go-mysql-elasticsearch,另外 Elasticsearch 環境的搭建,需要註意的就是安裝 Elasticsearch 的機器記憶體應該大於或者等於2G,否則可能會出現起不起來的情況,樓主也不在這裡贅述了,比較簡單,請小伙伴們自行google。

另外需要註意的是,在使用 go-mysql-elasticsearch 的時候應該開啟mysql的binlog功能,go-mysql-elasticsearch的實現同步數據的思想就是將自己作為MySQL的一個slave掛載在MySQL上,這樣就可以很輕鬆的將數據實時同步到 Elasticsearch 中,在啟動 go-mysql-elasticsearch 的機器上最少應該有MySQL client工具,否則會啟動報錯。樓主的建議是根MySQL部署在同一臺機器上,因為golang耗費記憶體極少,並不會有太大影響。下麵給出樓主同步數據時 go-mysql-elasticsearch 的配置文件:

# MySQL address, user and password
# user must have replication privilege in MySQL.
my_addr = "127.0.0.1:3306"
my_user = "root"
my_pass = "******"
my_charset = "utf8"

# Set true when elasticsearch use https
#es_https = false
# Elasticsearch address
es_addr = "127.0.0.1:9200"
# Elasticsearch user and password, maybe set by shield, nginx, or x-pack
es_user = ""
es_pass = ""

# Path to store data, like master.info, if not set or empty,
# we must use this to support breakpoint resume syncing.
# TODO: support other storage, like etcd.
data_dir = "./var"

# Inner Http status address
stat_addr = "127.0.0.1:12800"

# pseudo server id like a slave
server_id = 1001

# mysql or mariadb
flavor = "mysql"

# mysqldump execution path
# if not set or empty, ignore mysqldump.
mysqldump = "mysqldump"

# if we have no privilege to use mysqldump with --master-data,
# we must skip it.
#skip_master_data = false

# minimal items to be inserted in one bulk
bulk_size = 128

# force flush the pending requests if we don't have enough items >= bulk_size
flush_bulk_time = "200ms"

# Ignore table without primary key
skip_no_pk_table = false

# MySQL data source
[[source]]
schema = "billboard-blog"

# Only below tables will be synced into Elasticsearch.
tables = ["content"]
# Below is for special rule mapping
[[rule]]
schema = "billboard-blog"
table = "content"
index = "contentindex"
type = "content"

[rule.field]
title="title"
blog_desc="blog_desc"
content="content"


# Filter rule
[[rule]]
 schema = "billboard-blog"
 table = "content"
 index = "contentindex"
 type = "content"

# Only sync following columns
filter = ["title", "blog_desc", "content"]

# id rule
[[rule]]
 schema = "billboard-blog"
 table = "content"
 index = "contentindex"
 type = "content"
 id = ["id"]

實現全文檢索功能的服務

要想實現全文檢索的功能並對外提供服務,web服務必不可少,樓主使用Spring Boot搭建web服務,對Spring Boot感興趣的小伙伴也可以看一下樓主的另一篇文章,使用Spring Boot實現博客統計服務。好了廢話不多說了,請看代碼

介面實現代碼,代碼比較簡單就是接收參數,調用service代碼


    @ApiOperation(value="全文檢索介面", notes="")
    @ApiImplicitParam(name = "searchParam", value = "博客搜索條件(作者,描述,內容,標題)", required = true, dataType = "String")
    @RequestMapping(value = "/get_content_list_from_es", method = RequestMethod.GET)
    public ResultCode<List<ContentsWithBLOBs>> getContentListFromEs(String searchParam) {
        ResultCode<List<ContentsWithBLOBs>> resultCode = new ResultCode();
        try {
            LOGGER.info(">>>>>> method getContentListFromEs request params : {},{},{}",searchParam);
            resultCode = contentService.getContentListFromEs(searchParam);
            LOGGER.info(">>>>>> method getContentListFromEs return value : {}",JSON.toJSONString(resultCode));
        } catch (Exception e) {
            e.printStackTrace();
            resultCode.setCode(Messages.API_ERROR_CODE);
            resultCode.setMsg(Messages.API_ERROR_MSG);
        }
        return resultCode;
    }
    

service代碼實現,這裡代碼主要功能就是調用es的工具類,對博客描述,作者,博客標題,博客內容進行全文檢索。


    @Override
    public ResultCode<List<ContentsWithBLOBs>> getContentListFromEs(String searchParam) {
        ResultCode resultCode = new ResultCode();

        // 校驗參數,參數不能為空
        if (StringUtils.isBlank(searchParam)) {
            LOGGER.info(">>>>>> params not be null");
            resultCode.setMsg(Messages.INPUT_ERROR_MSG);
            resultCode.setCode(Messages.INPUT_ERROR_CODE);
            return resultCode;
        }

        String matchStr = "blog_desc=" + searchParam;
        List<Map<String, Object>> result = ElasticsearchUtils.searchListData(BillboardContants.ES_CONTENT_INDEX,BillboardContants.ES_CONTENT_TYPE,BillboardContants.ES_CONTENT_FIELD,true,matchStr);

        matchStr = "author=" + searchParam;
        result.addAll(ElasticsearchUtils.searchListData(BillboardContants.ES_CONTENT_INDEX,BillboardContants.ES_CONTENT_TYPE,BillboardContants.ES_CONTENT_FIELD,true,matchStr));

        matchStr = "title=" + searchParam;
        result.addAll(ElasticsearchUtils.searchListData(BillboardContants.ES_CONTENT_INDEX,BillboardContants.ES_CONTENT_TYPE,BillboardContants.ES_CONTENT_FIELD,true,matchStr));

        matchStr = "content=" + searchParam;
        result.addAll(ElasticsearchUtils.searchListData(BillboardContants.ES_CONTENT_INDEX,BillboardContants.ES_CONTENT_TYPE,BillboardContants.ES_CONTENT_FIELD,true,matchStr));

        List<ContentsWithBLOBs> data = JSON.parseArray(JSON.toJSONString(result),ContentsWithBLOBs.class);
        LOGGER.info("es return data : {}",JSON.toJSONString(result));
        resultCode.setData(data);
        return resultCode;
    }

樓主用到的es的工具類代碼實現,就是使用es的java客戶端對es進行檢索。


    /**
     * 使用分詞查詢
     *
     * @param index       索引名稱
     * @param type        類型名稱,可傳入多個type逗號分隔
     * @param fields      需要顯示的欄位,逗號分隔(預設為全部欄位)
     * @param matchPhrase true 使用,短語精準匹配
     * @param matchStr    過濾條件(xxx=111,aaa=222)
     * @return
     */
    public static List<Map<String, Object>> searchListData(String index, String type, String fields, boolean matchPhrase, String matchStr) {
        return searchListData(index, type, 0, 0, null, fields, null, matchPhrase, null, matchStr);
    }
    
    /**
     * 使用分詞查詢
     *
     * @param index          索引名稱
     * @param type           類型名稱,可傳入多個type逗號分隔
     * @param startTime      開始時間
     * @param endTime        結束時間
     * @param size           文檔大小限制
     * @param fields         需要顯示的欄位,逗號分隔(預設為全部欄位)
     * @param sortField      排序欄位
     * @param matchPhrase    true 使用,短語精準匹配
     * @param highlightField 高亮欄位
     * @param matchStr       過濾條件(xxx=111,aaa=222)
     * @return
     */
    public static List<Map<String, Object>> searchListData(String index, String type, long startTime, long endTime, Integer size, String fields, String sortField, boolean matchPhrase, String highlightField, String matchStr) {

        SearchRequestBuilder searchRequestBuilder = client.prepareSearch(index);
        if (StringUtils.isNotEmpty(type)) {
            searchRequestBuilder.setTypes(type.split(","));
        }
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();

        if (startTime > 0 && endTime > 0) {
            boolQuery.must(QueryBuilders.rangeQuery("processTime")
                    .format("epoch_millis")
                    .from(startTime)
                    .to(endTime)
                    .includeLower(true)
                    .includeUpper(true));
        }

        //搜索的的欄位
        if (StringUtils.isNotEmpty(matchStr)) {
            for (String s : matchStr.split(",")) {
                String[] ss = s.split("=");
                if (ss.length > 1) {
                    if (matchPhrase == Boolean.TRUE) {
                        boolQuery.must(QueryBuilders.matchPhraseQuery(s.split("=")[0], s.split("=")[1]));
                    } else {
                        boolQuery.must(QueryBuilders.matchQuery(s.split("=")[0], s.split("=")[1]));
                    }
                }

            }
        }

        // 高亮(xxx=111,aaa=222)
        if (StringUtils.isNotEmpty(highlightField)) {
            HighlightBuilder highlightBuilder = new HighlightBuilder();

            //highlightBuilder.preTags("<span style='color:red' >");//設置首碼
            //highlightBuilder.postTags("</span>");//設置尾碼

            // 設置高亮欄位
            highlightBuilder.field(highlightField);
            searchRequestBuilder.highlighter(highlightBuilder);
        }


        searchRequestBuilder.setQuery(boolQuery);

        if (StringUtils.isNotEmpty(fields)) {
            searchRequestBuilder.setFetchSource(fields.split(","), null);
        }
        searchRequestBuilder.setFetchSource(true);

        if (StringUtils.isNotEmpty(sortField)) {
            searchRequestBuilder.addSort(sortField, SortOrder.DESC);
        }

        if (size != null && size > 0) {
            searchRequestBuilder.setSize(size);
        }

        //列印的內容 可以在 Elasticsearch head 和 Kibana  上執行查詢
        LOGGER.info("\n{}", searchRequestBuilder);

        SearchResponse searchResponse = searchRequestBuilder.execute().actionGet();

        long totalHits = searchResponse.getHits().totalHits;
        long length = searchResponse.getHits().getHits().length;

        LOGGER.info("共查詢到[{}]條數據,處理數據條數[{}]", totalHits, length);

        if (searchResponse.status().getStatus() == 200) {
            // 解析對象
            return setSearchResponse(searchResponse, highlightField);
        }

        return null;

    }

最後,樓主使用postman測試web服務,如下圖所示:

postman圖

過程中遇到的坑

IK分詞器的設置

這裡需要註意的是,Elasticsearch的版本一定要與ik分詞器的版本對應,不對應的話 Elasticsearch 會報錯的。

$ ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.3.0/elasticsearch-analysis-ik-6.3.0.zip

接著,重新啟動 Elastic,就會自動載入這個新安裝的插件。

然後,新建一個 Index,指定需要分詞的欄位。這一步根據數據結構而異,下麵的命令只針對本文。基本上,凡是需要搜索的中文欄位,都要單獨設置一下。

$ curl -X PUT 'localhost:9200/contentindex'  -H 'Content-Type: application/json' -d '
{
  "mappings": {
    "content": {
      "properties": {
        "content": {
          "type": "text",
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_max_word"
        },
        "title": {
          "type": "text",
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_max_word"
        },
        "blog_desc": {
          "type": "text",
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_max_word"
        },
        "author": {
          "type": "text",
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_max_word"
        }
      }
    }
  }
}'

上面代碼中,首先新建一個名稱為contentindex的 Index,裡面有一個名稱為content的 Type。content有好多個欄位,這裡只為其中四個欄位指定分詞,contenttitleblog_descauthor

這四個欄位都是中文,而且類型都是文本(text),所以需要指定中文分詞器,不能使用預設的英文分詞器。

MySQL binlog的設置

因為樓主運行 go-mysql-elasticsearch 的時候使用的MySQL的客戶端跟要導出數據的MySQL server端的版本不一致導致報錯,最終在 go-mysql-elasticsearch 原作者的幫助下解決,所以一定要使用同版本的MySQL server 與client,因為不同版本的MySQL特性不一樣,也就導致了 go-mysql-elasticsearch 導出數據有略微的不同。

小結

整個過程相對來說比較簡單,當然樓主通過這個功能的實現,也對es有了一個相對的認識,學習了一項新的技能,可能有的小伙伴對樓主的整個工程的代碼比較感興趣,暫時先不能透露,等樓主完善好了一併貢獻出來。

參考文章


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文內容: 文件操作 文件過濾器 首發日期:2018-07-23 文件操作: Java中對文件和目錄的操作,通常通過File類來操作。 File類有幾個構造函數,常用的是下麵三個: File(String pathname):根據路徑名創建一個對象 File(String parent, Strin ...
  • [TOC]#30、第三周-第02章節-Python3.5-上節內容回顧1.列表,元組操作2.字元串操作3.字典操作#31、第三周-第03章節-Python3.5-集合及其運算##集合運算list_1 = [1,2,3,2]print("list_1:",list_1)set_1 = set(list ...
  • 1. 學習計劃 1、圖片上傳 a) 圖片伺服器FastDFS b) 圖片上傳功能實現 2、富文本編輯器的使用KindEditor 3、商品添加功能完成 2. 圖片伺服器的安裝 1、存儲空間可擴展。 2、提供一個統一的訪問方式。 使用FastDFS,分散式文件系統。存儲空間可以橫向擴展,可以實現伺服器 ...
  • 1.java的動態驗證碼我這裡將介紹兩種方法: 一:根據java本身提供的一種驗證碼的寫法,這種呢只限於大家瞭解就可以了,因為java自帶的模式編寫的在實際開發中是沒有意義的,所以只供學習一下就可以了,待會講解的第二種呢就是我們需要掌握的一種模式了: 第一種的代碼如下: 上面的代碼呢寫的很詳細了,這 ...
  • 使用update()方法合併字典,對原有字典的修改並不會影響合併的字典,ChainMap類合併字典後,同樣支持多數字典操作,比如len(), values(), keys(), items()等,對合併字典的添加和刪除總是針對第一個字典,值的查找總是從第一個字典開始查找 ...
  • 今天主要講解一下,activiti 工作流的插件安裝,以及用代碼、配置文件的方式實現工作流所需要用的25張表。 這是activiti 官方文檔:https://www.activiti.org/userguide/index.html#eclipseDesignerInstallation ,有興趣 ...
  • 博主是大三本科生,即將參加2019屆秋招,目前正在複習基礎知識,為了以後便於查找,故記錄在隨筆裡面,希望大家多多指教。 ...
  • 在開發中有些敏感介面,例如用戶餘額提現介面,需要考慮在併發情況下介面是否會發生問題。如果用戶將自己的多條提現請求同時發送到伺服器,代碼能否扛得住呢?一旦沒做鎖,那麼就真的會給用戶多次提現,給公司帶來損失。我來簡單介紹一下在這種介面開發過程中,我的做法。 第一階段: 我們使用的orm為xorm,提現表 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...