前言
ES的无论什么搜索,对于text类型字段其实都是基于倒排索引去进行搜索的,也就是进行分词后的,因此如果想像传统数据库一样的模糊匹配,一般可以使用它的keyword进行搜索。(keyword不会被分词)
以下的搜索在大型生产环境都不推荐使用。
前缀索引查询
以xx开头的搜索,不计算相关度评分,和filter比,没有bitcache。前缀搜索,尽量把前缀长度设置的更长,性能差,一般大规模产品不使用。(是去倒排索引中去匹配前缀,需要遍历每一个倒排索引才能找到所有匹配的)
语法
GET index/_search
{
"query": {
"prefix": {
"title": {
"value": "text"
}
}
}
}
为了加快前缀搜索速度,可以设置默认的 前缀索引 (空间换时间)
PUT my_index
{
"mappings": {
"properties": {
"text": {
"type": "text",
"index_prefixes": {
"min_chars":2,
"max_chars":4
}
}
}
}
}
上面这个设置的意思是,把分词后的每个词项的2-4个字符额外进行建立前缀倒排索引,从而提高后续前缀匹配的速度,但是占用空间也是相对变大。
index_prefixes: 默认 “min_chars” : 2, “max_chars” : 5 。
通配符查询
通配符查询类似于正则,但没正则强大,允许对匹配表达式进行通配符占位。
- 表示匹配任意长度的任意字符
? 表示匹配一个任意字符
[…]则表示匹配括号中列出的字符中的任意一个
[!..]表示不匹配括号中列出的字符中的任意一个
语法
{
"query": {
"wildcard": {
"text": {
"value": "eng?ish"
}
}
}
}
正则查询
regexp查询的性能可以根据提供的正则表达式而有所不同。为了提高性能,应避免使用通配符模式,如.或 .?+未经前缀或后缀
语法
{
"query": {
"regexp": {
"name": {
"value": "[\\s\\S]*nfc[\\s\\S]*",
"flags": "ALL",
"max_determinized_states": 10000, #防止正则内存过大的保护措施
"rewrite": "constant_score"
}
}
}
}
关于参数flags,有几个配置可选:
ALL (Default)
启用所有可选操作符。
COMPLEMENT
启用操作符。可以使用对下面最短的模式进行否定。例如
a~bc # matches ‘adc’ and ‘aec’ but not ‘abc’
INTERVAL
启用<>操作符。可以使用<>匹配数值范围。例如
foo<1-100> # matches ‘foo1’, ‘foo2’ … ‘foo99’, ‘foo100’
foo<01-100> # matches ‘foo01’, ‘foo02’ … ‘foo99’, ‘foo100’
INTERSECTION
启用&操作符,它充当AND操作符。如果左边和右边的模式都匹配,则匹配成功。例如:
aaa.+&.+bbb # matches ‘aaabbb’
ANYSTRING
启用@操作符。您可以使用@来匹配任何整个字符串。
您可以将@操作符与&和~操作符组合起来,创建一个“everything except”逻辑。例如:
@&~(abc.+) # matches everything except terms beginning with ‘abc’
Fuzzy模糊(容错)匹配
场景
1、混淆字符 (box → fox)
2、缺少字符 (black → lack)
3、多出字符 (sic → sick)
4、颠倒次序 (act → cat)
在出现上面情况的时候,我们也希望用户可以搜索到想要的内容,那么这个时候可以使用fuzzy。
语法
以下两种都可以:
1、第一种-手动档
可以手动多指定一些参数,但一般也不建议改动
{
"query": {
"fuzzy": {
"desc": {
"value": "quangemneng",
"fuzziness": 5
}
}
}
}
① value:(必需,字符串)
② fuzziness:(可选,字符串)最大误差 并非越大越好, 因为大了虽然召回率高 但是结果不准确
1) 两段文本之间的Damerau-Levenshtein距离是使一个字符串与另一个字符串匹配所需的插入、删除、替换和调换的数量
2) 距离公式:Levenshtein是lucene的,es改进版:Damerau-Levenshtein
3) axe=>aex Levenshtein=2 Damerau-Levenshtein=1
③ max_expansions:可选,整数)匹配的最大词项数量。默认为50。
④ prefix_length:创建扩展时保留不变的开始字符数。默认为0
1)避免在max_expansions参数中使用较高的值,尤其是当prefix_length参数值为时0。max_expansions由于检查的变量数量过多,参数中的高值 可能导致性能不佳。
⑤ transpositions:(可选,布尔值)指示编辑是否包括两个相邻字符的变位(ab→ba)。默认为true。
⑥ rewrite:(可选,字符串)用于重写查询的方法
https://www.elastic.co/cn/blog/found-fuzzy-search#performance-considerations
2、第二种,"自动挡"时代
{
"query": {
"match": {
"desc": {
"query": "quangengneng nfc",
"fuzziness": "AUTO"
}
}
}
}
match_phrase_prefix(最简陋的Suggest)
match_phrase_prefix与match_phrase相同,但是它多了一个特性,就是它允许在文本的最后一个词项(term)上的前缀匹配,如果 是一个单词,比如a,它会匹配文档字段所有以a开头的文档,如果是一个短语,比如 “this is ma” ,他会先在倒排索引中做以ma做前缀搜索,然后在匹配到的doc中做match_phrase查询,(网上有的说是先match_phrase,然后再进行前缀搜索, 是不对的)
语法
{
"query": {
"match_phrase_prefix": {
"desc": {
"query": "zhichi quangongneng nf",
"analyzer": "whitespace",
"max_expansions": 1,
"slop": 2,
"boost": 1
}
}
}
}
参数
analyzer
指定何种分析器来对该短语进行分词处理
max_expansions
限制匹配的最大term数。
- 一般来讲,前缀匹配是会全索引进行扫描匹配的,为了提高效率,可以进行限制它可以进行扫描的索引的个数,但即使设置为1,也不意味着返回的doc结果只有一个,主要有2点:
1、一个索引可能有多个doc
2、这个限制扫描个数是针对每个分片来说的(每个分片都可以扫描1个),因此也就是说该索引的每个分片扫描的第一个term都可能被匹配上。
boost
用于设置该查询的权重
slop
允许短语间的词项(term)间隔
slop 参数告诉 match_phrase 查询词条相隔多远时仍然能将文档视为匹配, 什么是相隔多远? 意思是说为了让查询和文档匹配你需要移动词条多少次?举个例子:
如果我们输入的是:de zhong shouji hongzhaji
而期望匹配的句子是:shouji zhong de hongzhaji
那么要怎么移动呢?
1、首先要把“shouji”词条向左移动2个词条:
shouji/de zhong shouji hongzhaji
2、接下来在把de向右移动2个词条:
shouji zhong de shouji hongzhaji
这样下来,共需要4次移动,因此slop需要设置为4的时候,输入的才能匹配上。
N-gram-tokenFilter
上面的前缀匹配还是存在性能问题,那有没有相对好一点的方法呢? 我们可以从分词角度出发。
在设置索引的时候,可以进行指定分词器的相关属性,其中有一项是指定fliter,可以通过指定ngram:
{
"settings": {
"analysis": {
"filter": {
"2_3_grams": {
"type": "ngram",
"min_gram": 1,
"max_gram": 2
}
},
"analyzer": {
"my_ngram": {
"type":"custom",
"tokenizer": "standard",
"filter": [ "2_3_grams" ]
}
}
}
},
"mappings": {
"properties": {
"text": {
"type": "text",
"analyzer":"my_ngram",
"search_analyzer": "standard"
}
}
}
}
经过ngram设置的min_gram:1和"max_gram": 3,分析以下语句:
GET _analyze
{
"tokenizer": "ik_max_word",
"filter": [ "edge_ngram" ],
"text": "reba always"
}
会先按最小粒度1进行拆分,也就是拆分出“r”,“e”,“b”,“a”,“a”,“l”,“w”,“a”,“y”,“s”
然后按粒度2拆分,拆分成:“re”,“eb”,“ba”,“al”,“lw”,“wa”,“ay”,“ys”
然后按最大粒度3进行拆分,拆分成:“reb”,“eba”,“alw”,“lwa”,“way”,“ays”
Edge-N-gram-tokenFilter
另外一个filter,根据min_gram和max_gram对分词的开头部分进行拆分。
这个可能更常用一些,因为更多的我们的搜索是从一个词的开头进行部分搜索,而不是中间进行搜索。
GET _analyze
{
"tokenizer": "ik_max_word",
"filter": [ "edge_ngram" ],
"text": "reba always loves me"
}
min_gram =1 “max_gram”: 1
拆分情况:r a l m
min_gram =1 “max_gram”: 2
拆分情况:
r a l m
re al lo me
min_gram =2 “max_gram”: 3
拆分情况:
re al lo me
reb alw lov me