远程字典
ES-ik的字典加载支持远程扩展字典,并且可以实现热加载,基于此可以实现自定义字典的近实时加载,从而做到对分词结果的灵活控制。
配置与原理
ES-ik是ES的一个分词插件,放在在elasticsearch\plugins下面,在ES-ik的config\ik\IKAnalyzer.cfg.xml中配置了分词依赖的相关字典信息,包括“用户自定义扩展字典”、“用户自定义扩展停止词字典”、“远程扩展字典”、“远程扩展停止词字典”,如图:
远程扩展字典是配置一个远程的url获取,将url返回的字典数据加载到ES-ik分词依赖的内存字典中,实现字典数据的非本地化管理。如图:
关于远程扩展字典,ES-ik的依赖包中的实现如下,会每隔1分钟检测下远程扩展字典是否有更新,如果有更新,那么就会将最新的数据加载进来。
Monitor.java中的关键代码如下,会根据Last-Modified字段或者ETag字段的变化来判断字典是否有更新。
表结构设计
功能设计
导入字典单词
将行业单词,比如设备的机型、品牌、型号等作为字典批量导入,这样就可以对行业单词按照字典进行准确的分词。
添加扩展字典单词
从管理后台系统操作添加字典单词将字典数据保存到ik_ext_dict表中,同时更新ik_ext_dict表的更新时间。例如:新的机型、品牌、型号等数据,
添加停用字典单词
将停用词数据保存到ik_ext_stop_dict表中,同时更新ik_ext_stop_dict表的更新时间,例如:一些不需要在全文检索过程中参与分词要被过滤掉的单词。
远程字典接口实现
“远程扩展字典”的数据加载,服务端提供接口,首先从redis的ext_dict中获取,如果ext_dict中有数据,直接返回,如果没有,从ik_ext_dict表中加载数据并返回,同时将数据set到redis中ext_dict缓存中。代码如下:
public void remoteExtDic(HttpServletRequest request, HttpServletResponse response) {
try {
OutputStream out = response.getOutputStream();
String modifyTime = request.getHeader("If-Modified-Since");
String remoteDicModifyTime = ikDicService.getRemoteDicModifyTime();
response.setHeader("ETag", "extDic");
response.setHeader("Last-Modified", remoteDicModifyTime);
response.setContentType(contentType);
if(!remoteDicModifyTime.equals(modifyTime)){
String remoteDicText = ikDicService.getRemoteDicText();
out.write(remoteDicText.getBytes(charsetName));
}
out.flush();
out.close();
} catch (Exception e) {
logger.error("远程扩展词字典接口异常", e);
}
}
“远程扩展停用词字典”的数据加载,服务端提供接口,从redis中的ext_stop_dict获取,如果没有,从ik_ext_stop_dict表中加载数据并返回,同时将数据set到redis中的ext_stop_dict缓存中。
public void remoteStopDic(HttpServletRequest request, HttpServletResponse response) {
try {
OutputStream out = response.getOutputStream();
String modifyTime = request.getHeader("If-Modified-Since");
String remoteStopDicModifyTime = ikDicService.getRemoteStopDicModifyTime();
response.setHeader("ETag", "stopDic");
response.setHeader("Last-Modified", remoteStopDicModifyTime);
response.setContentType(contentType);
if(!remoteStopDicModifyTime.equals(modifyTime)){
String remoteStopDicText = ikDicService.getRemoteStopDicText();
out.write(remoteStopDicText.getBytes(charsetName));
}
out.flush();
out.close();
} catch (Exception e) {
logger.error("远程扩展停用词字典接口异常", e);
}
}
索引版本管理
ES对索引字段的重新分词需要重建索引实现,在白天系统正在运行的时候如果重建索引就会影响到线上系统的使用。
可以对索引进行版本管理,每次索引的重建都是另外建立一个新的版本,在索引重建的过程中,系统还是从老版本的索引中获取数据,等新版本的索引完成后,将索引指向新版本,然后将老版本索引删掉。
以设备索引为例,程序中访问的索引名称是equipment,具体的操作如下:
1.为旧版本索引建立别名,例如:给equipment_old建立别名为equipment
PUT 'http://192.168.199.122:9200/e...'
创建成功后,查看如下:
2.建立新版本的索引,如:equipment_new
3.删除equipment_old的别名关联,切换索引到新版本equipment_new
POST http://192.168.199.122:9200/_aliases
{
"actions": [
{
"remove": {
"index": "equipment_old",
"alias": "equipment"
}
},
{
"add": {
"index": "equipment_new",
"alias": "equipment"
}
}
]
}
近义词转换
全文检索的时候,检索的keyword中有时候输入的是一些口头语,例如:输入“卡特勾机”,分词结果为:“卡特”,“勾机”。其实“卡特”指的是“卡特彼勒”,“勾机”指的是“挖掘机”。
在索引文档中只存在标准的“卡特彼勒”和“挖掘机”,所以上面的搜索结果为空,为了优化体验,就需要对keyword的分词结果“卡特”和“卡特彼勒”建立近义词,同理对“勾机”和“挖掘机”建立近义词。
所以在扩展字典中录入单词的时候,需要根据需要维护这个单词的近义词,比如:在录入“勾机”到扩展字典中的时候,指定近义词是“挖掘机”,那么在进行全文检索之前,先尽心一次分词得到“勾机”,然后判断勾机在系统中有没有近义词,此时是有近义词“挖掘机”,那么实际交给ES进行搜索的单词就是“挖掘机”,这样就实现了比较匹配的搜索效果。代码如下:
String searchWord = "";
List tokens = ESUtils.getTokens(keywords, null);
for (String token : tokens) {
searchWord = searchWord + token;
IkExtDic ikExtDic = ikDicService.getIkExtDicByWord(token);
if (ikExtDic == null) continue;
String similarWord = ikExtDic.getSimilarWord();
if (!StringUtils.isEmpty(similarWord)) searchWord = searchWord.replace(token, similarWord);
}
砍词再搜索
空的搜索结果是不太友好的,如果用户在搜索框中输入“陕西省西安市雁塔区挖掘机卡特彼勒320D”,如果没有这样的设备数据直接展示搜索结果为空,给用户的体验就比较差,比较生硬。
针对上面的情况,可以对搜索的keyword进行砍词处理,搜索出尽量匹配的结果,有可能就能达到用户的搜索需求,比如将上面的搜索文本经过砍词,变成搜索“陕西省西安市雁塔区挖掘机卡特彼勒”,那么就有结果出现了,体验上会比较友好,另外对SEO实现站点搜索优化有帮助。
在维护字典单词的时候,我们需要维护该单词的属性,比如:该单词是省份,还是城市,还是县等。这样,经过分词后,我们是知道这个单词是什么属性,对于砍词,需要根据业务规则定义一个砍词顺序,比如:县,城市,省份,型号,品牌,机型。然后按照分词结果每个单词的属性,结合砍词顺序进行砍词再搜索。
定义排序规则
定义设备评优的规则,对设备进行多维度的评分,搜索结果按照评分由高到低排序,把最优的设备展示在最前面。
可以实现一个存储过程,在应用代码中,通过定时任务调用存储过程,更新每个设备的评分。
就近位置优先
ES可以实现基于地理位置的查询,比如:查询周边n公里内的设备信息。在ES中,地理位置通过geo_point这个数据类型来表示,地理位置数据需要提供经纬度。
位置的表示
在ES中位置数据可以有三种表现形式,分别是字符串、对象、数组,下面分别以不同的方式表示北京的位置。
字符串的方式:"lat,lon":
{
"city" : "Beijing",
"location" : "39.91667,116.41667",
"state" : "BJ"
}
对象的方式:
{
"city" : "Beijing",
"location" : {
"lat" : "39.91667",
"lon" : "116.41667"
},
"state" : "BJ"
}
数组的方式:[lon,lat]
{
"city" : "Beijing",
"location" : [116.41667,39.91667],
"state" : "BJ"
}
位置过滤
ES中有四种位置过滤器,分别是:
1、geo_distance: 查找距离某个中心点距离在一定范围内的位置
2、geo_bounding_box: 查找某个长方形区域内的位置
3、geo_distance_range: 查找距离某个中心的距离在min和max之间的位置
4、geo_polygon: 查找位于多边形内的地点
功能测试
建立一个城市的索引,索引名称为city,索引的类型为city,索引中包含城市的名称,位置,区域描述等信息,其中位置信息的类型指定为geo_point类型。索引对应的mapping如下:
{
"city" : {
"properties" : {
"cityEname" : {
"type" : "string"
},
"location" : {
"type" : "geo_point"
},
"state" : {
"type" : "string"
}
}
}
}
建立5条城市数据到ES中,代码如下:
List
QueryRule queryRule = QueryRule.getInstance();
queryRule.setGeoField(new GeoField("location", 24.46667, 118.10000, SortOrder.ASC));
List
控制台打印结果如下:
{cityEname=Xiamen, location=24.46667,118.10000, state=FJ}
{cityEname=Fuzhou, location=26.08333,119.30000, state=FJ}
{cityEname=Guangzhou, location=23.16667,113.23333, state=GD}
{cityEname=Shanghai, location=34.50000,121.43333, state=SH}
{cityEname=Beijing, location=39.91667,116.41667, state=BJ}
实际场景
在建立设备索引信息的时候,把设备所在地的经纬度一起建立到设备索引中,用户在登录app进行设备查找的时候,首先前端获取到用户所在地的经纬度,然后ES在做设备搜索的时候就可以给用户推荐符合用户需求的就近的设备信息。