我们知道通过 Elasticsearch 实现全文搜索,在文档被导入到 ES 后,文档的每个字段都需要被分析,而这个分析阶段就会涉及到分词。上篇介绍了分词器的概念和常见分词器的使用,然而有些特定场景中,之前的分词器并不能满足我们的实际需求,那么就要进行定制分析器了。
ES 已经提供了丰富多样的开箱即用的分词 plugin,通过这些 plugin 可以创建自己的 token Analyzer,甚至可以利用已经有的 Char Filter,Tokenizer 及 Token Filter 来重新组合成一个新的 Analyzer,并对文档中的每个字段分别定义自己的 Analyzer。基于这些思路,我们则可以实现一个定制的分析器。
举个例子:
使用 standard 分词器对该文本字符串进行分词,得结果如下:
{
"tokens": [
{
"token": "the",
"start_offset": 0,
"end_offset": 3,
"type": "<ALPHANUM>",
"position": 0
},
{
"token": "third",
"start_offset": 4,
"end_offset": 9,
"type": "<ALPHANUM>",
"position": 1
},
{
"token": "dose",
"start_offset": 10,
"end_offset": 14,
"type": "<ALPHANUM>",
"position": 2
},
{
"token": "of",
"start_offset": 15,
"end_offset": 17,
"type": "<ALPHANUM>",
"position": 3
},
{
"token": "covid",
"start_offset": 18,
"end_offset": 23,
"type": "<ALPHANUM>",
"position": 4
},
{
"token": "19",
"start_offset": 24,
"end_offset": 26,
"type": "<NUM>",
"position": 5
},
{
"token": "vaccine",
"start_offset": 27,
"end_offset": 34,
"type": "<ALPHANUM>",
"position": 6
}
]
}
这段文本字符串中的"COVID-19",经过 standard 分词器处理后,却拆分成了"covid"和"19",明显不是我想要的结果,我预期得到的结果是"COVID19"或者"covid19"。那么,该如何实现我预期的分词结果呢?
这里有必要先普及下,我们平时创建某个索引的几个操作姿势:
方式一、简单不做作,直接 PUT
PUT 192.168.150.130:9200/mytest_index
方式二、创建索引同时指定"settings"
PUT 192.168.150.130:9200/mytest_index
{
"settings":{
"index":{
// 创建索引的同时设置分片数、副本数
"number_of_shards":"3",
"number_of_replicas":"1"
}
}
}
方式三、创建索引同时指定"mappings"
PUT 192.168.150.130:9200/mytest_index
{
"mappings":{
"_doc":{
"properties":{
"city":{
"type":"keyword"
},
"date":{
"type":"keyword"
},
"quantity":{
"type":"integer"
},
"description":{
"type":"text"
}
}
}
}
}
其他方式:创建索引同时指定"settings"、"mappings"、"aliases"等(看实际需求)
PUT 192.168.150.130:9200/mytest_index
{
"mappings":{
........
},
"aliases": {
........
},
"settings": {
........
}
}
有了这些知识点储备之后,实现预期的分词结果就简单了,基本思路是:创建索引的同时,通过 "mappings" 设置该字段的属性并为该字段自定义一个分析器,然后通过 "settings" 设置 Char Filter、Tokenizer 及 Token Filter 来重新组合成一个新的 Analyzer。具体实现如下:
PUT 192.168.150.130:9200/mytest_index
{
"mappings": {
"_doc": {
"properties": {
"city": {
"type": "keyword"
},
"date": {
"type": "keyword"
},
"quantity": {
"type": "integer"
},
"description": {
"type": "text",
"analyzer": "my_description_analyzer"
}
}
}
},
"settings": {
"analysis": {
"char_filter": {
"covid19_filter": {
"type": "mapping",
"mappings": [
"COVID-19 => COVID19"
]
}
},
"analyzer": {
"my_description_analyzer": {
"type": "custom",
"char_filter": [
"covid19_filter"
],
"tokenizer": "standard",
"filter": [
// 转小写输出covid19,如果注释掉的话则会输出COVID19
"lowercase"
]
}
}
}
}
}
请注意,由于我使用的 ES 版本是6.8.6,通过"mappings"设置字段属性时,需要加上文档类型"_doc",不然会报错的,而在高版本的 ES 中创建索引时,则不会再加上"_doc"(高版本 ES 的文档 type 已被抛弃)。
测试效果如下:
这样就实现了预期的分词结果。另外,像 "the"、"of" 这些停用词,standard 分词器是不会过滤掉的,如果我就要过滤掉这些停用词,或者加入我认为要过滤掉的单词,这个该如何实现呢?
在上篇文章分词器概念中,介绍说明过分词器的分析阶段(Analysis Phase),剔除已拆分的单词可在 Token Filter 实现。还是以 mytest_index 索引创建为例,在原来基础上加入以下内容(原设置已省略,重点在"filter"、"my_stop"):
PUT 192.168.150.130:9200/mytest_index1
{
"mappings": {
......
},
"settings": {
"analysis": {
"char_filter": {
......
},
"analyzer": {
"my_description_analyzer": {
"type": "custom",
"char_filter": [
"covid19_filter"
],
"tokenizer": "standard",
"filter": [
// 转小写输出covid19,如果注释掉的话则会输出COVID19
//"lowercase",
"my_stop"
]
}
},
"filter":{
"my_stop":{
"type":"stop",
"stopwords": ["the", "a", "of", "is"]
}
}
}
}
}
去掉停用词后,测试效果如下:
{
"tokens": [
{
"token": "third",
"start_offset": 4,
"end_offset": 9,
"type": "<ALPHANUM>",
"position": 1
},
{
"token": "dose",
"start_offset": 10,
"end_offset": 14,
"type": "<ALPHANUM>",
"position": 2
},
{
"token": "COVID19",
"start_offset": 18,
"end_offset": 26,
"type": "<ALPHANUM>",
"position": 4
},
{
"token": "vaccine",
"start_offset": 27,
"end_offset": 34,
"type": "<ALPHANUM>",
"position": 5
}
]
}
最后,请注意 Token Filter 过滤顺序的问题,假如我把文本字符串修改为"The third dose of COVID-19 vaccine",并把"lowercase" 和 "my_stop"调换下顺序,如下:
"analyzer": {
"my_description_analyzer": {
"type": "custom",
"char_filter": [
"covid19_filter"
],
"tokenizer": "standard",
"filter": [
"my_stop",
"lowercase"
]
}
}
那么得到的分词结果为:["the" "third" "dose" "covid19" "vaccine"],看出问题了吧?"the" 是被定义成停用词的,但分词结果却有这个单词。经过分析不难看出:这是由于执行"my_stop"时并没有把"The"看作是停止词,接着经过"lowercase"处理时输出了"the"。这就提示我们过滤顺序的重要性啊!!
在上篇文章提及过,Analyzer 将文本字符分解为 token 的过程,通常会发生在以下两种场景:一是索引建立的时候,二是进行文本搜索的时候。
第一种场景刚刚已经演示过了,在索引建立后,如果要录入一个文档的话,该文档在被写入索引之前,会将文档中的文本字符串分解成一个个 token,这些 token 是存放到数据库的;
第二种场景则是进行文本搜索,文本搜索的时候也会对该字符串进行分词,也会建立 token,但不会存放到数据库。默认情况下,我们查询搜索时会使用到第一种场景定制的分析器,很明显会导致某些查询问题,比如查询 "of" 是查不到结果的,因此有必要区分第一种场景的分析器。我们可以通过 search_analyzer 实现:
PUT 192.168.150.130:9200/mytest_index
{
"mappings": {
"_doc": {
"properties": {
"city": {
"type": "keyword"
},
"date": {
"type": "keyword"
},
"quantity": {
"type": "integer"
},
"description": {
"type": "text",
"analyzer": "my_description_analyzer",
"search_analyzer": "standard"
// 也可以使用自定义的分词器 "search_analyzer": "my_search_analyzer"
}
}
}
},
"settings": {
......
}
}
最后
本文重点在介绍说明如何实现自定义的分析器,以满足特定场景的需求,并通过演示说明了实现的思路。然而,实际需求总是五花八门多种多样的,掌握实现的思路和原理才是最重要的。比如,如何实现多个不同的分析器搜索查询相同的文本内容(思路是使用 multi-field 实现,即 fields 设置多字段),建议参考学习【Elasticsearch:将精确搜索与词干混合】这篇博文;又比如,在定制分析器时考虑定制相关性(通过 function_score 实现定制相关性),建议参考学习【Elasticsearch:定制分词器(analyzer)及相关性】这篇博文。
下篇将开始介绍说明 ES 索引及索引文档的 CRUD 操作,属于基础内容,但也是平时工作必备的知识点。