前言: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 ” >位置 entry >
<!-用户可以在这里配置远程扩展停止字典->
<输入 密钥 = “ remote_ext_stopwords “ >位置 entry >
其中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配置
词典内容格式:
重启ES:这里为了看到ES加载停用词词典的效果,cmd默认编码为unicode,需要改为utf-8,
在ES/bin/ 内进入控制台,使用chcp 65001 进入utf-8界面,然后elasticsearch.bat启动ES。
成功启动ES后,ES会加载一次词典,并把词典内容展示
到此停用词热加载词典文件加载成功。
需要安装同义词插件
地址:https://github.com/bells/elasticsearch-analysis-dynamic-synonym
但是现在正式的同义词插件是没有6.x版本的,所以我是通过修改5.x低版本的同义词插件到6.x的。
6.x版本同义词插件提供:
扫码或搜索‘程序员修炼宝典’关注公众号 回复:同义词插件
具体的修改方法:
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"
}
}
}
}
}
}
创建完索引后,在该索引内所有的数据,均可通过同义词词典进行同义词查询增强。
同义词词典这里采用的是同级同义词:
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();
}
}
如果有什么问题或优化建议,希望能留言,共同学习进步,谢阅!