IK分词器,elastcisearch插件中连接jdbc定时更新词库

 

由于工作,需要在elasticsearch中植入一个ik分词器,然后百度了一把发现文章很多,但能把整个功能写清楚的非常少,而且很多文章跟新版本相差甚远。所以,这里我自己花了点时间,自己增加了jdbc访问的功能,详细代码和过程如下:

1、版本选择

2、ik分词插件源码分析

3、ik分词插件修改源码

4、ik分词插件部署

5、测试

6、未来改进

 

1、版本选择

本次ik插件所对应的es版本为elasticsearch_6.4.2(6.X版本都可以),

IK分词器的源码下载地址:https://download.csdn.net/download/hq123xiao/10800913 

jdk1.8

开发环境macOS(window不影响)

 

2、ik分词插件源码分析

使用idea打开源码后,找到Dictionary.java文件,找到initial方法,有兴趣的可以去看看Monitor方法

任意找一个方法进去

然后我们找到loadDictFile的方法

 

3、ik分词插件修改源码

好了,基本的修改思路已经有了,我们开始修改源码了

首先,我们这里要写一个JDBC的工具类,方便后面的读取数据

先引入一个jar包

<!-- jdbc数据源 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.25</version>
</dependency>

注意:然后再到assemblies/plugin.xml文件中加入mysql的包,一定要加,不然打包的时候没有mysql的驱动包

<dependencySet>
    <outputDirectory/>
    <useProjectArtifact>true</useProjectArtifact>
    <useTransitiveFiltering>true</useTransitiveFiltering>
    <includes>
        <include>org.apache.httpcomponents:httpclient</include>
        <include>mysql:mysql-connector-java</include>
    </includes>
</dependencySet>

jdbc工具类

package org.wltea.analyzer.dic;

import org.apache.logging.log4j.Logger;
import org.elasticsearch.common.logging.ESLoggerFactory;

import java.sql.*;
import java.util.ArrayList;
import java.util.List;

/**
 * AUTHOR       : qizhifeng
 * TIME         : 2018-11-23 11:35
 * DESCRIPTION  :jdbc连接数据库
 */
public class JdbcUt {

    private static final Logger logger = ESLoggerFactory.getLogger(JdbcUt.class.getName());

    private Connection getConnection () throws Exception {
        String driver = "com.mysql.jdbc.Driver";
        String url = "jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF8&autoReconnect=true&failOverReadOnly=false";
        String user = "root";
        String pass = "123456";
        Class.forName(driver);
        return DriverManager.getConnection(url, user, pass);
    }

    private Statement getStatement (Connection conn) throws Exception {
        return conn.createStatement();
    }

    private ResultSet getResultSet (Statement stat, String querySql) throws Exception{
        return stat.executeQuery(querySql);
    }

    // 关闭数据库相关链接
    private void closeAll (Connection conn, Statement stmt, ResultSet rs) {
        try {
            if (rs != null) {
                rs.close();
            }
            if (stmt != null) {
                stmt.close();
            }
            if (conn != null) {
                conn.close();
            }
        } catch (SQLException e) {
            logger.error("数据库连接关闭出错,e = {}" , e);
        }
    }

    // 查询出词
    public List<String> searchSql(String querySql) {
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;
        List<String> list = new ArrayList<>();
        try {
            conn = getConnection();
            stmt = getStatement(conn);
            rs = getResultSet(stmt, querySql);
            while (rs.next()) {
                list.add(rs.getString("standard_name"));
            }
        }catch (Exception e) {
            logger.error("查询数据库出错-> size = {}, sql = {}, {}", list.size(), querySql, e);
        }finally {
            closeAll(conn, stmt, rs);
        }
        return list;
    }

    //测试一把
    public static void main(String[] args) { 
        try {
            JdbcUt test = new JdbcUt();
            List<String> word = test.searchSql("select standard_name from test_01 where safe_level=1") ;
            word.forEach(System.out::println);
        }catch (Exception e) {
            e.printStackTrace();
        }
    }

}

修改loadDictFile()

	private void loadDictFile(DictSegment dict, List<String> words, boolean critical, String name) {
		try {
			if ( words!=null && words.size()>0 ) {
				words.forEach(w-> {
					dict.fillSegment(w.toCharArray());
				});
			}
		} catch (Exception e) {
			logger.error("ik-analyzer: " + name + " not found", e);
		}
	}

修改loadMainDict()和loadStopWordDict(),如果需要加载其他词库的,按下面的方式加载即可

	/**
	 * 加载主词典及扩展词典
	 */
	private void loadMainDict() {
		// 建立一个主词典实例
		_MainDict = new DictSegment((char) 0);

		// 读取主词典文件
		JdbcUt jdbcUt = new JdbcUt();
		List<String> words = jdbcUt.searchSql("select standard_name from test_01 where safe_level=1");
		loadDictFile(_MainDict, words, false, "Main Dict");
		logger.info("【词库】词典从jdbc加载完成,word={}", words.size());
	}
/**
	 * 加载用户扩展的停止词词典
	 */
	private void loadStopWordDict() {
		// 建立主词典实例
		_StopWords = new DictSegment((char) 0);
		// 读取主词典文件
		JdbcUt jdbcUt = new JdbcUt();
		List<String> stopWords = jdbcUt.searchSql("select standard_name from test_01 where safe_level=2");
		logger.info("【停用词】词典从jdbc加载完成,word={}", stopWords.size());
		loadDictFile(_StopWords, stopWords, false, "Main Stopwords");
	}

我们加一个reload词库的方法,这里我们使用的时间间隔是1个小时,等下测试的时候注意修改这里的间隔时间,修改成10s

private void reloadDic () {
		pool.scheduleAtFixedRate(()-> {
			try {
				logger.info("重新加载词典...");
				// 新开一个实例加载词典,减少加载过程对当前词典使用的影响
				Dictionary tmpDict = new Dictionary(configuration);
				tmpDict.configuration = getSingleton().configuration;
				tmpDict.loadMainDict();
				tmpDict.loadStopWordDict();
				_MainDict = tmpDict._MainDict;
				_StopWords = tmpDict._StopWords;
				logger.info("重新加载词典完毕...");
			}catch (Exception e) {
				logger.error("【主词库&停用词库】加载出错,e = {}" , e);
			}
		}, 10, 60*60, TimeUnit.SECONDS);
	}

修改initial方法

/**
	 * 词典初始化 由于IK Analyzer的词典采用Dictionary类的静态方法进行词典初始化
	 * 只有当Dictionary类被实际调用时,才会开始载入词典, 这将延长首次分词操作的时间 该方法提供了一个在应用加载阶段就初始化字典的手段
	 * 
	 * @return Dictionary
	 */
	public static synchronized Dictionary initial(Configuration cfg) {
		if (singleton == null) {
			synchronized (Dictionary.class) {
				if (singleton == null) {

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

					//从jdbc加载词典
					singleton.reloadDic();

					return singleton;
				}
			}
		}
		return singleton;
	}

把其他load的词库注释掉,只要建立一个空的词库即可,不然会报错空指针异常

/**
     * 加载量词词典
     */
    private void loadQuantifierDict() {
        // 建立一个量词典实例
        _QuantifierDict = new DictSegment((char) 0);
//        // 读取量词词典文件
//        Path file = PathUtils.get(getDictRoot(), Dictionary.PATH_DIC_QUANTIFIER);
//
//        List<String> quantifier = new ArrayList<>();
//        loadDictFile(_QuantifierDict, quantifier, false, "Quantifier");
    }

    private void loadSurnameDict() {
        _SurnameDict = new DictSegment((char) 0);
//        Path file = PathUtils.get(getDictRoot(), Dictionary.PATH_DIC_SURNAME);
//
//        List<String> surname = new ArrayList<>();
//        loadDictFile(_SurnameDict, surname, true, "Surname");
    }

    private void loadSuffixDict() {
        _SuffixDict = new DictSegment((char) 0);
//        Path file = PathUtils.get(getDictRoot(), Dictionary.PATH_DIC_SUFFIX);
//
//        List<String> suffix = new ArrayList<>();
//        loadDictFile(_SuffixDict, suffix, true, "Suffix");
    }

    private void loadPrepDict() {
        _PrepDict = new DictSegment((char) 0);
//        Path file = PathUtils.get(getDictRoot(), Dictionary.PATH_DIC_PREP);
//
//        List<String> prep = new ArrayList<>();
//        loadDictFile(_PrepDict, prep, true, "Preposition");
    }

至此,所有的源码已经修改完成了

4、ik分词插件部署

首先我们先打包,主要:如果你的es版本和我不一样,一定要去pom.xml中修改<elasticsearch.version>的版本号,或者打包完成后安装插件的时候去修改plugin-descriptor.properties中的版本号也一样

打包完成后到target/releases目录下解压zip,看一下jar包是否齐全,特别是mysql的驱动包是不是在,确认后复制解压出来的内容

然后在es的plugin目录下新建一个ik的文件,然后把内容全部复制进去

还没有完成,如果现在去启动es你会发现报了一个错

com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failureaccess denied ("java.net.SocketPermission" "localhost:1527" "listen,resolve")

这个是一个没有socket访问的权限,或者网络访问的权限

解决方案,打开es的jvm.options,加入如下的内容,注意啦,这里的路径是要指向你刚才复制到es的plugins目录下的一个插件访问权限的文件。

## JVM configuration

-Djava.security.policy=/Users/qizhifeng/work/es/elasticsearch-6.4.1/plugins/ik/plugin-security.policy

文件内的内容

grant {
  permission java.net.SocketPermission "*:*", "connect,resolve";
};

已经全部修改完成,然后我们就可以启动es了

 

5、测试

词库中加入两个词:中国、共和

测试语句:

curl -X POST "localhost:9200/_analyze?pretty" -H 'Content-Type: application/json' -d'
{
  "analyzer": "ik_max_word",
  "text":     "中华人民共和国的人都是中国人"
}
'

测试结果

qizhifengdeMacBook-Pro:~ qizhifeng$ curl -X POST "localhost:9200/_analyze?pretty" -H 'Content-Type: application/json' -d'
> {
>   "analyzer": "ik_max_word",
>   "text":     "中华人民共和国的人都是中国人"
> }
> '
{
  "tokens" : [
    {
      "token" : "中",
      "start_offset" : 0,
      "end_offset" : 1,
      "type" : "CN_CHAR",
      "position" : 0
    },
    {
      "token" : "华",
      "start_offset" : 1,
      "end_offset" : 2,
      "type" : "CN_CHAR",
      "position" : 1
    },
    {
      "token" : "人",
      "start_offset" : 2,
      "end_offset" : 3,
      "type" : "CN_CHAR",
      "position" : 2
    },
    {
      "token" : "民",
      "start_offset" : 3,
      "end_offset" : 4,
      "type" : "CN_CHAR",
      "position" : 3
    },
    {
      "token" : "共和",
      "start_offset" : 4,
      "end_offset" : 6,
      "type" : "CN_WORD",
      "position" : 4
    },
    {
      "token" : "国",
      "start_offset" : 6,
      "end_offset" : 7,
      "type" : "CN_CHAR",
      "position" : 5
    },
    {
      "token" : "的",
      "start_offset" : 7,
      "end_offset" : 8,
      "type" : "CN_CHAR",
      "position" : 6
    },
    {
      "token" : "人",
      "start_offset" : 8,
      "end_offset" : 9,
      "type" : "CN_CHAR",
      "position" : 7
    },
    {
      "token" : "都",
      "start_offset" : 9,
      "end_offset" : 10,
      "type" : "CN_CHAR",
      "position" : 8
    },
    {
      "token" : "是",
      "start_offset" : 10,
      "end_offset" : 11,
      "type" : "CN_CHAR",
      "position" : 9
    },
    {
      "token" : "中国",
      "start_offset" : 11,
      "end_offset" : 13,
      "type" : "CN_WORD",
      "position" : 10
    },
    {
      "token" : "人",
      "start_offset" : 13,
      "end_offset" : 14,
      "type" : "CN_CHAR",
      "position" : 11
    }
  ]

好了,已经全部圆满完成,

 

6、未来改进

这里虽然已经完成定时连接jdbc然后更新词库,但是在实际业务中,可能还有更多的业务场景,该词库还需要改进

a、实时更新词库:

         1、接入mq消息队列

         2、定时扫描词库表,有新词就加入到词库中         

b、紧急业务reload词库:接入zookeeper,监听节点的reload事件

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值