ElasticSearch IK热词自动热更新原理与Golang实现
热更新概述
- 借助Nginx,在其某个目录结构下放一个dic.txt,我们只要更新这个文件,不需要重启ES也能达到热更新的目的。优点是简单,无需开发,缺点就是不够灵活。
- 自己开发一个HTTP接口,返回词库。注意:一行代表一个词,http body中,自己追加\n换行。
热更新原理
/**
* 监控流程:
* ①向词库服务器发送Head请求
* ②从响应中获取Last-Modify、ETags字段值,判断是否变化
* ③如果未变化,休眠1min,返回第①步
* ④如果有变化,重新加载词典
* ⑤休眠1min,返回第①步
*/ public void runUnprivileged() {
//超时设置
RequestConfig rc = RequestConfig.custom().setConnectionRequestTimeout(10*1000)
.setConnectTimeout(10*1000).setSocketTimeout(15*1000).build(); HttpHead head = new HttpHead(location);
head.setConfig(rc); //设置请求头
if (last_modified != null) {
head.setHeader("If-Modified-Since", last_modified);
}
if (eTags != null) {
head.setHeader("If-None-Match", eTags);
} CloseableHttpResponse response = null;
try {
response = httpclient.execute(head); //返回200 才做操作
if(response.getStatusLine().getStatusCode()==200){ if (((response.getLastHeader("Last-Modified")!=null) && !response.getLastHeader("Last-Modified").getValue().equalsIgnoreCase(last_modified))
||((response.getLastHeader("ETag")!=null) && !response.getLastHeader("ETag").getValue().equalsIgnoreCase(eTags))) { // 远程词库有更新,需要重新加载词典,并修改last_modified,eTags
Dictionary.getSingleton().reLoadMainDict();
last_modified = response.getLastHeader("Last-Modified")==null?null:response.getLastHeader("Last-Modified").getValue();
eTags = response.getLastHeader("ETag")==null?null:response.getLastHeader("ETag").getValue();
}
}else if (response.getStatusLine().getStatusCode()==304) {
//没有修改,不做操作
//noop
}else{
logger.info("remote_ext_dict {} return bad code {}" , location , response.getStatusLine().getStatusCode() );
}
} catch (Exception e) {
logger.error("remote_ext_dict {} error!",e , location);
}finally{
try {
if (response != null) {
response.close();
}
} catch (IOException e) {
logger.error(e.getMessage(), e);
}
}
}
- 先发送Http HEAD请求,获取Last-Modified、ETag(里面都是字符串)
- 如果其中有一个变化,则继续发送Get请求,获取词库内容。
HEAD 格式
HEAD方法跟GET方法相同,只不过服务器响应时不会返回消息体。一个HEAD请求的响应中,HTTP头中包含的元信息应该和一个GET请求的响应消息相同。这种方法可以用来获取请求中隐含的元信息,而不用传输实体本身。也经常用来测试超链接的有效性、可用性和最近的修改。
一个HEAD请求的响应可被缓存,也就是说,响应中的信息可能用来更新之前缓存的实体。如果当前实体跟缓存实体的阈值不同(可通过Content-Length、Content-MD5、ETag或Last-Modified的变化来表明),那么这个缓存就被视为过期了。
$ curl --head http://127.0.0.1:9800/es/steelDict
HTTP/1.1 200 OK
Etag: DefaultTags
Last-Modified: 2021-10-15 14:49:35
Date: Fri, 15 Oct 2021 07:23:15 GMT
GET 格式
- 返回词库时,Content-Length、charset=UTF-8一定要有。
- Last-Modified和Etag 只需要1个有变化即可。只有当HEAD请求返回时,这2个其中一个字段的值变了,才会发送GET请求获取内容,请注意!
- 一行代表一个词,自己追加\n换行
$ curl -i http://127.0.0.1:9800/es/steelDict
HTTP/1.1 200 OK
Content-Length: 130
Content-Type: text/html;charset=UTF-8
Etag: DefaultTags
Last-Modified: 2021-10-15 14:49:35
Date: Fri, 15 Oct 2021 07:37:47 GMT 装饰管
装饰板
圆钢
无缝管
无缝方管
卫生级无缝管
卫生级焊管
热轧中厚板
热轧平板
热轧卷平板
实现
配置ES IK分词器
# 这里以centos 7为例,通过rpm安装
$ vim /usr/share/elasticsearch/plugins/ik/config/IKAnalyzer.cfg.xml
# 改这一行,换成我们的地址
<entry key="remote_ext_dict">http://10.16.52.52:9800/es/steelDict</entry>
$ systemctl restart elasticsearch # 重启es # 这里还可以实时看到日志,比较方便
$ tail -f /var/log/elasticsearch/my-application.log
[2021-10-15T15:02:31,448][INFO ][o.w.a.d.Monitor ] [node-1] 获取远程词典成功,总数为:0
[2021-10-15T15:02:31,952][INFO ][o.e.l.LicenseService ] [node-1] license [3ca1dc7b-3722-40e5-916e-3b2093980b75] mode [basic] - valid
[2021-10-15T15:02:31,962][INFO ][o.e.g.GatewayService ] [node-1] recovered [1] indices into cluster_state
[2021-10-15T15:02:32,812][INFO ][o.e.c.r.a.AllocationService] [node-1] Cluster health status changed from [RED] to [YELLOW] (reason: [shards started [[steel-category-mapping][2]] ...]).
[2021-10-15T15:02:41,630][INFO ][o.w.a.d.Monitor ] [node-1] 重新加载词典...
[2021-10-15T15:02:41,631][INFO ][o.w.a.d.Monitor ] [node-1] try load config from /etc/elasticsearch/analysis-ik/IKAnalyzer.cfg.xml
[2021-10-15T15:02:41,631][INFO ][o.w.a.d.Monitor ] [node-1] try load config from /usr/share/elasticsearch/plugins/ik/config/IKAnalyzer.cfg.xml
[2021-10-15T15:02:41,886][INFO ][o.w.a.d.Monitor ] [node-1] [Dict Loading] http://10.16.52.52:9800/es/steelDict
[2021-10-15T15:02:43,958][INFO ][o.w.a.d.Monitor ] [node-1] 获取远程词典成功,总数为:0
[2021-10-15T15:02:43,959][INFO ][o.w.a.d.Monitor ] [node-1] 重新加载词典完毕...
Golang接口
const (
kUrlSyncESIndex = "/syncESIndex" // 同步钢材品名、材质、规格、产地、仓库到ES索引中
kUrlGetSteelHotDict = "/steelDict" // 获取钢材字典(品材规产仓)
) func InitRouter(router *gin.Engine) {
// ... esRouter := router.Group("es")
// 同一个接口,根据head/get来决定是否返回数据部,避免宽带浪费
esRouter.HEAD(kUrlGetSteelHotDict, onHttpGetSteelHotDictHead)
esRouter.GET(kUrlGetSteelHotDict, onHttpGetSteelHotDict) // ...
}
// onHttpGetSteelHotDictHead 处理head请求,只有当Last-Modified 或 ETag 其中1个值改变时,才会出发GET请求获取词库列表
func onHttpGetSteelHotDictHead(ctx *gin.Context) {
t, err := biz.QueryEsLastSyncTime()
if err != nil {
ctx.JSON(http.StatusOK, gin.H{
"code": biz.StatusError,
"msg": "server internal error",
})
logger.Warn(err)
return
}
ctx.Header("Last-Modified", t)
ctx.Header("ETag", kDefaultTags)
}
// onHttpGetSteelHotDict 处理GET请求,返回真正的词库,每一行一个词
func onHttpGetSteelHotDict(ctx *gin.Context) {
// 这里从mysql查询词库,dic是一个[]string切片
dic, err := biz.QuerySteelHotDic()
if err != nil {
ctx.JSON(http.StatusOK, gin.H{
"code": biz.StatusError,
"msg": "server internal error",
})
logger.Warn(err)
return
} // 这里查询最后一次更新时间,作为判断词库需要更新的标准
t, err := biz.QueryEsLastSyncTime()
if err != nil {
ctx.JSON(http.StatusOK, gin.H{
"code": biz.StatusError,
"msg": "server internal error",
})
logger.Warn(err)
return
} ctx.Header("Last-Modified", t)
ctx.Header("ETag", kDefaultTags) body := ""
for _, v := range dic {
if v != "" {
body += v + "\n"
}
}
logger.Infof("%s query steel dict success, count = %d", ctx.Request.URL, len(dic)) buffer := []byte(body)
ctx.Header("Content-Length", strconv.Itoa(len(buffer)))
ctx.Data(http.StatusOK, "text/html;charset=UTF-8", buffer)
}
效果
POST http://10.0.56.153:9200/_analyze
{
"analyzer": "ik_smart",
"text": "武钢 Q235B 3*1500*3000 6780 佰隆库 在途整件出"
} {
"tokens": [
{
"token": "武钢",
"start_offset": 0,
"end_offset": 2,
"type": "CN_WORD",
"position": 0
},
{
"token": "q235b",
"start_offset": 3,
"end_offset": 8,
"type": "CN_WORD",
"position": 1
},
{
"token": "3*1500*3000",
"start_offset": 9,
"end_offset": 20,
"type": "ARABIC",
"position": 2
},
{
"token": "6780",
"start_offset": 21,
"end_offset": 25,
"type": "ARABIC",
"position": 3
},
{
"token": "佰隆库",
"start_offset": 26,
"end_offset": 29,
"type": "CN_WORD",
"position": 4
},
{
"token": "在途",
"start_offset": 30,
"end_offset": 32,
"type": "CN_WORD",
"position": 5
},
{
"token": "整件",
"start_offset": 32,
"end_offset": 34,
"type": "CN_WORD",
"position": 6
},
{
"token": "出",
"start_offset": 34,
"end_offset": 35,
"type": "CN_CHAR",
"position": 7
}
]
}
/**
* 加载远程扩展词典到主词库表
*/
private void loadRemoteExtDict() {
// ...
for (String theWord : lists) {
if (theWord != null && !"".equals(theWord.trim())) {
// 加载扩展词典数据到主内存词典中
// 注释这一行:
// logger.info(theWord);
_MainDict.fillSegment(theWord.trim().toLowerCase().toCharArray());
}
}
// ...
}
mvn package

后记
调试接口不生效
/**
* 加载远程扩展词典到主词库表
*/
private void loadRemoteExtDict() {
List<String> remoteExtDictFiles = getRemoteExtDictionarys();
for (String location : remoteExtDictFiles) {
logger.info("[Dict Loading] " + location);
List<String> lists = getRemoteWords(location);
// 如果找不到扩展的字典,则忽略
if (lists == null) {
logger.error("[Dict Loading] " + location + "加载失败");
continue;
} else {
logger.info("获取远程词典成功,总数为:" + lists.size());
}
for (String theWord : lists) {
if (theWord != null && !"".equals(theWord.trim())) {
// 加载扩展词典数据到主内存词典中
logger.info(theWord);
_MainDict.fillSegment(theWord.trim().toLowerCase().toCharArray());
}
}
}
}
[2021-10-15T15:02:41,886][INFO ][o.w.a.d.Monitor] [node-1] [Dict Loading] http://10.16.52.52:9800/es/steelDict
[2021-10-15T15:02:43,958][INFO ][o.w.a.d.Monitor] [node-1] 获取远程词典成功,总数为:0
[2021-10-15T15:02:43,959][INFO ][o.w.a.d.Monitor] [node-1] 重新加载词典完毕...
public static void main(String[] args) {
List<String> words = getRemoteWordsUnprivileged("http://127.0.0.1:9800/es/steelDict");
System.out.println(words.size());
}
数字分词如何把*号不过滤
/**
* 英文字符及阿拉伯数字子分词器
*/
class LetterSegmenter implements ISegmenter {
// ... //链接符号(这里追加*号)
private static final char[] Letter_Connector = new char[]{'#', '&', '+', '-', '.', '@', '_', '*'};
//数字符号(这里追加*号)
private static final char[] Num_Connector = new char[]{',', '.', '*'}; // ...
}
关于作者
推荐下自己的开源IM,纯Golang编写:
CoffeeChat:https://github.com/xmcy0011/CoffeeChat
opensource im with server(go) and client(flutter+swift)
参考了TeamTalk、瓜子IM等知名项目,包含服务端(go)和客户端(flutter+swift),单聊和机器人(小微、图灵、思知)聊天功能已完成,目前正在研发群聊功能,欢迎对golang感兴趣的小伙伴Star加关注。
ElasticSearch IK热词自动热更新原理与Golang实现的更多相关文章
- solr6.3.0升级与IK动态词库自动加载
摘要:对于中文的搜索来说,词库系统是一个很比较重要的模块,本篇以IK分词器为例子,介绍如何让分词器从缓存或文件系统中自动按照一定频次进行加载扩展词库 Lucene.Solr或ElasticStack如 ...
- Python 爬取 热词并进行分类数据分析-[热词关系图+报告生成]
日期:2020.02.05 博客期:144 星期三 [本博客的代码如若要使用,请在下方评论区留言,之后再用(就是跟我说一声)] 所有相关跳转: a.[简单准备] b.[云图制作+数据导入] c.[拓扑 ...
- 【热更新IK词典】ElasticSearch IK 自动热更新原理与实现
一.热更新原理 elasticsearch开启加载外部词典功功能后,会每60s间隔进行刷新字典.具体原理代码如下所示: public void loadDic(HttpServletRequest r ...
- ElasticStack系列之七 & IK自动热更新原理与实现
一.热更新原理 elasticsearch开启加载外部词典功功能后,会每60s间隔进行刷新字典.具体原理代码如下所示: public void loadDic(HttpServletRequest r ...
- es之词库热更新解决方案
1. 下载tomcat,作为远程词库的容器 , 需要在tomcat中配置词库 /webapp/ROOT这个路径下新建一个远程词库:Vim hot.dicHot.dic中存放的就是实时热词 2.测试t ...
- 深入理解xLua热更新原理
热更新简介 热更新是指在不需要重新编译打包游戏的情况下,在线更新游戏中的一些非核心代码和资源,比如活动运营和打补丁.热更新分为资源热更新和代码热更新两种,代码热更新实际上也是把代码当成资源的一种热更新 ...
- webpack与browser-sync热更新原理深度讲解
本文首发于CSDN网站,下面的版本又经过进一步的修订.原文:webpack与browser-sync热更新原理深度讲解本文包含如下内容: webpack-hot-middleware EventSou ...
- Unity3D热更新之LuaFramework篇[08]--热更新原理及热更服务器搭建
前言 前面铺垫了这么久,终于要开始写热更新了. Unity游戏热更新包含两个方面,一个是资源的更新,一个是脚本的更新. 资源更新是Unity本来就支持的,在各大平台也都能用.而脚本的热更新在iOS平台 ...
- 图解 Webpack 4.x 热更新原理
图解 Webpack 4.x 热更新原理 Webpack HMR ️ module.hot & module.hot.accept if (module.hot) { module.hot.a ...
随机推荐
- CNN的Pytorch实现(LeNet)
CNN的Pytorch实现(LeNet) 上次写了一篇CNN的详解,可是累坏了老僧我.写完后拿给朋友看,朋友说你这Pytorch的实现方式对于新人来讲会很不友好,然后反问我说里面所有的细节你都明白 ...
- django1.9和mysql
修改setting.py DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql',#使用mysql 'NAME': 'jiank ...
- 一个基于activiti审批流程示例,如何与系统整合
前言 目前市场上有很多开源平台没有整合工作流,即使有,也是价格不菲的商业版,来看这篇文章的估计也了解了行情,肯定不便宜.我这个快速开发平台在系统基础功能(用户管理,部门管理-)上整合了工作流,你可以直 ...
- PS-头发丝抠图
[PS版本]Photoshop CS5 [主题]头发丝抠图 [操作步骤] 第一步:打开待处理图片,复制图层: 第二步:快速选择工具选择主体(也可用魔术棒用具选择背景色,然后反向): 第三步:点击&qu ...
- Git&Github介绍
git&github 什么是GIT 是一个源代码管理工具 源代码为什么要管理起来? 你写的东西就叫源代码,第三方的库和框架都不算. 让源代码可以被追溯,主要记录每次变更了什么,谁主导这次变化. ...
- JavaScript高级程序设计(读书笔记)之BOM
BOM(Browser Object Model)提供了很多对象用于访问浏览器的功能,这些功能与任何网页内容无关. 8.1 window对象 BOM的核心对象是window,它表示一个浏览器实例.在浏 ...
- thymeleaf+layui加载页面渲染时报错
将freemaker替换成thymeleaf时出现以下问题: org.thymeleaf.exceptions.TemplateProcessingException: Could not parse ...
- rtl8188eu 驱动移植
测试平台 宿主机平台:Ubuntu 16.04.6 目标机:iMX6ULL 目标机内核:Linux 4.1.15 rtl8188eu 驱动移植 在网上下载Linux版的驱动源码: wifi驱动的实现有 ...
- Gitlab(2)- centos7.x 下安装社区版 Gitlab 以及它的配置管理
前置准备:虚拟机安装以及配置相关 包含安装 centos7.8 虚拟机.设置静态 ip 等 https://www.cnblogs.com/poloyy/category/1703784.html 注 ...
- 10分钟学会VS NuGet包私有化部署
前言 我们之前实现了打包发布NuGet,但是发布后的引用是公有的,谁都可以访问,显然这种方式是不可取的. 命令版本:10分钟学会Visual Studio将自己创建的类库打包到NuGet进行引用(ne ...