定制化中文分词器
对于搜索引擎来说,很多词都属于专有名字,比如凯悦,这是一个品牌,而对于分词器来说并不知道,所以对于凯悦的分词会是把两个字分开:
因此,我们需要定制分词词库。
扩展专有名词
首先来到es的config\analysis-ik目录下,新建new_word.dic,在里面写“凯悦”,注意编码格式。然后打开IKAnalyzer.cfg.xml把文件写进去
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict">new_word.dic</entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords"></entry>
<!--用户可以在这里配置远程扩展字典 -->
<!-- <entry key="remote_ext_dict">words_location</entry> -->
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
然后把这两处修改同步到其他节点上。重启es后再测试一下:
虽然配置成功了,但是在搜索的时候会发现是空的,这是因为配置上已经改了过来,但是在es索引中还是两个字分开的,所以需要更新索引,局部更新:
POST /shop/_update_by_query
{
"query":{
"bool":{
"must":[
{"term":{"name":"凯"}},
{"term":{"name":"悦"}}
]
}
}
}
这样,再搜索“凯悦”,就只会出来含有专有名词“凯悦”的结果了。
扩展同义词
对于中文来说,大多数时候搜索需要同义词,比如搜索苹果,对于买手机的来说可以跟iphone一起搜索出来,这就需要我们来配置同义词了,对于同义词来说,需要我们在创建索引结构的时候就加进去,所以需要把索引删掉重新导入。
首先新建一个synomyms.txt,里面写凯悦,锡伯,红桃
PUT /shop
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1,
"analysis": {
"filter": {
"my_synonym_filter":{
"type":"synonym",
"synonyms_path":"analysis-ik/synomyms.txt"
}
},
"analyzer": {
"ik_syno":{
"type":"custom",
"tokenizer":"ik_smart",
"filter":["my_synonym_filter"]
},
"ik_syno_max":{
"type":"custom",
"tokenizer":"ik_max_word",
"filter":["my_synonym_filter"]
}
}
}
},
"mappings": {
"properties": {
"id":{"type":"integer"},
"name":{"type": "text","analyzer": "ik_syno_max","search_analyzer": "ik_syno"},
"tags":{"type": "text","analyzer": "whitespace","fielddata": true},
"location":{"type": "geo_point"},
"remark_score":{"type": "double"},
"price_per_man":{"type": "integer"},
"category_id":{"type": "integer"},
"category_name":{"type": "keyword"},
"seller_id":{"type": "integer"},
"seller_remark_score":{"type": "double"},
"seller_disabled_flag":{"type": "integer"}
}
}
}
加了一个过滤器,搜索关键字的时候会查询配置的文件中有没有这个词,有的话就都一起搜索出来。然后在要搜索的索引分词器改成自定义的,然后重新导入索引,先看下分词器的凯悦:
接下来搜索“锡伯”,结果中也可以看到,可以搜索出凯悦的结果。
相关性重塑
什么是相关性?例如搜索凯悦,可以出来很多结果,但是如果搜索住宿,按照人脑的想法,应该会搜出来酒店相关的结果,但是现阶段内,如果标题没有“住宿”是肯定搜索不出来的。要解决这样一个问题,明显需要在查询语句的判断中做些修改,例如:
GET /shop/_search
{
"_source": "*",
"script_fields": {
"distance": {
"script": {
"source": "haversin(lat,lon,doc['location'].lat,doc['location'].lon)",
"lang": "expression",
"params": {
"lat":31.306171,
"lon":121.525841
}
}
}
},
"query": {
"function_score": {
"query": {
"bool": {
"must": [
{
"bool": {
"should": [
{"match": {"name": {"query": "住宿","boost": 0.1}}},
{"term": {"category_id": {"value": "2","boost": 0.1}}}
]
}
},
{"term": {"seller_disabled_flag": 0}}
]
}
},
"functions": [
{
"gauss":{
"location":{
"origin":"31.306171,121.525841",
"scale":"100km",
"offset":"0km",
"decay":0.5
}
},
"weight": 9
},
{
"field_value_factor": {
"field": "remark_score"
},
"weight": 0.2
},
{
"field_value_factor": {
"field": "seller_remark_score"
},
"weight": 0.1
}
],
"score_mode": "sum",
"boost_mode": "sum"
}
},
"sort": [
{
"_score": {
"order": "desc"
}
}
],
"aggs": {
"group_by_tags": {
"terms": {
"field": "tags"
}
}
}
}
将关键字和类目放在一起,这样二者任意一个满足就可以出结果了,在java代码中:
private Map<Integer,List<String>> categoryWorkMap = new HashMap<>();
//构造分词函数识别器
@PostConstruct
public void init(){
categoryWorkMap.put(1,new ArrayList<>());
categoryWorkMap.put(2,new ArrayList<>());
categoryWorkMap.get(1).add("吃饭");
categoryWorkMap.get(1).add("下午茶");
categoryWorkMap.get(2).add("休息");
categoryWorkMap.get(2).add("睡觉");
categoryWorkMap.get(2).add("住宿");
}
private Integer getCategoryIdByToken(String token){
for(Integer key : categoryWorkMap.keySet()){
List<String> tokenList = categoryWorkMap.get(key);
if(tokenList.contains(token)){
return key;
}
}
return null;
}
//构造分词函数识别器
private Map<String,Object> analyzeCategoryKeyword(String keyword) throws IOException {
Map<String,Object> res = new HashMap<>();
Request request = new Request("GET","/shop/_analyze");
request.setJsonEntity("{" + " \"field\": \"name\"," + " \"text\":\""+keyword+"\"\n" + "}");
Response response = restHighLevelClient.getLowLevelClient().performRequest(request);
String responseStr = EntityUtils.toString(response.getEntity());
JSONObject jsonObject = JSONObject.parseObject(responseStr);
JSONArray jsonArray = jsonObject.getJSONArray("tokens");
for(int i = 0; i < jsonArray.size(); i++){
String token = jsonArray.getJSONObject(i).getString("token");
Integer categoryId = getCategoryIdByToken(token);
if(categoryId != null){
res.put(token,categoryId);
}
}
return res;
}
代码中的categoryWorkMap 可以写配置文件中,这里主要是讲方法,所以就不写了。这样,当搜索进来的时候,可以先匹配categoryWorkMap ,如果有关键字,就可以按照对应的类目进行搜索。
再说下打分机制,在搜索类目的时候,设置boost就可以了。
不过为了统一起来,可以在这里把数值改为0,不影响打分策略, 然后在function中配置,这样清晰明了,也可以统一管理打分机制。
当然,这是大多数情况。