ElasticSearch 2 (25) - 语言处理系列之同义词
摘要
词干提取有助于通过简化屈折词到它们词根的形式来扩展搜索的范围,而同义词是通过关联概念和想法来扩展搜索范围的。或许没有文档能与查询 “English queen” 相匹配,但是包含 “British monarch” 的文档会很可能被认为是一个好的匹配。
用户搜索 “the US” 可能期望找到文档包含 United States、USA、U.S.A.、America 或 the States。但是,他们不希望看见结果里有关于 the states of matter
或 state machines
这样的内容。
示例给我们上了很好的一课,它展现了人区分不同概念的方式是怎样简单,但对于机器来说却那样麻烦。尝试为每个词都提供同义词,从而保证即使用关系较远的词也能找到文档,这是一个很自然的倾向。
但这是个错误,就如同我们偏向轻量或较小的词干提取程度而不是激进的提取方式,同义词应该仅在必要时使用。用户可以理解为什么他们的查询结果受查询条件的限制,但他们却不那么理解为什么他们的查询结果看上去总是随机的。
同义词可以被用来合并那些具有相同含义的词,比如:jump
、leap
和 hop
或者 pamphlet
、 leaflet
和 brochure
。另外,它还可以使词更通用。比如,bird
可以作为 owl
或 pigeon
更通用的同义词,adult
可以作为 man
或 woman
的同义词。
同义词看似一个简单的概念,但是想让它们用得正确却十分微妙。本章中,我们会解释同义词的机制并且讨论它在使用上局限和陷阱。
小贴士
同义词是被用以扩展匹配文档的范围的。就和词干提取方式或部分匹配一样,同义词字段无法独立使用,而是需要将它和主字段查询联合使用,主字段里包含原始文本未经修改的形式。参见 多数字段(Most Fields) 了解在使用同义词时如何维护相关度的值。
版本
elasticsearch版本: elasticsearch-2.x
内容
同义词的使用(Using Synonyms)
同义词可以替换当前存在的标记,或者也可以通过使用同义词标记过滤器将其加入到标记流中:
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
]
}
}
}
}
}
#1 首先,我们定义了一个 synonym
类型的标记过滤器。
#2 我们会在 同义词格式化(Formatting Synonyms) 中讨论同义词的格式。
#3 使用 my_synonym_filter
过滤器创建一个自定义分析器。
小贴士
同义词可以通过内联参数
synonyms
指定,或者通过一个存在于每个节点的同义词文件指定。同义词的文件路径参数为synonyms_path
,它可以是 Elasticsearch 配置目录的相对路径,也可以是绝对路径。参见 更新停用词(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) #2
#1 #2 所有的同义词都与它们的原始词占据同一位置。
这个文档会与一下任何一个查询匹配:English queen
、British queen
、English monarch
或 British monarch
。短语查询也能奏效,因为每个词项的位置都被保留了。
小贴士
同时在索引时和搜索时使用相同的同义词标记过滤器是多余的。如果我们在索引时将
English
替换成了english
和british
,那么在搜索时只需要搜索其中的一个词项。另外,如果我们不在索引时使用同义词,那么在搜索时就需要将查询从English
转换为english
或british
。是在搜索时还是在索引时中使用同义词是个难以取舍的选择,我们会在 扩展与收缩(Expand or contract) 中对这个问题的选择进行更多探索。
同义词的格式化(Formatting Synonyms)
同义词用逗号这种最简单的形式将值进行分隔:
"jump,leap,hop"
如果碰到任何词项,它会被列表里的所有同义词替换,例如:
Original terms: Replaced by:
────────────────────────────────
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"
Original terms: Replaced by:
────────────────────────────────
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)
。
扩展与收缩(Expand or contract)
在 格式化同义词(Formatting Synonyms) 中,我们已经看到可以通过简单扩展、简单收缩、或通用扩展来替换同义词,本小节中我们会看看如何在这些技术中作权衡。
小贴士
本小节只处理单个词的同义词,多词同义词会增加问题的复杂度,会在 多词同义词与短语查询(Multiword Synonyms and Phrase Queries) 中进行讨论。
简单扩展(Simple Expansion)
在简单扩展中,任何同义词都会被扩展替换成同义词列表里的所有词:
"jump,hop,leap"
扩展可以被应用于索引时或查询时。每种方式都有它的优势(⬆)和劣势(⬇)。何时使用何种方式对性能和灵活性产生的影响。
(原)
| Index time | Query time
-------------------------------------------------------------------------------------
Index size | ⬇︎ Bigger index because | ⬆︎ Normal.
| all synonyms must be indexed. |
-------------------------------------------------------------------------------------
Relevance | ⬇︎ All synonyms will have the same | ⬆︎ The IDF for each
| IDF (see What Is Relevance?), | synonym will be correct.
| meaning that more commonly used |
| words will have the same weight |
| as less commonly used words. |
-------------------------------------------------------------------------------------
Performance | ⬆︎ A query needs to find only the | ⬇︎ A query for a single
| single term specified | term is rewritten to
| in the query string. | look up all synonyms,
| | which decreases performance.
-------------------------------------------------------------------------------------
Flexibility | ⬇︎ The synonym rules can’t be | ⬆︎ Synonym rules can be
| changed for existing documents. | updated without reindexing
| For the new rules to have effect, | documents.
| existing documents have to be |
| reindexed. |
-------------------------------------------------------------------------------------
(译)
| 索引时 | 查询时
-------------------------------------------------------------------------------------
索引大小 | ⬇︎ 索引占用空间更大,因为所有同义词 |
| 都需要被索引 | ⬆︎ 正常
-------------------------------------------------------------------------------------
相关性 | ⬇︎ 所有的同义词都有相同的IDF (参见 | ⬆︎ 每个索引的 IDF 是正确的
| 什么是相关性?)这表示较常用词与 |
| 次常用词具有相同的权重 |
-------------------------------------------------------------------------------------
性能 | ⬆︎ 查询只需要对查询字符串中的单个指定 | ⬇︎ 单个词的查询被重写为查找
| 词项进行查找 | 所有的同义词这会降低搜索性能
| |
-------------------------------------------------------------------------------------
灵活性 | ⬇︎ 同义词规则无法更改现有文档,要想使 | ⬆︎ 无需重建索引就能更新同义
| 用规则生效就必须对现有文档重建索引 | 词规则
| |
-------------------------------------------------------------------------------------
简单收缩(Simple Contraction)
简单搜索是将左边的一组同义词映射到右边的单个值:
"leap,hop => jump"
它需要同时应用于索引时和查询时,来确保查询词项能与索引中已有的同一值映射。
这种方式与简单扩展方式相比既有优势也有不足:
索引大小
⬆︎ 索引大小正常,因为只有单个词项需要被索引。
相关性
⬇︎ 所有词项的 IDF 都是相同的,所以我们无法区分较常用词和次常用词。
性能
⬆︎ 查询需要在索引中找到唯一的单个词项。
灵活性
⬆︎ 新同义词可以被加到规则左边并在查询时应用。例如,假设我们想要将单词
bound
加入之前指定的规则,以下这个规则可以用来查询已有或新增的包含bound
的文档:"leap,hop,bound => jump"
但我们也可以扩展这个效果,将 已有 包含
bound
的文档考虑在内,规则如下:"leap,hop,bound => jump,bound"
当我们重建索引后,我们可以回撤到前一个规则,从而在对单个词项进行查询时,获取性能收益。
类型扩展(Genre Expansion)
类型扩展与简单收缩或简单扩展大不相同。它不是平等对待所有的同义词,而是扩展了词项的含义,使它变得更加抽象通用。用以下规则来举例:
"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
或 pets
的文档。
我们还有个一箭双雕的做法,就是在索引时应用扩展来确保索引里存在该类型,然后在查询时,我们既可以不应用同义词(这样查询 kitten
就只会返回仅关于 kittens 小猫的文档)或者也可以选择应用同义词来匹配 kittens
、cats
和 pets
(包括多种犬类)。
有了以上示例中的规则,kitten
的 IDF 会是正确的,尽管 cat
和 pet
的 IDF 都被认为弱化了。尽管如此,它还是能满足我们的要求,一个类型扩展查询 kitten OR cat OR pet
会将 kitten
的相关文档排在最前,随后是 cat
的相关文档,最后是 pet
出现在最后。
同义词及其分析链(Synonyms and The Analysis Chain)
在 格式化同义词(Formatting Synonyms) 这一小节的例子中,用 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"
大小写敏感的同义词(Case-Sensitive Synonyms)
通常情况下,同义词过滤器被置于小写标记过滤器之后,这样所有的同义词都可以转换成小写形式,但这在有些时候会带来奇怪的词项合并。例如,CAT scan
和 cat
有着很大不同,PET
(positron emmision tomography,正电子放射断层造影术)和 pet
同样如此。正因为这样,姓 Little
也与形容词 little
有区别(尽管如果形容词在句首,首字母也会被大写)。
如果我们有的应用场景需要区分词义,我们可以将同义词过滤器置于小写过滤器之前,当然这样做也意味着需要在同义词规则中列出所有我们期望匹配的大小写变化形式(例如,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 scan
这样看上去很奇怪的规则用来处理错误替换。
小贴士
我们可以看见它发展得如此之快以至于变得无比复杂。一如往常,
analyze
API 总能成为我们检查分析器配置正确性的良师益友。参见 测试分析器(Testing Analyzers) 。
多词同义词与短语查询(Multiword Synonyms and Phrase Queries)
目前为止同义词看起来比较明确,不幸的是,这却恰恰是错误的开始。为了能使短语查询正确工作,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
的文档。
小贴士
多词同义词同样影响高亮的功能。查询
USA
会返回高亮片段:“The United States is wealthy”。
为短语查询使用简单收缩(Use Simple Contraction for Phrase Queries)
避免这种混乱的方式可以通过以下方式解决:应用简单收缩用单个词项代表所有的同义词,然后在查询时使用相同的同义词标记过滤器:
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
。我们需要使用独立的字段以及不同的分析链达到这个目的。
同义词和 query_string 查询(Synonyms and the query_string Query)
前面以及尝试避免讨论 query_string 查询,因为我们并不推荐使用它。在 "更复杂的查询(More-Complicated Queries)" 中,因为 query_string 查询支持一种简单短小的搜索语法,它会经常导致令人意外的结果甚至语法错误。
这个查询有一个陷阱与多词同义词相关。为了支持它的搜索语法,它需要解析查询字符串从而识别特殊的操作符,如 AND
、OR
、+
、-
、field:
等等。(参见 完整的 query_string 语法 了解更多信息)。
作为解析过程的一部分,它在空格处对查询字符串进行分解,然后将每个词分别传入对应的分析器,这意味着同义词分析器永远都不会收到多词同义词。它无法看到 United States
作为单个字符串出现,分析器会分别收到 United
和 States
这两个词。
幸运的是,可靠的 match
查询不支持这种语法,多词同义词会以它们自身的完整形式被传入分析器。
符号的同义词(Symbol Synonyms)
本章的最后部分会讨论符号的同义词,这与我们前面讨论的同义词不同。符号同义词是字符串的别名形式用以表示符号,它们通常会在标记化阶段被移除。
尽管大多数标点符号对于全文搜索不是那么重要,像表情这样的字符组合却很有意义,它甚至可能改变文本的意思。比较以下句子:
- I am thrilled to be at work on Sunday.
- I am thrilled to be at work on Sunday :(
standard
标记器会简单的将第二个句子里的表情符号剔除,将两个意图截然不同的句子合并在一起。
我们可以在文本传入标记器之前,使用 mapping
字符过滤器用符号同义词(如: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
#1 mappings
过滤器替换会用 =>
右边的内容替换它左边的内容。
#2 输出标记 i
、am
、emoticon_happy
、not
、emoticon_sad
。
通常不会有人搜索 emoticon_happy
,但是确保表情这样的重要符号存在于索引中对于情感分析十分有帮助。当然我们也可以使用真实词,如 happy
和 sad
。
小贴士
mapping
字符过滤器对简单替换精确字符的顺序十分有用。对于更灵活的模式匹配,我们可以采用pattern_replace
字符过滤器的正则表达式。