不管是全文搜索引擎,还是垂直搜索系统中,当用户在搜索输入框中输入几个字的时候,会自动下来一些词去自动补全用户可能要搜的词语,这部分的功能,我们称作搜索建议器的功能(英文叫做"suggest")。本文将介绍下目前主流的搜索建议器的做法,并且给出了一个我们认为更好的搜索建议器的做法。
这里,我们给出如下搜索联想词指导原则:
在这部分我们给出了搜索建议器需要实现的功能,这部分功能不仅是程序员需要考虑能够实现的功能,也可以用于测试用于进行验证搜索建议器的功能是否能够满足基本的使用要求。
具体例子如下:
苹果
=》 在A市,不应该出现"苹果醋",在深圳和广州应该出现“苹果醋”( 因为“苹果醋”只在广州和深圳有卖)即搜索建议词具有区域性
平果
=》 纠错成"苹果",即拼音纠错
PingGUO
=> 出现苹果,即归一化输入词
pinguo
=》出现苹果,即后鼻音纠错
pg
=> 出现pg开头的拼音的前缀,即首字母返回
虾n
=》不应该出现“鲜花”,即不能将虾n,转成xian去查询
虾r
=》 出现“虾仁”,即汉字和首字母可出现正确的词
长f奶
=> 不出现结果,这里不出现是因为要汉字和字母要连着,不能中间插入字母
chanfu
zhangfu
=》应该可以出现“长富”,即支持多音字搜索
Kafeii
=》 出现咖啡(基于编辑距离进行纠正,推荐大于5个字母才进行)
白萝卜
、白罗卜
=》 只出现白萝卜
皇上皇 煌上煌的问题,即建议词只出现正确的词
囗=> 纠错成
口`,出现口罩相关的名词,即把手写错误的词能够纠正过来
祙
=> 纠错成 袜
牛奶
=》 深圳地区会出现"燕塘牛奶"相关,长沙地区出现"花园牛奶"
蘋果
=> 繁体字也能够搜索出结果,这个也是通过词归一化处理
suggester基本原理是将输入的文本分解为token,然后在索引的字典里查找相似的term并返回。根据使用场景的不同,在Elasticsearch里面涉及了4种类别的suggester,分别是:
- Term Suggester
- Phrase Suggester
- Completion Suggester
- Context Suggester
我们依次讲解下,上述4中类别的suggester的用法。
三种suggest_mode:
missing:如果词存在,则不给出相似项。
popular :如果词存在,且有相似项,则给出。
always:不管token是否存在词典里,都给出相似项。
尝试了下,貌似term suggester对于中文是不适用的
下面这段是中文的代码:
DELETE blogs
PUT /blogs/
{
"mappings": {
"tech": {
"properties": {
"body": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
}
}
}
}
}
POST _bulk/?refresh=true
{ "index" : { "_index" : "blogs", "_type" : "tech" } }
{ "body": "长富牛奶"}
{ "index" : { "_index" : "blogs", "_type" : "tech" } }
{ "body": "长富奶"}
{ "index" : { "_index" : "blogs", "_type" : "tech" } }
{ "body": "奶粉"}
{ "index" : { "_index" : "blogs", "_type" : "tech" } }
{ "body": "牛奶"}
POST _bulk/?refresh=true
{ "index" : { "_index" : "blogs", "_type" : "tech" } }
{ "body": "niunai"}
POST _analyze
{
"analyzer": "ik_smart",
"text": [
"长富牛奶",
"长富奶",
"奶粉",
"牛奶"
]
}
POST /blogs/_search
{
"suggest": {
"my-suggestion": {
"text": "niunai",
"term": {
"suggest_mode": "popular",
"field": "body"
}
}
}
}
在Term Suggester的基础之上,会考虑多个Term之间的关系,比如:是否同时出现在索引的原文中,相邻程度,以及词频,
主要应用场景是自动自动补全,每输入一个字符,即时发送一次请求到服务端查询可能的匹配项,将数据变成FST,只能用于前缀匹配,这也是Completion Suggester的局限所在。为了使用Completion Suggester,字段的类型需要专门定义。
有两个参数:
preserve_separators:
preserve_position_increments:
request_cache=true 查询从5ms变成1ms
基于ES suggester completion,内部用FST(Finite State Transducer),只能用于前缀匹配,这也是Completion Suggester的局限所在。我们现在的搜索联想词可以中缀匹配,是因为使用了ngram(min_gram: 1, max_gram: 10),在入库的时候把所有的词进行拆分。
比如:长富牛奶,会拆成“长、富、牛、奶、长富、富牛、牛奶、长富牛、富牛奶、长富牛奶”
另外,使用了ES 的拼音分词器,支持用户输入拼音搜索相关的联想词,以及将输入的中文词也利用对应的拼音进行匹配。
基于ES,不使用ES自带的搜索建议器Suggester,我们打算自己构建一个用于搜索联想词的索引,与旧有的搜索联想词对比如下:
70M
,而 新的搜索联想词占用空间为15M
我们的方案,mapping构建如下:
PUT search_suggester?include_type_name=false
{
"aliases": {
"alias_search_suggester": {
}
},
"settings": {
"number_of_shards": 3,
"number_of_routing_shards": 9,
"number_of_replicas": 0,
"refresh_interval": "1s",
"index":{
"sort.field":["frequency", "sku_num"],
"sort.order":["desc", "desc"]
},
"index.search.slowlog.threshold.query.trace": "20ms",
"index.search.slowlog.threshold.query.debug": "100ms",
"index.search.slowlog.threshold.query.info": "250ms",
"index.search.slowlog.threshold.query.warn": "500ms",
"index.search.slowlog.threshold.fetch.trace": "20ms",
"index.search.slowlog.threshold.fetch.debug": "100ms",
"index.search.slowlog.threshold.fetch.info": "250ms",
"index.search.slowlog.threshold.fetch.warn": "500ms",
"index.indexing.slowlog.threshold.index.trace": "20ms",
"index.indexing.slowlog.threshold.index.debug": "100ms",
"index.indexing.slowlog.threshold.index.info": "250ms",
"index.indexing.slowlog.threshold.index.warn": "500ms"
},
"mappings": {
"_routing": {
"required": true
},
"dynamic":"strict",
"properties": {
"query": {
"type": "keyword",
"doc_values": false,
"norms": false
},
"city_zip": {
"type": "keyword",
"doc_values": false,
"norms": false
},
"term_prefixs": {
"type": "keyword",
"doc_values": false,
"norms": false,
"copy_to": "search_suggester_all"
},
"term_pinyin": {
"type": "keyword",
"doc_values": false,
"norms": false,
"copy_to": "search_suggester_all"
},
"term_shouzimu": {
"type": "keyword",
"doc_values": false,
"norms": false,
"copy_to": "search_suggester_all"
},
"pinyin_prefixs": {
"type": "keyword",
"doc_values": false,
"norms": false,
"copy_to": "search_suggester_all"
},
"shouzimu_prefixs": {
"type": "keyword",
"doc_values": false,
"norms": false,
"copy_to": "search_suggester_all"
},
"frequency": {
"type": "long"
},
"sku_num": {
"type": "long"
},
"search_suggester_all": {
"type": "keyword",
"doc_values": false,
"norms": false
}
}
}
}
说明:
将相应的词,ik拆词,拼音拆词,组合中文和拼音等,按照上述需要的规则,进行拆分,然后插入到索引中。
查询的时候,对于ES的查询可以只查询search_suggester_all这个字段啦。
查询DSL语句如下:
GET alias_search_suggester/_search
{
"from": 0,
"size": 10,
"query": {
"bool": {
"filter": [
{
"term": {
"city_zip": {
"value": "400100",
"boost": 1
}
}
},
{
"term": {
"search_suggester_all": {
"value": "花园",
"boost": 1
}
}
}
],
"adjust_pure_negative": true,
"boost": 1
}
},
"_source": {
"includes": [
"query",
"sku_num"
],
"excludes": []
},
"sort": [
{
"frequency": {
"order": "desc"
}
},
{
"sku_num": {
"order": "desc"
}
}
],
"track_total_hits": false
}
使用到的工具包有:
org.apache.lucene
lucene-core
8.0.0
org.apache.lucene
lucene-queryparser
8.0.0
org.apache.lucene
lucene-analyzers-common
8.0.0
com.github.houbb
opencc4j
1.0.2
com.belerweb
pinyin4j
2.5.0