前言
眼前有景道不得,崔颢题诗在上头;(转载:https://blog.51cto.com/u_13270529/5962113)
在IK分词器中添加扩展词典或远程扩展词典,每次词典更新后都需要重启ES服务器,这在生产环境中是绝对不被允许的;如果我们把扩展词典数据存放在三方组件中如Redis、Mysql中ElasticSearch每隔一分钟去Redis或Mysql中同步最新的词典数据来更新词库,这样每次更新词库时不用重启ElasticSeach服务器就可以达到IK词典数据热更新;
热更新是全量更新还是增量更新?字典本身数据量并不大,充其量也就小几十万,一般也不会把所有的词语都放在词库中,而且数据库中的字典数据是为整个 ES 集群服务的,增量的话考虑的细节就会很多,同时在进行字典更新的时候是不会影响先有字典的使用的,综合来看全量更新更好。
一.环境参数
ElasticSearch 8.1.0
Mysql 8.0.28
二.热更新词典步骤
1.在Mysql中新建“扩展词典”,“扩展停止词典”数据表
# 扩展停用词典数据表
CREATE TABLE stop_words (word VARCHAR (200));
# 扩展词典数据表
CREATE TABLE ext_words (word VARCHAR (200));
2.下载对应ES版本的IK分词器源码包,并使用Idea打开项目
下载地址: https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v8.1.0
3.pom.xml中添加如下依赖
# Mysql依赖用于ES重Mysql数据库中拉取词典数据
<!-- 引入Mysql依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version> 8.0.20</version>
</dependency>
# Hutool核心依赖只使用其下的StrUtil.isNotBlank()这一个方法判断字符串是否为空串
<!-- Hutool工具类 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>5.6.3</version>
</dependency>
4.修改org.wltea.analyzer.dic包中Dictionar类
4.1.注释Dictionar类中的initial方法
4.2.注释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();
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);
}
}
new Thread(()->{
Properties pro = new Properties();
try {
// 读取Mysql配置文件,mysql.properties配置文件自行在IK文件夹下的config目录中新建
pro.load(new FileInputStream(PathUtils.get(singleton.getDictRoot(), "mysql.properties").toFile()));
} catch (IOException e) {
e.printStackTrace();
}
while (true){
try {
TimeUnit.SECONDS.sleep(5);
logger.info("开始从MySQL加载.....");
// 读取配置文件中url、user、password用来创建Mysql连接
try (Connection conn = DriverManager.getConnection(pro.getProperty("mysql.url"), pro.getProperty("mysql.user"), pro.getProperty("mysql.password"))) {
// 重数据库中加载词典
reLoadFromMySQL(conn, pro);
}
logger.info("从MySQL加载完成.....");
}catch (Exception e){
logger.error("load from mysql error..",e);
}
}
}).start();
}
}
}
}
// 加载连接驱动
static {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException ignored) {
}
}
// 重Mysql中重新加载词典
private static void reLoadFromMySQL(Connection conn, Properties pro) throws Exception {
logger.info("从MySQL重新加载词典...start");
// 新开一个实例加载词典,减少加载过程对当前词典使用的影响
Dictionary tmpDict = new Dictionary(getSingleton().configuration);
tmpDict.configuration = getSingleton().configuration;
// IK分词器自身的加载远程词典方法
tmpDict.loadMainDict();
// 重Mysql中加载扩展词典
tmpDict.reloadExtDictFromMySQL(conn);
// IK分词器自身的加载远程停用词典方法
tmpDict.loadStopWordDict();
// 重Mysql中加载停用词典
tmpDict.reloadStopDictFromMySQL(conn);
getSingleton()._MainDict = tmpDict._MainDict;
getSingleton()._StopWords = tmpDict._StopWords;
logger.info("从MySQL重新加载词典完毕...end");
}
// 重Mysql中加载停用词典
private void reloadStopDictFromMySQL(Connection conn) throws SQLException {
try (Statement statement = conn.createStatement();ResultSet rs = statement.executeQuery("select word from stop_words")) {
while(rs.next()) {
String word = rs.getString("word");
if (StrUtil.isNotBlank(word)){
_StopWords.fillSegment(word.toCharArray());
}
}
}
}
// 重Mysql中加载扩展词典
private void reloadExtDictFromMySQL(Connection conn) throws SQLException {
try (Statement statement = conn.createStatement(); ResultSet rs = statement.executeQuery("select word from ext_words")) {
while(rs.next()) {
String word = rs.getString("word");
if (StrUtil.isNotBlank(word)){
_MainDict.fillSegment(word.toCharArray());
}
}
}
}
5.使用Maven打包插件打工具类包
6.进入ElasticSearch下的IK分词器根目录
6.1.删除原有elasticsearch-analysis-ik-8.1.0.jar
6.2.添加上面打好的elasticsearch-analysis-ik-7.16.0.jar
6.3.添加Mysql的连接驱动包
6.4.添加Hutool-core依赖包
形如:
7.进入IK分词器根目录下的config目录中新建mysql.properties文件及socketPolicy.policy文件
配置内容:
mysql.properties:
mysql.url=jdbc:mysql://localhost:3306/ik?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=false
mysql.user=root
mysql.password=123456
socketPolicy.policy:
grant {
permission java.net.SocketPermission "*:*","accept,connect,resolve";
permission java.lang.RuntimePermission "setContextClassLoader";
};
8.进入ES根目录下的config文件夹编辑jvm.options文件添加如下配置
# 解决ES控制台中文乱码
-Dfile.encoding=GBK
# socketPolicy.policy文件的绝对路径
-Djava.security.policy=D:\software\ElasticSearch8\elasticsearch-8.1.0\plugins\elasticsearch-analysis-ik-8.1.0\config\socketPolicy.policy
9.启动ElasticSearch
三.测试
# 1.使用IK分词器ik_max_word粒度对“弗雷尔卓德”进行分词测试
GET /_analyze
{
"text":"弗雷尔卓德",
"analyzer":"ik_max_word"
}
响应:
{
"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": 5,
"type": "CN_CHAR",
"position": 4
}
]
}
# 2.在Mysql扩展词典表ext_words中添加“弗雷尔卓德”记录后再次测试分词效果(无需重启ES服务器)
GET /_analyze
{
"text":"弗雷尔卓德",
"analyzer":"ik_max_word"
}
响应:
{
"tokens": [
{
"token": "弗雷尔卓德",
"start_offset": 0,
"end_offset": 5,
"type": "CN_WORD",
"position": 0
}
]
}
# 3.在Mysql扩展停用词典表stop_words中添加“弗雷尔卓德”记录后再次测试分词效果(无需重启ES服务
GET /_analyze
{
"text":"弗雷尔卓德",
"analyzer":"ik_max_word"
}
响应:
{
"tokens": []
}