同义词
词干提取是通过简化他们的词根形式来扩大搜索的范围,同义词 通过相关的观念和概念来扩大搜索范围。 也许没有文档匹配查询 “英国女王“ ,但是包含 “英国君主” 的文档可能会被认为是很好的匹配。
用户搜索 “美国” 并且期望找到包含 美利坚合众国 、 美国 、 美洲 、或者 美国各州 的文档。 然而,他们不希望搜索到关于 国事
或者 政府机构
的结果。
这个例子提供了宝贵的经验,它向我们阐述了,区分不同的概念对于人类是多么简单而对于纯粹的机器是多么棘手的事情。通常我们会对语言中的每一个词去尝试提供同义词以确保任何一个文档都是可发现的,以保证不管文档之间有多么微小的关联性都能够被检索出来。
这样做是不对的。就像我们更喜欢不用或少用词根而不是过分使用词根一样,同义词也应该只在必要的时候使用。 这是因为用户可以理解他们的搜索结果受限于他们的搜索词,如果搜索结果看上去几乎是随机时,他们就会变得无法理解(注:大规模使用同义词会导致查询结果趋向于让人觉得是随机的)。
同义词可以用来合并几乎相同含义的词,如 跳
、 跳越
或者 单脚跳行
,和 小册子
、 传单
或者 资料手册
。 或者,它们可以用来让一个词变得更通用。例如, 鸟
可以作为 猫头鹰
或 鸽子
的通用代名词,还有, 成人
可以被用于 男人
或者 女人
。
同义词似乎是一个简单的概念,但是正确的使用它们却是非常困难的。在这一章,我们会介绍使用同义词的技巧和讨论它的局限性和陷阱。
Tip | 同义词扩大了一个匹配文件的范围。正如 词干提取 或者 部分匹配 ,同义词的字段不应该被单独使用,而应该与一个针对主字段的查询操作一起使用,这个主字段应该包含纯净格式的原始文本。 在使用同义词时,参阅 多数字段 的解释来维护相关性。 |
---|---|
使用同义词
同义词可以取代现有的语汇单元或 通过使用 同义词
语汇单元过滤器,添加到语汇单元流中:
PUT /my_index
{
"settings": {
"analysis": {
"filter": {
"my_synonym_filter": {
"type": "synonym", (1)
"synonyms": [ (2)
"british,english",
"queen,monarch"
]
}
},
"analyzer": {
"my_synonyms": {
"tokenizer": "standard",
"filter": [
"lowercase",
"my_synonym_filter" (3)
]
}
}
}
}
}
首先,我们定义了一个
同义词
类型的语汇单元过滤器。我们在 同义词格式 中讨论同义词格式。
然后我们创建了一个使用
my_synonym_filter
的自定义分析器。
Tip | 同义词可以使用 synonym 参数来内嵌指定,或者必须 存在于集群每一个节点上的同义词文件中。 同义词文件路径由 synonyms_path 参数指定,应绝对或相对于 Elasticsearch config 目录。参照 更新停用词(Updating Stopwords) 的技巧,可以用来刷新的同义词列表。 |
---|---|
通过 analyze
API 来测试我们的分析器,显示如下:
GET /my_index/_analyze?analyzer=my_synonyms
Elizabeth is the English queen
Pos 1: (elizabeth)
Pos 2: (is)
Pos 3: (the)
Pos 4: (british,english) (1)
Pos 5: (queen,monarch) (1)
- 所有同义词与原始词项占有同一个位置。
这样的一个文件将匹配任何以下的查询: English queen
、British queen
、 English monarch
或 British monarch
。 即使是一个短语查询也将会工作,因为每个词项的位置已被保存。
Tip | 在索引和搜索中使用相同的同义词语汇单元过滤器是多余的。 如果在索引的时候,我们用 english 和 british 这两个术语代替 English , 然后在搜索的时候,我们只需要搜索这些词项中的一个。或者,如果在索引的时候我们不使用同义词,然后在搜索的时候,我们将需要把对 English 的查询转换为 english 或者 british 的查询。 |
---|---|
同义词格式
同义词最简单的表达形式是 逗号分隔:
"jump,leap,hop"
如果遇到这些词项中的任何一项,则将其替换为所有列出的同义词。例如:
原始词项: 取代:
────────────────────────────────
jump → (jump,leap,hop)
leap → (jump,leap,hop)
hop → (jump,leap,hop)
或者, 使用 ⇒
语法,可以指定一个词项列表(在左边),和一个或多个替换(右边)的列表:
"u s a,united states,united states of america => usa" "g b,gb,great britain => britain,england,scotland,wales"
原始词项: 取代:
────────────────────────────────
u s a → (usa)
united states → (usa)
great britain → (britain,england,scotland,wales)
如果多个规则指定同一个同义词,它们将被合并在一起,且顺序无关,否则使用最长匹配。以下面的规则为例:
"united states => usa", "united states of america => usa"
如果这些规则相互冲突,Elasticsearch 会将 United States of America
转换为词项 (usa),(of),(america)
。否则,会使用最长的序列,即最终得到词项 (usa)
。
扩展或收缩
在 同义词格式 中,我们看到了可以通过 简单扩展 、 简单收缩 、或类型扩展 来指明同义词规则。 本章节我们将在这三者间做个权衡比较。
Tip | 本节仅处理单词同义词。多词同义词又增添了一层复杂性,在 多词同义词和短语查询 中,我们将会讨论。 |
---|---|
简单扩展
通过 简单扩展 ,我们可以把同义词列表中的任意一个词扩展成同义词列表 所有 的词:
"jump,hop,leap"
扩展可以应用在索引阶段或查询阶段。两者都有优点 (⬆)︎ 和缺点 (⬇)︎。到底要在哪个阶段使用,则取决于性能与灵活性:
索引 | 查询 | |
---|---|---|
索引的大小 | ⬇︎ 大索引。因为所有的同义词都会被索引,所以索引的大小相对会变大一些。 | ⬆︎ 正常大小。 |
关联 | ⬇︎ 所有同义词都有相同的 IDF(至于什么是 IDF ,参见 什么是相关性?),这意味着通用的词和较常用的词都拥有着相同的权重。 | ⬆︎ 每个同义词 IDF 都和原来一样。 |
性能 | ⬆︎ 查询只需要找到查询字符串中指定单个词项。 | ⬇︎ 对一个词项的查询重写来查找所有的同义词,从而降低性能。 |
灵活性 | ⬇︎ 同义词规则不能改变现有的文件。对于有影响的新规则,现有的文件都要重建(注:重新索引一次文档)。 | ⬆︎ 同义词规则可以更新不需要索引文件。 |
简单收缩
简单收缩 ,把 左边的多个同义词映射到了右边的单个词:
"leap,hop => jump"
它必须同时应用于索引和查询阶段,以确保查询词项映射到索引中存在的同一个值。
相对于简单扩展方法,这种方法也有一些优点和一些缺点:
- 索引的大小
⬆︎ 索引大小是正常的,因为只有单一词项被索引。
关联
⬇︎ 所有词项的 IDF 是一样的,所以你不能区分比较常用的词、不常用的单词。
性能
⬆︎ 查询只需要在索引中找到单词的出现。
灵活性
⬆︎ 新同义词可以添加到规则的左侧并在查询阶段使用。例如,我们想添加
bound
到先前指定的同义词规则中。那么下面的规则将作用于包含bound
的查询或包含bound
的文档索引:"leap,hop,bound => jump"
似乎对旧有的文档不起作用是么?其实我们可以把上面这个同义词规则改写下,以便对旧有文档同样起作用:
"leap,hop,bound => jump,bound"
当你重建索引文件,你可以恢复到上面的规则(注:
leap,hop,bound ⇒ jump
)来获得查询单个词项的性能优势(注:因为上面那个规则相比这个而言,查询阶段就只要查询一个词了)。
类型扩展
类型扩展是完全不同于简单收缩 或扩张, 并不是平等看待所有的同义词,而是扩大了词的意义,使被拓展的词更为通用。以这些规则为例:
"cat => cat,pet", "kitten => kitten,cat,pet", "dog => dog,pet" "puppy => puppy,dog,pet"
通过在索引阶段使用类型扩展:
一个关于
kitten
的查询会发现关于 kittens 的文档。查询一个
cat
会找到关于 kittens 和 cats 的文档。一个
pet
的查询将发现有关的 kittens、cats、puppies、dogs 或者 pets 的文档。
或者在查询阶段使用类型扩展, kitten
的查询结果就会被拓展成涉及到 kittens、cats、dogs。
您也可以有两全其美的办法,通过在索引阶段应用类型扩展同义词规则,以确保类型在索引中存在。然后,在查询阶段, 你可以选择不采用同义词(使 kitten
查询只返回 kittens 的文件)或采用同义词, kitten
的查询操作就会返回包括 kittens、cats、pets(也包括 dogs 和 puppies)的相关结果。
前面的示例规则,对 kitten
的 IDF 将是正确的,而 cat
和 pet
的 IDF 将会被 Elasticsearch 降权。然而, 这是对你有利的,当一个针对 kitten
的查询被拓展成了针对 kitten OR cat OR pet
的查询, 那么 kitten
相关的文档就应该排在最上方,其次是 cat
的文件, pet
的文件将被排在最底部。
同义词和分析链
在 同义词格式 一章中,我们使用 u s a
来举例阐述一些同义词相关的知识。那么为什么 我们使用的不是 U.S.A.
呢?原因是, 这个 同义词
的语汇单元过滤器只能接收到在它前面的语汇单元过滤器或者分词器的输出结果(这里看不到原始文本)。
假设我们有一个分析器,它由 standard
分词器、 lowercase
的语汇单元过滤器、 synonym
的语汇单元过滤器组成。文本 U.S.A.
的分析过程,看起来像这样的:
original string(原始文本) → "U.S.A."
standard tokenizer(分词器) → (U),(S),(A)
lowercase token filter(语汇单元过滤器) → (u),(s),(a)
synonym token filter(语汇单元过滤器) → (usa)
如果我们有指定的同义词 U.S.A.
,它永远不会匹配任何东西。因为, my_synonym_filter
看到词项的时候,句号已经被移除了,并且字母已经被小写了。
这其实是一个非常需要注意的地方。如果我们想同时使用同义词特性与词根提取特性,那么 jumps
、 jumped
、 jump
、 leaps
、 leaped
和 leap
这些词是否都会被索引成一个 jump
? 我们 可以把同义词过滤器放置在词根提取之前,然后把所有同义词以及词形变化都列举出来:
"jumps,jumped,leap,leaps,leaped => jump"
但更简洁的方式将同义词过滤器放置在词根过滤器之后,然后把词根形式的同义词列举出来:
"leap => jump"
大小写敏感的同义词
通常,我们把同义词过滤器放置在 lowercase
语汇单元过滤器之后,因此,所有的同义词 都是小写。 但有时会导致奇怪的合并。例如, CAT
扫描和一只 cat
有很大的不同,或者 PET
(正电子发射断层扫描)和 pet
。 就此而言,姓 Little
也是不同于形容词 little
的 (尽管当一个句子以它开头时,首字母会被大写)。
如果根据使用情况来区分词义,则需要将同义词过滤器放置在 lowercase
筛选器之前。当然,这意味着同义词规则需要列出所有想匹配的变化(例如, Little、LITTLE、little
)。
相反,可以有两个同义词过滤器:一个匹配大小写敏感的同义词,一个匹配大小写不敏感的同义词。例如,大小写敏感的同义词规则可以是这个样子:
"CAT,CAT scan => cat_scan" "PET,PET scan => pet_scan" "Johnny Little,J Little => johnny_little" "Johnny Small,J Small => johnny_small"
大小不敏感的同义词规则可以是这个样子:
"cat => cat,pet" "dog => dog,pet" "cat scan,cat_scan scan => cat_scan" "pet scan,pet_scan scan => pet_scan" "little,small"
大小写敏感的同义词规则不仅会处理 CAT scan
,而且有时候也可能会匹配到 CAT scan
中的 CAT
(注:从而导致 CAT scan
被转化成了同义词 cat_scan scan
)。出于这个原因,在大小写敏感的同义词列表中会有一个针对较坏替换情况的特异规则 cat_scan scan
。
提示: 可以看到它们可以多么轻易地变得复杂。同平时一样, analyze
API 是帮手,用它来检查分析器是否正确配置。参阅 测试分析器。
多词同义词和短语查询
至此,同义词看上去还挺简单的。然而不幸的是,复杂的部分才刚刚开始。 为了能使 短语查询 正常工作, Elasticsearch 需要知道每个词在初始文本中的位置。多词同义词会严重破坏词的位置信息,尤其当新增的同义词标记长度各不相同的时候。
我们创建一个同义词语汇单元过滤器,然后使用下面这样的同义词规则:
"usa,united states,u s a,united states of america"
PUT /my_index
{
"settings": {
"analysis": {
"filter": {
"my_synonym_filter": {
"type": "synonym",
"synonyms": [
"usa,united states,u s a,united states of america"
]
}
},
"analyzer": {
"my_synonyms": {
"tokenizer": "standard",
"filter": [
"lowercase",
"my_synonym_filter"
]
}
}
}
}
}
GET /my_index/_analyze?analyzer=my_synonyms&text=
The United States is wealthy
解析器
会输出下面这样的结果:
Pos 1: (the)
Pos 2: (usa,united,u,united)
Pos 3: (states,s,states)
Pos 4: (is,a,of)
Pos 5: (wealthy,america)
如果你用上面这个同义词语汇单元过滤器索引一个文档,然后执行一个短语查询,那你就会得到惊人的结果,下面这些短语都不会匹配成功:
The usa is wealthy
The united states of america is wealthy
The U.S.A. is wealthy
但是这些短语会:
United states is wealthy
Usa states of wealthy
The U.S. of wealthy
U.S. is america
如果你是在查询阶段使同义词,那你就会看到更加诡异的匹配结果。看下这个 validate-query
查询:
GET /my_index/_validate/query?explain
{
"query": {
"match_phrase": {
"text": {
"query": "usa is wealthy",
"analyzer": "my_synonyms"
}
}
}
}
查询关键字会被同义词语汇单元过滤器处理成类似这样的信息:
"(usa united u united) (is states s states) (wealthy a of) america"
这会匹配包含有 u is of america
的文档,但是匹配不出任何含有 america
的文档。
Tip | 多词同义对高亮匹配结果也会造成影响。一个针对 USA 的查询,返回的结果可能却高亮了: The United States is wealthy 。 |
---|---|
使用简单收缩进行短语查询
避免这种混乱的方法是使用 简单收缩, 用单个词项表示所有的同义词, 然后在查询阶段,就只需要针对这单个词进行查询了:
PUT /my_index
{
"settings": {
"analysis": {
"filter": {
"my_synonym_filter": {
"type": "synonym",
"synonyms": [
"united states,u s a,united states of america=>usa"
]
}
},
"analyzer": {
"my_synonyms": {
"tokenizer": "standard",
"filter": [
"lowercase",
"my_synonym_filter"
]
}
}
}
}
}
GET /my_index/_analyze?analyzer=my_synonyms
The United States is wealthy
上面那个查询信息就会被处理成类似下面这样:
Pos 1: (the)
Pos 2: (usa)
Pos 3: (is)
Pos 5: (wealthy)
现在我们再次执行我们之前做过的那个 validate-query
查询,就会输出一个简单又合理的结果:
"usa is wealthy"
这个方法的缺点是,因为把 united states of america
转换成了同义词 usa
, 你就不能使用 united states of america
去搜索出 united
或者 states
。 你需要使用一个额外的字段并用另一个解析器链来达到这个目的。
同义词与 query_string 查询
本书很少谈论到 query_string
查询,因为真心不推荐你用它。 在 复杂查询 一节中有提到,由于 query_string
查询支持一个精简的 查询语法 ,因此,可能这会导致它搜出一些出人意料的结果或者甚至是含有语法错误的结果。
这种查询方式存在不少问题,而其中之一便与多词同义有关。为了支持它的查询语法,你必须用指定的、该语法所能识别的操作符号来标示,比如 AND
、 OR
、 +
、 -
、 field:
等等。 (更多相关内容参阅 query_string
语法 。)
而在这种语法的解析过程中,解析动作会把查询文本在空格符处作切分,然后分别把每个切分出来的词传递给相关性解析器。 这也即意味着你的同义词解析器永远都不可能收到类似 United States
这样的多个单词组成的同义词。由于不会把 United States
作为一个原子性的文本,所以同义词解析器的输入信息永远都是两个被切分开的词 United
和 States
。
所幸, match
查询相比而言就可靠得多了,因为它不支持上述语法,所以多个字组成的同义词不会被切分开,而是会完整地交给解析器处理。
符号同义词
最后一节内容我们来阐述下怎么对符号进行同义词处理,这和我们前面讲的同义词处理不太一样。 符号同义词 是用别名来表示这个符号,以防止它在分词过程中被误认为是不重要的标点符号而被移除。
虽然绝大多数情况下,符号对于全文搜索而言都无关紧要,但是字符组合而成的表情,或许又会是很有意义的东西,甚至有时候会改变整个句子的含义,对比一下这两句话:
我很高兴能在星期天工作。
我很高兴能在星期天工作 :( (注:难过的表情)
标准
(注:standard)分词器或许会简单地消除掉第二个句子里的字符表情,致使两个原本意思相去甚远的句子变得相同。
我们可以先使用 映射
字符过滤器,在文本被递交给分词器处理之前, 把字符表情替换成符号同义词 emoticon_happy
或者 emoticon_sad
:
PUT /my_index
{
"settings": {
"analysis": {
"char_filter": {
"emoticons": {
"type": "mapping",
"mappings": [ (1)
":)=>emoticon_happy",
":(=>emoticon_sad"
]
}
},
"analyzer": {
"my_emoticons": {
"char_filter": "emoticons",
"tokenizer": "standard",
"filter": [ "lowercase" ]
]
}
}
}
}
}
GET /my_index/_analyze?analyzer=my_emoticons
I am :) not :( (2)
映射
过滤器把字符从⇒
左边的格式转变成右边的样子。输出:
i
、am
、emoticon_happy
、not
、emoticon_sad
。
很少有人会搜 emoticon_happy
这个词,但是确保类似字符表情的这类重要符号被存储到索引中是非常好的做法,在进行情感分析的时候会很有用。当然,我们也可以用真实的词汇来处理符号同义词,比如: happy
或者 sad
。
提示: 映射
字符过滤器是个非常有用的过滤器,它可以用来对一些已有的字词进行替换操作, 你如果想要采用更灵活的正则表达式去替换字词的话,那你可以使用 pattern_replace
字符过滤器。