写在前面:
在我阅读学习其它资料的过程,发现Analyzer和Tokenizer有时候同时会被翻译成"分词器",可能会让像我一样的小白读者将两者混淆。因此在这篇笔记里面,在容易混淆的地方直接用的英文名词。
通常说"ES分词器"指的其实是Analyzer,"分析器"也是指的Analyzer,但Tokenizer(中文翻译过来也是分词器)是Analyzer组成不可缺少的部分。
一、Analyzer基础概念
分词
分词是指将文本转换成一系列单词( term or token )的过程,也可以叫做文本分析,在es里面称为Analysis ,如下图所示:
Analyzer与ES
Analyzer是ES中专门处理分词的组件,称为"分词器"或者"分析器" ,ES建立索引和查询过程都离不开它。
- 建立倒排索引:将文档通过分词器分成词条(term),每一个Term都指向包含这个Term的文档集合。 name字段的倒排索引
- 查询:ES会根据搜索类型决定是否对query进行分词,然后和倒排索引中的term进行相关性查询,匹配相应的文档。
当搜索"小米手机" 的时候,小米手机会被分词为"小米"和"手机",去name字段对应的倒排索引里面查询,通过"小米"得到文档id集合:[1,2,3],通过"手机"得到文档id集合:[3,4],id为3的文档同时命中"小米"和"手机",相关性最高,会被优先展示。 默认情况下,相关性越大,分数越高,会被优先展示。理想状态下,我们想要的当然是同时匹配"小米"和"手机"的结果。但是如果因为想把华为产品进行优先展示,可以人为调整分数,把包含"华为"的文档进行优先展示。比较典型的是,以前一些搜索引擎在搜索医院和疾病的时候,会对搜索结果进行分数调控,优先把莆田系医院放在搜索结果的上面。
Analyzer的组成
这三个部分是有先后顺序的,过程比较好理解,先对原始文本处理,然后进行切分,最后对切分的单词进行再处理。
如果我们自己想要自定义一个分词器,也是由这三部分组成,数量如下:
Analyzer = Charater Filters(0个或多个) + Tokenizer(有且仅有一个) + Token Filters(0个或多个)
二、Charater Filters
HTML Strip Character Filter
-
从输入中去除HTML元素,只保留文本内容,比如<br></br>,<p></p>等标签,<p>Hello World</p> 被处理为 "Hello World"。
-
HTML中预定义字符的替换,比如&apos替换为单引号'
HTML中预定义字符对照可参考:www.madore.org/\\~david/co...
示例:
Markdown
GET /_analyze
{
"tokenizer": "keyword",
"char_filter": [
"html_strip"
],
"text": "<p>I'm so <b>happy</b>!</p>"
}
处理之后:
Markdown
I'm so happy!
可配置项:
- escaped_tags:可选,字符串数组,不处理包含尖括号 ( < >) 的 HTML 元素数组。从文本中剥离 HTML 时,过滤器会跳过这些 HTML 元素。例如,"escaped_tags": ["p"] 会跳过<p></p>标签,不会删除这对标签。
Mapping Character Filter
设置预定义的映射关系,将指定的字符或字符串替换为其他字符或字符串。例如,可以定义一个规则将 "&" 替换为 "和",将 "||" 替换为 "或者"。
Markdown
GET /_analyze
{
"tokenizer": "keyword",
"char_filter": [
{
"type":"mapping",
"mappings": [
"& => 和",
"|| => 或者"
]
}
],
"text": "午餐吃面条||米饭,菜要牛肉&青菜"
}
处理之后:
Markdown
午餐吃面条或者米饭,菜要牛肉和青菜
Pattern Replace Character Filter
使用正则表达式匹配字符,并将匹配的内容替换为指定的字符串。这对于处理有复杂需求的文本非常有用。
参数:
-
pattern:一个Java正则表达式. 必填。
-
replacement:替换字符串,可以使用 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1.. 1 .. </math>1.. 9语法来引用捕获组,可以参考这里。
-
flags:Java正则表达式标志。标志应该用"|"进行分割,例如"CASE_INSENSITIVE | COMMENTS"。
示例:
Markdown
PUT my-index-000001
{
"settings": {
"analysis": {
"analyzer": {
"my_analyzer": {
"tokenizer": "standard",
"char_filter": [
"my_char_filter"
]
}
},
"char_filter": {
"my_char_filter": {
"type": "pattern_replace",
"pattern": "(\\d+)-(?=\\d)",
"replacement": "$1_"
}
}
}
}
}
POST my-index-000001/_analyze
{
"analyzer": "my_analyzer",
"text": "My credit card is 123-456-789"
}
处理之后;
Markdown
[ My, credit, card, is, 123_456_789 ]
三、Tokenizer
单词分词器(Word Oriented Tokenizers)
1.1 Standard Tokenizer
standard tokenizer 按照 Unicode 文本分段算法的定义,将文本划分为字边界上的术语。它删除了大多数标点符号,是大多数语言的最佳选择。
可选参数:
- max_token_length:单个 token 的最大长度。如果一个 token 超过这个长度,则以max_token_length 为间隔分割。默认值是255.
示例:
Markdown
PUT my-index-000001
{
"settings": {
"analysis": {
"analyzer": {
"my_analyzer": {
"tokenizer": "my_tokenizer"
}
},
"tokenizer": {
"my_tokenizer": {
"type": "standard",
"max_token_length": 5
}
}
}
}
}
POST my-index-000001/_analyze
{
"analyzer": "my_analyzer",
"text": "The 2 QUICK Brown-Foxes jumped over the lazy dog's bone."
}
处理之后:
Markdown
[ The, 2, QUICK, Brown, Foxes, jumpe, d, over, the, lazy, dog's, bone ]
1.2 Letter Tokenizer
Letter Tokenizer每当遇到非字母的字符时,分词器就会将文本划分为词条 。
Markdown
POST _analyze
{
"tokenizer": "letter",
"text": "The 2 QUICK Brown-Foxes jumped over the lazy dog's bone."
}
上面的句子会生成如下的词元:
Markdown
[ The, QUICK, Brown, Foxes, jumped, over, the, lazy, dog, s, bone ]
1.3 Lowercase Tokenizer
类似于 letter tokenizer,遇到非字母时分割文本,并将所有分割后的词元转为小写。功能上等同于 letter tokenizer + lowercase token filter,但是由于单次执行了所有步骤,所以效率更高。
1.4 whitespace Tokenizer
类似 standard tokenizer,每当遇到 whitespace 字符时将文本分解成词条。
Markdown
POST _analyze
{
"analyzer": "whitespace",
"text": "The 2 QUICK Brown-Foxes jumped over the lazy dog's bone."
}
处理之后:
Markdown
[ The, 2, QUICK, Brown-Foxes, jumped, over, the, lazy, dog's, bone. ]
1.5 UAX URL Email Tokenizer
和Standard Tokenizer类似,只不过它会把 URL和 email 地址当成一个词元,可选参数也一样是max_token_length。
Markdown
POST _analyze
{
"tokenizer": "uax_url_email",
"text": "Email me at john.smith@global-international.com"
}
分解结果:
Markdown
[ Email, me, at, john.smith@global-international.com ]
而 standard tokenizer 会生成:
Markdown
[ Email, me, at, john.smith, global, international.com ]
1.6 Classic Tokenizer
Classic Tokenizer是一种基于语法的标记器,适用于英文文档。 特殊处理首字母缩略词、公司名称、电子邮件地址和互联网主机名。 但是,这些规则并不总是起作用,而且除了英文之外,大多数语言中并不能正常工作。
1.7 Thai Tokenizer
将泰文文本分成单词,使用的是 java 的泰语分割算法。文本中的其他语言按照standard tokenizer 处理。
部分词分词器(Partial Word Tokenizers)
Partial Word Tokenizers将文本或单词分解成小片段,以进行部分单词匹配。主要有两个。
2.1 N-gram tokenizer
N-gram tokenizer遇到指定的字符(如 : 空白、标点)时分割文本,然后返回指定长度的每个单词的 N-grams。
N-grams 就像一个滑动窗口在单词上移动,是一个连续的指定长度的字符序列。 通常用于查询不使用空格或具有较长复合词(如德语)的语言。
使用默认设置,ngram tokenizer将初始文本视为单个词元,并生成最小长度为1且最大长度为2的 N-gram:
Markdown
POST _analyze
{
"tokenizer": "ngram",
"text": "Quick Fox"
}
分词结果:
Markdown
[ Q, Qu, u, ui, i, ic, c, ck, k, "k ", " ", " F", F, Fo, o, ox, x ]
可选参数:
-
min_gram:以 gram 为单位的最小字符长度。 默认为1。
-
max_gram:以 gram 为单位的最大字符长度。 默认为2。
-
token_chars:应包含在词元中的字符类。 Elasticsearch将分割不属于指定类的字符。 默认为[](保留所有字符)。字符类可能是以下任何一种:(1)单词:例如a,b等;(2)数字 :例如3或17;(3)空格 : 例如" "或换行符 (4)标点符号: 例如"!",","等 (5)一些特殊符号:例如$或√
2.2 Edge NGram Tokenizer
首先将文本分解为单词,然后发出每个单词的N 元语法,其中 N 元语法的开头锚定到单词的开头。
具体可以参考;www.elastic.co/guide/en/el...
结构化分词器(Structured Text Tokenizers)
这里介绍三种常见的结构化分词器
3.1 Keyword Tokenizer
Keyword Tokenizer将整个输入字符串作为单个词条返回,适用于如小写电子邮件地址等不想分割的地方。
Markdown
POST _analyze
{
"tokenizer": "keyword",
"text": "New York"
}
处理之后:
Markdown
[ New York ]
3.2 Pattern Tokenizer
使用正则表达式分割文本。遇到单词分隔符将文本分割为词元, 或者将捕获到匹配的文本作为词元。默认的匹配模式时 \W+ ,遇到非单词的字符时分割文本。
3.3Path Tokenizer
path_hierarchy tokenizer 把分层的值看成是文件路径,用路径分隔符分割文本,输出树上的各个节点。
Markdown
POST _analyze
{
"tokenizer": "path_hierarchy",
"text": "/one/two/three"
}
处理之后:
Markdown
[ /one, /one/two, /one/two/three ]
Analyzer的第三部分是Token Filters,可以翻译成词元过滤器,它接受来自 Tokenizer(分词器) 的 tokens 流,并且可以修改 tokens(例如小写转换),删除 tokens(例如,删除 stopwords停用词)或添加 tokens(例如同义词)。
Elasticsearch 提供了很多内置的词元过滤器,可以用于构建自定义分词器。
在第四部分的1.4,讲了Stop词元过滤器的配置与使用。
四、Analyzer
ES自带分词器
1.1 Standard Analyzer
这里对"The 2 QUICK Brown-Foxes jumped over the lazy dog's bone."进行分词,其它Analyzer也会使用这个例子分词,从而比较它们的异同。
在上文提到了Analyzer 的组成: Charater Filters(0个或多个) + Tokenizer(有且仅有一个) + Token Filters(0个或多个),并且它们的处理顺序是:Charater Filters------> Tokenizer------> Token Filters。下面给出Standard Analyzer的组成部分,并且给出每一部分处理之后的结果(其它Analyzer也是这样)。
Standard Analyzer是默认的分词器,特性为:
-
按词切分,基于Unicode文本分割算法,支持多语言。对于中文支持不太好,汉字会被处理按照每个字进行拆分。
-
大写的字母会被转为小写
1.2 Simple Analyzer
特性为:
-
按照非字母切分,数字和符号会被过滤掉
-
小写处理
代码示例:
Markdown
POST _analyze
{
"analyzer": "simple",
"text": "Happy NewYear%春节快 乐"
}
根据空格和%切分,Happy和NewYear里面的大写字母会被转为小写:
Markdown
[happy,newyear,春节快,乐]
1.3 Whitespace Analyzer
按照空格切分,不过滤内容
Markdown
POST _analyze
{
"analyzer": "whitespace",
"text": "Happy NewYear%春节快 乐"
}
处理结果:
Markdown
[Happy,NewYear%春节快,乐]
1.4 Stop Analyzer
相比 Simple Analyzer多了Stop Word处理。
Stop Word指语气助词等修饰性的词语,比如the、an等等。
扩展:Stop token filter
Stop token filter在没有自定义的时候,过滤器默认删除以下英文停用词:
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
Stop token filter支持自定义词语,从而让过滤器删除它们,下面举个🌰
(1)设置Stop token filter的过滤词为:[ "there","are","two","and"],同时在过滤的时候忽略大小写。
Markdown
PUT /my-index-02
{
"settings": {
"analysis": {
"analyzer": {
"default": {
"tokenizer": "whitespace",
"filter": [ "my_custom_stop_words_filter" ]
}
},
"filter": {
"my_custom_stop_words_filter": {
"type": "stop",
"ignore_case": true,
"stopwords": [ "there","are","two","and"]
}
}
}
}
}
(2)对"There are two dogs and a cat"用空格进行分词,用自定义的词语过滤
Markdown
POST my-index-02/_analyze
{
"analyzer": "default",
"text": "There are two dogs and a cat"
}
处理之后的结果:
Markdown
[dogs, a, cat]
1.5 Keyword Analyzer
不分词,直接将输入作为一个单词输出,所以这也是为什么keyword 用于精准匹配。
1.6 Pattern Analyzer
特性为:
通过正则表达式自定义分割符
默认是\W+ ,即非字词的符号作为分隔符
自定义分词器
在前面,我们提到了 Analyzer由三部分组成,Analyzer = Charater Filters(0个或多个) + Tokenizer(有且仅有一个) + Token Filters(0个或多个),在一些场景,我们想要实现实现业务特定功能。
下面举个🌰
对于"I'm a :) <b>person</b>, and you?",我们想把输入的英文都转为小写,且过滤掉里面的HTML标签,并且想把 :) 映射为 happy,:( 映射为 sad
处理过程如下:
Markdown
PUT my-index-000002
{
"settings": {
"analysis": {
"analyzer": {
"my_custom_analyzer": {
"char_filter": [
"html_strip",
"emoticons"
],
"tokenizer": "standard",
"filter": [
"lowercase"
]
}
},
"char_filter": {
"emoticons": {
"type": "mapping",
"mappings": [
":) => _happy_",
":( => _sad_"
]
}
}
}
}
}
Markdown
POST my-index-000002/_analyze
{
"analyzer": "my_custom_analyzer",
"text": "I'm a :) <b>person</b>, and you?"
}
分词结果:
Markdown
["i'm","a","_happy_","person","and","you"]
五.中文分词器
中文分词指的是将汉字的序列切分成单独的词。在英文中,单词之间是以空格作为自然分界符,但汉语中词没有一个形式上的分界符。
IK
可自定义词库,支持热更新分词词典 网址:github.com/medcl/elast...
1.1 IK的两种模式
实现中英文单词的切分
ik分词器包含两种模式:
- ik_smart:最少切分,粗粒度
Markdown
GET /_analyze
{
"text":"中国人民",
"analyzer":"ik_smart"
}
分词结果:
Markdown
[中国人民]
- ik_max_word:最细切分,细粒度
Markdown
GET /_analyze
{
"text":"中国人民",
"analyzer":"ik_max_word"
}
分词结果:
Markdown
[中国人民,中国人,中国,国人,人民]
1.2 实践与踩坑:IK的拓展词典和停用词典
设想我们常见的一种场景,一些敏感词我们想把它屏蔽掉,一些网络新兴词我们想把它加入分词的词条行列。
1.原始模式下,我们没做额外配置,但是假如我们想把"Happy新春"作为一个词,并且不需要"新春"这个词条。
Markdown
GET /_analyze
{
"text":"Happy新春快乐",
"analyzer":"ik_max_word"
}
没做额外配置的分词结果如下:
Markdown
[happy,春节快乐,春节,快乐]
2.ik在es_plugins下面,路径是:
es-plugins\_data\ik\config
这个文件夹下有很多配置文件。
ik配置文件描述
- IKAnalyzer.cfg.xml:IK分词配置文件。
- main.dic:主词库。
- stopword.dic:英文停用词,不会建立在倒排索引中。
- quantifier.dic:特殊词库:计量单位等。
- suffix.dic:特殊词库:行政单位。
- surname.dic:特殊词库:百家姓。
- preposition:特殊词库:语气词。
3.拓展
要拓展ik分词器的词库,在名为ext.dic的文件中,添加想要拓展的词语即可:
4.禁用
要禁用某些敏感词条,只需要修改一个ik分词器目录中的config目录中的IkAnalyzer.cfg.xml文件
然后在名为stopword.dic的文件中,添加想要禁用的词语:
4.生效
接下来只需要修改分词器目录中的config目录中的IkAnalyzer.cfg.xml文件:
然后在生效之后,我们输入原来的语句,可以发现结果是一开始我们想要的
Markdown
GET /_analyze
{
"text":"Happy新春快乐",
"analyzer":"ik_max_word"
}
这就是我们想要的结果了
分词结果:
Markdown
[happy新春,happy,新春快乐,快乐]
踩坑实录(自己的修改一开始没有生效,在对下面两项进行修改才真正生效):
- 注意ext.dic,stop.dic的编码方式是UTF-8编码
- 重启es
1.3 IK原理浅析
IK的设计包含了统计学,多种算法的知识,这里从浅层概括一下主要内容:
1.词典
词典是IK的核心部分,包括了主词典、停用词词典、姓氏词典等。Dictionary字典管理类加载它们到内存结构。
词典结构使用了Tire Tree(字典树),它是一种结构相当简单的树型结构,用于构建词典,通过前缀字符逐一比较来快速查找词,所以有时也称为前缀树。这里举个简单的例子:
中国,中国人,蓝天,蓝色这些词就是存在树中的单词。
2.算法:
-
在ik_max_word模式下,IK分词器使用一种正向最大匹配法(FMM),从文本的最左侧开始匹配最长的词语,直到无法匹配为止,然后再从剩余文本中继续执行相同的过程。
-
在ik_smart模式下,IK分词器借鉴Viterbi算法,这是一种动态规划算法,用于寻找最有可能的词组合,以此优化分词结果。
3.优化
为了提高处理速度,Ik做了一些优化。例如:
-
懒加载:在不影响启动速度的前提下,按需加载词典,减少系统启动时的资源消耗。
-
缓存:缓存热点数据,如一些常用词汇,避免重复分词带来的性能损耗。
-
多线程提高兵发能力:对于大规模文本,支持多线程并发处理,提高分词速度。
2.jieba
python中最流行的分词系统,支持分词和词性标注 支持繁体分词、自定义词典、并行分词等 github.com/sing1ee/ela...
拓展:可根据上下文内容进行分词,基于自然语言处理的分词系统 Hanlp:
- 由一系列模型与算法组成的Java工具包,目标是普及自然语言处理在生产环境中 的应用
- github.com/hankcs/HanL...
THULAC:
- THU Lexical Analyzer for Chinese ,由清华大学自然语言处理与社会人文计算 实验室研制推出的一套中文词法分析工具包,具有中文分词和词性标注功能
- github.com/microbun/el...
感谢阅读,如果本篇文章有任何错误和建议,欢迎给我留言指正,感谢🙏
参考:
-
《Elasticsearch高手之路》