1. 常用热词词库的配置方式

1.采用IK 内置词库

优点:部署方便,不用额外指定其他词库位置
缺点:分词单一化,不能指定想分词的词条

2.IK 外置静态词库

优点:部署相对方便,可以通过编辑指定文件分词文件得到想要的词条
缺点:需要指定外部静态文件,每次需要手动编辑整个分词文件,然后放到指定的文件目录下,重启ES后才能生效

3.IK 远程词库

优点:通过指定一个静态文件代理服务器来设置IK分词的词库信息
缺点:需要手动编辑整个分词文件来进行词条的添加, IK源码中判断头信息Last-Modified ETag 标识来判断是否更新,有时不生效
结合上面的优缺点,决定采用Mysql作为外置热词词库,定时更新热词 和 停用词。

2. 准备工作

1.下载合适的ElasticSearch对应版本的IK分词器

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

2.我们来查看它config文件夹下的文件:

image.png

因为我本地安装的是ES是6.6.2版本,所以下载的IK为6.6.2的适配版

3.分析IKAnalyzer.cfg.xml 配置文件:

image.png

ext_dict:对应的扩展热词词典的位置,多个热词文件之间使用分号来进行间隔
ext_stopwords:对应扩展停用词词典位置,多个之间用分号进行间隔
remote_ext_dict:远程扩展热词位置 如:https://xxx.xxx.xxx.xxx/ext_hot.txt
remote_ext_stopwords:远程扩展停用词位置 如:https://xxx.xxx.xxx.xxx/ext_stop.txt

4.Dictionary类

Dictionary中单例方法public static synchronized Dictionary initial(Configuration cfg)

  1. public static synchronized Dictionary initial(Configuration cfg) {
  2. if (singleton == null) {
  3. synchronized (Dictionary.class) {
  4. if (singleton == null) {
  5. singleton = new Dictionary(cfg);
  6. singleton.loadMainDict();
  7. singleton.loadSurnameDict();
  8. singleton.loadQuantifierDict();
  9. singleton.loadSuffixDict();
  10. singleton.loadPrepDict();
  11. singleton.loadStopWordDict();
  12. if(cfg.isEnableRemoteDict()){
  13. // 建立监控线程
  14. for (String location : singleton.getRemoteExtDictionarys()) {
  15. // 10 秒是初始延迟可以修改的 60是间隔时间 单位秒
  16. pool.scheduleAtFixedRate(new Monitor(location), 10, 60, TimeUnit.SECONDS);
  17. }
  18. for (String location : singleton.getRemoteExtStopWordDictionarys()) {
  19. pool.scheduleAtFixedRate(new Monitor(location), 10, 60, TimeUnit.SECONDS);
  20. }
  21. }
  22. return singleton;
  23. }
  24. }
  25. }
  26. return singleton;
  27. }

initial中 load*中方法是利用config中其他文本文件来初始化Dictionary中的上面声明的成员变量:
_MainDict : 主词典对象,也是用来存储热词的对象
_SurnameDict : 姓氏词典
_QuantifierDict : 量词词典,例如1个中的 个 2两种的两
_SuffixDict : 后缀词典
_PrepDict : 副词/介词词典
_StopWords : 停用词词典

3. 修改Dictionary源码

1. Dictionary类:更新词典 this.loadMySQLExtDict()

  1. /**
  2. * 加载主词典及扩展词典
  3. */
  4. private void loadMainDict() {
  5. // 建立一个主词典实例
  6. _MainDict = new DictSegment((char) 0);
  7. // 读取主词典文件
  8. Path file = PathUtils.get(getDictRoot(), Dictionary.PATH_DIC_MAIN);
  9. loadDictFile(_MainDict, file, false, "Main Dict");
  10. // 加载扩展词典
  11. this.loadExtDict();
  12. // 加载远程自定义词库
  13. this.loadRemoteExtDict();
  14. // 2. 从mysql加载词典
  15. this.loadMySQLExtDict();
  16. }
  17. private static Properties prop = new Properties();
  18. static {
  19. try {
  20. Class.forName("com.mysql.jdbc.Driver");
  21. } catch (ClassNotFoundException e) {
  22. logger.error("error", e);
  23. }
  24. }
  25. /**
  26. * @Title: loadMySQLExtDict
  27. * @Description: 从mysql加载热更新词典
  28. * @author 石鹏程
  29. * @created 2019年3月3日
  30. * @param:
  31. * @return: void
  32. * @throws
  33. */
  34. private void loadMySQLExtDict() {
  35. Connection conn = null;
  36. Statement stmt = null;
  37. ResultSet rs = null;
  38. try {
  39. Path file = PathUtils.get(getDictRoot(), "jdbc-reload.properties");
  40. prop.load(new FileInputStream(file.toFile()));
  41. /*logger.info("[==========]jdbc-reload.properties");
  42. for(Object key : prop.keySet()) {
  43. logger.info("[==========]" + key + "=" + prop.getProperty(String.valueOf(key)));
  44. }
  45. logger.info("[==========]query hot dict from mysql, " + prop.getProperty("jdbc.reload.sql") + "......");
  46. */
  47. conn = DriverManager.getConnection(
  48. prop.getProperty("jdbc.url"),
  49. prop.getProperty("jdbc.user"),
  50. prop.getProperty("jdbc.password"));
  51. stmt = conn.createStatement();
  52. rs = stmt.executeQuery(prop.getProperty("jdbc.reload.sql"));
  53. int i=0;
  54. while(rs.next()) {
  55. String theWord = rs.getString("word");
  56. //logger.info("[==========]hot word from mysql: " + theWord);
  57. _MainDict.fillSegment(theWord.trim().toCharArray());
  58. i++;
  59. }
  60. logger.info("[==========] 加载分词数量: " + i+"个");
  61. } catch (Exception e) {
  62. logger.error("erorr", e);
  63. } finally {
  64. if(rs != null) {
  65. try {
  66. rs.close();
  67. } catch (SQLException e) {
  68. logger.error("error", e);
  69. }
  70. }
  71. if(stmt != null) {
  72. try {
  73. stmt.close();
  74. } catch (SQLException e) {
  75. logger.error("error", e);
  76. }
  77. }
  78. if(conn != null) {
  79. try {
  80. conn.close();
  81. } catch (SQLException e) {
  82. logger.error("error", e);
  83. }
  84. }
  85. }
  86. }

2. Dictionary类:更新停用词 this.loadMySQLStopwordDict()

  1. /**
  2. * 加载用户扩展的停止词词典
  3. */
  4. private void loadStopWordDict() {
  5. // 建立主词典实例
  6. _StopWords = new DictSegment((char) 0);
  7. // 读取主词典文件
  8. Path file = PathUtils.get(getDictRoot(), Dictionary.PATH_DIC_STOP);
  9. loadDictFile(_StopWords, file, false, "Main Stopwords");
  10. // 加载扩展停止词典
  11. List<String> extStopWordDictFiles = getExtStopWordDictionarys();
  12. if (extStopWordDictFiles != null) {
  13. for (String extStopWordDictName : extStopWordDictFiles) {
  14. logger.info("[Dict Loading] " + extStopWordDictName);
  15. // 读取扩展词典文件
  16. file = PathUtils.get(extStopWordDictName);
  17. loadDictFile(_StopWords, file, false, "Extra Stopwords");
  18. }
  19. }
  20. // 加载远程停用词典
  21. List<String> remoteExtStopWordDictFiles = getRemoteExtStopWordDictionarys();
  22. for (String location : remoteExtStopWordDictFiles) {
  23. logger.info("[Dict Loading] " + location);
  24. List<String> lists = getRemoteWords(location);
  25. // 如果找不到扩展的字典,则忽略
  26. if (lists == null) {
  27. logger.error("[Dict Loading] " + location + "加载失败");
  28. continue;
  29. }
  30. for (String theWord : lists) {
  31. if (theWord != null && !"".equals(theWord.trim())) {
  32. // 加载远程词典数据到主内存中
  33. logger.info(theWord);
  34. _StopWords.fillSegment(theWord.trim().toLowerCase().toCharArray());
  35. }
  36. }
  37. }
  38. //3.加载自定义停用词
  39. this.loadMySQLStopwordDict();
  40. }
  41. /**
  42. * 从mysql加载停用词
  43. */
  44. private void loadMySQLStopwordDict() {
  45. Connection conn = null;
  46. Statement stmt = null;
  47. ResultSet rs = null;
  48. try {
  49. Path file = PathUtils.get(getDictRoot(), "jdbc-reload.properties");
  50. prop.load(new FileInputStream(file.toFile()));
  51. conn = DriverManager.getConnection(
  52. prop.getProperty("jdbc.url"),
  53. prop.getProperty("jdbc.user"),
  54. prop.getProperty("jdbc.password"));
  55. stmt = conn.createStatement();
  56. rs = stmt.executeQuery(prop.getProperty("jdbc.reload.stopword.sql"));
  57. int i=0;
  58. while(rs.next()) {
  59. String theWord = rs.getString("word");
  60. //logger.info("[==========]hot stopword from mysql: " + theWord);
  61. _StopWords.fillSegment(theWord.trim().toCharArray());
  62. i++;
  63. }
  64. logger.info("[==========] 加载停用词数量: " + i+"个");
  65. } catch (Exception e) {
  66. logger.error("erorr", e);
  67. } finally {
  68. if(rs != null) {
  69. try {
  70. rs.close();
  71. } catch (SQLException e) {
  72. logger.error("error", e);
  73. }
  74. }
  75. if(stmt != null) {
  76. try {
  77. stmt.close();
  78. } catch (SQLException e) {
  79. logger.error("error", e);
  80. }
  81. }
  82. if(conn != null) {
  83. try {
  84. conn.close();
  85. } catch (SQLException e) {
  86. logger.error("error", e);
  87. }
  88. }
  89. }
  90. }

3. 对外暴露方法:

  1. public void reLoadMainDict() {
  2. try {
  3. Path file = PathUtils.get(getDictRoot(), "jdbc-reload.properties");
  4. prop.load(new FileInputStream(file.toFile()));
  5. String enble = prop.getProperty("ik.mysql.enable");
  6. logger.info("ik.mysql.enable:"+enble);
  7. if(enble.equals("yes")){
  8. //logger.info("重新加载词典...");
  9. // 新开一个实例加载词典,减少加载过程对当前词典使用的影响
  10. Dictionary tmpDict = new Dictionary(configuration);
  11. tmpDict.configuration = getSingleton().configuration;
  12. tmpDict.loadMainDict();
  13. tmpDict.loadStopWordDict();
  14. _MainDict = tmpDict._MainDict;
  15. _StopWords = tmpDict._StopWords;
  16. //logger.info("重新加载词典完毕...");
  17. //logger.info("当前排队线程数:"+((ThreadPoolExecutor)pool).getQueue().size());
  18. //logger.info("当前活动线程数:"+((ThreadPoolExecutor)pool).getActiveCount());
  19. //logger.info("执行完成线程数:"+((ThreadPoolExecutor)pool).getCompletedTaskCount());
  20. //logger.info("总线程数(排队线程数+活动线程数+执行完成线程数):"+ ((ThreadPoolExecutor)pool).getTaskCount());
  21. }else{
  22. logger.info("ik分词mysql热加载关闭......");
  23. }
  24. } catch (IOException e) {
  25. logger.info("ik分词器 - jdbc-reload.properties文件解析出错....");
  26. e.printStackTrace();
  27. }
  28. }

4. HotDictReloadThread Runnable实现类,去执行 reLoadMainDict 加载热词

image.png

最后代码为定时调用:
image.png

其中一些细节就不讲述了。
image.png

4. 打包

因为我们链接的是mysql数据库,所以maven项目要引入mysql驱动:

  1. <dependency>
  2. <groupId>mysql</groupId>
  3. <artifactId>mysql-connector-java</artifactId>
  4. <version>6.0.6</version>
  5. </dependency>

准备完毕:执行打包。 mvn clean package
image.png

打包完毕。 上传,重启进行实验啦

5.实验结果

数据库插入记录
image.png
image.png

  1. GET http://127.0.0.1:9200/g_index/_analyze?text=王者荣耀&analyzer=ik_max_word
  2. {
  3. "tokens": [
  4. {
  5. "token": "王者荣耀",
  6. "start_offset": 0,
  7. "end_offset": 5,
  8. "type": "CN_WORD",
  9. "position": 0
  10. },
  11. {
  12. "token": "王者",
  13. "start_offset": 1,
  14. "end_offset": 3,
  15. "type": "CN_WORD",
  16. "position": 1
  17. },
  18. {
  19. "token": "王",
  20. "start_offset": 1,
  21. "end_offset": 2,
  22. "type": "CN_WORD",
  23. "position": 2
  24. },
  25. {
  26. "token": "者",
  27. "start_offset": 2,
  28. "end_offset": 3,
  29. "type": "CN_WORD",
  30. "position": 3
  31. },
  32. {
  33. "token": "荣耀",
  34. "start_offset": 3,
  35. "end_offset": 5,
  36. "type": "CN_WORD",
  37. "position": 4
  38. },
  39. {
  40. "token": "荣",
  41. "start_offset": 3,
  42. "end_offset": 4,
  43. "type": "CN_WORD",
  44. "position": 5
  45. },
  46. {
  47. "token": "耀",
  48. "start_offset": 4,
  49. "end_offset": 5,
  50. "type": "CN_CHAR",
  51. "position": 6
  52. }
  53. ]
  54. }