目录
1、文本分析简介
全文检索与常规关系数据库SQL查询的显著区别,就是全文检索具备对大段文本进行分析的能力,它可以通过文本分析把大段的文本切分为细粒度的分词。Elasticsearch在两种情况下会用到文本分析,一是原始数据写入索引时,如果索引的某个字段类型是text,则会将分析之后的内容写入索引;二是对text类型字段的索引数据做全文检索时,搜索内容也会经过文本分析。如果你不能真正弄懂文本分析的过程并正确地使用分析器,你也许会无法解释某些搜索为什么搜不到预期的数据。
2、文本分析的原理
Elasticsearch规定,一个完整的文本分析过程需要经过大于等于零个字符过滤器、一个分词器、大于等于零个分词过滤器的处理过程。文本分析的顺序是先进行字符过滤器的处理,然后是分词器的处理,最后是分词过滤器的处理。相关说明如下。
- 字符过滤器:用于对原始文本做简单的字符过滤和转换,例如,Elasticsearch内置的HTML strip字符过滤器可以用于方便地剔除文本中的HTML标签。
- 分词器:分词器的功能就是把原始的文本按照一定的规则切分成一个个单词,对于中文文本而言分词的效果和中文分词器的类型有关。分词器还会保留每个关键词在原始文本中出现的位置数据。Elasticsearch内置的分词器有几十种,通常针对不同语言的文本需要使用不同的分词器,你也可以安装一些第三方的分词器来扩展分词的功能。
- 分词过滤器:用于对用分词器切词后的单词做进一步过滤和转换,例如,停用词分词过滤器(stop token filter)可以把分词器切分出来的冠词a、介词of等无实际意义的单词直接丢弃,避免它们影响搜索结果。
在Elasticsearch中,触发文本分析的时机有两个。
- 索引时:当索引映射中存在text字段时,默认会使用标准分析器进行文本分析,如果不喜欢默认的分析器,也可以在映射中指定某个text类型字段使用其他分析器。
- 全文检索时:当你对一个索引的text类型字段做全文检索时也会触发文本分析,这时文本分析的对象是搜索的内容。默认的分析器也是标准分析器,如果需要改变分析器,可以通过搜索参数analyzer进行设置。为了保持搜索效果的一致性,索引时的分析器和全文检索时的分析器一般会设置成相同的。
3、使用内置的分析器分析文本
Elasticsearch 7.9.1内置了8种分析器,可以直接使用它们。这些分析器有不同的使用效果,具体内容如表所示。
分析器名称 | 说明 |
---|---|
standard analyzer | 标准分析器,按照Unicode文本分割算法切分单词,会删除大多数标点符号并会将单词转为小写形式,支持过滤停用词 |
simple analyzer | 简单分析器,在任意非字母的地方把单词切分开并将单词转为小写形式,非字母或汉字字符都会被丢弃 |
whitespace analvzer | 空格分析器,遇到空格就切分字符,但不改变每个字符的内容 |
stop analyzer | 停用词分析器,在简单分析器的基础上添加了删除停用词的功能 |
keyword analyzer | 关键字分析器,不进行任何切词,把文本作为整体输出 |
pattern analyzer | 模式分析器,按照正则表达式切分文本,支持把字母转为小写形式和过滤停用词 |
language analyzers | 包含一组内置的多种语言分析器,需要按照特定语言选择使用 |
fingerprint analyzer | 指纹分析器,实现了一种指纹算法,它会把文本转为小写形式,删除扩展词和重复词,并且将每个分词按字典值排序输出 |
接下来介绍前3种常见的分析器并介绍如何将它们配置到索引映射中做文本分析,大家可以对比用它们切词的效果。
3.1、标准分析器
标准分析器(standard analyzer)是文本分析默认的分析器,标准分析器的内部包含一个标准分词器(standard tokenizer)和一个小写分词过滤器(lower case token filter)。你可以使用_analyze的REST端点测试标准分析器默认的分词效果。
POST _analyze
{
"analyzer": "standard",
"text": "my 名字 is ShanBen56。"
}
会得到以下结果。
{
"tokens" : [
{
"token" : "my",
"start_offset" : 0,
"end_offset" : 2,
"type" : "<ALPHANUM>",
"position" : 0
},
{
"token" : "名",
"start_offset" : 3,
"end_offset" : 4,
"type" : "<IDEOGRAPHIC>",
"position" : 1
},
{
"token" : "字",
"start_offset" : 4,
"end_offset" : 5,
"type" : "<IDEOGRAPHIC>",
"position" : 2
},
{
"token" : "is",
"start_offset" : 6,
"end_offset" : 8,
"type" : "<ALPHANUM>",
"position" : 3
},
{
"token" : "shanben56",
"start_offset" : 9,
"end_offset" : 18,
"type" : "<ALPHANUM>",
"position" : 4
}
]
}
可以看到,标准分析器默认的切词效果可以实现将英语大写字母转为小写字母,这是因为该分析器内置了一个将大写字母转为小写字母的小写分词过滤器;另外,它按照空格来切分英语单词,数字作为一个整体不会被切分,中文文本会被切分成一个个汉字,标点符号则会被自动去掉。标准分析器可以配置停用词分词过滤器去掉没有实际意义的虚词,还可以通过设置切词粒度来配置分词的长度。下面的映射可以把标准分析器设置到索引中。
PUT standard-text
{
"settings": {
"analysis": {
"analyzer": {
"my_standard_analyzer": {
"type": "standard",
"max_token_length": 3,
"stopwords": "_english_"
}
}
}
},
"mappings": {
"properties": {
"content": {
"type": "text",
"analyzer": "my_standard_analyzer"
}
}
}
}
在上面的索引standard-text中,配置了一个名为my_standard_analyzer的分析器。type为standard表明它使用了内置的标准分析器;max_token_length为3表示把切词的最大长度设置为3,这样数字和英文长度都会被切分为不超过3个字符的形式;stopwords为_english_表示该分析器会自动过滤英文的停用词。该索引只有一个content文本字段,映射中指定了content字段的分析器为my_standard_analyzer,表示建索引时该字段的文本数据使用my_standard_analyzer做文本分析。下面来看看它的切词效果,代码如下。
POST standard-text/_analyze
{
"analyzer": "my_standard_analyzer",
"text": "my 名字 is ShanBen56。"
}
得到的结果如下。
{
"tokens" : [
{
"token" : "my",
"start_offset" : 0,
"end_offset" : 2,
"type" : "<ALPHANUM>",
"position" : 0
},
{
"token" : "名",
"start_offset" : 3,
"end_offset" : 4,
"type" : "<IDEOGRAPHIC>",
"position" : 1
},
{
"token" : "字",
"start_offset" : 4,
"end_offset" : 5,
"type" : "<IDEOGRAPHIC>",
"position" : 2
},
{
"token" : "sha",
"start_offset" : 9,
"end_offset" : 12,
"type" : "<ALPHANUM>",
"position" : 4
},
{
"token" : "nbe",
"start_offset" : 12,
"end_offset" : 15,
"type" : "<ALPHANUM>",
"position" : 5
},
{
"token" : "n56",
"start_offset" : 15,
"end_offset" : 18,
"type" : "<ALPHANUM>",
"position" : 6
}
]
}
可以明显看出该结果跟使用默认的标准分析器standard的结果有两点不同,一是分词“is”作为停用词被过滤掉了,二是每个分词的长度都不超过3。上述代码实现了切分数字和英文,如果你把字符切分粒度设置为最细粒度1,就可以用于数字或者字符的模糊搜索,例如搜索身份证号和手机号码。
3.2、简单分析器
简单分析器(simple analyzer)只由一个小写分词器(lowercase tokenizer)组成,它的功能是将文本在任意非字母字符(如数字、空格、连字符)处进行切分,丢弃非字母字符,并将大写字母改为小写字母。
可以使用以下请求测试简单分析器的效果。
POST _analyze
{
"analyzer": "simple",
"text": "my 名字 is ShanBen56。"
}
会得到以下结果。
{
"tokens" : [
{
"token" : "my",
"start_offset" : 0,
"end_offset" : 2,
"type" : "word",
"position" : 0
},
{
"token" : "名字",
"start_offset" : 3,
"end_offset" : 5,
"type" : "word",
"position" : 1
},
{
"token" : "is",
"start_offset" : 6,
"end_offset" : 8,
"type" : "word",
"position" : 2
},
{
"token" : "shanben",
"start_offset" : 9,
"end_offset" : 16,
"type" : "word",
"position" : 3
}
]
}
可以看出,简单分析器丢弃了所有的非字母字符并从非字母字符处切词,大写字母会自动转为小写字母。
3.3、空格分析器
空格分析器(whitespace analyzer)的结构也非常简单,只由一个空格分词器(whitespace tokenizer)构成,它会在所有出现空格的地方切词,而每个分词本身的内容不变。
尝试用以下代码测试空格分析器的切词效果。
POST _analyze
{
"analyzer": "whitespace",
"text": "my 名字 is ShanBen56。"
}
从以下切词结果可以看到该分析器只是从空格处切词,并没有改变字符本身的内容。
{
"tokens" : [
{
"token" : "my",
"start_offset" : 0,
"end_offset" : 2,
"type" : "word",
"position" : 0
},
{
"token" : "名字",
"start_offset" : 3,
"end_offset" : 5,
"type" : "word",
"position" : 1
},
{
"token" : "is",
"start_offset" : 6,
"end_offset" : 8,
"type" : "word",
"position" : 2
},
{
"token" : "ShanBen56。",
"start_offset" : 9,
"end_offset" : 19,
"type" : "word",
"position" : 3
}
]
}
4、使用IK分词器分析文本
由于中文字符是方块字,默认的标准分词器把中文文本切成孤立的汉字不能正确地反映中文文本的语义。IK分词器是比较受欢迎的中文分词器,本节就来谈谈如何使用IK分词器进行文本分析。
4.1、安装IK分词器
K分词器的安装十分简单,主要分为3个步骤。
- 进入IK分词器的GitHub页面,下载与Elasticsearch 7.9.1配套的分词器安装包,也要选择7.9.1版本。
- 进入Elasticsearch的安装目录,找到plugins文件夹,在里面新建一个名为ik的文件夹,把下载的安装包解压后放进ik文件夹中。
- 重启Elasticsearch服务。
4.2、在索引中使用IK分词器
K分词器提供了两种分析器供开发人员使用:ik_smart和ik_max_word。
先来验证IK分词器的切词效果,代码如下。
POST _analyze
{
"analyzer": "ik_smart",
"text": "床前明月光"
}
从以下结果可以看到ik_smart把文本切分成了3个分词。
{
"tokens" : [
{
"token" : "床",
"start_offset" : 0,
"end_offset" : 1,
"type" : "CN_CHAR",
"position" : 0
},
{
"token" : "前",
"start_offset" : 1,
"end_offset" : 2,
"type" : "CN_CHAR",
"position" : 1
},
{
"token" : "明月光",
"start_offset" : 2,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 2
}
]
}
如果使用ik_max_word分析器,则切出来的分词更多,代码如下。
POST _analyze
{
"analyzer": "ik_max_word",
"text": "床前明月光"
}
从以下结果可知同样的文本被切分成了5个分词,这种模式下,切出来的分词会尽可能多,更便于被搜索到。
{
"tokens" : [
{
"token" : "床",
"start_offset" : 0,
"end_offset" : 1,
"type" : "CN_CHAR",
"position" : 0
},
{
"token" : "前",
"start_offset" : 1,
"end_offset" : 2,
"type" : "CN_CHAR",
"position" : 1
},
{
"token" : "明月光",
"start_offset" : 2,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "明月",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 3
},
{
"token" : "月光",
"start_offset" : 3,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 4
}
]
}
通常,索引时的文本分析使用ik_max_word更加合适,而全文检索时的文本分析使用ik_smart较为多见。值得注意的是,每个分词结果用偏移量保存了每个分词在原始文本中出现的位置,这类位置信息在全文检索结果高亮展示时会被用到。
下面的请求将新建一个索引ik-text并使用IK分词器进行文本分析。
PUT ik-text
{
"settings": {
"analysis": {
"analyzer": {
"default": {
"type": "ik_max_word"
},
"default_search": {
"type": "ik_smart"
}
}
}
},
"mappings": {
"properties": {
"content": {
"type": "text"
},
"abstract":{
"type": "text",
"analyzer": "ik_smart",
"search_analyzer": "ik_max_word"
}
}
}
}
在索引ik-text中,指定text类型的字段默认的索引分析器是ik_max_word,而默认的全文检索分析器是ik_smart。content字段正是采用的这种分析方式,而abstract字段则是在映射中显式地指定了索引时的分析器为ik_smart,搜索时的分析器为ik_max_word,这会覆盖掉索引默认的分析器配置。
IK分词器虽然能够识别很多中文词,但是它无法自动发现新词,如果你想扩展自己的词典,则需要把扩展的内容配置到IK分词器目录下的IKAnalyzer.cfg.xml文件中。即便如此,由于IK分词器切词的粒度不够细,在面对姓名、数字、英文单词的检索时常常达不到预期结果。
5、自定义文本分析器分析文本
Elasticsearch 7.9.1内置了3种字符过滤器、10多种分词器和数十种分词过滤器,本节将有选择性地介绍它们的功能,最后会介绍如何按照实际需要自行组建一个分析器来分析文本数据。
5.1、字符过滤器
使用字符过滤器(character filter)是文本分析的第一个环节,在开始分词之前,字符过滤器可以用于丢弃一些无意义的特殊字符。
5.1.1、 HTML strip字符过滤器
HTML strip字符过淲器用于去掉文本中的HTML标签,还可以用于解析类似于&的转义字符串。你可以用以下请求测试HTML strip字符过滤器的效果。
POST _analyze
{
"char_filter": ["html_strip"],
"tokenizer": {
"type": "keyword"
},
"filter": [],
"text": [
"Tom & Jerrey < <b>world</b>"
]
}
这里配置了HTML strip字符过滤器和关键字分词器,得到的结果如下。
{
"tokens" : [
{
"token" : "Tom & Jerrey < world",
"start_offset" : 0,
"end_offset" : 34,
"type" : "word",
"position" : 0
}
]
}
可以看出,分析后的文本中,HTML标签被去掉了,转义字符串在输出结果中也转义成功。
5.1.2、映射字符过滤器
映射字符过滤器会根据提供的字符映射,把文本中出现的字符转换为映射的另一种字符。例如:
POST _analyze
{
"char_filter": [{
"type": "mapping",
"mappings": [
"& => and"
]
}],
"tokenizer": {
"type": "keyword"
},
"filter": [],
"text": [
"Tom & Jerrey"
]
}
这里配置了一个映射字符过滤器mapping,把字符“&”转为“and”,结果如下。
{
"tokens" : [
{
"token" : "Tom and Jerrey",
"start_offset" : 0,
"end_offset" : 12,
"type" : "word",
"position" : 0
}
]
}
5.1.3、模式替换字符过滤器
模式替换字符过滤器会根据指定的正则表达式把匹配的文本转换成指定的字符串。例如:
POST _analyze
{
"char_filter": [
{
"type": "pattern_replace",
"pattern": "runoo+b",
"replacement": "tomcat"
}
],
"tokenizer": {
"type": "keyword"
},
"filter": [],
"text": [
"runooobbbb"
]
}
上面的请求配置了一个正则表达式“runoo+b”,它匹配到了文本“runooob”并将其替换为“tomcat”,结果如下。
{
"tokens" : [
{
"token" : "tomcatbbb",
"start_offset" : 0,
"end_offset" : 10,
"type" : "word",
"position" : 0
}
]
}
5.2、分词器
分词器(tokenizer)是文本分析的“灵魂”,它是文本分析过程中不可缺少的一部分,因为它直接决定按照怎样的算法来切分文本。分词器会把原始文本切分为一个个分词(token),通常分词器会保存每个分词的以下3种信息。
- 文本分析后每个分词的相对顺序,主要用于短语搜索和单词邻近搜索。
- 字符偏移量,记录分词在原始文本中出现的位置。
- 分词类型,记录分词的种类,例如单词、数字等。
分词器按照切分方式大致可分为3种类型:面向单词的分词器会把文本切分成独立的单词;部分单词分词器会把每个单词按照某种规则切分成小的字符串碎片;特定结构的分词器主要针对特定格式的文本分词,比如邮箱、邮政编码、路径等。
5.2.1、面向单词的分词器
Elasticsearch 7.9.1内置的面向单词的分词器如表4所示,其中的标准分词器、小写分词器和空格分词器在前面介绍内置的分析器时已经讲过。有些分词器只对特定的语言有效,这一点在使用时需要注意。
分词器名称 | 说明 |
---|---|
standard tokenizer | 标准分词器,是标准分析器所采用的分词器,它会删除大多数标点符号,把文本切分为独立单词 |
letter tokenizer | 字母分词器,在任意非字母的地方把单词切分开,非字母字符会被丢弃 |
lowercase tokenizer | 小写分词器,在字母分词器的基础上把大写字母转为小写字母,它是简单分析器的组成部分 |
whitespace tokenizer | 空格分词器,是空格分析器的组成部分,在空格处把文本切分开并保持文本内容不变 |
UAX URL email tokenizer | 标准URL Emal分词器,跟标准分词器的唯-区别是遇到邮箱和超接地址不会进行分词 |
classic tokenizer | 经典分词器,是一种基于英文语法分词的分词器 |
thai tokenizer | 泰语分词器,用于将泰语文本切分成单词 |
5.2.2、部分单词分词器
这种分词器会把完整的单词按照某种规则切分成一些字符串碎片,搜索时其可用于部分匹配,这种分词器主要有两种:N元语法分词器(N-gram tokenizer)和侧边N元语法分词器(edge N-gram tokenizer)。
对于N元语法分词器,你可以使用以下请求查看分词效果。
POST _analyze
{
"char_filter": [],
"tokenizer": {
"type": "ngram",
"min_gram": 2,
"max_gram": 3,
"token_chars": [
"letter"
]
},
"filter": [],
"text": [
"tom cat8"
]
}
在这个ngram分词器的配置中,设置了每个分词的最小长度为2,最大长度为3,表示ngram分词器会通过长度为2和3的滑动窗口切分文本,得到的子串作为分词。token_chars配置了只保留字母作为分词,其余的字符(例如空格和数字)则会被过滤掉。结果如下。
{
"tokens" : [
{
"token" : "to",
"start_offset" : 0,
"end_offset" : 2,
"type" : "word",
"position" : 0
},
{
"token" : "tom",
"start_offset" : 0,
"end_offset" : 3,
"type" : "word",
"position" : 1
},
{
"token" : "om",
"start_offset" : 1,
"end_offset" : 3,
"type" : "word",
"position" : 2
},
{
"token" : "ca",
"start_offset" : 4,
"end_offset" : 6,
"type" : "word",
"position" : 3
},
{
"token" : "cat",
"start_offset" : 4,
"end_offset" : 7,
"type" : "word",
"position" : 4
},
{
"token" : "at",
"start_offset" : 5,
"end_offset" : 7,
"type" : "word",
"position" : 5
}
]
}
侧边N元语法分词器与N元语法分词器的区别在于,其在分词时总是从第一个字母开始,会保留一定长度的前缀字符。例如:
POST _analyze
{
"char_filter": [],
"tokenizer": {
"type": "edge_ngram",
"min_gram": 2,
"max_gram": 5
},
"filter": [],
"text": [
"tom cat8"
]
}
这里设置了每个分词的长度最小为2,最大为5,这种分词器很适合做前缀搜索。结果如下。
{
"tokens" : [
{
"token" : "to",
"start_offset" : 0,
"end_offset" : 2,
"type" : "word",
"position" : 0
},
{
"token" : "tom",
"start_offset" : 0,
"end_offset" : 3,
"type" : "word",
"position" : 1
},
{
"token" : "tom ",
"start_offset" : 0,
"end_offset" : 4,
"type" : "word",
"position" : 2
},
{
"token" : "tom c",
"start_offset" : 0,
"end_offset" : 5,
"type" : "word",
"position" : 3
}
]
}
5.2.3、特定结构的分词器
下表中的几种特定结构的分词器可以对正则表达式、分隔符或者路径等进行切分,不同于前两类分词器把文本切分为完整或部分单词。
分词器名称 | 说明 |
---|---|
keyword tokenizer | 关键字分词器,保留文本本身,不做任何处理 |
pattern tokenizer | 模式分词器,使用正则表达式在匹配单词分隔符时将文本拆分为分词,或者将匹配的文本捕获为分词,默认会在非字母字符的地方切分文本 |
simple pattern tokenizer | 简单模式分词器,使用正则表达式捕获匹配的文本作为术语,所支持的正则表达式较为简单,不支持直接从分隔符切分单词 |
simple pattern splittokenizer | 简单模式切分分词器,可以定义简单的正则表达式来切分文本使之成为分词 |
char group tokenizer | 字符组分词器,可以定义一些字符集,当在文本中遇到这些字符时就把文本切分开,这样做往往比使用模式分词器更加简单.高效 |
path tokenizer | 路径分词器,会对文件路径格式的字符串按照路径规则切分,这种分词方法适合文件路径搜索的场景 |
5.3、分词过滤器
分词过滤器(token filter)用于对分词后的文本做进一步处理,例如删除停用词、添加同义词、把字母转为小写形式等。Elasticsearch 7.9.1内置了数十种分词过滤器,由于篇幅所限不能全部介绍,这里介绍几种有代表性的分词过滤器供大家参考。
5.3.1、N元语法分词过滤器
N元语法分词过滤器(N-gram token filter)跟N元语法分词器的功能基本一样,只不过这里以分词过滤器的形式介绍,它可以与其他的分词器组合使用来对每个分词做N-gram处理。例如:
POST _analyze
{
"char_filter": [],
"tokenizer": "standard",
"filter": [
{
"type": "ngram",
"min_gram": 3,
"max_gram": 3
}
],
"text": "Quick fox"
}
这里配置了先对文本做标准分词以切分成独立单词,然后对每个单词做N-gram切分,最大长度为3。结果如下。
{
"tokens" : [
{
"token" : "Qui",
"start_offset" : 0,
"end_offset" : 5,
"type" : "<ALPHANUM>",
"position" : 0
},
{
"token" : "uic",
"start_offset" : 0,
"end_offset" : 5,
"type" : "<ALPHANUM>",
"position" : 0
},
{
"token" : "ick",
"start_offset" : 0,
"end_offset" : 5,
"type" : "<ALPHANUM>",
"position" : 0
},
{
"token" : "fox",
"start_offset" : 6,
"end_offset" : 9,
"type" : "<ALPHANUM>",
"position" : 1
}
]
}
5.3.2、侧边N元语法分词过滤器
侧边N元语法分词过滤器(edge N-gram token filter)跟侧边N元语法分词器的效果也是基本一样的,只是在这里它可以配合其他的分词器使用,它在切词时会从每个分词的首字母开始。例如:
POST _analyze
{
"char_filter": [],
"tokenizer": "standard",
"filter": [
{
"type": "edge_ngram",
"min_gram": 2,
"max_gram": 3
}
],
"text": "hello world2"
}
这里配置了每个分词的最小长度为2,最大长度为3,会把用标准分词器切分后的每个单词保留长度为2和3的前缀并将其作为最终的结果。结果如下。
{
"tokens" : [
{
"token" : "he",
"start_offset" : 0,
"end_offset" : 5,
"type" : "<ALPHANUM>",
"position" : 0
},
{
"token" : "hel",
"start_offset" : 0,
"end_offset" : 5,
"type" : "<ALPHANUM>",
"position" : 0
},
{
"token" : "wo",
"start_offset" : 6,
"end_offset" : 12,
"type" : "<ALPHANUM>",
"position" : 1
},
{
"token" : "wor",
"start_offset" : 6,
"end_offset" : 12,
"type" : "<ALPHANUM>",
"position" : 1
}
]
}
5.3.3、词源分词过滤器
词源分词过滤器(stemmer token filter)会把每个分词转换成对应的原型(例如去掉复数、时态等),这在英文搜索时无疑是有用的。例如:
POST _analyze
{
"char_filter": [],
"tokenizer": "standard",
"filter": [
"stemmer"
],
"text": "apples worlds flying"
}
这里先用标准分词器把文本切分为单词,再用词源分词过滤器把每个单词转换成对应的原型。结果如下。
{
"tokens" : [
{
"token" : "appl",
"start_offset" : 0,
"end_offset" : 6,
"type" : "<ALPHANUM>",
"position" : 0
},
{
"token" : "world",
"start_offset" : 7,
"end_offset" : 13,
"type" : "<ALPHANUM>",
"position" : 1
},
{
"token" : "fly",
"start_offset" : 14,
"end_offset" : 20,
"type" : "<ALPHANUM>",
"position" : 2
}
]
}
5.3.4、停用词分词过滤器
停用词分词过滤器(stop token filter)用于过滤掉分词中无实际意义的停用词,主要包含冠词、介词等,你也可以自定义一个“黑名单”把不需要放入索引的词通过它过滤掉。例如:
POST _analyze
{
"char_filter": [],
"tokenizer": "standard",
"filter": [
"stop"
],
"text": "there is an apple on a big tree"
}
可以看到以下的最终文本分析结果中去掉了停用词。
{
"tokens" : [
{
"token" : "apple",
"start_offset" : 12,
"end_offset" : 17,
"type" : "<ALPHANUM>",
"position" : 3
},
{
"token" : "big",
"start_offset" : 23,
"end_offset" : 26,
"type" : "<ALPHANUM>",
"position" : 6
},
{
"token" : "tree",
"start_offset" : 27,
"end_offset" : 31,
"type" : "<ALPHANUM>",
"position" : 7
}
]
}
5.4、给索引添加自定义分析器
前面已经介绍了Elasticsearch自带的几种字符过滤器、分词器和分词过滤器,你可以按照实际需要自由地组合它们,从而形成自己的分析器。
例如,你可以定义一个分析器,使用字符过滤器过滤掉HTML标签,用标准分词器把文本、单词和数字切分到最细粒度,同时添加一个停用词黑名单过滤掉无意义的中文文本,代码如下。
POST _analyze
{
"char_filter": [
"html_strip"
],
"tokenizer": {
"type": "standard",
"max_token_length": "1"
},
"filter": [
{
"type": "stop",
"stopwords": [
"是",
"一",
"个",
"着",
"的"
]
}
],
"text": "<b>2023年</b>北京是一个有雾霾的城市。"
}
分词的结果如下。
{
"tokens" : [
{
"token" : "2",
"start_offset" : 3,
"end_offset" : 4,
"type" : "<NUM>",
"position" : 0
},
{
"token" : "0",
"start_offset" : 4,
"end_offset" : 5,
"type" : "<NUM>",
"position" : 1
},
{
"token" : "2",
"start_offset" : 5,
"end_offset" : 6,
"type" : "<NUM>",
"position" : 2
},
{
"token" : "0",
"start_offset" : 6,
"end_offset" : 7,
"type" : "<NUM>",
"position" : 3
},
{
"token" : "年",
"start_offset" : 7,
"end_offset" : 8,
"type" : "<IDEOGRAPHIC>",
"position" : 4
},
{
"token" : "北",
"start_offset" : 12,
"end_offset" : 13,
"type" : "<IDEOGRAPHIC>",
"position" : 5
},
{
"token" : "京",
"start_offset" : 13,
"end_offset" : 14,
"type" : "<IDEOGRAPHIC>",
"position" : 6
},
{
"token" : "有",
"start_offset" : 17,
"end_offset" : 18,
"type" : "<IDEOGRAPHIC>",
"position" : 10
},
{
"token" : "雾",
"start_offset" : 18,
"end_offset" : 19,
"type" : "<IDEOGRAPHIC>",
"position" : 11
},
{
"token" : "霾",
"start_offset" : 19,
"end_offset" : 20,
"type" : "<IDEOGRAPHIC>",
"position" : 12
},
{
"token" : "城",
"start_offset" : 21,
"end_offset" : 22,
"type" : "<IDEOGRAPHIC>",
"position" : 14
},
{
"token" : "市",
"start_offset" : 22,
"end_offset" : 23,
"type" : "<IDEOGRAPHIC>",
"position" : 15
}
]
}
可见分词后确实达到了预期效果,包括数字在内的文本被切分到最细粒度,去掉了HTML标签和停用词。现在你可以把这个自定义的分析器配置到索引映射中使其生效,代码如下。
PUT my_analyzer-text
{
"settings": {
"analysis": {
"tokenizer": {
"my_tokenizer": {
"type": "standard",
"max_token_length": "1"
}
},
"filter": {
"my_filter": {
"type": "stop",
"stopwords": [
"是",
"一",
"个",
"着",
"的"
]
}
},
"analyzer": {
"my_analyzer": {
"type": "custom",
"char_filter": "html_strip",
"tokenizer": "my_tokenizer",
"filter": "my_filter"
}
},
"default": {
"type": "my_analyzer"
},
"default_search": {
"type": "my_analyzer"
}
}
},
"mappings": {
"properties": {
"content": {
"type": "text",
"analyzer": "my_analyzer"
},
"abstract": {
"type": "text",
"analyzer": "my_analyzer"
}
}
}
}
上面的索引映射定义了一个分词器my_tokenizer,它使用标准分词器把文本切分到最细粒度;又定义了一个分词过滤器my_filter,它遇到stopwords中指定的汉字时就会将其丢弃;最后使用HTML strip字符过滤器、分词器my_tokenizer和分词过滤器my_filter组成了一个自定义的分析器my_analyzer,还把它设置为索引默认的分析器。
6、查看文档的词条向量
使用词条向量可以直观地反映某个文档的各个分词在索引中出现的次数、位置等统计信息,词条向量包含的信息主要分为三大类。
- 词条信息:记录文档的每个分词在查询参数所指定字段出现的频次(term_freq)、位置(position)、起始/结束偏移量(offset)、负载(payload)。所谓的负载是用户自定义的数据信息,以Base64编码的格式进行保存。若要让词条向量保存负载则需要在映射中进行相关配置。
- 词条统计信息:统计文档的各个分词在多少(doc_freq)个索引文档中出现,以及各个分词在索引中一共出现了多少(ttf)次。
- 字段统计信息:统计索引中拥有查询参数所指定字段的文档数(doc_count)、该指定字段的所有词条在索引中出现的文档数之和(sum_doc_freq),以及该指定字段的所有词条在索引中出现的频次之和(sum_ttf)。
新建一个索引term-vector,内容如下。
PUT term-vector
{
"settings": {
"analysis": {
"analyzer": {
"fulltext_analyzer": {
"type": "custom",
"tokenizer": "standard",
"filter": [
"type_as_payload"
]
}
}
}
},
"mappings": {
"properties": {
"content": {
"type": "text",
"term_vector": "with_positions_offsets_payloads",
"analyzer": "fulltext_analyzer"
}
}
}
}
该索引映射只有一个content字段,为了能在词条向量中查看词条的位置、偏移量和负载,给content字段配置了词条向量参数with_positions_offsets_payloads。同时,在索引映射中自定义了一个分析器fulltext_analyzer,该分析器先使用标准分词器分词,然后使用type_as_payload分词过滤器把每个分词的类型数据以Base64编码的格式写入词条向量作为负载。
下面给索引添加两条数据。
PUT term-vector/_doc/1
{
"content" : "apple 121."
}
PUT term-vector/_doc/2
{
"content" : "Black phone apple apple"
}
为了实现查看文档1的词条向量信息,发起下面的请求。
GET term-vector/_termvectors/1
{
"fields" : ["content"],
"offsets" : true,
"payloads" : true,
"positions" : true,
"term_statistics" : true,
"field_statistics" : true
}
在请求的url中指定了查看的索引名称和文档主键,请求体的fields参数指定了要查看的词条向量的字段,后面的几个参数用于设置词条向量要返回的具体信息,该请求得到的结果如下。
"term_vectors" : {
"content" : {
"field_statistics" : {
"sum_doc_freq" : 5,
"doc_count" : 2,
"sum_ttf" : 6
},
"terms" : {
"121" : {
"doc_freq" : 1,
"ttf" : 1,
"term_freq" : 1,
"tokens" : [
{
"position" : 1,
"start_offset" : 6,
"end_offset" : 9,
"payload" : "PE5VTT4="
}
]
},
"apple" : {
"doc_freq" : 2,
"ttf" : 3,
"term_freq" : 1,
"tokens" : [
{
"position" : 0,
"start_offset" : 0,
"end_offset" : 5,
"payload" : "PEFMUEhBTlVNPg=="
}
]
}
}
}
}
在返回的结果中,field_statistics包含字段content的统计信息,terms中包含文档1中各个词条的词条信息和统计信息,payload保存着每个词条类型数据的Base64编码,读者可以自行验证。
Elasticsearch还允许查看一个自定义文档的词条向量,此时在查询参数中不需要提供文档主键,但需要提供自定义文档的内容。例如:
GET term-vector/_termvectors
{
"doc": {
"content": "app apple"
},
"offsets": true,
"payloads": true,
"positions": true,
"term_statistics": true,
"field_statistics": true
}
该请求的doc部分提供了自定义文档的内容,该文档每个分词的统计信息会在结果中返回,由于该文档不属于索引中的数据,因此负载payload是空的,该请求返回的结果如下。
"term_vectors" : {
"content" : {
"field_statistics" : {
"sum_doc_freq" : 8,
"doc_count" : 3,
"sum_ttf" : 10
},
"terms" : {
"app" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 0,
"start_offset" : 0,
"end_offset" : 3
}
]
},
"apple" : {
"doc_freq" : 3,
"ttf" : 5,
"term_freq" : 1,
"tokens" : [
{
"position" : 1,
"start_offset" : 4,
"end_offset" : 9
}
]
}
}
}
}
7、keyword类型字段的标准化
文本分析器是针对text类型的字段在其写入索引前对文本内容做字符过滤和切词操作。本节探讨的标准化器是用于给keyword类型的字段做文本预处理的,它可以实现在文本内容写入keyword类型的字段前对文本进行统一的标准化转换,保证写入keyword类型字段的文本的统一和规范。
下面的请求将新建一个索引normalize-keyword,其只包含一个keyword类型的字段country,并为该字段配置了一个自定义标准化器my_normalizer。
PUT normalize-keyword
{
"settings": {
"analysis": {
"normalizer": {
"my_normalizer": {
"type": "custom",
"char_filter": [],
"filter": ["lowercase"]
}
}
}
},
"mappings": {
"properties": {
"country": {
"type": "keyword",
"normalizer": "my_normalizer",
"store": true
}
}
}
}
由于keyword类型的字段不能分词,所以标准化器中不包含分词器,由字符过滤器和分词过滤器组成。在标准化器my_normalizer中,只配置了一个分词过滤器lowercase,表示在数据写入country字段前会统一对其进行小写转换的规范化处理。添加两条数据,代码如下。
PUT normalize-keyword/_doc/1
{
"country": "China"
}
PUT normalize-keyword/_doc/2
{
"country": "chinA"
}
下面对country字段进行term query,即使搜索文本全部是大写形式的也会被标准化器自动转为小写形式进行检索。
POST normalize-keyword/_search
{
"query": {
"term": {
"country": "CHINA"
}
}
}
从以下的返回结果可以看出,该请求成功搜索到了索引的两条数据,由于_source字段保存的是写入索引的原始数据,所以其中的文本没有进行小写转换,它并不能反映索引中真实的文本数据。
"hits" : [
{
"_index" : "normalize-keyword",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.18232156,
"_source" : {
"country" : "China"
}
},
{
"_index" : "normalize-keyword",
"_type" : "_doc",
"_id" : "2",
"_score" : 0.18232156,
"_source" : {
"country" : "chinA"
}
}
]
为了实现查看索引中country字段的真实数据,可以使用stored_fields。
POST normalize-keyword/_search
{
"stored_fields": ["country"]
}
从以下返回的结果中可以看出,两条数据的内容确实都先转为小写形式再写入索引 进行保存。
"hits" : [
{
"_index" : "normalize-keyword",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.0,
"fields" : {
"country" : [
"china"
]
}
},
{
"_index" : "normalize-keyword",
"_type" : "_doc",
"_id" : "2",
"_score" : 1.0,
"fields" : {
"country" : [
"china"
]
}
}
]
8、小结
文章介绍了文本分析的过程和方法,主要内容总结如下。
- 文本分析需要经历字符过滤器、分词器和分词过滤器处理这3个过程,只有索引中的text类型的字段才会用到文本分析。
- 触发文本分析的时机有两个,一是向索引的text类型的字段写入数据时,二是使用全文检索时,在这两个时机都会对搜索文本进行文本分析。
- Elasticsearch 7.9.1内置了8种分析器,都可以直接使用,其中标准分析器是默认的分析器。你可以在映射中配置索引和搜索时默认采用的分析器。
- IK分词器需要安装到Elasticsearch的安装目录中成为插件才能使用,它提供了ik_max_word和ik_smart两种分析器。其中ik_max_word切分文本的粒度较细,ik_smart切分文本的粒度较粗。
- IK分词器可以通过扩展词典的方式识别更多的中文词,但是对于姓名、数字或英文单词等并不能达到很好的切词效果。
- Elasticsearch 7.9.1内置了3种字符过滤器、10余种分词器和数十种分词过滤器,你可以按照实际的需要组合它们,从而定义自己的分析器来做文本分析。
- 词条向量用于查看某个文档中各个分词的词条信息、统计信息以及某个字段在索引中的统计信息,要查看词条的偏移量、负载等信息需要在映射中设置词条向量的有关参数。
- keyword类型字段的标准化可以在数据写入索引前进行统一的数据标准化处理,以保证keyword类型字段的数据拥有一致的规范。标准化器不仅对写入keyword类型字段的文本有效,而且对检索keyword类型字段的搜索文本有效。