ES 實現實時從Mysql資料庫中讀取熱詞,停用詞

彼岸舞發表於2020-09-13

IK分詞器雖然自帶詞庫

 

 

 但是在實際開發應用中對於詞庫的靈活度的要求是遠遠不夠的,IK分詞器雖然配置檔案中能新增擴充套件詞庫,但是需要重啟ES

這章就當寫一篇擴充套件了

 

其實IK本身是支援熱更新詞庫的,但是需要我感覺不是很好

詞庫熱更新方案:

1:IK 原生的熱更新方案,部署一個WEB伺服器,提供一個Http介面,通過Modified和tag兩個Http響應頭,來完成詞庫的熱更新

2:通過修改IK原始碼支援Mysql定時更新資料

注意:推薦使用第二種方案,也是比較常用的方式,雖然第一種是官方提供的,但是官方也不建議使用

 

方案一:IK原生方案

  1:外掛詞庫,就是在IK配置檔案中新增擴充套件詞庫檔案多個之間使用分號分割

    優點:編輯指定詞庫檔案,部署比較方便

    缺點:每次編輯更新後都需要重啟ES

  2:遠端詞庫,就是在IK配置檔案中配置一個Http請求,可以是.dic檔案,也可以是介面,同樣多個之間使用分號分割

    優點:指定靜態檔案,或者介面設定詞庫實現熱更新詞庫,不用重啟ES,是IK原生自帶的

    缺點:需要通過Modified和tag兩個Http響應頭,來提供詞庫的熱更新,有時候會不生效

具體使用就不說了,在這裡具體說第二種方案

方案二:通過定時讀取Mysql完成詞庫的熱更新

首先要下載IK分詞器的原始碼

  網址:https://github.com/medcl/elasticsearch-analysis-ik

下載的時候一定要選對版本,保持和ES的版本一致,否則會啟動的時候報錯,版本不一致

接著把原始碼匯入IDEA中,並在POM.xml中新增Mysql的依賴,根據自己的Mysql版本需要新增

我的Mysql是5.6.1所以新增5的驅動包

<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.49</version>
        </dependency>

然後再config目錄下建立一個新的.properties配置檔案

 

 

 在裡面配置Mysql的一些配置,以及我們需要的配置

jdbc.url=jdbc:mysql://192.168.43.154:3306/es?characterEncoding=UTF-8&serverTimezone=GMT&nullCatalogMeansCurrent=true
jdbc.user=root
jdbc.password=root
# 更新詞庫
jdbc.reload.sql=select word from hot_words
# 更新停用詞詞庫
jdbc.reload.stopword.sql=select stopword as word from hot_stopwords
# 重新拉取時間間隔
jdbc.reload.interval=5000

建立一個新的執行緒,用於呼叫Dictionary得reLoadMainDict()方法重新載入詞庫

 

 

 

package org.wltea.analyzer.dic;

import org.wltea.analyzer.help.ESPluginLoggerFactory;

public class HotDicReloadThread implements Runnable{

    private static final org.apache.logging.log4j.Logger logger = ESPluginLoggerFactory.getLogger(Dictionary.class.getName());

    @Override
    public void run() {

        while (true){
            logger.info("-------重新載入mysql詞典--------");

            Dictionary.getSingleton().reLoadMainDict();
        }

    }
}

修改org.wltea.analyzer.dic資料夾下的Dictionary

在Dictionary類中載入mysql驅動類

private static Properties prop = new Properties();

    static {
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            logger.error("error", e);
        }
    }

接著,建立重Mysql中載入詞典的方法

/**
     * 從mysql中載入熱更新詞典
     */
    private void loadMySqlExtDict(){
        Connection connection = null;
        Statement statement = null;
        ResultSet resultSet = null;

        try {
            Path file = PathUtils.get(getDictRoot(),"jdbc-reload.properties");
            prop.load(new FileInputStream(file.toFile()));

            logger.info("-------jdbc-reload.properties-------");
            for (Object key : prop.keySet()) {
                logger.info("key:{}", prop.getProperty(String.valueOf(key)));
            }

            logger.info("------- 查詢詞典, sql:{}-------", prop.getProperty("jdbc.reload.sql"));

            // 建立mysql連線
            connection = DriverManager.getConnection(
                    prop.getProperty("jdbc.url"),
                    prop.getProperty("jdbc.user"),
                    prop.getProperty("jdbc.password")
            );

            // 執行查詢
            statement = connection.createStatement();
            resultSet = statement.executeQuery(prop.getProperty("jdbc.reload.sql"));

            // 迴圈輸出查詢啊結果,新增到Main.dict中去
            while (resultSet.next()) {
                String theWord = resultSet.getString("word");
                logger.info("------熱更新詞典:{}------", theWord);

                // 加到mainDict裡面
                _MainDict.fillSegment(theWord.trim().toCharArray());
            }
        } catch (Exception e) {
            logger.error("error:{}", e);
        } finally {
            try {
                if (resultSet != null) {
                    resultSet.close();
                }
                if (statement != null) {
                    statement.close();
                }
                if (connection != null) {
                    connection.close();
                }
            } catch (SQLException e){
                logger.error("error", e);
            }
        }
    }

接著,建立載入停用詞詞典方法

/**
     * 從mysql中載入停用詞
     */
    private void loadMySqlStopwordDict(){
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;

        try {
            Path file = PathUtils.get(getDictRoot(), "jdbc-reload.properties");
            prop.load(new FileInputStream(file.toFile()));

            logger.info("-------jdbc-reload.properties-------");
            for(Object key : prop.keySet()) {
                logger.info("-------key:{}", prop.getProperty(String.valueOf(key)));
            }

            logger.info("-------查詢停用詞, sql:{}",prop.getProperty("jdbc.reload.stopword.sql"));

            conn = DriverManager.getConnection(
                    prop.getProperty("jdbc.url"),
                    prop.getProperty("jdbc.user"),
                    prop.getProperty("jdbc.password"));
            stmt = conn.createStatement();
            rs = stmt.executeQuery(prop.getProperty("jdbc.reload.stopword.sql"));

            while(rs.next()) {
                String theWord = rs.getString("word");
                logger.info("------- 載入停用詞 : {}", theWord);
                _StopWords.fillSegment(theWord.trim().toCharArray());
            }

            Thread.sleep(Integer.valueOf(String.valueOf(prop.get("jdbc.reload.interval"))));
        } catch (Exception e) {
            logger.error("error", e);
        } finally {
            try {
                if(rs != null) {
                    rs.close();
                }
                if(stmt != null) {
                    stmt.close();
                }
                if(conn != null) {
                    conn.close();
                }
            } catch (SQLException e){
                logger.error("error:{}", e);
            }

        }
    }

接下來,分別在loadMainDict()方法和loadStopWordDict()方法結尾處呼叫

/**
     * 載入主詞典及擴充套件詞典
     */
    private void loadMainDict() {
        // 建立一個主詞典例項
        _MainDict = new DictSegment((char) 0);

        // 讀取主詞典檔案
        Path file = PathUtils.get(getDictRoot(), Dictionary.PATH_DIC_MAIN);
        loadDictFile(_MainDict, file, false, "Main Dict");
        // 載入擴充套件詞典
        this.loadExtDict();
        // 載入遠端自定義詞庫
        this.loadRemoteExtDict();
        // 載入Mysql外掛詞庫
        this.loadMySqlExtDict();
    }
/**
     * 載入使用者擴充套件的停止詞詞典
     */
    private void loadStopWordDict() {
        // 建立主詞典例項
        _StopWords = new DictSegment((char) 0);

        // 讀取主詞典檔案
        Path file = PathUtils.get(getDictRoot(), Dictionary.PATH_DIC_STOP);
        loadDictFile(_StopWords, file, false, "Main Stopwords");

        // 載入擴充套件停止詞典
        List<String> extStopWordDictFiles = getExtStopWordDictionarys();
        if (extStopWordDictFiles != null) {
            for (String extStopWordDictName : extStopWordDictFiles) {
                logger.info("[Dict Loading] " + extStopWordDictName);

                // 讀取擴充套件詞典檔案
                file = PathUtils.get(extStopWordDictName);
                loadDictFile(_StopWords, file, false, "Extra Stopwords");
            }
        }

        // 載入遠端停用詞典
        List<String> remoteExtStopWordDictFiles = getRemoteExtStopWordDictionarys();
        for (String location : remoteExtStopWordDictFiles) {
            logger.info("[Dict Loading] " + location);
            List<String> lists = getRemoteWords(location);
            // 如果找不到擴充套件的字典,則忽略
            if (lists == null) {
                logger.error("[Dict Loading] " + location + " load failed");
                continue;
            }
            for (String theWord : lists) {
                if (theWord != null && !"".equals(theWord.trim())) {
                    // 載入遠端詞典資料到主記憶體中
                    logger.info(theWord);
                    _StopWords.fillSegment(theWord.trim().toLowerCase().toCharArray());
                }
            }
        }

        // 載入Mysql停用詞詞庫
        this.loadMySqlStopwordDict();

    }

最後在initial()方法中啟動更新執行緒

/**
     * 詞典初始化 由於IK Analyzer的詞典採用Dictionary類的靜態方法進行詞典初始化
     * 只有當Dictionary類被實際呼叫時,才會開始載入詞典, 這將延長首次分詞操作的時間 該方法提供了一個在應用載入階段就初始化字典的手段
     *
     * @return Dictionary
     */
    public static synchronized void initial(Configuration cfg) {
        if (singleton == null) {
            synchronized (Dictionary.class) {
                if (singleton == null) {

                    singleton = new Dictionary(cfg);
                    singleton.loadMainDict();
                    singleton.loadSurnameDict();
                    singleton.loadQuantifierDict();
                    singleton.loadSuffixDict();
                    singleton.loadPrepDict();
                    singleton.loadStopWordDict();

                    // 執行更新mysql詞庫的執行緒
                    new Thread(new HotDicReloadThread()).start();

                    if(cfg.isEnableRemoteDict()){
                        // 建立監控執行緒
                        for (String location : singleton.getRemoteExtDictionarys()) {
                            // 10 秒是初始延遲可以修改的 60是間隔時間 單位秒
                            pool.scheduleAtFixedRate(new Monitor(location), 10, 60, TimeUnit.SECONDS);
                        }
                        for (String location : singleton.getRemoteExtStopWordDictionarys()) {
                            pool.scheduleAtFixedRate(new Monitor(location), 10, 60, TimeUnit.SECONDS);
                        }
                    }

                }
            }
        }
    }

然後,修改src/main/assemblies/plugin.xml檔案中,加入Mysql

<dependencySet>
            <outputDirectory>/</outputDirectory>
            <useProjectArtifact>true</useProjectArtifact>
            <useTransitiveFiltering>true</useTransitiveFiltering>
            <includes>
                <include>mysql:mysql-connector-java</include>
            </includes>
        </dependencySet>

原始碼到此修改完成,在自己的資料庫中建立兩張新的表

建表SQL

CREATE TABLE hot_words (
id bigint(20) NOT NULL AUTO_INCREMENT,
word varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '詞語',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

CREATE TABLE hot_stopwords (
id bigint(20) NOT NULL AUTO_INCREMENT,
stopword varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '停用詞',
PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

接下來對原始碼進行打包:

打包之前檢查自己的POM.xml中的elasticsearch.version的版本,記得和自己的ES的版本對應,否則到時候會報錯

檢查完畢後,點選IDEA右側的package進行專案打包,如果版本不對,修改版本並點選IDEA右側的重新整理同步,進行版本的更換,然後打包

 

 

 打包完成後在左側專案中會出現target目錄,會看到一個zip,我的是因為解壓了,所以有資料夾

 

 

 點選右鍵在資料夾中展示,然後使用解壓工具解壓

 

 

解壓完成後,雙擊進入

 

 

 

 先把原來ES下的plugins下的IK資料夾中的東西刪除,可以先備份,然後把自己打包解壓后里面的東西全部拷貝到ES下的plugins下的IK資料夾中

 

 

 接下來進入bin目錄下啟動就可以了

當然按照慣例,我的啟動時不會那麼簡單的,很高興,我的報錯了,所有的坑都踩了一遍,之前的版本不對就踩了兩次

第一次是原始碼下載的版本不對

第二次的ES依賴版本不對

好了說報錯:報錯只貼主要內容

第三次報錯:

Caused by: java.security.AccessControlException: access denied ("java.lang.RuntimePermission" "setContextClassLoader")

這個是JRE的類的建立設值許可權不對

在jre/lib/security資料夾中有一個java.policy檔案,在其grant{}中加入授權即可

permission java.lang.RuntimePermission "createClassLoader"; 
permission java.lang.RuntimePermission "getClassLoader"; 
permission java.lang.RuntimePermission "accessDeclaredMembers";
permission java.lang.RuntimePermission "setContextClassLoader";

第四次報錯:

Caused by: java.security.AccessControlException: access denied ("java.net.SocketPermission" "192.168.43.154:3306" "connect,resolve")

這個是通訊連結等許可權不對

也是,在jre/lib/security資料夾中有一個java.policy檔案,在其grant{}中加入授權即可

permission java.net.SocketPermission "192.168.43.154:3306","accept";
permission java.net.SocketPermission "192.168.43.154:3306","listen";
permission java.net.SocketPermission "192.168.43.154:3306","resolve";
permission java.net.SocketPermission "192.168.43.154:3306","connect";

到此之後啟動無異常

 

最後就是測試了,啟動我的head外掛和kibana,這兩個沒有或者不會的可以看我之前寫的,也可以百度

執行分詞

 

 但是我想要  天青色

在Mysql中新增記錄

insert into hot_words(word) value("天青色");

重新執行

 

 也比如我想要這就是一個詞  天青色等煙雨

在Mysql中新增記錄

insert into hot_words(word) value("天青色等煙雨");

再次執行

 

 到此實現了ES定時從mysql中讀取熱詞,停用詞這個一般用的比較少,有興趣自己測測,在使用的時候,通過業務系統往資料庫熱詞表和停用詞表新增記錄就可以了

作者:彼岸舞

時間:2020\09\13

內容關於:ElasticSearch

本文來源於網路,只做技術分享,一概不負任何責任

相關文章