springboot基于Elasticsearch6.x版本进行ES同义词、停用词(停止词)插件配置,远程词典热加载及数据库词典热加载总结,es停用词热更新,es同义词热更新

 

前言ES版本差异较大,建议跨版本的同学,可以先了解一下版本区别,建议不要跨版本使用插件或者进行项目调试。

     本总结主要基于6.x版本的6.5.1(6.2.2实测可用),分词器为IK,下载地址:https://github.com/medcl/elasticsearch-analysis-ik

     不做ES入门普及,直入正题。

     ES操作系统:win10(如ES部署在linux,相应操作需调整)
 


正题

请先阅读官方热更新文档:

热更新IK分词使用方法
目前该插件支持热更新IK分词,通过上文在IK配置文件中提到的如下配置

 	<!-用户可以在这里配置远程扩展字典-> 
	<输入 密钥 = “ remote_ext_dict ” >位置
 	 <!-用户可以在这里配置远程扩展停止字典-> 
	<输入 密钥 = “ remote_ext_stopwords “ >位置
其中location是指一个网址,其中http://yoursite.com/getCustomDict,该请求只需满足以下两点即可完成分词热更新。

该http请求需要返回两个头部(header),一个是Last-Modified,一个是ETag,这两个都是字符串类型,只要有一个发生变化,
该插件就会去抓取新的分词套筒更新词库。

该http请求返回的内容格式是一行一个分词,换行符用\n即可。

满足上面两点要求就可以实现热更新分词了,不需要重启ES实例。

可以将需自动更新的热词放在一个UTF-8编码的.txt文件里,放在nginx或其他简易http服务器下,当.txt文件修改时,
http服务器会在客户端请求该文件时自动返回相应的Last-Modified和ETag。可以另外做一个工具来从业务系统提取相关词汇,
并更新这个.txt文件。

注意:需要远程热加载,需要放开对应的远程访问安全策略

在java安装的jre/lib/security 下的java.policy文件内,

grant中添加一行
permission java.net.SocketPermission “*”, “connect,resolve”;   或者将*改为需要放开的host:port

非常重要,否则ES将无权限访问远程服务器

停用词(停止词)

由官方文档可知,对于停用词,我们可以直接在IK的配置文件ik/config/IKAnalyzer.cfg.xml配置

词典内容格式:

springboot基于Elasticsearch6.x版本进行ES同义词、停用词(停止词)插件配置,远程词典热加载及数据库词典热加载总结,es停用词热更新,es同义词热更新_第1张图片

停用词远程词典文件热加载:

springboot基于Elasticsearch6.x版本进行ES同义词、停用词(停止词)插件配置,远程词典热加载及数据库词典热加载总结,es停用词热更新,es同义词热更新_第2张图片

重启ES:这里为了看到ES加载停用词词典的效果,cmd默认编码为unicode,需要改为utf-8,

在ES/bin/ 内进入控制台,使用chcp 65001 进入utf-8界面,然后elasticsearch.bat启动ES。

成功启动ES后,ES会加载一次词典,并把词典内容展示

springboot基于Elasticsearch6.x版本进行ES同义词、停用词(停止词)插件配置,远程词典热加载及数据库词典热加载总结,es停用词热更新,es同义词热更新_第3张图片

到此停用词热加载词典文件加载成功。

同义词远程词典热加载

需要安装同义词插件

地址:https://github.com/bells/elasticsearch-analysis-dynamic-synonym

但是现在正式的同义词插件是没有6.x版本的,所以我是通过修改5.x低版本的同义词插件到6.x的。

6.x版本同义词插件提供:

扫码或搜索‘程序员修炼宝典’关注公众号   回复:同义词插件

springboot基于Elasticsearch6.x版本进行ES同义词、停用词(停止词)插件配置,远程词典热加载及数据库词典热加载总结,es停用词热更新,es同义词热更新_第4张图片

具体的修改方法:

https://blog.csdn.net/like_java_/article/details/107381018

 

下载完插件后,在ES/plugin下新建文件夹:dynamic-synonym,并将下载的插件解压到这个文件夹内,注意不要将插件压缩文件留在该文件夹内。

修改同义词插件的配置文件:plugin-descriptor.properties

elasticsearch.version=6.x   将版本修改为自己ES的对应版本,另外注释掉jvm site isolate 否则启动报错

Unknown properties in plugin descriptor: [jvm, site, isolated]

  • description:插件的描述信息,用来描述该插件的作用
  • version:插件的版本信息
  • name:插件在elasticsearch plugin中显示的名称
  • classname:插件的入口,需要实现Iplugin接口
  • java.version:插件采用的java版本信息
  • elasticsearch.version:插件发布到elasticsearch的那个特定版本上
  • 可选属性(作用暂时未知)
    • site:true表示发布为网站形式,_site目录下的内容将会起作用。
    • jvm:true表示设置的classname对应的类将会被加载,对于依赖的资源,配置等信息也需要打包成jar
    • isolation:如果插件应该有自己的类加载器,则为真。传递false是不赞成的,它只用于支持插件相互之间有很强的依赖性。如果这是如果不指定,则默认隔离插件。

重启ES,成功启动则说明同义词插件已载入ES。

ES同义词热加载的实现:

同义词远程加载需要在创建索引时加入:

PUT /test_remote_index
{
"settings":{
	"index" : {
	    "analysis" : {
	        "analyzer" : {
	            "synonym" : {
	                "tokenizer" : "whitespace",
	                "filter" : ["remote_synonym"]
 	           }
	        },
	        "filter" : {
	            "remote_synonym" : {
	                "type" : "dynamic_synonym",
	                "synonyms_path" : "http://127.0.0.1:9090/synonyms.txt", //在这里改为你的远程同义词词典即可
	                "interval": 30
	            },
	            "local_synonym" : {
	                "type" : "dynamic_synonym",
	                "synonyms_path" : "synonym.txt"
	            }
	        }
	    }
	}
}
}

创建完索引后,在该索引内所有的数据,均可通过同义词词典进行同义词查询增强。

同义词词典这里采用的是同级同义词:

springboot基于Elasticsearch6.x版本进行ES同义词、停用词(停止词)插件配置,远程词典热加载及数据库词典热加载总结,es停用词热更新,es同义词热更新_第5张图片

加载远程词典进行ES同义词、停用词热更新的问题:

1.热更新时效问题:

阅读官方文档可知,ES的更新取决于获取远程词典的lastmodify和Etag;由于我的词典是放在tomcat服务器中的,tomcat服务器在远程调用响应中是没有ETag属性的,所以ES只能根据lastmodify来进行更新,然而lastmodify只精确到分钟,一旦在同一分钟内多次调整词典,ES是监控不到词典变化,影响词典的及时热更新。

2.词典动态配置问题

由于词典以文件的方式保存在服务器中,如果想要实现词典的修改,是非常麻烦的,所以实现远程词典的动态配置是必要的。

3.更新同义词、停用词ES历史数据未生效问题

在成功配置停用词、同义词后,使用中发现一个问题,在ES更新停用词后,之前已载入ES的数据,依然是按照旧停用词词典进行的分词。ES更新同义词词典后,之前已建好的索引内的数据,查询时仍然按照旧的同义词词典进行。因此,需要使ES历史数据动态更新。

 

问题解决方案:

1.热更新时效问题解决方案:

通过阅读官方文档,我们知道ES停用词远程热加载配置,有另一种方法即引入API。

通过API,响应ETag和lastModify即可满足ES及时更新。

http://host:port/goods-search/goodsSearchRuleController/getDictionary?path=文件路径

因此需要一个后端接口如下:

/**
     * ES调用,实现词典热更新API from File
     * Description: 
* * @author author
* @taskId
* @param request * @param response * @return
*/ @RequestMapping(value = "/getDictionary", method = {RequestMethod.HEAD, RequestMethod.POST, RequestMethod.GET}) public String getDictionary(HttpServletRequest request, HttpServletResponse response) { DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); // es最后更新词库时间 LocalDateTime esDictionaryLastUpdateTime = LocalDateTime.now(); cacheUtil.setValue("searchrule", "esDictionaryLastUpdateTime", esDictionaryLastUpdateTime.format(dateTimeFormatter)); logger.info("===================开始进行ES热更新 {}===============", esDictionaryLastUpdateTime); try { String path = request.getParameter("path"); File file = new File(path); // 词典最后更新时间 LocalDateTime fileLastModifiedTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(file.lastModified()), ZoneId.systemDefault()); cacheUtil.setValue("searchrule", "fileLastModifiedTime", fileLastModifiedTime.format(dateTimeFormatter)); InputStream fileInput = new FileInputStream(file); BufferedReader read = new BufferedReader(new InputStreamReader(fileInput, "utf-8")); StringBuffer buff = new StringBuffer(); String str = null; while ((str = read.readLine()) != null) { buff.append(str + "\n"); } String content = buff.toString().trim(); read.close(); fileInput.close(); response.addHeader("ETag", MD5Util.encryption(content)); response.addHeader("Last-Modified", file.lastModified() + ""); return content; } catch (Exception e) { logger.error("ES实时热更新词典异常!", e.getMessage(), e); response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); return HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(); } }

lastmodify为词典文件的最后更新时间,ETag为文件内容的md5值。

2.词典动态配置问题解决方案

既然通过API可以实现ES热更新词典,响应也是通过response直接返回词典内容。

那为了实现词典的可动态配置。我们也可以直接将词典放入数据库,在这里从数据库读取也可以。

http://host:port/goods-search/goodsSearchRuleController/getDictionaryFromDB?analysisType=1
/**
     * ES调用,实现词典热更新API from DB
     * Description: 
* * @author author
* @taskId
* @param request * @param response * @return
*/ @RequestMapping(value = "/getDictionaryFromDB", method = {RequestMethod.HEAD, RequestMethod.POST, RequestMethod.GET}) public String getDictionaryFromDB(HttpServletRequest request, HttpServletResponse response) { DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); // es最后更新词库时间 记录时间是为了解决ES历史数据更新的问题 LocalDateTime esDictionaryLastUpdateTime = LocalDateTime.now(); cacheUtil.setValue("searchrule", "esDictionaryLastUpdateTime", esDictionaryLastUpdateTime.format(dateTimeFormatter)); String analysisType = request.getParameter("analysisType"); //1表示停用词 2表示同义词 if (analysisType == null || (!"1".equals(analysisType) && !"2".equals(analysisType))) { logger.error("参数不正确!"); response.setStatus(HttpStatus.PRECONDITION_FAILED.value()); return "analysisType incorrect"; } logger.info("===================开始进行ES {}热更新 {}===============", analysisType, esDictionaryLastUpdateTime); try { FilterWordContentResponse contentResponse = searchRuleService.getDictionaryFromDB(analysisType); if (contentResponse == null) { logger.warn("数据库暂无可用词典!"); response.addHeader("ETag", MD5Util.encryption("")); response.addHeader("Last-Modified", new Date(0) + ""); return ""; } response.addHeader("ETag", MD5Util.encryption(contentResponse.getContent())); response.addHeader("Last-Modified", contentResponse.getLastModify() + ""); return contentResponse.getContent(); } catch (Exception e) { logger.error("ES实时热更新词典异常!", e.getMessage(), e); response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); return HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(); } } @Override public FilterWordContentResponse getDictionaryFromDB(String analysisType) { String content = null; Date lastModify = null; SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); FilterWordContentResponse contentResponse = new FilterWordContentResponse(); if ("1".equals(analysisType)) { List stopwords = searchFilterWordMapper.selectByTypeAndStatus(); lastModify = searchFilterWordMapper.selectStopwordLastModify(); content = analysisList(stopwords); if (content == null) { return null; } cacheUtil.setValue("searchrule", "stopwordLastModifiedTime", dateFormat.format(lastModify)); } else if ("2".equals(analysisType)) { List synonymList = searchFilterWordMapper.selectSynonymWords(); lastModify = searchFilterWordMapper.selectSynonymLastModify(); content = analysisList(synonymList); if (content == null) { return null; } cacheUtil.setValue("searchrule", "synonymwordLastModifiedTime", dateFormat.format(lastModify)); } contentResponse.setContent(content); contentResponse.setLastModify(lastModify); return contentResponse; }

Etag为读出数据库词典内容的md5值,lastmodify是数据库内词典的最新更新时间。

 

3.更新同义词、停用词ES历史数据未生效问题解决方案

ES自身带有一个更新历史索引的API,updateByQuery,可按正则更新目标索引。

http://127.0.0.1:9200/*/_update_by_query?conflicts=proceed

因此,我们可通过springboot调用该API实现ES热更新词典后历史数据的更新。

该更新历史数据暂时设置为定时更新,为了不造成ES和工程系统压力,我们只需ES在停用词、同义词词库有变动并成功更新后执行一次历史数据更新。

细心地同学可能注意到了,我在解决热更新时效问题的代码中记录的三个时间:

词库最后的更新时间,ES加载词库的更新时间,ES历史数据的更新时间

通过这三个时间确保updateByQuery在每次热加载后只执行一次。

代码如下:

/**
     * 更新索引历史数据,用于同义词及停用词典热更新后,ES历史数据的新规则同步
     * 未进行速率测试
     * Description: 
* * @author
* @taskId
* @return
*/ @PostMapping("/updateByQuery") public BssResult updateByQuery(){ try { LocalDateTime oldesLastUpdateByQueryTime = null; LocalDateTime wordLastModifiedTime = null; DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); String esLastUpdateByQueryTimeStr = cacheUtil.getValue("searchrule", "esLastUpdateByQueryTime"); String stopwordLastModifiedTimeStr = cacheUtil.getValue("searchrule", "stopwordLastModifiedTime"); String synonymwordLastModifiedTimeStr = cacheUtil.getValue("searchrule", "synonymwordLastModifiedTime"); String esDictionaryLastUpdateTimeStr = cacheUtil.getValue("searchrule", "esDictionaryLastUpdateTime"); if (esLastUpdateByQueryTimeStr == null) { logger.info("无更新ES历史数据记录! 需要进行更新~"); } else { oldesLastUpdateByQueryTime = LocalDateTime.parse(esLastUpdateByQueryTimeStr,dateTimeFormatter); } if (stopwordLastModifiedTimeStr == null || synonymwordLastModifiedTimeStr == null) { return new BssResult<>(BusiCodeEnum.SYSTEM_ERROR, "文件最后修改时间为空!"); } LocalDateTime stopwordLastModifiedTime = LocalDateTime.parse(stopwordLastModifiedTimeStr,dateTimeFormatter); LocalDateTime synonymwordLastModifiedTime = LocalDateTime.parse(synonymwordLastModifiedTimeStr,dateTimeFormatter); wordLastModifiedTime = stopwordLastModifiedTime.isBefore(synonymwordLastModifiedTime) ? synonymwordLastModifiedTime : stopwordLastModifiedTime; if (esDictionaryLastUpdateTimeStr == null) { return new BssResult<>(BusiCodeEnum.SYSTEM_ERROR, "ES最新加载词典时间为空!"); } LocalDateTime esDictionaryLastUpdateTime = LocalDateTime.parse(esDictionaryLastUpdateTimeStr,dateTimeFormatter); if ((oldesLastUpdateByQueryTime == null || oldesLastUpdateByQueryTime.isBefore(wordLastModifiedTime))) { //表示未按照最新词典更新历史数据 while (true) { if (esDictionaryLastUpdateTime.isAfter(wordLastModifiedTime)) { //es最后更新词库时间在文件最后修改后,说明最新词典已加载到es //最后更新es历史数据时间 历史数据只在ES更新词库后更新一次 LocalDateTime esLastUpdateByQueryTime = LocalDateTime.now(); cacheUtil.setValue("searchrule", "esLastUpdateByQueryTime", esLastUpdateByQueryTime.format(dateTimeFormatter)); return searchRuleService.updateByQuery(); } else { logger.warn("ES未加载最新词典,一秒后重试~"); Thread.sleep(1000); } } } else { return new BssResult<>(BusiCodeEnum.SUCCESS, "无需更新!词典最后更新时间【"+ wordLastModifiedTime+"】,ES最后更新词典时间【"+esDictionaryLastUpdateTime+"】,ES索引最后更新时间【"+oldesLastUpdateByQueryTime+"】"); } } catch (UnknownHostException e) { logger.error("ES历史数据更新异常!", e.getMessage(), e); return new BssResult<>(BusiCodeEnum.SYSTEM_ERROR); } catch (Exception e) { logger.error("ES历史数据更新异常!", e.getMessage(), e); return new BssResult<>(BusiCodeEnum.SYSTEM_ERROR); } } @Override public BssResult updateByQuery() throws UnknownHostException { TransportClient client = searchStrategyUtil.getTransportClient(); logger.info("连接es成功--------------------------"); UpdateByQueryRequestBuilder updateByQuery = UpdateByQueryAction.INSTANCE.newRequestBuilder(client); updateByQuery.source("*").abortOnVersionConflict(false); BulkByScrollResponse response = updateByQuery.get(); logger.info("ES历史数据更新成功----------------"); return new BssResult<>(BusiCodeEnum.SUCCESS, response.getUpdated()); }

这里需要注意的是,ES的版本区别,导致updateByQuery调用方式不同,这里采用的是tcp连接方式来执行的更新,跳槽tcp的端口注意一下与http是不同的。

 /**
     * transportClient http方式连接
     * Description: 
* * @author
* @taskId
* @return * @throws UnknownHostException
*/ public TransportClient getTransportClient() throws UnknownHostException { Settings settings = Settings.builder() .put("cluster.name", "es") .put("client.transport.sniff", true) //自动翻译为节点树 .put("client.transport.ping_timeout", "60s") .put("client.transport.nodes_sampler_interval","20s") .build(); ; try { /* TransportAddress master = new TransportAddress(InetAddress.getByName(host), httpPort); TransportClient client = new PreBuiltTransportClient(settings) .addTransportAddress(master);//单机版 */ TransportClient client = new PreBuiltTransportClient(settings) .addTransportAddresses(new TransportAddress(InetAddress.getByName(localHost1), localHttpPort1) , new TransportAddress(InetAddress.getByName(localHost2), localHttpPort2) , new TransportAddress(InetAddress.getByName(localHost3), localHttpPort3)); return client; } catch (UnknownHostException e) { throw new UnknownHostException(); } }

如果有什么问题或优化建议,希望能留言,共同学习进步,谢阅!

 

 

你可能感兴趣的:(中间件,java,elasticsearch)