ElasticSearch 2 (21) - 语言处理系列之单词识别

ElasticSearch 2 (21) - 语言处理系列之单词识别

摘要

一个英语单词相对容易识别:因为英语单词是被空格或(某些)标点符号隔开的。但在英语中也有反例:you’re 这个词是一个单词还是两个?那 o’clockcooperatehalf-bakedeyewitness 这些词呢?

如德语或荷兰语这样的语言将多个独立单词组合在一起创建更长的复合词(如:Weißkopfseeadler 在英文里是 “white-headed sea eagle” 意思为“白头海鹰”)。为了使 Weißkopfseeadler 能够作为 Adler (在英文里是 “eagle” 意思为“鹰”)的查询结果返回,我们需要了解如何将复合词分解成它的各自组成部分。

亚洲的各种语言更为复杂:有些语言的词语、句子,甚至段落之间没有空格,有些语言的词语可以用单个字表示,但是当同一个字与其他字放在一起,组成一个长的词语时,新词语的意思可能截然不同。

显然万能金钥匙式的分析器并不存在,想要处理所有的语言也是天方夜谭。Elasticsearch 为很多语言都配备了专门的分析器,而且更多的语言分析器以插件形式提供。

尽管如此,不是所有的语言都有专门的分析器,有时我们并不十分清楚正在处理的语言种类。这种情况下,我们需要一组标准工具,无论对何种语言都能进行合理的处理。

版本

elasticsearch版本: elasticsearch-2.x

内容

标准分析器(standard Analyzer)

standard 分析器是所有全文 analyzed 字符串字段的默认分析器。如果我们要重新实现一个自定义的standard 分析器,可以用以下方式定义:

{
    "type":      "custom",
    "tokenizer": "standard",
    "filter":  [ "lowercase", "stop" ]
}

标记的规范化(Normalizing Tokens)停用词:性能与精度(Stopwords: Performance Versus Precision) 中,我们提到过 lowercase 小写字母和 stop token filters 停用词标记过滤器,但现在,让我们先关注 standard 标准标记器

标准标记器(standard Tokenizer)

标记器(tokenizer) 接受字符串输入,对字符串进行处理分解成独立的单词、或 标记(token)(可能会丢弃某些标点符号),然后以 标记流(token stream) 的形式作为输出。

令人感兴趣的是用来识别单词的算法。whitespace 空位标记器只是简单的在空位处(如:空格、制表符、行符等等)将单词拆解出来,并认为连续的非空位字符会组成一个标记。例如:

GET /_analyze?tokenizer=whitespace
You're the 1st runner home!

这个请求会返回以下这些词项:You'rethe1strunnerhome!

另一方面,letter 字母标记器在所有非字母处拆解,这样就会得到以下词项:Yourethestrunnerhome

standard 分析器使用 Unicode Text Segmentation 算法(定义在 Unicode Standard Annex #29 中)找到词语之间的边界,然后输出边界间的所有内容。由于采用 Unicdoe,它可以成功的对混合语言的文本进行标记。

标点符号可以作为,也可以不作为词语的一部分,这取决于它出现的地方:

GET /_analyze?tokenizer=standard
You're my 'favorite'.

本例中,You're里的撇号是作为词语的一部分的,但 'favorite' 里的引号却没有,结果中的词项为:You'remyfavorite

小贴士

uax_url_email 标记器与 standard 标记器的工作方式一样,不过它还可以识别电子邮件地址和 URL 然后将它们作为单个标记输出。与其相反,standard 标记器会尝试将它们拆解。例如:电子邮件地址 joe-bloggs@foo-bar.com 可能会被拆解成 joebloggsfoobar.com

standard 分析器是一个用来标记大多数语言的不错起点,特别是西方的语言。实际上,它是多数语言分析器的基础,如:英语法语西班牙语言分析器,但它对亚系语言的支持是有限的。对于亚系语言,我们应该考虑使用 icu_tokenizer 它以 ICU 插件的形式提供。

安装 ICU 插件(Installing the ICU Plug-in)

Elasticsearch 的 ICU 分析插件 使用 International Components for Unicode (ICU) 函数库(参见 site.project.org)为 Unicode 的处理提供了一组丰富的工具。它包括专门针对处理亚系语言的 icu_tokenizer 的分析器,以及针对除英语以外的其他语言的标记过滤器,它们对正确的匹配和排序至关重要。

注意:

ICU 插件是一个能处理英语以外其他语言的必备工具,在此强烈推荐安装使用。不幸的是,因为它是基于外部的 ICU 函数库,不同的插件版本可能不向前兼容,所以升级时可能需要重新索引我们的数据。

要安装插件,首先需要关闭 Elasticsearch 节点然后在其安装路径下执行以下命令:

./bin/plugin -install elasticsearch/elasticsearch-analysis-icu/$VERSION #1

#1 当前版本 $VERSION 可以在 https://github.com/elasticsearch/elasticsearch-analysis-icu 找到。

一旦安装成功,重启 Elasticsearch,我们会在启动日志中看到类似下面这行信息:

[INFO][plugins] [Mysterio] loaded [marvel, analysis-icu], sites [marvel]

如果我们在使用集群,需要为每个集群下的每个节点都安装这个插件。

ICU插件(icu_tokenizer

icu_tokenizer 同样使用 Unicode Text Segmentation 算法作为 standard 标准标记器,但对某些亚系语言支持得更好,因为它使用一种基于字典的方式去识别泰语、老挝语、汉语、日语和汉语里的词,并利用自定义规则将缅甸语和高棉语标记成音节。

例如,比较 standardicu_tokenizers 分析器分别生成的标记,当对 “Hello. I am from Bangkok.” 使用泰语进行标记时:

GET /_analyze?tokenizer=standard
สวัสดี ผมมาจากกรุงเทพฯ

standard 标记器生成两个标记,每个句子一个:สวัสดี, ผมมาจากกรุงเทพฯ。这只能在想搜索完整句子的时候起到作用 “I am from Bangkok”,但如果只搜索 “Bangkok” 会什么都找不到。

GET /_analyze?tokenizer=icu_tokenizer
สวัสดี ผมมาจากกรุงเทพฯ

icu_tokenizer 标记器正好相反,它能将文本拆解成多个独立的单词 (สวัสดีผมมาจากกรุงเทพฯ),这让搜索更容易。

与之形成对比的是 standard 分析器会对汉语和日语文本 “过度标记”,常常将完整的词语拆解成独立的字。因为词语之间没有任何空格,这是它难以判断连续的字是应该拆解成单独的词,还是应该组成一个词语。例如:

  • 的意思是 facing 的意思是 sun 的意思是 hollyhock。但当写在一起时,向日葵 的意思是 sunflower

  • 的意思是 fivefifth 的意思是 month 的意思是 rain。前两个字写在一起 五月 的意思是 the month of May,再加上第三个字,五月雨 的意思是 continuous rain。然后与第四个字 (意思为:style)组合,词 五月雨式 变成了个形容词,用来形容连续不断或永无止境。

尽管每个字可能都有它自己的正确意思,当能保留完整而不是部分的更多原始概念时,标记会更有意义:

GET /_analyze?tokenizer=standard
向日葵

GET /_analyze?tokenizer=icu_tokenizer
向日葵

之前示例中的 standard 会将每个字作为独立标记输出:icu_tokenizer会输出单个标记 向日葵 (sunflower)。

standardicu_tokenizer 分析器还有一个区别是:后者会将不同的书写形式的词(如:βeta 被分成 βeta)拆解成独立的标记,前者会以单个标记的形式输出:βeta

清理输入文本(Tidying Up Input Text)

标记器在输入的文本整洁、有效时可以生成最佳结果,这里 有效 指的是它遵从 Unicode 算法期望的标点符号规则。但往往我们需要处理的文本就是不如我们的期望。在标记化过程之前整理好文本能有效提高输出标记的质量。

标记 HTML(Tokenizing HTML)

将 HTML 内容传入 standardicu_tokenizer 分析器的输出效果会很差。这些分析器完全不知如何处理 HTML 标签。例如:

GET /_analyzer?tokenizer=standard
<p>Some d&eacute;j&agrave; vu <a href="http://somedomain.com>">website</a>

standard 分析器无法区分 HTML 的标签和实体,输出以下标记:pSomedeacutejagravevuahrefhttpsomedomain.comwebsitea。这显然不是我们的初衷。

字符过滤器可以被加入到分析器,在文本传入标记器之前进行预处理。本例中,我们可以用 html_strip 字符过滤器先移除 HTML 标签再对 HTML 实体(如:&eacute)解码成对于的 Unicode 字符。

字符过滤器可以通过给 analyze API 提供一个查询字符串来测试:

GET /_analyzer?tokenizer=standard&char_filters=html_strip
<p>Some d&eacute;j&agrave; vu <a href="http://somedomain.com>">website</a>

为了把它们作为分析器的一部分使用,可以将它们加到一个自定义分析器定义中:

PUT /my_index
{
    "settings": {
        "analysis": {
            "analyzer": {
                "my_html_analyzer": {
                    "tokenizer":     "standard",
                    "char_filter": [ "html_strip" ]
                }
            }
        }
    }
}

一旦创建,就可以使用 analyze API 来测试我们新的分析器 my_html_analyzer

GET /my_index/_analyzer?analyzer=my_html_analyzer
<p>Some d&eacute;j&agrave; vu <a href="http://somedomain.com>">website</a>

输出标记正如我们所想:Somedéjàvuwebsite

清理标点符号(Tidying Up Punctuation)

standardicu_tokenizer 标记器都知道单词里的撇号(’)应该作为词的一部分来处理,但不知如何处理词周围的单引号。标记文本 You're my 'favorite' 会正确输出标记:You'remyfavorite

不幸的是,Unicode 列出了一些字符有时会当作撇号处理:

  • U+0027

    撇号(')—— 原始 ASCII 字符

  • U+2018

    左单引号() —— 引用的开始符号

  • U+2019

    右单引号() —— 引用的结束符号,同时也作为撇号使用。

当它们与单词一起出现时,两个标记器都将这三个字符当作撇号处理(即作为单词的一部分)。然后有其他三个与撇号类似的字符:

  • U+201B

    反撇号( high-reversed-9)—— 与 U+2018 相同但呈现形态不一样。

  • U+0091

    ISO-8859-1 里的左单引号 —— 不应在 Unicode 中使用。

  • U+0092

    ISO-8859-1 里的右单引号 —— 不应在 Unicode 中使用。

两个标记器都将这三个字符作为词的边界进行处理,即将文本拆解成标记的位置。不幸的是,有些出版者用 U+201B 作为一种样式来表示名字 M‛coy,根据我们文本编辑器年代的不同,可以很容易的用它们制作出后两个字符。

尽管使用了“可接受”的引号,用右单引号标记的词(You’re)还是与撇号词(You're)不一样,这意味着以一种形式作为查询就无法找到另外一种形式的结果。

幸运的是,我们可以使用 mapping 映射字符过滤器来解决这个问题,它可以为我们将一个字符全文替换成另一个,这样,我们就可以将所有的撇号替换成简单的 U+0027 符号:

PUT /my_index
{
  "settings": {
    "analysis": {
      "char_filter": { #1
        "quotes": {
          "type": "mapping",
          "mappings": [ #2
            "\\u0091=>\\u0027",
            "\\u0092=>\\u0027",
            "\\u2018=>\\u0027",
            "\\u2019=>\\u0027",
            "\\u201B=>\\u0027"
          ]
        }
      },
      "analyzer": {
        "quotes_analyzer": {
          "tokenizer":     "standard",
          "char_filter": [ "quotes" ] #3
        }
      }
    }
  }
}

#1 用自定义 char_filter 名为 quotes 的过滤器,将所有撇号的变体映射到一个简单撇号 u0027

#2 为了清楚,我们为每个字符使用 JSON Unicode 语法形式,不过我们也可以用字符本身来表示:"‘=>'"

#3 用自定义 quotes 字符过滤器来创建一个新的分析器,quotes_analyzer

同样,在创建后对其测试:

GET /my_index/_analyze?analyzer=quotes_analyzer
You’re my ‘favorite’ M‛Coy

本例中返回以下标记,所有词内的单引号都被撇号替代:You'remyfavoriteM'Coy

在确保分析器接受高质量的输入方面所花的功夫越多,搜索的结果就会越好。

参考

elastic.co: Identifying Words

posted @ 2016-03-16 10:26  Richaaaard  阅读(1850)  评论(0编辑  收藏  举报