ElasticSearch 2 (20) - 语言处理系列之如何开始
摘要
Elasticsearch 配备了一组语言分析器,为世界上大多数常见的语言提供良好的现成基础支持。
阿拉伯语、亚美尼亚语,巴斯克语,加泰罗尼亚语,巴西语、保加利亚语、汉语、捷克语、丹麦语、荷兰语、英语、芬兰语、法语、德语、希腊语、加利西亚语、印度语、匈牙利语、印尼语、爱尔兰语、意大利语、日语、韩语、库尔德语、挪威语、波斯语、葡萄牙语、俄罗斯语、西班牙语、罗马尼亚语、瑞典语、土耳其语、泰语。
这些分析器通常扮演四种角色:
-
将文本标记为独立的单词:
The quick brown foxes
→ [The
,quick
,brown
,foxes
] -
将标记小写:
The
→the
-
移除普通词(停用词):
[
The
,quick
,brown
,foxes
] → [quick
,brown
,foxes
] -
提取词干形式:
foxes
→fox
每个分析器会根据具体不同语言应用其他转换方式,让该语言的词语更易于搜索:
-
英语分析器移除所有格形式
's
:John's
→john
-
法语分析器移除省音形式(如:
l'
与qu'
)及变音形式(如:¨
或^
):l'église
→eglis
-
德语分析器规范化词项,将
ä
与ae
替换为a
,将ß
替换为ss
:äußerst
→ausserst
版本
elasticsearch版本: elasticsearch-2.x
内容
使用语言分析器(Using Language Analyzers)
内置语言分析器可在全局获得,使用前无需配置。它们可以在字段映射里直接指定:
PUT /my_index
{
"mappings": {
"blog": {
"properties": {
"title": {
"type": "string",
"analyzer": "english" #1
}
}
}
}
}
#1 title
字段会使用 english
英语分析器而不是默认的 standard
标准分析器。
当然,将文本传入 english
分析,我们会丢失信息:
GET /my_index/_analyze?field=title #1
I'm not happy about the foxes
#1 生成标记: i'm
、 happi
、 about
、 fox
我们无法说清文档是提及一个 fox
或是许多 foxes
;单词 not
是个停用词所以被移除了,以致我们无法说清是否因为 fox
而高兴。在使用 english
分析器时,我们匹配更加松散从而提高了召回率,但是这也使我们丧失了对文档进行准确排序的能力。
为了鱼与熊掌兼得,我们可以使用多字段(multifields)对 title
字段索引两次:一次使用 english
英语分析器,一次使用 standard
标准分析器:
PUT /my_index
{
"mappings": {
"blog": {
"properties": {
"title": { #1
"type": "string",
"fields": {
"english": { #2
"type": "string",
"analyzer": "english"
}
}
}
}
}
}
}
#1 title
主字段使用 standard
标准分析器。
#2 title.english
辅字段使用 english
英语分析器。
With this mapping in place, we can index some test documents to demonstrate how to use both fields at query time:
有了这个映射,我们可以用一些测试文档来演示说明如何在查询时将两个字段一起使用:
PUT /my_index/blog/1
{ "title": "I'm happy for this fox" }
PUT /my_index/blog/2
{ "title": "I'm not happy about my fox problem" }
GET /_search
{
"query": {
"multi_match": {
"type": "most_fields", #1
"query": "not happy foxes",
"fields": [ "title", "title.english" ]
}
}
}
#1 用 most_fields
查询类型尽可能的去匹配相同文本的更多字段。
尽管我们的两个文档都没有包含单词 foxes
,但由于 title.english
字段的词干提取,两者都能作为结果返回。第二个文档的相关度更高是因为它的 title
字段能与单词 not
匹配。
配置语言分析器(Configuring Language Analyzers)
尽管语言分析器可以不需要配置随时使用,不过大多数都提供了让我们控制它们行为的途径,具体上说就是:
-
Stem-word exclusion
想象一下,用户搜索 “World Health Organization” 却在结果中得到 “organ health”。导致这种混乱现象的原因是 “organ” 和 “organization” 两个词都来自同一词根:
organ
。通常情况下这并不是问题,但是在本例这个特定文档集合中,它会导致一个令人困惑的结果。我们希望避免对organization
和organizations
这两个词做词干提取。 -
Custom stopwords
英语停用词的默认列表如下:
a, an, and, are, as, at, be, but, by, for, if, in, into, is, it, no, not, of, on, or, such, that, the, their, then, there, these, they, this, to, was, will, with
不一样的是
no
和not
这两个词,它们对跟随词的意思有否定作用。或许我们决定这两个词是重要的,不应该将它们看成停用词。
为了自定义 english
分析器的行为,我们需要创建一个自定义的分析器,即以 english
分析器作为基础但会增加一些配置:
PUT /my_index
{
"settings": {
"analysis": {
"analyzer": {
"my_english": {
"type": "english",
"stem_exclusion": [ "organization", "organizations" ], #1
"stopwords": [ #2
"a", "an", "and", "are", "as", "at", "be", "but", "by", "for",
"if", "in", "into", "is", "it", "of", "on", "or", "such", "that",
"the", "their", "then", "there", "these", "they", "this", "to",
"was", "will", "with"
]
}
}
}
}
}
GET /my_index/_analyze?analyzer=my_english #3
The World Health Organization does not sell organs.
#1 防止 organization
和 organizations
被提取
#2 指定一个自定义停用词表
#3 输出标记 world
、health
、 organization
、 does
、 not
、 sell
、 organ
我们在 词干提取 和 停用词:性能与精度 分别对词干提取和停用词有更详细的讨论。
混合语言的缺陷(Pitfalls of Mixing Languages)
如果只处理一种语言,那么我们是幸运的。探寻多种语言文档的处理策略是十分具有挑战的事情。
索引时(At Index Time)
多语言文档有三种主要形式:
每个文档使用一种主要语言,但是会有其他语言的片段 (参见 一个文档一种语言)
每个字段使用一种主要语言,但是会有其他语言的片段(参见 一个字段一种语言)
每个字段使用多种混合语言 (参见 混合语言字段)
尽管目标并不总能达成,应该对不用语言有不同的处理方式,将不同语言存入同一倒排索引可能会成为问题。
-
错误的词干提取(Incorrect stemming)
德语的词干提取规则与英语、法语、瑞典语等语言都各不相同。对不同语言应用相同的词干提取规则可能会导致有些词能能正确提取,而有些词不能,甚至有的词根本无法提取。也甚至可能导致不同语言不同意思的单词都被提取为同一词根,把不同意思的词混为一谈,进一步导致用户对搜索结果难以理解。
如果依次对同一文本应用多个提取器很可能会产生很多垃圾结果,因为后一个提取器可能会尝试提取一个已经被提取过的单词,将问题复杂化。
每种语言版本一个提取器(Stemmer per Script)
只有一个提取器(only-one-stemmer) 这个规则有个例外,即文字用不同版本语言表达时。例如,在以色列,一个文档里同时包含希伯来语、阿拉伯语、俄罗斯语和英语是非常可能的:
אזהרה - Предупреждение - تحذير - Warning
每种语言都使用了不同的版本,所以一种语言的提取器不会影响另外一种,这使多个提取器得以应用到同一文本。
-
错误的逆向文档频率(Incorrect inverse document frequencies)
在 什么是相关?(What Is Relevance?) 中,我们解释过,如果一个词项在文档集中出现的越频繁,这个词项的权重就越低,为了准确计算相关度,我们需要准确的词频统计。
在个以英语为主的文本中出现一小段德语会使这些德语单词有更高的权重,因为相对这段文本来说,它们并不普通。但是如果将它们混入一段以德语为主的文本,这段德语的权重就会大大降低。
查询时(At Query Time)
仅考虑文档是不够的,我们还需要考虑用户是如何查询这些文档的。通常我们可以通过用户选择的网站(比如:mysite.de
与 mysite.fr
)或用户浏览器 HTTP 头上的 accept-language
来判断用户使用的主要语言。
用户搜索通常有以下三种主要形式:
用户使用他们的主要语言进行搜索。
用户使用不同语言进行搜索,但是期望的结果是他们的主要使用语言。
用户使用不同语言进行搜索,同时期望结果也是他们搜索时使用的语言。(比如:具有双语能力的人,或网咖里的一位外国游客)
依赖于我们搜索数据的类型,下面的做法可能都是合适的:将一种语言的结果返回(比如:用户在一个西班牙语的网站上搜索要购买商品),或者将用户的主要语言与其他语言的结果结合返回。
为用户提供他们的语言偏好往往是合理的。一个英语用户在网站上搜索 “deja vu” 很可能是想看到这个词的英语*页面,而不是一个法语的页面。
识别语言(Identifying Language)
我们可能已经了解文档所使用的语言。可能我们的文档是在我们组织中创建的,而且被翻译成了一系列的预定语言。人为的预识别很有可能是正确分类语言的最可靠方法。
或许,我们的文档来自于外部资源,它们没有任何语言分类亦或不正确的分类。这种情况下,我们可能需要采取一种探寻式的方法来识别其主要语言。幸运的是,有针对不同语言的函数库来帮助我们解决这个问题。
一个值得关注的是由 Mike McCandless 创建的 chromium-compact-language-detector 函数库,它使用了 Google 的开源库 Compact Language Detector(Apache License 2.0)。它具备轻量、快速和准确的特点,可以从仅有的两句话中探测 160 多种语言,甚至可以在一小段文字中探测多种语言。这个函数库有多种语言的实现,包括:Python、Perl、JavaScript、PHP、C#/.NET、和 R.
要想识别用户搜索请求所使用的语言并不简单,CLD 要求它识别的文本至少有 200 字母,对于更少的文本(比如:搜索关键字),它识别的准确度会降低。这种情况就需要使用简单的探寻方式,比如用户所在的国家、用户选择的语言以及 HTTP 头 accept-language
的内容。
一种语言一个文档(One Language per Document)
具有单个主要语言的文档的设置相对简单。多语言的文档可以分别索引(blogs-en
、blogs-fr
等等)并为每个索引使用相同的类型和相同的字段,不同的只是分析器:
PUT /blogs-en
{
"mappings": {
"post": {
"properties": {
"title": {
"type": "string", #1
"fields": {
"stemmed": {
"type": "string",
"analyzer": "english" #2
}
}}}}}}
PUT /blogs-fr
{
"mappings": {
"post": {
"properties": {
"title": {
"type": "string", #3
"fields": {
"stemmed": {
"type": "string",
"analyzer": "french" #4
}
}}}}}}
#1 #3 blogs-en
和 blogs-fr
两者都有一个叫做 post
的类型包含字段 title
。
#2 #4 title.stemmed
辅字段作为语言相关的分析器。
This approach is clean and flexible. New languages are easy to add—just create a new index—and because each language is completely separate, we don’t suffer from the term-frequency and stemming problems described in Pitfalls of Mixing Languages.
这种方式既整洁又灵活。新的语言只需要通过创建新索引这种简单方式完成,因为每种语言都是完全独立的,我们无须忍受在 混合语言的缺陷(Pitfalls of Mixing Languages) 中提到的关于词频和词干的相关问题。
单语言的文档可以被独立查询,查询语句也可以针对不同语言查询多个索引。我们可以使用 indices_boost
参数为指定偏好语言提升索引的权重:
GET /blogs-*/post/_search #1
{
"query": {
"multi_match": {
"query": "deja vu",
"fields": [ "title", "title.stemmed" ] #2
"type": "most_fields"
}
},
"indices_boost": { #3
"blogs-en": 3,
"blogs-fr": 2
}
}
#1 这个搜索会查询任何以 blogs-
为开头的索引。
#2 title.stemmed
字段根据每个索引指定的分析器来查询。
#3 可能用户的 accept-language
头表明偏好语言是英语然后是法语,所以我们可以为每个索引分配不同的权重提升值,而对其他所有语言使用一个中性值 1 。
外来词(Foreign Words)
当然,这些文档可能包含其他语言的单词或句子,这些词很可能无法正确提取词干。在具有主要语言的文档中,这通常并不是个大问题。用户通常会用精确词语进行搜索(比如:另一语言的引用),而不是使用词语的变体。召回率可以通过 标记的规则化(Normalizing Tokens) 里描述的方法改进。
可能有些地点名词应该可以在其主要语言和原语言中被查询,比如: Munich 和 München 。 这些词语实际上是同义词,我们将会在 同义词(Synonyms) 中讨论。
不要为语言使用类型(Don't Use Types for Languages)
可能有人会尝试对每种语言都使用不同的类型,而不是不同的索引。为了获得最佳搜索结果,我们应该避免这样使用类型,如 类型与映射(Types and Mappings) 里解释的,类型不同但名字相同的字段会被索引到同一倒排索引中,这意味着每种类型(即每种语言)的词频会被混合在一起。
为了确保一种语言的词频不会污染其他语言的,可以为每种语言使用独立的索引或者独立的字段,将在下一部分解释这点。
一种语言一个字段(One Language per Field)
那些表示实体(如:产品、电影、法律条文)的文档,将同一文字翻译成不同语言是很常见的。尽管每种翻译都可以单个文档来表示,不同语言使用不同索引,但另外一种合理方法是将所有翻译保存在同一文档中:
{
"title": "Fight club",
"title_br": "Clube de Luta",
"title_cz": "Klub rváčů",
"title_en": "Fight club",
"title_es": "El club de la lucha",
...
}
每种翻译都存于独立字段,并根据映射指定的不同的语言分析器进行分析:
PUT /movies
{
"mappings": {
"movie": {
"properties": {
"title": { #1
"type": "string"
},
"title_br": { #2
"type": "string",
"analyzer": "brazilian"
},
"title_cz": { #3
"type": "string",
"analyzer": "czech"
},
"title_en": { #4
"type": "string",
"analyzer": "english"
},
"title_es": { #5
"type": "string",
"analyzer": "spanish"
}
}
}
}
}
#1 The title
字段包含原始的标题并使用 standard
分析器
#2 #3 #4 #5 其他的每个字段使用与之语言合适的分析器。
和 index-per-language 一种语言一个索引的方式类似,field-per-language 一种语言一个字段这种方式维护整洁的词频。它与独立索引相比不是很灵活,尽管可以通过 update-mapping
API 很容易就能增加新字段,这些新字段还是可能需要指定新的自定义分析器,而这些设置只能在索引创建的时候完成。作为一种替代方案,我们可以先关闭索引,然后使用 update-settings
API 来增加新分析器,最后重新开启索引,但关闭索引意味着需要一些停机时间。
单语言的文档可以被独立查询,查询语句也可以针对不同语言查询多个字段。我们可以使用 indices_boost
参数为指定偏好语言提升字段的权重:
GET /movies/movie/_search
{
"query": {
"multi_match": {
"query": "club de la lucha",
"fields": [ "title*", "title_es^2" ], #1
"type": "most_fields"
}
}
}
#1 这个搜索查询任何以 title
为开头的字段,但只为 title_es
字段指定权重提升值为 2
。其他所有字段的权重提升值都为 1
。
混合语言字段(Mixed-Language Fields)
通常单个字段中有多种语言的文档的来源超出我们的控制,比如从网络爬下来的网页:
{ "body": "Page not found / Seite nicht gefunden / Page non trouvée" }
这种类型的多语言文档最难正确处理。尽管我们可以简单的对所有字段都使用 standard
分析器,但与使用了合适的词干提取器相比还是不那么容易被搜出的。当然,我们不能只选择一种提取器,因为提取器是语言相关的,甚至是与语言和文字都相关的。如 一种文字一个提取器(Stemmer per Script) 中讨论的,如果每种语言都使用不同的文字,那么提取器是可以被合并的。
假设我们的混合语言使用了相同的文字(比如:拉丁文),有三种选择:
- 拆分到不同字段(Split into separate fields)
- 分析多次(Analyze multiple times)
- 使用n-grams(Use n-grams)
拆分到不同字段(Split into Separate Fields)
在 识别语言(Identifying Language) 中提到的 Compact Language Detector 可以告诉我们文档里的哪个部分使用了哪种语言。我们可以将文本根据语言来拆分,然后使用 一个字段一种语言(One Language per Field) 里同样的方式来处理。
分析多次(Analyze Multiple Times)
如果我们主要处理一定数量的语言,我们可以用 multi-fields
来为每种语言分析一次:
PUT /movies
{
"mappings": {
"title": {
"properties": {
"title": { #1
"type": "string",
"fields": {
"de": { #2
"type": "string",
"analyzer": "german"
},
"en": { #3
"type": "string",
"analyzer": "english"
},
"fr": { #4
"type": "string",
"analyzer": "french"
},
"es": { #5
"type": "string",
"analyzer": "spanish"
}
}
}
}
}
}
}
#1 主要的 title
字段使用 standard
分析器。
#2 #3 #4 #5 每个辅助字段对 title
字段里的文字使用不同的语言分析器。
使用(Use n-grams)
我们可以将所有单词都使用 n-grams 方法,索引依照 Ngrams复合词(Ngrams for Compound Words) 里的方式。多数的语言变体都体现在为单词添加后缀的方式(有些语言是前缀),所以为每个词生成 n-gram,有助于我们匹配那些相似但又不完全一样的词。此种方式可以与 多次分析(analyze-multiple times) 相结合来为不支持的语言提供一个万能字段:
PUT /movies
{
"settings": {
"analysis": {...} #1
},
"mappings": {
"title": {
"properties": {
"title": {
"type": "string",
"fields": {
"de": {
"type": "string",
"analyzer": "german"
},
"en": {
"type": "string",
"analyzer": "english"
},
"fr": {
"type": "string",
"analyzer": "french"
},
"es": {
"type": "string",
"analyzer": "spanish"
},
"general": { #2
"type": "string",
"analyzer": "trigrams"
}
}
}
}
}
}
}
#1 在 分析(analysis)
这个部分中,我们们也定义了 trigrams
分析器。详细内容在 Ngrams复合词(Ngrams for Compound Words) 中描述。
#2 title.general
字段使用 trigrams
分析器索引任何语言。
当查询万能字段的时候,我们可以用 minimum_should_match
来减少低质量的匹配。与万能字段相比,稍稍提升其他字段的权重也是必要的,这样能使以主语言匹配的字段能比万能字段有更多的权重:
GET /movies/movie/_search
{
"query": {
"multi_match": {
"query": "club de la lucha",
"fields": [ "title*^1.5", "title.general" ], #1
"type": "most_fields",
"minimum_should_match": "75%" #2
}
}
}
#1 所有 title
或 title.*
字段都被赋予比 title.general
字段稍高的权重。
#2 参数 minimum_should_match
减少低质量匹配的数量,这对 title.general
字段格外重要.