作者:刘晓国
Ngrams 和 edge ngrams 是在 Elasticsearch 中标记文本的两种更独特的方式。 Ngrams 是一种将一个标记分成一个单词的每个部分的多个子字符的方法。 ngram 和 edge ngram 过滤器都允许你指定 min_gram 以及 max_gram 设置。 这些设置控制单词被分割成的标记的大小。 这可能令人困惑,让我们看一个例子。 假设你想用 ngram 分析仪分析 “spaghetti” 这个词,让我们从最简单的情况开始,1-gams(也称为 unigrams)。
在实际的搜索例子中,比如谷歌搜索:
每当我们打入前面的几个字母时,就会出现相应的很多的候选名单。这个就是 autocomplete 功能。在 Elasticsearch 中,我们可以通过 Edge ngram 来实现这个目的。
1-grams
“spaghetti” 的 1-grams 是 s,p,a,g,h,e,t,t,i。 根据 ngram 的大小将字符串拆分为较小的 token。 在这种情况下,每个 token 都是一个字符,因为我们谈论的是 unigrams。
Bigrams
如果你要将字符串拆分为双字母组(这意味着大小为2),你将获得以下较小的 token:sp,pa,ag,gh,he,et,tt,ti。
Trigrams
再说一次,如果你要使用三个大小,你将得到 token 为 spa,pag,agh,ghe,het,ett,tti。
设置 min_gram 和 max_gram
使用此分析器时,需要设置两种不同的大小:一种指定要生成的最小 ngrams(min_gram 设置),另一种指定要生成的最大 ngrams。 使用前面的示例,如果你指定 min_gram 为2且 max_gram 为3,则你将获得前两个示例中的组合标记:
sp, spa, pa, pag, ag, agh, gh, ghe, he, het, et, ett, tt, tti, ti
如果你要将 min_gram 设置为1并将 max_gram 设置为3,那么你将得到更多的标记,从 s,sp,spa,p,pa,pag,a,....开始。
以这种方式分析文本具有一个有趣的优点。 当你查询文本时,你的查询将以相同的方式被分割成文本,所以说你正在寻找拼写错误的单词“ spaghety”。搜索这个的一种方法是做一个 fuzzy query,它允许你 指定单词的编辑距离以检查匹配。 但是你可以通过使用 ngrams 来获得类似的行为。 让我们将原始单词(“spaghetti”)生成的 bigrams 与拼写错误的单词(“spaghety”)进行比较:
“spaghetti” 的 bigrams:sp,pa,ag,gh,he,et,tt,ti
“spaghety” 的 bigrams:sp,pa,ag,gh,he,et,ty
你可以看到六个 token 重叠,因此当查询包含 “spaghety” 时,其中带有 “spaghetti” 的单词仍然匹配。请记住,这意味着你可能不打算使用的原始 “spaghetti” 单词更多的单词 ,所以请务必测试你的查询相关性!
ngrams 做的另一个有用的事情是允许你在事先不了解语言时或者当你使用与其他欧洲语言不同的方式组合单词的语言时分析文本。 这还有一个优点,即能够使用单个分析器处理多种语言,而不必指定。
Edge ngrams
常规 ngram 拆分的变体称为 edge ngrams,仅从前沿构建 ngram。 在 “spaghetti” 示例中,如果将 min_gram 设置为2并将 max_gram 设置为6,则会获得以下标记:
sp, spa, spag, spagh, spaghe
你可以看到每个标记都是从边缘构建的。 这有助于搜索共享相同前缀的单词而无需实际执行前缀查询。 如果你需要从一个单词的后面构建 ngrams,你可以使用 side 属性从后面而不是默认前面获取边缘。
Ngram 设置
当你不知道语言是什么时,Ngrams 是分析文本的好方法,因为它们可以分析单词之间没有空格的语言。 使用 min 和 max grams 配置 edge ngram analyzer 的示例如下所示:
PUT my_index { "settings": { "analysis": { "analyzer": { "my_analyzer": { "tokenizer": "my_tokenizer" } }, "tokenizer": { "my_tokenizer": { "type": "edge_ngram", "min_gram": 2, "max_gram": 10, "token_chars": [ "letter", "digit" ] } } } } }
我们可以用刚才创建的 my_tokenizer 来分析我们的字符串:
POST my_index/_analyze { "analyzer": "my_analyzer", "text": "2 Quick Foxes." }
显示的结果是:
{ "tokens" : [ { "token" : "Qu", "start_offset" : 2, "end_offset" : 4, "type" : "word", "position" : 0 }, { "token" : "Qui", "start_offset" : 2, "end_offset" : 5, "type" : "word", "position" : 1 }, { "token" : "Quic", "start_offset" : 2, "end_offset" : 6, "type" : "word", "position" : 2 }, { "token" : "Quick", "start_offset" : 2, "end_offset" : 7, "type" : "word", "position" : 3 }, { "token" : "Fo", "start_offset" : 8, "end_offset" : 10, "type" : "word", "position" : 4 }, { "token" : "Fox", "start_offset" : 8, "end_offset" : 11, "type" : "word", "position" : 5 }, { "token" : "Foxe", "start_offset" : 8, "end_offset" : 12, "type" : "word", "position" : 6 }, { "token" : "Foxes", "start_offset" : 8, "end_offset" : 13, "type" : "word", "position" : 7 } ] }
因为我们定义的 min_gram 是2,所以生成的 token 的长度是从2开始的。
通常我们建议在索引时和搜索时使用相同的分析器。 在 edge_ngram tokenizer 的情况下,建议是不同的。 仅在索引时使用 edge_ngram 标记生成器才有意义,以确保部分单词可用于索引中的匹配。 在搜索时,只需搜索用户输入的术语,例如:Quick Fo。
下面是如何为搜索类型设置字段的示例:
PUT my_index { "settings": { "analysis": { "analyzer": { "autocomplete": { "tokenizer": "autocomplete", "filter": [ "lowercase" ] }, "autocomplete_search": { "tokenizer": "lowercase" } }, "tokenizer": { "autocomplete": { "type": "edge_ngram", "min_gram": 2, "max_gram": 10, "token_chars": [ "letter" ] } } } }, "mappings": { "properties": { "title": { "type": "text", "analyzer": "autocomplete", "search_analyzer": "autocomplete_search" } } } }
在我们的例子中,我们索引时和搜索时时用了两个不同的 analyzer:autocomplete 及 autocomplete_search。
PUT my_index/_doc/1 { "title": "Quick Foxes" } POST my_index/_refresh
上面我们加入一个文档。下面我们来进行搜索:
GET my_index/_search { "query": { "match": { "title": { "query": "Quick Fo", "operator": "and" } } } }
显示结果:
{ "took" : 3, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 1, "relation" : "eq" }, "max_score" : 0.5753642, "hits" : [ { "_index" : "my_index", "_type" : "_doc", "_id" : "1", "_score" : 0.5753642, "_source" : { "title" : "Quick Foxes" } } ] } }
在这里 autocomplete analyzer 可以把字符串 “Quick Foxes” 分解为[qu, qui, quic, quick, fo, fox, foxe, foxes]。而自 autocomplete_search analyzer 搜索条目[quick,fo],两者都出现在索引中。
当然我们也可以做如下的搜索:
GET my_index/_search { "query": { "match": { "title": { "query": "Fo" } } } }
显示的和上面一样的结果。
Shingles
与 ngrams 和 edge ngrams一样,有一个称为 shingle 的过滤器(不,不是疾病的那个shingle!)。 Shingle token 过滤器基本上是 token 级别的 ngrams 而不是字符级别。
想想我们最喜欢的单词 “spaghetti”。使用最小和最大设置为1和3的 ngrams,Elasticsearch 将生成标记s,sp,spa,p,pa,pag,a,ag等。 一个 shingle 过滤器在 token 级别执行此操作,因此如果你有文本 “foo bar baz” 并再次使用 in_shingle_size 为2且 max_shingle_size 为3,则你将生成以下 token:
foo, foo bar, foo bar baz, bar, bar baz, baz
为什么仍然包含单 token 输出? 这是因为默认情况下,shingle 过滤器包含原始 token,因此原始标记生成令牌 foo,bar 和 baz,然后将其传递给 shingle token 过滤器,生成标记foo bar,foo bar baz 和 bar baz。 所有这些 token 组合在一起形成最终 token 流。 你可以通过将 output_unigrams 选项设置为 false 来禁用此行为,也即不需要最原始的 token:foo, bar 及 baz
下一个清单显示了 shingle token 过滤器的示例; 请注意,min_shingle_size 选项必须大于或等于2。
PUT my_index { "settings": { "analysis": { "analyzer": { "shingle": { "type": "custom", "tokenizer": "standard", "filter": [ "shingle-filter" ] } }, "filter": { "shingle-filter": { "type": "shingle", "min_shingle_size": 2, "max_shingle_size": 3, "output_unigrams": false } } } } }
在这里,我们定义了一个叫做 shingle-filter 的过滤器。最小的 shangle 大小是2,最大的 shingle 大小是3。同时我们设置 output_unigrams 为 false,这样最初的那些 token 将不被包含在最终的结果之中。
下面我们来做一个例子,看看显示的结果:
GET /my_index/_analyze { "text": "foo bar baz", "analyzer": "shingle" }
显示的结果为:
{ "tokens" : [ { "token" : "foo bar", "start_offset" : 0, "end_offset" : 7, "type" : "shingle", "position" : 0 }, { "token" : "foo bar baz", "start_offset" : 0, "end_offset" : 11, "type" : "shingle", "position" : 0, "positionLength" : 2 }, { "token" : "bar baz", "start_offset" : 4, "end_offset" : 11, "type" : "shingle", "position" : 1 } ] }
参考:
【1】 https://www.elastic.co/guide/en/elasticsearch/reference/7.3/analysis-edgengram-tokenizer.html