由于工作,需要在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事件