乐趣区

关于elasticsearch:ES全文检索优化

近程字典

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<String> 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<Map<String, Object>> cdata = new ArrayList<>();
Map<String, Object> d1 = new HashMap<>();
d1.put("cityEname", "Beijing");
d1.put("state", "BJ");
d1.put("location", "39.91667,116.41667");

Map<String, Object> d2 = new HashMap<>();
d2.put("cityEname", "Xiamen");
d2.put("state", "FJ");
d2.put("location", "24.46667,118.10000");

Map<String, Object> d3 = new HashMap<>();
d3.put("cityEname", "Shanghai");
d3.put("state", "SH");
d3.put("location", "34.50000,121.43333");

Map<String, Object> d4 = new HashMap<>();
d4.put("cityEname", "Fuzhou");
d4.put("state", "FJ");
d4.put("location", "26.08333,119.30000");

Map<String, Object> d5 = new HashMap<>();
d5.put("cityEname", "Guangzhou");
d5.put("state", "GD");
d5.put("location", "23.16667,113.23333");

cdata.add(d1);
cdata.add(d2);
cdata.add(d3);
cdata.add(d4);
cdata.add(d5);
ESUtils.batchIndex("city", "city", cdata, "City");


依照厦门远近进行查找,间隔厦门近的排在最前,代码如下:

QueryRule queryRule = QueryRule.getInstance();
queryRule.setGeoField(new GeoField("location", 24.46667, 118.10000, SortOrder.ASC));
List<Map> list = ESUtils.list("city", "city", Map.class, queryRule, 10);
for(Map map : list){System.out.println(map);
}

控制台打印后果如下:

{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 在做设施搜寻的时候就能够给用户举荐合乎用户需要的就近的设施信息。

退出移动版