一、match系列查询
前面讲到的query中的查询,都是精准查询。可以理解成跟在关系型数据库中的查询类似。match系列的查询,是全文检索的查询。会通过分词进行评分,匹配,再返回搜索结果。
1.1 match 查询
"query": {"match": {"elk": "Elasticsearch LogStash Kibana"}
}
查询字符串是“Elasticsearch LogStash Kibana”,被分析器分词之后,产生三个小写的单词:elasticsearch logstash kibana,然后根据分析的结果构造一个布尔查询,默认情况下,引擎内部执行的查询逻辑是:只要 elk 字段值中包含有任意一个关键字 elasticsearch 或 logStash 或 kibana,那么返回该文档。
匹配查询的行为受到两个参数的控制:
operator:表示单个字段如何匹配查询条件的分词
minimum_should_match:表示字段匹配的数量。
通过调整 operator 和 minimum_should_match 属性值,控制匹配查询的逻
辑条件,进而控制引擎返回的结果。默认情况下 operator 的值是 or,在构造查询时设置分词之间的逻辑运算符,如果设置为 and,那么只有分词的每个词都匹配的文档,才能返回。
对于 minimum_should_match 属性值,默认值是 1,如果设置其值为 2,表示分词必须匹配查询条件的数量为 2,这意味着,只要文档的 elk 字段包含任意两个关键字,就满足查询条件,但是如果文档中只有 1 个关键字,这个文档就不满足条件。
例如:
POST indexname/_search{"query": {"match": {"message": {"query": "firefox chrome","operator": "and"}}}
}
或者:
POST indexname/_search{"query": {"match": {"message": {"query": "firefox chrome","minimum_should_match": 2}}}
}
只会返回文档中message字段同时包含firefox和chrome的文档,而只包含其中之一的不会返回。
1.2 multi_match 查询
多个字段上执行匹配相同的查询,叫做"multi_match"查询。比如:
POST indexname/_search{"query": {"multi_match": {"query": "AT","fields": ["DestCountry","OriginCountry"]}}
}
请求将同时检索文档中 DestCountry 和 OriginCountry 这两个字段,只要有一个字段包含 AT 词项该文档就满足查询条件。
1.3 match_phrase 查询
当你希望寻找邻近的单词时,match_phrase 查询可以帮你达到目的。比如:
假设我们要找到 title 字段包含这么一段文本“quick brown fox”的文档,然后我们用
GET indexname/_search{"query": {"match_phrase": {"title": "quick brown fox"}}
}
match_phrase 查询首先解析查询字符串来产生一个词条列表。然后会搜索所有的词条,但只保留包含了所有搜索词条的文档,并且词条的位置要邻接。
但是对于
GET /my_index/my_type/_search{"query": {"match_phrase": {"title": "quick fox"}}
}
这个查询查询不会匹配我们的任何文档,因为没有文档含有邻接在一起的
quick 和 fox 词条。也就是说,匹配的文档必须满足:
1、quick、brown 和 fox 必须全部出现在 title 字段中。
2、brown 的位置必须比 quick 的位置大 1。
3、fox 的位置必须比 quick 的位置大 2。
如果以上的任何一个条件没有被满足,那么文档就不能被匹配。
精确短语(Exact-phrase)匹配也许太过于严格了。也许我们希望含有"quick
brown fox"的文档也能够匹配"quick fox"查询,即使位置并不是完全相等的。
我们可以在短语匹配使用 slop 参数来引入一些灵活性:
GET /my_index/my_type/_search{"query": {"match_phrase": {"title": {"query": "quick fox","slop": 1}}}
}
match_phrase_prefix 查询
被称为基于前缀的短语匹配,比如:
{"match_phrase_prefix": {"brand": "johnnie walker bl"}
}
这种查询的行为与 match_phrase 查询一致,不同的是它将查询字符串的最
后一个词作为前缀使用,换句话说,可以将之前的例子看成如下这样:
johnnie跟着 walker,跟着以 bl 开始的词。
或者可以干脆理解为: “johnnie walker bl*”。
与 match_phrase 一样,它也可以接受 slop 参数(参照 slop )让相对词
序位置不那么严格。
prefix 查询存在严重的资源消耗问题,短语查询的这种方式也同样如此。前
缀 a 可能会匹配成千上万的词,这不仅会消耗很多系统资源,而且结果的用处也不大。
可以通过设置 max_expansions 参数来限制前缀扩展的影响,一个合理的值是 50 ,这也是系统默认的值:
{"match_phrase_prefix": {"brand": {"query": "johnnie walker bl","max_expansions": 50}}
}
二、模糊查询、纠错与提示器
2.1 编辑距离算法
在 Elasticsearch 基于全文的查询中,除了与短语相关的查询以外,其余查询都包含有一个名为 fuzziness 的参数用于支持模糊查询。Elasticsearch 支持的模糊查询与 SQL 语言中模糊查询还不一样,SQL 的模糊查询使用“% keyword%"的形式,效果是查询字段值中包含 keyword 的记录。
Elaticsearch 支持的模糊查询比这个要强大得多,它可以根据一个拼写错误的词项匹配正确的结果,例如根据firefix 匹配 firefox。在自然语言处理领域,两个词项之间的差异通常称为距离或编辑距离,距离的大小用于说明两个词项之间差异的大小。
编辑距离的算法有多种,在 Elasticsearch 中主要使用 Levenshtein 和 NGram两种。其他与此相关的算法也都是在这两种算法基础上进行的改造,基本思想都是一致的。所以理解这两个算法的核心思想是学习这部分内容的关键。
Levenshtein 与 NGram
Levenshein算法是前苏联数学家 Vladimir Levenshein在1965 年开发的一套算法, 这个算法可以对两个字符申的差异程度做量化。量化结果是一一个正整数,反映的是一个字符申变成另一个字符申最少需要多少次的处理。由于 Levenshtein算法是最为普遍接受的编辑距离算法,所以在很多文献中如果没有特殊说明编辑距离算法就是指 Levenshtein 算法。
在 Levenshtein 算法中定义了三种字符操作,即替换、插人和删除,后来又
由其他科学家补充了一个换位操作。在转换过程中,每执行次操作编辑距离就加1, 编辑距离越大越能说明两个字符串之间的差距大。
比如从 firefix 到 firefox 需要将“i"替换成“o”, 所以编辑距离为 1;而从 fax
到 fair 则需要将“x”替换为“i"并在结尾处插人“r”,所以编辑距离为 2。显然
在编辑距离相同的情况下,单词越长错误与正确就越接近。比如编辑距离同样为2 的情况下,从 fax 到 fair 与从 elascsearxh 到 elasticsearch,后者elastesearsh 是由拼写错误引起的可能性就更大此。所以编辑距离这种量化标准一般还需要与单词长度结合起来 明虑,在一些极端情况下编辑距离还应该设置为 0,比如像 at、on 这类长度只有 2 的短单词。
NGram 一般是指 N 个连续的字符,具体的字符个数被定义为 NGram 的 size。size 为 1 的 NGram 称为 Unigram, size 为 2 时称为 Bigram,而 size 为 3 时则称为Trigram。如果 NGram 处理的单元不是字符而是单词,一般称之为 Shingle。使用NGram 计算编辑距离的基本思路是让字符串分解为 NGram,然后比较分解后共有 NGram 的数量。假设有 a、b 两个字符申,则 NGram 距离的具体运算公式为:
ngram( a )+ngram(b) -2 * ngram(a)∩ngram( b)
ngram(a)和 ngram(b)代表 a、b 两个字符串 NGram 的数量; ngram(a) ∩
ngram(b)则是两者共有 NGram 的数量。
例如按 Bigram 处理 firefix 和 firefox 两个单词,分别为“fi,ir,re, ef, fi,ix”和“fi,ir, re, ef, fi, ox"。 那么两个字符申的 Bigram 个数都为 6,而共有 Bigram 为 4,则最终 NGram 距离为 6+6-2x4=4。
在应用上,Levenshtein 算法更多地应用于对单个词项的模糊查询上,而
NGram 则应用于多词项匹配中。Elasticseareh 同时应用了两种算法。
2.2 模糊查询
返回包含与搜索字词相似的字词文档;为了找到相似的术语,fuzzy 查询将
在指定的编辑距离内创建一组搜索词的所有可能的变体或扩展。查询然后返回每个扩展的完全匹配。
比如:
get indexname/_search{"query": {"fuzzy": {"message": {"value": "firefix","fuzziness": "1"}}}
}
我们想找到文档中 message 字段包含 firefox,而查询条件中给出的是 firefix,因为两者的编辑距离为 1,所以包含 firefox 的文档依然可以找到,但是,如果使用 firefit,因为编辑距离为 2,则不会找到任何文档。
相关的参数有:
value: 必填项,希望在 field 中找到的术语。
fuzziness: 选填项,匹配允许的最大编辑距离;可以被设置为“0”, “1”,“2”或“auto”。“auto”是推荐的选项,它会根据查询词的长度定义距离。
max_expansions: 选填项,创建的最大变体项,默认为 50。应该避免使用较大的值,尤其是当 prefix_length 参数值为 0 时,如果过多,会影响查找性能。
prefix_length: 选填项,创建扩展时保留不变的开始字符数。默认为 0。
transpositions: 选填项,指示编辑是否包括两个相邻字符串的转置(ab→ba)。默认为 true。
2.3 纠错与提示器
纠错是在用户提交了错误的词项时给出正确词项的提示,而输人提示则是在
用户输人关键字时给出智能提示,甚至可以将用户未输人完的内容自动补全。大多数互联网搜索引擎都同时支持纠错和提示的功能,比如在用户提交了错误的搜索关键字时会提示:“ 你是不是想查找…”.而在用户输人搜索关键字时还能自动弹出提示框将用户可能要输人的内容全都列出来供用户选择。
Elasticsearch 也同时支持纠错与提示功能,由于这两个功能从实现的角度来说并没有本质区别,所以它们都由一种被称为提示器或建议器( Suggester)的特殊检索实现。由于输人提示需要在用户输人的同时给出提示词,所以这种功能要求速度必须快,否则就失去了提示的意义。在实现上,输人提示是由单独的提示器完成。而在使用上,提示器则是通过检索接口_ search 的一个参数设置,例如:
POST indexname/_search?filter_path=suggest{"suggest": {"msg-suggest": {"text": "firefit chrom","term": {"field": "message"}}}
}
在示例中,search 接口的 suggest 参数中定义了一个提示 msg- suggest,并通过 text 参数给出需要提示的内容。另一个参数 term 实际上是一种提示器的名称,它会分析 text 参数中的字符串并提取词项,再根据 Levenshtein 算法找到满足编辑距离的提示词项。所以在返回结果中会包含一个 suguggest 字段,其中列举了依照 term 提示器找到的提示词项:
2.3 提示器
Elaticearch 提供了三种提示器,它们在本质上都是基于编辑距离算法。下面就来看看这此提示器如何使用。
term 提示器
这种提示器默认使用的算法是称为 internal 的编辑距离算法。intermal 算法本质上就是 Levenshtein 算法,但根据 Elasticsearch 索引特征做了一些优化而效率更高,可以通过 string distance 参数更改算法。
term 提示器使用的编辑距离可通过 max edits 参数设置,默认值为 2。
phrase 提示器
terms 会将需要提示的文本拆分成词项,然后对每一个词项做单独的提示,
而 phrase 提示器则会使用整个文本内容做提示。所以在 phrase 提示器的返回结果中,不会看到一个词项一个词项的提示,而是针对整个短语的提示。但从使用的角度来看几乎是一样的,例如:
POST indexname/_search{"suggest": {"msg-suggest": {"text": "firefix with chrime","phrase": {"field": "message"}}}
}
但不要被 phrase 提示器返回结果欺骗,这个提示器在执行时也会对需要提
示的文本内容做词项分析,然后再通过 NGram 算法计算整个短语的编辑距离。所以本质上来说,phrase 提示是基于 term 提示器的提示器,同时使用了Levenshtein 和 NGram 算法。
completion 提示器
completion 提示器一般应用于输人提示和自动补全,也就是在用户输人的同时给出提示或补全未输入内容。这就要求 completion 提示器必须在用户输人结束前快速地给出提示,所以这个提示器在性能上做了优化以达到快速检索的目的。
首先要求提示词产生的字段为 completion 类型,这是一种专门为completion提示器而设计的字段类型,它会在内存中创建特殊的数据结构以满足快速生成提示词的要求。例如在示例中创建了 aricles 索引,并向其中添加了 1 份文档:
PUT articles{"mappings": {"properties": {"author": {"type": "keyword"},"content": {"type": "text"},"suggestions": {"type": "completion"}}}
}
POST articles/_doc/{"author": "taylor","content": "an introduction of elastic stack and elasticsearch","suggestions": {"input": ["elastic stack","elasticsearch"],"weight": 10}
}
POST articles/_doc/{"author": "taylor","content": "an introduction of elastic stack and elasticsearch","suggestions": [{"input": "elasticsearch","weight": 30},{"input": "elastic stack","weight": 1}]
}
在向 completion 类型的字段添加内容时可以使用两个参数,input 参数设置
字段实际保存的提示词;而 weight 参数则设置了这些提示词的权重,权重越高它在返回的提示词中越靠前。在示例 5-29 中给出了两种设置提示词权重的方式,第一种是将一组提示词的权重设置为统一值,另一种则是分开设置它们的权重值。需要注意的是,completion 类型字段保存的提示词是不会分析词项的。
completion 提示器专门用于输人提示或补全,它根据用户已经输人的内容提示完整词项,所以在 completion 提示器中没有 text 参数而是使用 prefix 参数。例如:
POST articles/_search{"_source": "suggest","suggest": {"article_suggestion": {"prefix": "ela","completion": {"field": "suggestions"}}}
}
总结一下,term 和 phrase 提示器主要用于纠错,term 提示器用于对单个词项的纠错而 phrase 提示器则主要针对短语做纠错。completion 提示器是专门用于输人提示和自动补全的提示器,在使用上依赖前缀产生提示并且速度更快。