es 基于match_phrase/fuzzy的模糊匹配原理及使用
[版权声明]:本文章由danvid发布于http://danvid.cnblogs.com/,如需转载或部分使用请注明出处
在业务中经常会遇到类似数据库的"like"的模糊匹配需求,而es基于分词的全文检索也是有类似的功能,这个就是短语匹配match_phrase,但往往业务需求都不是那么简单,他想要有like的功能,又要允许有一定的容错(就是我搜索"东方宾馆"时,"广州花园宾馆酒店"也要出来,这个就不是单纯的"like"),下面就是我需要解析的问题(在此吐槽一下业务就是这么变态。。)
描述一个问题时首先需要描述业务场景:假设es中有一索引字段name存储有以下文本信息:
doc[1]:{"name":"广州东方宾馆酒店"}
doc[2]:{"name":"广州花园宾馆酒店"}
doc[3]:{"name":"东方公园宾馆"}
需求要求在输入:"东方宾馆"的时候doc[1]排最前面doc[3]排第二doc[2]排第三,对于这个需求从简单的全文检索match来说,doc[3]:{"name":"东方公园宾馆"}应该是第一位(注意:为了简化原理分析,分词我们使用standard即按单个字分词)
业务分析:显然对于上面的业务场景如果单独使用match的话,显然是不合适,因为按照standard分词,doc[3]的词条长度要比doc[1]的词条长度短,而词频又是都出现了[东][方][宾][馆]4个词,使用match匹配的话就会吧doc[3]排到最前面,显然业务希望把输入的文字顺序匹配度最高的数据排前面,因为我确实要找的是"广州东方宾馆酒店"而不是"东方公园宾馆"你不能把doc[3]给我排前面,OK业务逻辑好像是对的那么怎么解决问题;
解决问题前介绍一哈match_phrase原理(match的原理我就不说了自己回去看文档),简单点说match_phrase就是高级"like"。api如下:
GET test_index/_search { "query": { "match_phrase" : { "message" : { "query" : "东方宾馆", "slop" : 2 } } } }
es在给文本分词的时候,除了分词之外还有一个词条标记,就是position,例如我按照standard对以上三个doc的name进行分词会变成这样:
doc[1]:广[0],州[1],东[2],方[3],宾[4],馆[5],酒[6],店[7]; doc[2]:广[0],州[1],花[2],园[3],宾[4],馆[5],酒[6],店[7]; doc[3]:东[0],方[1],公[2],园[3],宾[4],馆[5]; query文本分词:东[0],方[1],宾[2],馆[3];
使用match_phrase时:
1.es会先过滤掉不符合的query条件的doc,即doc[2]中没有"东方"两个词汇,会被过滤掉
2.es会根据分词的position对分词进行过滤和评分,这个是就slop参数,默认是0,意思是查询分词只需要经过距离为0的转换就可以变成跟doc一样的文档数据,例如:对于doc[1]来说slop就是0了,对于doc[3]slop就是2,即"宾"和"馆"最大位移这两个分词只需要最多移动2个位置就可以变成"东方宾馆"(反过来也一样,query的文本中的"宾"和"馆"只需要移动2个位置就可以变成"东方**宾馆"),用数学的理解就是doc[3]的宾[4]-东[0]=4,query文本中的宾[2]-东[0]=2,那么转换距离slop就是4-2=2,同理doc[3]的馆[5]-东[0]=5,query的是3,slop结果也是2,那么"宾"和"馆"最大的slop就是2,则query时设置slop等于2就能把doc[3]匹配出来,当设置为0时就是我们数据库的"like"
原理解析完了,就知道使用match只能匹配相关度即tf/idf,而分词之间的位置关系却无法保证,而match_phrase能保证分词间的邻近关系,那么就可以利用两者优势,结合搜索进行评分
GET test_index/_search { "query": { "bool": { "must": { "match": { "name": { "query": "东方宾馆" } } }, "should": { "match_phrase": { "name": { "query": "东方宾馆", "slop": 0 } } } } } }
这样就的结果就是相当于match_phrase帮match进行了相关度分数的加分,当然你也可以通过修改slop的参数来进行步控制分数,这个就根据用户需求来了;
性能问题:其实使用match_phrase性能是要比单纯的全文检索性能低的,因为他要计算位置嘛,那么想提高性能可以通过先使用match进行过滤数据,然后利用rescore api对已经match的结果进行加分,这样就减少了部分不必要的非match过滤:
GET test_index/_search { "query": { "match": { "name":"东方宾馆" } },
"rescore": { "window_size": 30, "query": { "rescore_query": { "match_phrase": { "name": { "query": "东方宾馆", "slop": 0 } } } } } }
#window_size 是每一分片进行重新评分的顶部文档数量这个只要大于你可能分页的总数*每页的数量即可(pageNumber*pageSize)实际上不需要这么多因为翻页不可能很深,这个根据业务调整即可。
总结及注意点:
1.rescore其实跟bool结合一样是评分的相加,评分不在这里细说了;
2.虽然可以提高相关度评分,但是还是存在可能match很低+一个很低的match_phrase结果没有单独只匹配了一个match的分数高的情况,但是这是很极限了,也是符合相关度评分原理的;
3.由于match_phrase是在搜索阶段进行的计算,会影响搜索效率,据说比term查询慢20倍,所以不要进行大文本量的字段搜索,尽量进行标题,名字这种类型的搜索才使用这个;
4.本文章没有讨论在文本数据重复时的情况,即文本中有多个"[东][方][宾][馆]"和query文本中有多个"[东][方][宾][馆]"分词的情况,但是原理是一样的还是取距离转换的最小值;
5.文中使用了standard分词,实际上可能会用不同的分词器,但是建议使用match_phrase时使用标准的一个个分词,这样是方便进行邻近搜索的控制的,如果使用ik等分词,执行match_phrase时分词是不可控的,所以结果也是不可控。match使用ik,match_phrase用standard结合一起使用也是可以的;
6.邻近搜索效率较低,其实可以通过增加词库的方式进行单纯使用match匹配效率是最高的,前提是你知道客户会搜索什么,这又是另一个研究话题了
更新[2019-05-22]:
补充:短语匹配match_phrase必须要满足下面的要求才能认定和["东方宾馆"]这个词条匹配(以standard分析器为例)
1.搜索的词必须有且仅有["东","方","宾","馆"]这几个词(对于中文是字)的一个或者多个,如果有其他的词(对于中文是字)是不会匹配到的,slop不是完全等同于莱文斯坦距离,可以理解成字符的偏移
2.查询词中偏移量应该跟文档中词的偏移量一样,或者在slop的偏差范围内,就是上文解析的意思。
这里讲解一下fuzzy和match_phrase的区别
1.fuzzy是词/项级别的模糊匹配,match_phrase是基于短语级别的
例如对于英文(standard分析器)来说"dog cat bird"来说"dog"就是一个词/词项,而"dog cat"就是一个短语,因此作用范围不一样
2.fuzzy是基于莱文斯坦距离的,所以fuzzy是可以容错的例如你输入"dcg" 你也可以匹配到"dog cat bird",但是这里注意的是你的查询只能是单词条的查询,不能"dcg cat",如果你需要查询短语里面的拼写错误,可以使用match的fuzziness参数,match_phrase是不允许出现不存在的词条的。
下面是对于fuzzy和match_phrase和match 添加fuzziness参数进行对比
文档内容是{"name":"dog cat bird"} 分析器是standard
--------------------------------------------------------------------------------------------------
1.使用拼写错误的"cot"可以使用fuzzy匹配但是,如果是下面这种,短语是不可以的,输入应当是词条,而不是短语
GET test_save/_search { "query": { "fuzzy": { "name":{ "value": "bird cot", "fuzziness": 1 } } } }
--------------------------------------------------------------------------------------------------
2.这里可以匹配到因为match先分解成bird 和 cot 其中bird可以匹配到,同时cot也是可以匹配到,只不过分数要比输入"bird cat"要低
GET test_save/_search { "query": { "match": { "name":{ "query": "bird cot", "fuzziness":1 } } } }
-----------------------------------------------------------------------------------------------
3.这里由于cot和文本中的cat不是同一个词,所以是无法匹配到的
GET test_save/_search { "query": { "match_phrase": { "name": { "query": "bird cot", "slop": 1 } } } }
以上是对于英文的单词的解析,对于中文其实也是一样,只是由于中文如果使用standard一个词项就是一个字,因此使用因此分词后的数据对于fuzzy模糊匹配来说意义不大,但是可以使用keyword进行
GET test_save/_search { "query": { "fuzzy": { "name.keyword":{ "value": "东日宾馆", "fuzziness": 1 } } } }
这样是可以匹配到"东方宾馆"的数据的,但是无法匹配"广州东方宾馆"因为莱文斯坦距离已经不止1了
其实短语匹配应该叫临近查询更适合些
以上就是对模糊查询和短语匹配的解析和补充~
[说明]:elasticsearch版本5.6.4