Fuzzy Query亦称为模糊查询,查找与指定词项相似的词项,并返回该词项所在的文档。其中词项的相似性由编辑距离测算。脑图如下:
内容说明:
本文内容同微信公众号【凡登】,关注不迷路,学习上高速,欢迎关注共同学习。原文链接:
本文基于Elasticsearch 7.13 版本。文档地址:
https://www.elastic.co/guide/en/elasticsearch/reference/7.13/query-dsl-fuzzy-query.html
目录
一、概念
模糊查询类似于查询纠错,如检索boy,但是不知道单词拼写,那么检索boa就可以将你需要的内容文档检索出来。
为了更深入理解Fuzzy Query,我们必须熟悉的一些概念;
1、什么是相似词项?
通过处理(替换,删除,新增,转换相邻位置)词项中的某个字符,从而将一个词项(如:boy)改变为另外一个词项(如:joy),那我们就称boy和joy这两个词相似。
为了帮助理解,如:
替换:box ——> fox
移除:black ——> lack
新增:sic ——> sick
改变相邻字符位置:act ——> cat
2、什么是编辑距离?
简单理解通过调整(替换、删除、插入)词项中的某个字符的次数将一个词项(如:boy)改变为另外一个词项(如:joy)。详细概念请自行谷歌,为了帮助理解示例如:
如:box ——> fox 调整1次,编辑距离为1
如:box ——> fox——> fix 调整2次,编辑距离为2
如:act ——> cct——> cat 调整2次,编辑距离为2
3、fuzzy query 如何查询相似词项?
fuzzy query 用指定的编辑距离为指定的搜索词项创建多个可能的词项变体或扩展,再通过bool query和term query将这些词项作为搜索词项进行精确匹配,最终返回匹配的文档;如:
GET fuzzy_query_demo_index_001/_search
{
"query": {
"fuzzy": {
"name.keyword": { "value": "Fan" }
}
}
}
可转换语义相同bool query 查询:
# 查询顺序 如:
# 原字符、新增字符、交换相邻字符、替换字符、删除字符
GET fuzzy_query_demo_index_001/_search
{
"query": {
"bool": {
"should": [
{
"term": { "name.keyword": { "value": "Fan" } }
},
{
"term": { "name.keyword": { "value": "Fand" } }
},
{
"term": { "name.keyword": { "value": "aFn" } }
},
{
"term": { "name.keyword": { "value": "fan" } }
},
{
"term": { "name.keyword": { "value": "Fa" } }
}
]
}
}
}
二、语法
GET /_search
{
"query": {
"fuzzy": {
"<field>": {
"value": "Fan",
"fuzziness": "AUTO",
"max_expansions": 50,
"prefix_length": 0,
"transpositions": true,
"rewrite": "constant_score"
}
}
}
}
示例:
示例1:
GET fuzzy_query_demo_index_001/_search
{
"query": {
"fuzzy":{
"name": { "value": "Fan" }
}
}
}
示例2:语义等同【示例1】:
GET fuzzy_query_demo_index_001/_search
{
"query": {
"fuzzy":{
"name": {
"value": "Fan",
"fuzziness": "AUTO",
"max_expansions": 50,
"prefix_length": 0,
"transpositions": true,
"rewrite": "constant_score"
}
}
}
}
<field>:fuzzy query 参数,想要搜索的字段名,字段有层级则用点号连接, 如:name, name.last;
参数说明如下:
1、value
【必填】搜索词项。
2、fuzziness
【可选,默认:AUTO】定义最大的编辑距离,有效值:[AUTO, 0, 1, 2]
-
当值为AUTO时候,根据搜索词项的长度确定一个编辑距离,
-
当搜索词项长度为0~2 ,即精准匹配,则无编辑距离。
-
当搜索词项长度为3~5,则编辑距离为1。
-
当搜索词项长度 > 5,则编辑距离为2。
AUTO可以指定词项编辑距离的最小和最大值,语法:AUTO:[low],[high], 如不指定最小和最大值,那么AUTO 等同于 AUTO:3,6。建议优先使用AUTO
示例如下:
# 定义测试索引mapping
DELETE fuzzy_query_demo_index_001
PUT fuzzy_query_demo_index_001
{
"mappings": {
"properties": {
"name":{
"type":"text", # text字段类型
"fields": {
"keyword":{
"type":"keyword" # keyword字段类型
}
}
}
}
}
}
2.1、fuzziness=0
可编辑距离为0,不处理搜索词项,精确匹配。
# 创建测试数据
# 使用_bulk批量插入数据,refresh=true使插入文档可以立即被检索到。
POST fuzzy_query_demo_index_001/_bulk?refresh=true
{"index":{"_id":"fa"}}
{"name":"fa"}
{"index":{"_id":"Fa"}}
{"name":"Fa"}
示例如下:
# 示例1:
GET fuzzy_query_demo_index_001/_search
{
"track_total_hits": true,
"size": 20,
"query": {
"fuzzy": {
"name": {
"value": "fa",
"fuzziness":0
}
}
}
}
# 结果匹配到2个文档
# "name":"fa" 和 "name":"Fa"
# 思考:为什么会出现 "name":"Fa" 的文档 ?
#示例2:
GET fuzzy_query_demo_index_001/_search
{
"track_total_hits": true,
"size": 20,
"query": {
"fuzzy": {
"name": {
"value": "Fa",
"fuzziness":0
}
}
}
}
# 结果没有匹配到任何文档
# 思考 为什么没有出现 "name":"Fa" 的文档 ,为什么没有文档 ?
示例3:
GET fuzzy_query_demo_index_001/_search
{
"track_total_hits": true,
"size": 20,
"query": {
"fuzzy": {
"name.keyword": {
"value": "fa",
"fuzziness":0
}
}
}
}
# 结果匹配到1个文档
# "name":"fa"
# 思考 为什么只出现 "name":"fa" 的文档 ,为什么没有"name":"Fa" 文档 ?
示例4:
GET fuzzy_query_demo_index_001/_search
{
"track_total_hits": true,
"size": 20,
"query": {
"fuzzy": {
"name.keyword": {
"value": "Fa",
"fuzziness":0
}
}
}
}
# 结果匹配到1个文档
"name":"Fa"
# 思考 为什么只出现 "name":"Fa" 的文档 ?
示例5:查看text字段类型分词
POST _analyze
{
"analyzer": "standard",
"text": "Fa"
}
结果:Fa分词后为fa
{
"tokens" : [
{
"token" : "fa",
"start_offset" : 0,
"end_offset" : 2,
"type" : "<ALPHANUM>",
"position" : 0
}
]
}
为什么会出现上述不同结果?从上述5个示例总结如下:
1、当fuzziness=0时,fuzzy query使用精确查询。
2、当fuzzy query查询text字段类型时,搜索词项区分大小写,被检索词项实际都是小写。原因:text字段类型下的值经过分词处理后转为小写。
3、当fuzzy query查询keyword字段类型时,搜索词项区分大小写,被检索词项则区分大小写。原因:keyword字段类型下的值存储原值(长度小于256)。
经过上述的示例演示和总结,我们知道了fuzzy query对于text字段和keyword字段类型查询的区别。为了简化后文测试数据全部为小写。
2.2、fuzziness=1
可编辑距离为1,仅处理一个字符。fuzziness=1表示最大的编辑距离为1,同时包含编辑距离为0的匹配结果集。
# 添加测试数据
# 避免干扰,删除之前演示测试数据,
POST fuzzy_query_demo_index_001/_delete_by_query?refresh=true
{
"query": {
"match_all": {}
}
}
# 按照1个编辑距离,新增测试数据
# (移除字符、原字符、替换字符、交换相邻字符、新增字符)
POST fuzzy_query_demo_index_001/_bulk?refresh=true
{"index":{"_id":"f"}}
{"name":"f"}
{"index":{"_id":"fa"}}
{"name":"fa"}
{"index":{"_id":"fo"}}
{"name":"fo"}
{"index":{"_id":"af"}}
{"name":"af"}
{"index":{"_id":"fan"}}
{"name":"fan"}
查询示例:
GET fuzzy_query_demo_index_001/_search
{
"track_total_hits": true, // 查询结果中显示文档总数
"query": {
"fuzzy": {
"name": {
"value": "fa",
"fuzziness":1
}
}
}
}
结果:匹配上述所有文档。
2.2、fuzziness=2
可编辑距离为2,可处理2个字符。fuzziness=2 表示最大的编辑距离为2,同时包含编辑距离为0和1的匹配结果集。
# 在fuzziness=1示例测试数据上新增以下数据
# (交互相邻字符 + 新增字符)、(新增两个字符)、(替换字符 + 新增字符)其他组合数据省略,暂不举例
POST fuzzy_query_demo_index_001/_bulk?refresh=true
{"index":{"_id":"afn"}}
{"name":"afn"}
{"index":{"_id":"fand"}}
{"name":"fand"}
{"index":{"_id":"fod"}}
{"name":"fod"}
# 检索
GET fuzzy_query_demo_index_001/_search
{
"track_total_hits": true,
"size": 20,
"query": {
"fuzzy": {
"name": {
"value": "fa",
"fuzziness":2
}
}
}
}
# 结果包含所有文档。
2.3、fuzziness=AUTO
可编辑距离根据搜索词项长度进行测算。
# 沿用2.2的测试数据并新增测试数据:
POST fuzzy_query_demo_index_001/_bulk?refresh=true
{"index":{"_id":"fandeng"}}
{"name":"fandeng"}
{"index":{"_id":"fandong"}}
{"name":"fandeng"}
{"index":{"_id":"fandng"}}
{"name":"fandng"}
示例1:搜索词长度为2个字符
GET fuzzy_query_demo_index_001/_search
{
"track_total_hits": true,
"size": 20,
"query": {
"fuzzy": {
"name": {
"value": "fa",
"fuzziness":"AUTO"
}
}
}
}
结果:"name" : "fa"
思考:为什么仅匹配一个,似乎精确匹配?
示例2:搜索词长度为3个字符
GET fuzzy_query_demo_index_001/_search
{
"track_total_hits": true,
"size": 20,
"query": {
"fuzzy": {
"name": {
"value": "fan",
"fuzziness":"AUTO"
}
}
}
}
结果:
"name" : "fan"
"name" : "afn"
"name" : "fand"
"name" : "fa"
思考:为什么匹配这几个,似乎一个编辑距离下进行匹配的?
示例3:搜索词长度为3个字符
GET fuzzy_query_demo_index_001/_search
{
"track_total_hits": true,
"size": 20,
"query": {
"fuzzy": {
"name": {
"value": "fanden",
"fuzziness":"AUTO"
}
}
}
}
结果:
"name" : "fandeng"
"name" : "fandeng"
"name" : "fandng"
"name" : "fand"
思考:似乎2个编辑距离下进行匹配的?
总结:
当fuzziness=AUTO时,编辑距离的值由搜索词项字符长度决定的
搜索词项字符长度为0-2,fuzziness=0;
搜索词项字符长度为3-5,fuzziness=1;
搜索词项字符长度 > 5,fuzziness=2;
2.4、fuzziness=AUTO:3,5
指定AUTO编辑距离
示例1:
GET fuzzy_query_demo_index_001/_search
{
"track_total_hits": true,
"size": 20,
"query": {
"fuzzy": {
"name": {
"value": "fand",
"fuzziness":"AUTO:1,4"
}
}
}
}
结果:
"name" : "fand"
"name" : "fan"
"name" : "fandng"
"name" : "afn"
"name" : "fod"
"name" : "fa"
示例2:
GET fuzzy_query_demo_index_001/_search
{
"track_total_hits": true,
"size": 20,
"query": {
"fuzzy": {
"name": {
"value": "fand",
"fuzziness":"AUTO:1,5"
}
}
}
}
结果:
"name" : "fand"
"name" : "fan"
思考:为什么只有2个结果?其他的结果?
总结:
当指定AUTO编辑距离时,
如果指定值没有超过了搜索词项的长度,则匹配fuzziness最大编辑距离。如:AUTO:1,4 ,则最大编辑取值为2
如果指定值超过了搜索词项的长度,则最大编辑根据搜索词项长度决定,具体参考【2.3 fuzziness=AUTO】。
3、max_expansions
【可选】在指定编辑距离下将搜索词项处理为新词项, max_expansions定义这种新词项的最大数量。举例下面的7种新词项:
Fan -> Fan (原字符)
Fan -> Fand (新增字符) Fan -> Fian (新增字符)
Fan -> aFn (相邻字符调整)
Fan -> fan (替换原字符)
Fan -> Fa (删除原字符) Fan -> Fn (删除原字符)
【默认:50】 即根据指定编辑距离为搜索词项创建最多50个新词项。
注:建议不要为max_expansions定义太大值,尤其是当prefix_length值为0的时候,太大值意味创建更多的新词项进行匹配,性能较差。
清空历史测试数据
POST fuzzy_query_demo_index_001/_delete_by_query?refresh=true
{
"query": {
"match_all": {}
}
}
新增测试数据
(原字符、交换相邻字符、新增字符、替换字符、删除字符)
POST fuzzy_query_demo_index_001/_bulk?refresh=true
{"index":{"_id":"f"}}
{"name":"f"}
{"index":{"_id":"fa"}}
{"name":"fa"}
{"index":{"_id":"fo"}}
{"name":"fo"}
{"index":{"_id":"af"}}
{"name":"af"}
{"index":{"_id":"fan"}}
{"name":"fan"}
示例1:fuzziness=1, max_expansions=50(默认值)。即一个编辑距离下最多匹配50个新词项
GET fuzzy_query_demo_index_001/_search
{
"track_total_hits": true,
"size": 20,
"query": {
"fuzzy": {
"name": {
"value": "fa",
"fuzziness":1,
"max_expansions": 50
}
}
}
}
结果:匹配所有数据
示例1:fuzziness=1, max_expansions=1
GET fuzzy_query_demo_index_001/_search
{
"track_total_hits": true,
"size": 20,
"query": {
"fuzzy": {
"name": {
"value": "fa",
"fuzziness":1,
"max_expansions": 1
}
}
}
}
结果:"name" : "fa" 仅匹配原字符
示例2:fuzziness=1, max_expansions=2
GET fuzzy_query_demo_index_001/_search
{
"track_total_hits": true,
"size": 20,
"query": {
"fuzzy": {
"name": {
"value": "fa",
"fuzziness":1,
"max_expansions": 2
}
}
}
}
结果:仅匹配原字符 和 相邻交互字符
"name" : "fa"
"name" : "af"
示例3:fuzziness=1, max_expansions=3
GET fuzzy_query_demo_index_001/_search
{
"track_total_hits": true,
"size": 20,
"query": {
"fuzzy": {
"name": {
"value": "fa",
"fuzziness":1,
"max_expansions": 3
}
}
}
}
结果:匹配原字符 和 相邻交互字符 和 新增字符
"name" : "fa"
"name" : "af"
"name" : "fan"
示例4:fuzziness=1, max_expansions=4
GET fuzzy_query_demo_index_001/_search
{
"track_total_hits": true,
"size": 20,
"query": {
"fuzzy": {
"name": {
"value": "fa",
"fuzziness":1,
"max_expansions": 4
}
}
}
}
结果:匹配原字符 和 相邻交互字符 和 新增字符 和 替换字符
"name" : "fa"
"name" : "fo"
"name" : "af"
"name" : "fan"
示例5:fuzziness=1, max_expansions=5
GET fuzzy_query_demo_index_001/_search
{
"track_total_hits": true,
"size": 20,
"query": {
"fuzzy": {
"name": {
"value": "fa",
"fuzziness":1,
"max_expansions": 5
}
}
}
}
结果:匹配原字符 和 相邻交互字符 和 新增字符 和 替换字符 和 删除字符
"name" : "fa"
"name" : "fo"
"name" : "af"
"name" : "fan"
"name" : "f"
总结:
max_expansions 即根据指定编辑距离为搜索词项创建最大数量的新词项。
优先匹配顺序:匹配原字符 > 相邻交互字符 > 新增字符 > 替换字符 > 删除字符
4、prefix_length
【可选】表示:当搜索词项创建新词项时候,保持搜索词项不变的起始字符数,即搜索词项这个字符串中左边不做处理的字符数。【默认0】即可以从搜索词项的起始字符进行调整。
示例:搜索词项为Fan时,
当prefix_length=0, 即可从起始字符调整,Fan -> fan
当prefix_length=1, 即可从第二个字符调整,Fan -> FAn
演示如下:
删除数据
POST fuzzy_query_demo_index_001/_delete_by_query?refresh=true
{
"query": {
"match_all": {}
}
}
新增数据
POST fuzzy_query_demo_index_001/_bulk?refresh=true
{"index":{"_id":"fan"}}
{"name":"fan"}
{"index":{"_id":"ian"}}
{"name":"ian"}
{"index":{"_id":"fin"}}
{"name":"fin"}
{"index":{"_id":"fai"}}
{"name":"fai"}
示例1:"prefix_length": 0
GET fuzzy_query_demo_index_001/_search
{
"track_total_hits": true,
"size": 20,
"query": {
"fuzzy": {
"name": {
"value": "fan",
"fuzziness":1,
"prefix_length": 0
}
}
}
}
结果匹配所有文档
示例2:"prefix_length": 1
GET fuzzy_query_demo_index_001/_search
{
"track_total_hits": true,
"size": 20,
"query": {
"fuzzy": {
"name": {
"value": "fan",
"fuzziness":1,
"prefix_length": 1
}
}
}
}
结果:
"name" : "fan"
"name" : "fin"
"name" : "fai"
示例3:"prefix_length": 2
GET fuzzy_query_demo_index_001/_search
{
"track_total_hits": true,
"size": 20,
"query": {
"fuzzy": {
"name": {
"value": "fan",
"fuzziness":1,
"prefix_length": 2
}
}
}
}
结果:
"name" : "fan"
"name" : "fai"
示例4:"prefix_length": 3
GET fuzzy_query_demo_index_001/_search
{
"track_total_hits": true,
"size": 20,
"query": {
"fuzzy": {
"name": {
"value": "fan",
"fuzziness":1,
"prefix_length": 3
}
}
}
}
结果:仅匹配"name" : "fan"
示例5:"prefix_length": 4
GET fuzzy_query_demo_index_001/_search
{
"track_total_hits": true,
"size": 20,
"query": {
"fuzzy": {
"name": {
"value": "fan",
"fuzziness":1,
"prefix_length": 4
}
}
}
}
结果:仅匹配"name" : "fan"
5、transpositions
【可选】表示编辑距离是否包含调整两个相邻的字符,【默认:true】即包含。设置false,则编辑距离调整不包含相邻字符交换
清空数据
POST fuzzy_query_demo_index_001/_delete_by_query?refresh=true
{
"query": {
"match_all": {}
}
}
新增测试数据
POST fuzzy_query_demo_index_001/_bulk?refresh=true
{"index":{"_id":"fa"}}
{"name":"fa"}
{"index":{"_id":"af"}}
{"name":"af"}
示例1:"transpositions":true
GET fuzzy_query_demo_index_001/_search
{
"track_total_hits": true,
"size": 20,
"query": {
"fuzzy": {
"name": {
"value": "fa",
"fuzziness":1,
"transpositions":true
}
}
}
}
结果:匹配"name" : "fa" 和 "name" : "af"
示例2:"transpositions":false
GET fuzzy_query_demo_index_001/_search
{
"track_total_hits": true,
"size": 20,
"query": {
"fuzzy": {
"name": {
"value": "fa",
"fuzziness":1,
"transpositions":false
}
}
}
}
结果:仅匹配"name" : "fa"
6、rewrite
【可选,默认:constant_score】由于Elasticsearch内部检索依赖Luence,而Luence不支持Fuzzy Query查询,为了使Elasticsearch支持此类查询,需要将Fuzzy Query转变相同语义的bool query或bit set。
Luence不支持的类似查询如下:
-
fuzzy Query
-
prefix Query
-
regexp Query
-
wildcard Query
-
query_string query
以上查询都需要通过转变为相同语义的bool query或bit set,而rewrite 参数控制查询语义转变的行为。
-
控制文档相关性评分计算
-
控制bool query 还是 bit set
-
当使用bool query,即控制包括那些term query
6.1、rewrite有效值
-
constant_score:默认,当使用较少的term查询时,使用constant_score_boolean查询,否则按照顺序执行所有的term query,并使用bit set存储匹配到的文档。
-
constant_score_boolean:该方法给匹配的文档指定相关的评分,同时将原来的查询改为语义相同的包含should和term query的bool query语句,
-
scoring_boolean:该方法给匹配的文档计算相关的评分,同时将原来的查询改为语义相同的包含should和term query的bool query语句,
-
top_terms_blended_freqs_N:该方法给匹配的文档计算相关的评分,就好像所有的词项有相同的词频。词频为所有匹配文档中最大的词频,同时将原来的查询改为语义相同的包含should和term query的bool query语句,并包含评分前N个的term query
-
top_terms_boost_N:该方法给匹配的文档指定相关的评分,同时将原来的查询改为语义相同的包含should和term query的bool query语句,并包含评分前N个的term query
-
top_terms_N:该方法给匹配的文档计算相关的评分,同时将原来的查询改为语义相同的包含should和term query的bool query语句,并包含评分前N个的term query
对于大部分用户,我们推荐constant_score, constant_score_boolean, or top_terms_boost_N 方法。
其他方法需要计算相关性评分,不仅对查询结果没有帮助,反而降低了查询性能。详情参见:
https://www.elastic.co/guide/en/elasticsearch/reference/7.13/query-dsl-multi-term-rewrite.html
6.2、静态配置
可以通过indices.query.bool.max_clause_count(静态)配置,避免以上方法在将原查询转换bool查询语句过程中超出限制,默认1024。
参见:https://www.elastic.co/guide/en/elasticsearch/reference/7.13/search-settings.html#indices-query-bool-max-clause-count
# 在每个未启动节点的elasticsearch.yml配置
indices.query.bool.max_clause_count:1024
三、注意事项
Fuzzy query (除wildcard类型字段外)查询开销较大,
可设置search.allow_expensive_queries为false ,不执行fuzzy query
PUT _cluster/settings
{
"persistent":{
"search.allow_expensive_queries": true
}
}
四、总结
使用Fuzzy Query进行模糊匹配注意问题,
1、搜索词项尽量在2个字符以上,否则不会进行模糊查询,而是精确匹配查询。
2、控制max_expansions值,即搜索词项转变新词项的最大数量,避免性能问题。
3、由于fuzziness=【0~2】,编辑距离最大值为2,使用模糊匹配注意搜索词项检索范围。