英语单词相对而言比较容易辨认:单词之间都是以空格或者(一些)标点隔开。 然而即使在英语词汇中也会有一些争议: you’re 是一个单词还是两个? o’clock , cooperate , half-baked ,或者 eyewitness 这些呢?
德语或者荷兰语把独立的单词合并起来创造一个长的合成词如 Weißkopfseeadler (white-headed sea eagle) , 但是为了在查询 Adler (eagle)的时候返回查询 Weißkopfseeadler 的结果,我们需要懂得怎么将合并词拆成词组。
亚洲的语言更复杂:很多语言在单词,句子,甚至段落之间没有空格。 有些词可以用一个字来表达,但是同样的字在另一个字旁边的时候就是不同意思的长词的一部分。
显而易见的是没有能够奇迹般处理所有人类语言的万能分析器,Elasticsearch 为很多语言提供了专用的分析器, 其他特殊语言的分析器以插件的形式提供。
然而并不是所有语言都有专用分析器,而且有时候你甚至无法确定处理的是什么语言。这种情况,我们需要一些忽略语言也能合理工作的标准工具包。
标准分析器
任何全文检索的字符串域都默认使用 standard 分析器。 如果我们想要一个 自定义 分析器 ,可以按照如下定义方式重新实现 标准 分析器:
{
"type": "custom",
"tokenizer": "standard",
"filter": [ "lowercase", "stop" ]
}
在 归一化词元 (标准化词汇单元)和 停用词: 性能与精度 (停用词)中,我们讨论了 lowercase (小写字母)和 stop (停用词) 词汇单元过滤器 ,但是现在,我们专注于 standard tokenizer (标准分词器)。
标准分词器
分词器 接受一个字符串作为输入,将这个字符串拆分成独立的词或 语汇单元(token) (可能会丢弃一些标点符号等字符),然后输出一个 语汇单元流(token stream) 。
有趣的是用于词汇 识别 的算法。 whitespace (空白字符)分词器按空白字符 —— 空格、tabs、换行符等等进行简单拆分 —— 然后假定连续的非空格字符组成了一个语汇单元。例如:
GET /_analyze
{
"text": "You're the 1st runner home!",
"analyzer": "whitespace"
}
这个请求会返回如下词项(terms): You’re 、 the 、 1st 、 runner 、 home!
letter 分词器 ,采用另外一种策略,按照任何非字符进行拆分, 这样将会返回如下单词: You 、 re 、 the 、 st 、 runner 、 home 。
standard 分词器使用 Unicode 文本分割算法 (定义来源于 Unicode Standard Annex #29)来寻找单词 之间 的界限,并且输出所有界限之间的内容。 Unicode 内含的知识使其可以成功的对包含混合语言的文本进行分词。
标点符号可能是单词的一部分,也可能不是,这取决于它出现的位置:
GET /_analyze
{
"text": "You're my 'favorite'.",
"analyzer": "standard"
}
在这个例子中,You’re 中的撇号被视为单词的一部分,然而 ‘favorite’ 中的单引号则不会被视为单词的一部分, 所以分词结果如下: You’re 、 my 、 favorite 。
uax_url_email 分词器和 standard 分词器工作方式极其相同。 区别只在于它能识别 email 地址和 URLs 并输出为单个语汇单元。 standard 分词器则不一样,会将 email 地址和 URLs 拆分成独立的单词。 例如,email 地址 joe-bloggs@foo-bar.com 的分词结果为 joe 、 bloggs 、 foo 、 bar.com 。
standard 分词器是大多数语言分词的一个合理的起点,特别是西方语言。 事实上,它构成了大多数特定语言分析器的基础,如 english 、french 和 spanish 分析器。 它也支持亚洲语言,只是有些缺陷,你可以考虑通过 ICU 插件的方式使用 icu_tokenizer 进行替换。
安装 ICU 插件
Elasticsearch的 ICU 分析器插件 使用 国际化组件 Unicode (ICU) 函数库(详情查看 site.project.org )提供丰富的处理 Unicode 工具。 这些包含对处理亚洲语言特别有用的 icu_分词器 ,还有大量对除英语外其他语言进行正确匹配和排序所必须的分词过滤器。
ICU 插件是处理英语之外语言的必需工具,非常推荐你安装并使用它,不幸的是,因为是基于额外的 ICU 函数库, 不同版本的ICU插件可能并不兼容之前的版本,当更新插件的时候,你需要重新索引你的数据。
安装这个插件,第一步先关掉你的Elasticsearch节点,然后在Elasticsearch的主目录运行以下命令:
./bin/plugin -install elasticsearch/elasticsearch-analysis-icu/$VERSION
- 当前 $VERSION (版本)可以在以下地址找到 https://github.com/elasticsearch/elasticsearch-analysis-icu.
一旦安装后,重启Elasticsearch,你将会看到类似如下的一条启动日志:
[INFO][plugins] [Mysterio] loaded [marvel, analysis-icu], sites [marvel]
如果你有很多节点并以集群方式运行的,你需要在集群的每个节点都安装这个插件。
icu_分词器
icu_分词器 和 标准分词器 使用同样的 Unicode 文本分段算法, 只是为了更好的支持亚洲语,添加了泰语、老挝语、中文、日文、和韩文基于词典的词汇识别方法,并且可以使用自定义规则将缅甸语和柬埔寨语文本拆分成音节。
例如,分别比较 标准分词器 和 icu_分词器 在分词泰语中的 ‘Hello. I am from Bangkok.’ 产生的词汇单元:
GET /_analyze
{
"text": " สวัสดี ผมมาจากกรุงเทพฯ",
"tokenizer": "standard"
}
标准分词器 产生了两个词汇单元,每个句子一个: สวัสดี , ผมมาจากกรุงเทพฯ 。这个只是你想搜索整个句子 ‘I am from Bangkok.’ 的时候有用,但是如果你仅想搜索 ‘Bangkok.’ 则不行。
GET /_analyze
{
"text": " สวัสดี ผมมาจากกรุงเทพฯ",
"tokenizer": "icu_tokenizer"
}
相反, icu_分词器 可以把文本分成独立的单词( สวัสดี , ผม , มา , จาก , กรุงเทพฯ ),这使得文档更容易被搜索到。
相较而言, 标准分词器 分词中文和日文的时候“过度分词”了,经常将一个完整的词拆分为独立的字符,因为单词之间并没有空格,很难区分连续的字符是间隔的单词还是一个句子中的单字:
- 向的意思是 facing (面对), 日的意思是 sun (太阳),葵的意思是 hollyhock (蜀葵)。当写在一起的时候, 向日葵的意思是 sunflower (向日葵)。
- 五的意思是 five (五)或者 fifth (第五), 月的意思是 month (月份),雨的意思是 rain (下雨)。 第一个和第二个字符写在一起成了五月,意思是 the month of May(一年中的五月), 然而添加上第三个字符, 五月雨的意思是 continuous rain (连续不断的下雨,梅雨)。当在合并第四个字符, 式, 意思是 style (样式),五月雨式这个单词则成了一种不屈不挠持续不断的东西的形容词。
虽然每个字符本身可以是一个单词,但使词汇单元保持更大的原始概念比使其仅作为一个词组的一部分要有意义的多:
GET /_analyze
{
"text": "向日葵",
"tokenizer": "icu_tokenizer"
}
GET /_analyze
{
"text": "向日葵",
"tokenizer": "standard"
}
# GET /_analyze 200 OK
{
"tokens": [
{
"token": "向日葵",
"start_offset": 0,
"end_offset": 3,
"type": "<IDEOGRAPHIC>",
"position": 0
}
]
}
# GET /_analyze 200 OK
{
"tokens": [
{
"token": "向",
"start_offset": 0,
"end_offset": 1,
"type": "<IDEOGRAPHIC>",
"position": 0
},
{
"token": "日",
"start_offset": 1,
"end_offset": 2,
"type": "<IDEOGRAPHIC>",
"position": 1
},
{
"token": "葵",
"start_offset": 2,
"end_offset": 3,
"type": "<IDEOGRAPHIC>",
"position": 2
}
]
}
标准分词器 在前面的例子中将每个字符输出为单独的词汇单元: 向 , 日 , 葵 。 icu_分词器 会输出单个词汇单元 向日葵 (sunflower) 。
标准分词器 和 icu_分词器 的另一个不同的地方是后者会将不同书写方式的字符(例如,βeta )拆分成独立的词汇单元 — β 和 eta— ,而前者则会输出单个词汇单元: βeta 。
整理输入文本
当输入文本是干净的时候分词器提供最佳分词结果,有效文本,这里 有效 指的是遵从 Unicode 算法期望的标点符号规则。 然而很多时候,我们需要处理的文本会是除了干净文本之外的任何文本。在分词之前整理文本会提升输出结果的质量。
HTML 分词
将 HTML 通过 标准分词器 或 icu_分词器 分词将产生糟糕的结果。这些分词器不知道如何处理 HTML 标签。例如:
GET /_analyze
{
"text": "<p>Some déjà vu <a href=\"http://somedomain.com>\">website</a>",
"tokenizer": "standard"
}
标准分词器 会混淆 HTML 标签和实体,并且输出以下词汇单元: p 、 Some 、 d 、 eacute 、 j 、 agrave 、 vu 、 a 、 href 、 http 、 somedomain.com 、 website 、 a 。这些词汇单元显然不知所云!
字符过滤器 可以添加进分析器中,在将文本传给分词器之前预处理该文本。在这种情况下,我们可以用 html_strip 字符过滤器移除 HTML 标签并编码 HTML 实体如 é 为一致的 Unicode 字符。
字符过滤器可以通过 analyze API 进行测试,这需要在查询字符串中指明它们:
GET /_analyze
{
"text": "<p>Some déjà vu <a href=\"http://somedomain.com>\">website</a>",
"tokenizer": "standard",
"char_filter": ["html_strip"]
}
想将它们作为分析器的一部分使用,需要把它们添加到 custom 类型的自定义分析器里:
PUT /my_index
{
"settings": {
"analysis": {
"analyzer": {
"my_html_analyzer": {
"tokenizer": "standard",
"char_filter": [
"html_strip"
]
}
}
}
}
}
一旦自定义分析器创建好之后, 我们新的 my_html_analyzer 就可以用 analyze API 测试:
GET my_index/_analyze
{
"text": "<p>Some déjà vu <a href=\"http://somedomain.com>\">website</a>",
"analyzer": "my_html_analyzer"
}
这次输出的词汇单元才是我们期望的: Some , déjà , vu , website 。
整理标点符号
标准分词器 和 icu_分词器 都能理解单词中的撇号应当被视为单词的一部分,然而包围单词的单引号在不应该。分词文本 You’re my ‘favorite’ , 会被输出正确的词汇单元 You’re , my , favorite 。
不幸的是, Unicode 列出了一些有时会被用为撇号的字符:
U+0027
撇号标记为 (')— 原始 ASCII 符号
U+2018
左单引号标记为 (‘)— 当单引用时作为一个引用的开始
U+2019
右单引号标记为 (’)— 当单引用时座位一个引用的结束,也是撇号的首选字符。
当这三个字符出现在单词中间的时候, 标准分词器 和 icu_分词器 都会将这三个字符视为撇号(这会被视为单词的一部分)。 然而还有另外三个长得很像撇号的字符:
U+201B
Single high-reversed-9 (高反单引号)标记为 (‛)— 跟 U+2018 一样,但是外观上有区别
U+0091
ISO-8859-1 中的左单引号 — 不会被用于 Unicode 中
U+0092
ISO-8859-1 中的右单引号 — 不会被用于 Unicode 中
标准分词器 和 icu_分词器 把这三个字符视为单词的分界线 — 一个将文本拆分为词汇单元的位置。不幸的是,一些出版社用 U+201B 作为名字的典型书写方式例如 M‛coy , 第二个俩字符或许可以被你的文字处理软件打出来,这取决于这款软件的年纪。
即使在使用可以“接受”的引号标记时,一个用单引号书写的词 — You’re — 也和一个用撇号书写的词 — You’re — 不一样,这意味着搜索其中的一个变体将会找不到另一个。
幸运的是,可以用 mapping 对这些混乱的字符进行分类, 该过滤器可以运行我们用另一个字符替换所有实例中的一个字符。这种情况下,我们可以简单的用 U+0027 替换所有的撇号变体:
PUT /my_index
{
"settings": {
"analysis": {
"char_filter": {
"quotes": {
"type": "mapping",
"mappings": [
"\\u0091=>\\u0027",
"\\u0092=>\\u0027",
"\\u2018=>\\u0027",
"\\u2019=>\\u0027",
"\\u201B=>\\u0027"
]
}
},
"analyzer": {
"quotes_analyzer": {
"tokenizer": "standard",
"char_filter": [ "quotes" ]
}
}
}
}
}
- 我们自定义了一个 char_filter (字符过滤器)叫做 quotes ,提供所有撇号变体到简单撇号的映射。
- 为了更清晰,我们使用每个字符的 JSON Unicode 转义语句,当然我们也可以使用他们本身字符表示: “‘=>'” 。
- 我们用自定义的 quotes 字符过滤器创建一个新的分析器叫做 quotes_analyzer 。
像以前一样,我们需要在创建了分析器后测试它:
GET my_index1/_analyze
{
"text": "You’re my ‘favorite’ M‛Coy",
"analyzer": "quotes_analyzer"
}
这个例子返回如下词汇单元,其中所有的单词中的引号标记都被替换为了撇号: You’re, my, favorite, M’Coy 。
投入更多的努力确保你的分词器接收到高质量的输入,你的搜索结果质量也将会更好。
{
"tokens": [
{
"token": "You're",
"start_offset": 0,
"end_offset": 6,
"type": "<ALPHANUM>",
"position": 0
},
{
"token": "my",
"start_offset": 7,
"end_offset": 9,
"type": "<ALPHANUM>",
"position": 1
},
{
"token": "favorite",
"start_offset": 11,
"end_offset": 19,
"type": "<ALPHANUM>",
"position": 2
},
{
"token": "M'Coy",
"start_offset": 21,
"end_offset": 26,
"type": "<ALPHANUM>",
"position": 3
}
]
}