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 ” >位置</ 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"
	            }
	        }
	    }
	}
}
}

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

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

加载远程词典进行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及时更新。

<entry key="remote_ext_stopwords">http://host:port/goods-search/goodsSearchRuleController/getDictionary?path=文件路径</entry>

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

/**
     * ES调用,实现词典热更新API from File
     * Description: <br> 
     *  
     * @author author<br>
     * @taskId <br>
     * @param request
     * @param response
     * @return <br>
     */
    @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直接返回词典内容。

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

<entry key="remote_ext_stopwords">http://host:port/goods-search/goodsSearchRuleController/getDictionaryFromDB?analysisType=1</entry>
/**
     * ES调用,实现词典热更新API from DB
     * Description: <br> 
     *  
     * @author author<br>
     * @taskId <br>
     * @param request
     * @param response
     * @return <br>
     */
    @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<String> 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<SynonymWordsResponse> 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: <br> 
     *  
     * @author <br>
     * @taskId <br>
     * @return <br>
     */
    @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: <br> 
     *  
     * @author <br>
     * @taskId <br>
     * @return
     * @throws UnknownHostException <br>
     */
    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();
        }
        
    }

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

 

 

  • 3
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Elasticsearch同义词过滤器实际上是一个基于词典的映射表,将同义词映射为一个或多个主。在分析文本时,Elasticsearch 将会根据同义词过滤器中的词典,自动将同义词替换为对应的主。 具体来说,当一个文本被索引到 Elasticsearch 中时,Elasticsearch 会先将文本分,然后对每个进行分析。在分析过程中,如果遇到一个同义词Elasticsearch 就会将其替换为对应的主,然后继续进行分析。这样,同义词就能够被视为相同的汇,从而实现同义词检索。 同义词过滤器的实现依赖于 Elasticsearch 的分析器。分析器由一系列分器和过滤器组成,其中分器将文本分割成单,而过滤器则对单进行处理。同义词过滤器就是一种特殊的过滤器,它会在分析过程中,对单进行同义词替换。 同义词过滤器的词典可以是一个文本文件,也可以是一个 Elasticsearch 索引。如果使用文本文件,可以在词典中指定同义词,每行一个同义词,用空格或逗号分隔。如果使用 Elasticsearch 索引,可以通过查询 API 动态获取同义词,从而实现动态的同义词检索。 总之,Elasticsearch同义词过滤器实现了同义词检索的功能,通过自动将同义词转换为主,实现了对同义词的处理和索引。这使得 Elasticsearch 能够更准确地匹配用户的查询,并返回更精确的搜索结果。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值