目录
1.简介
部分匹配 允许用户指定查找词的一部分并找出所有包含这部分片段的词。
Elasticsearch 提供分析过程,倒排索引让我们不需要使用这种粗笨的技术。
在某些情况下部分匹配会比较有用,常见的应用如下:
-
匹配邮编、产品序列号或其他
not_analyzed
未分析值,这些值可以是以某个特定前缀开始,也可以是与某种模式匹配的,甚至可以是与某个正则式相匹配的。 -
输入即搜索(search-as-you-type) ——在用户键入搜索词过程的同时就呈现最可能的结果。
-
匹配如德语或荷兰语这样有长组合词的语言,如: Weltgesundheitsorganisation (世界卫生组织,英文 World Health Organization)。
1.1.邮编与结构化数据
【举例】
美国目前使用的邮编形式(United Kingdom postcodes 标准)来说明如何用部分匹配查询结构化数据。这种邮编形式有很好的结构定义。
例如,邮编 W1V 3DG
可以分解成如下形式:
-
W1V
:这是邮编的外部,它定义了邮件的区域和行政区:-
W
代表区域( 1 或 2 个字母) -
1V
代表行政区( 1 或 2 个数字,可能跟着一个字符)
-
-
3DG
:内部定义了街道或建筑:-
3
代表街区区块( 1 个数字) -
DG
代表单元( 2 个字母)
-
PUT /my_index
{
"mappings": {
"address": {
"properties": {
"postcode": {
"type": "string",
"index": "not_analyzed"
}
}
}
}
}
PUT /my_index/address/1
{ "postcode": "W1V 3DG" }
PUT /my_index/address/2
{ "postcode": "W2F 8HW" }
PUT /my_index/address/3
{ "postcode": "W1F 7HW" }
PUT /my_index/address/4
{ "postcode": "WC1N 1LZ" }
PUT /my_index/address/5
{ "postcode": "SW5 0BE" }
1.2.prefix 前缀查询
为了找到所有以 W1
开始的邮编,可以使用简单的 prefix
查询:
【举例】
GET /my_index/address/_search
{
"query": {
"prefix": {
"postcode": "W1"
}
}
}
【说明】
prefix
查询是一个词级别的底层的查询,它不会在搜索之前分析查询字符串,它假定传入前缀就正是要查找的前缀。
注意
默认状态下,prefix 查询不做相关度评分计算,它只是将所有匹配的文档返回,并为每条结果赋予评分值 1 。
它的行为更像是过滤器而不是查询。
prefix 查询和 prefix 过滤器这两者实际的区别就是过滤器是可以被缓存的,而查询不行。
对于每个词,倒排索引都会将包含词的文档 ID 列入 倒排表(postings list) 。
为了支持前缀匹配,查询会做以下事情:
- 扫描词列表并查找到第一个以
W1
开始的词。 - 搜集关联的文档 ID 。
- 移动到下一个词。
- 如果这个词也是以
W1
开头,查询跳回到第二步再重复执行,直到下一个词不以W1
为止。
注意
prefix 查询或过滤对于一些特定的匹配是有效的,但使用方式还是应当注意。
当字段中词的集合很小时,可以放心使用,但是它的伸缩性并不好,会对我们的集群带来很多压力。
可以使用较长的前缀来限制这种影响,减少需要访问的量。
1.3.通配符与正则表达式查询
【举例】
GET /my_index/address/_search
{
"query": {
"wildcard": {
"postcode": "W?F*HW"
}
}
}
GET /my_index/address/_search
{
"query": {
"regexp": {
"postcode": "W[0-9].+"
}
}
}
【注意】
这也意味着需要同样注意前缀查询存在性能问题,对有很多唯一词的字段执行这些查询可能会消耗非常多的资源,所以要避免使用左通配这样的模式匹配(如: *foo
或 .*foo
这样的正则式)。
数据在索引时的预处理有助于提高前缀匹配的效率,而通配符和正则表达式查询只能在查询时完成,尽管这些查询有其应用场景,但使用仍需谨慎。
prefix 、wildcard 和 regexp 查询是基于词操作的,如果用它们来查询 analyzed 字段,
它们会检查字段里面的每个词,而不是将字段作为整体来处理。
"Quick brown fox"
会匹配以下这个查询:
{ "regexp": { "title": "br.*" }}
但是不会匹配以下两个查询:
{ "regexp": { "title": "Qu.*" }} // 在索引里的词是 quick 而不是 Quick 。
{ "regexp": { "title": "quick br*" }} // quick 和 brown 在词表中是分开的。
1.4.查询时输入即搜索
即时搜索(instant search) 或 输入即搜索(search-as-you-type)
在 短语匹配 中,引入了 match_phrase
短语匹配查询,它匹配相对顺序一致的所有指定词语,对于查询时的输入即搜索,可以使用 match_phrase
的一种特殊形式, match_phrase_prefix
查询
【举例】
{
"match_phrase_prefix" : {
"brand" : "johnnie walker bl"
}
}
{
"match_phrase_prefix" : {
"brand" : {
"query": "walker johnnie bl",
"slop": 10
}
}
}
【说明】
这种查询的行为与 match_phrase
查询一致,不同的是它将查询字符串的最后一个词作为前缀使用
"johnnie walker bl*"
【max_expansions
】
{
"match_phrase_prefix" : {
"brand" : {
"query": "johnnie walker bl",
"max_expansions": 50
}
}
}
参数 max_expansions
控制着可以与前缀匹配的词的数量,它会先查找第一个与前缀 bl
匹配的词,然后依次查找搜集与之匹配的词(按字母顺序),直到没有更多可匹配的词或当数量超过 max_expansions
时结束
1.5.索引时优化
可以通过在索引时处理数据提高搜索的灵活性以及提升系统性能。为此仍然需要付出应有的代价:增加的索引空间与变慢的索引能力,但这与每次查询都需要付出代价不同,索引时的代价只用付出一次。
1.6.Ngrams 在部分匹配的应用
在索引时准备数据意味着要选择合适的分析链,这里部分匹配使用的工具是 n-gram 。可以将 n-gram 看成一个在词语上 滑动窗口, n 代表这个 “窗口” 的长度。如果要 n-gram quick
这个词 —— 它的结果取决于 n 的选择长度:
- 长度 1(unigram): [
q
,u
,i
,c
,k
] - 长度 2(bigram): [
qu
,ui
,ic
,ck
] - 长度 3(trigram): [
qui
,uic
,ick
] - 长度 4(four-gram): [
quic
,uick
] - 长度 5(five-gram): [
quick
]
朴素的 n-gram 对 词语内部的匹配 非常有用。但对于输入即搜索(search-as-you-type)这种应用场景,我们会使用一种特殊的 n-gram 称为 边界 n-grams (edge n-grams)。所谓的边界 n-gram 是说它会固定词语开始的一边,以单词 quick
为例,它的边界 n-gram 的结果为:
q
qu
qui
quic
quick
索引时输入即搜索
【举例】
创建索引、实例化 token 过滤器和分析器
PUT /my_index
{
"settings": {
"number_of_shards": 1,
"analysis": {
"filter": {
"autocomplete_filter": { // 自定义的 edge_ngram
token 过滤器(autocomplete_filter
) ,这个分析器使用 standard
分词器将字符串拆分为独立的词,并且将它们都变成小写形式,然后为每个词生成一个边界 n-gram
"type": "edge_ngram",
"min_gram": 1,
"max_gram": 20
}
},
"analyzer": {
"autocomplete": {// 自定义分析器 autocomplete,
这个分析器使用 standard
分词器将字符串拆分为独立的词,并且将它们都变成小写形式,然后为每个词生成一个边界 n-gram
"type": "custom",
"tokenizer": "standard",
"filter": [
"lowercase",
"autocomplete_filter"
]
}
}
}
}
}
【测试】
GET /my_index/_analyze?analyzer=autocomplete
quick brown
【结果】
q
qu
qui
quic
quick
b
br
bro
brow
brown
【可以用 update-mapping
API 将这个分析器应用到具体字段】
PUT /my_index/_mapping/my_type
{
"my_type": {
"properties": {
"name": {
"type": "string",
"analyzer": "autocomplete"
}
}
}
}
【查询】
GET /my_index/my_type/_search
{
"query": {
"match": {
"name": "brown fo"
}
}
}
[结果]
{
"hits": [
{
"_id": "1",
"_score": 1.5753809,
"_source": {
"name": "Brown foxes"
}
},
{
"_id": "2",
"_score": 0.012520773,
"_source": {
"name": "Yellow furballs"
}
}
]
}
【分析】
GET /my_index/my_type/_validate/query?explain
{
"query": {
"match": {
"name": "brown fo"
}
}
}
【结果】不正确返回 Brown foxes
这个文档
name:b name:br name:bro name:brow name:brown name:f name:fo
name:f
条件可以满足第二个文档,因为 furballs
是以 f
、 fu
、 fur
形式索引的。回过头看这并不令人惊讶,相同的 autocomplete
分析器同时被应用于索引时和搜索时,这在大多数情况下是正确的,只有在少数场景下才需要改变这种行为。
GET /my_index/my_type/_search
{
"query": {
"match": {
"name": {
"query": "brown fo",
"analyzer": "standard" // 覆盖了 name
字段 analyzer
的设置。
}
}
}
}
PUT /my_index/my_type/_mapping
{
"my_type": {
"properties": {
"name": {
"type": "string",
"index_analyzer": "autocomplete", // 在索引时,使用 autocomplete
分析器生成边界 n-grams 的每个词。
"search_analyzer": "standard" // 在搜索时,使用 standard
分析器只搜索用户输入的词。
}
}
}
}
GET /my_index/my_type/_validate/query?explain
{
"query": {
"match": {
"name": "brown fo"
}
}
}
【结果】正确返回 Brown foxes
这个文档
name:brown name:fo
补全提示(Completion Suggester)
使用边界 n-grams 进行输入即搜索(search-as-you-type)的查询设置简单、灵活且快速,但有时候它并不够快,特别是当试图立刻获得反馈时,
延迟的问题就会凸显,很多时候不搜索才是最快的搜索方式。
Elasticsearch 里的 {ref}/search-suggesters-completion.html[completion suggester] 采用与上面完全不同的方式,
需要为搜索条件生成一个所有可能完成的词列表,然后将它们置入一个 有限状态机(finite state transducer) 内,这是个经优化的图结构。
为了搜索建议提示,Elasticsearch 从图的开始处顺着匹配路径一个字符一个字符地进行匹配,
一旦它处于用户输入的末尾,Elasticsearch 就会查找所有可能结束的当前路径,然后生成一个建议列表。
本数据结构存于内存中,能使前缀查找非常快,比任何一种基于词的查询都要快很多,
这对名字或品牌的自动补全非常适用,因为这些词通常是以普通顺序组织的:用 “Johnny Rotten” 而不是 “Rotten Johnny” 。
当词序不是那么容易被预见时,边界 n-grams 比完成建议者(Completion Suggester)更合适。
即使说不是所有猫都是一个花色,那这只猫的花色也是相当特殊的。
边界 n-grams 与邮编
边界 n-gram 的方式可以用来查询结构化的数据,比如邮编(postcode)。当然 postcode
字段需要 analyzed
而不是 not_analyzed
,不过可以用 keyword
分词器来处理它,就好像他们是 not_analyzed
的一样。
keyword 分词器是一个非操作型分词器,这个分词器不做任何事情,它接收的任何字符串都会被原样发出,
因此它可以用来处理 not_analyzed 的字段值,但这也需要其他的一些分析转换,如将字母转换成小写。
【举例】
{
"analysis": {
"filter": {
"postcode_filter": {
"type": "edge_ngram",
"min_gram": 1,
"max_gram": 8
}
},
"analyzer": {
"postcode_index": { // 析器使用 postcode_filter
将邮编转换成边界 n-gram 形式。
"tokenizer": "keyword",
"filter": [ "postcode_filter" ]
},
"postcode_search": { // 分析器可以将搜索词看成 not_analyzed
未分析的。
"tokenizer": "keyword"
}
}
}
}
1.7.Ngrams 在复合词的应用
【举例】
假设某个 n-gram 是一个词上的滑动窗口,那么任何长度的 n-gram 都可以遍历这个词。我们既希望选择足够长的值让拆分的词项具有意义,又不至于因为太长而生成过多的唯一词。一个长度为 3 的 trigram 可能是一个不错的开始:
PUT /my_index
{
"settings": {
"analysis": {
"filter": {
"trigrams_filter": {
"type": "ngram",
"min_gram": 3,
"max_gram": 3
}
},
"analyzer": {
"trigrams": {
"type": "custom",
"tokenizer": "standard",
"filter": [
"lowercase",
"trigrams_filter"
]
}
}
}
},
"mappings": {
"my_type": {
"properties": {
"text": {
"type": "string",
"analyzer": "trigrams" // text
字段用 trigrams
分析器索引它的内容,这里 n-gram 的长度是 3 。
}
}
}
}
}
[测试]
GET /my_index/_analyze?analyzer=trigrams
Weißkopfseeadler
[返回]
wei, eiß, ißk, ßko, kop, opf, pfs, fse, see, eea,ead, adl, dle, ler
POST /my_index/my_type/_bulk
{ "index": { "_id": 1 }}
{ "text": "Aussprachewörterbuch" }
{ "index": { "_id": 2 }}
{ "text": "Militärgeschichte" }
{ "index": { "_id": 3 }}
{ "text": "Weißkopfseeadler" }
{ "index": { "_id": 4 }}
{ "text": "Weltgesundheitsorganisation" }
{ "index": { "_id": 5 }}
{ "text": "Rindfleischetikettierungsüberwachungsaufgabenübertragungsgesetz" }
GET /my_index/my_type/_search
{
"query": {
"match": {
"text": "Adler"
}
}
}
[结果]
{
"hits": [
{
"_id": "3",
"_score": 3.3191128,
"_source": {
"text": "Weißkopfseeadler"
}
}
]
}
[结果排除]
GET /my_index/my_type/_search
{
"query": {
"match": {
"text": {
"query": "Gesundheit",
"minimum_should_match": "80%"
}
}
}
}