这是一个系列的第四篇,这篇主要介绍了中文分词的概念,以及如何使用分词器来做一个更好的中文搜索引擎。

目录

分词的概念

分词的英文叫做tokenizer,在ES官方文档上的解释如下:

A tokenizer receives a stream of characters, breaks it up into individual tokens (usually individual words), and outputs a stream of tokens.

简单来说,搜索引擎在保存文本的时候,会把一段话切割成一个个词,然后分别保存这些词。当你搜索的时候,搜索引擎同样会把你的搜索词切割成一个个词,然后分别取数据库里面进行搜索。

拿我们第一章最开始说的新奥尔良鸡翅来解释,中文分词器可能会把他分成:新奥尔良/鸡翅,这两个token。但这里要注意,这个时候,如果你单独输入一个字进行搜索,则不会匹配到新奥尔良/鸡翅,因为对于每一个token来说,匹配都要字符的完全匹配才行。

那这个时候怎么办呢?没关系,一般来说,我们确实可以认为,新奥尔良本来就代表不同的含义。另一方面,大多数中文分词器都有参数可以调整,你可以选择多分割一些token。那也许会分割成新/新奥尔良/奥尔良/鸡/鸡翅。这样,搜索,也能搜索到了。

使用Analyzer来体验分词

为了帮助debug,ES提供了一个analyzer的工具,可以帮你看到分词具体是如何分的。

在看中文分词之前,我们先来看一看英文。对于这段英文:The quick brown fox.,ES究竟是如何进行分词的呢?现在在console中输入以下请求:

1
2
3
4
POST _analyze
{
"text": "The quick brown fox."
}

返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
"tokens": [
{
"token": "the",
"start_offset": 0,
"end_offset": 3,
"type": "<ALPHANUM>",
"position": 0
},
{
"token": "quick",
"start_offset": 4,
"end_offset": 9,
"type": "<ALPHANUM>",
"position": 1
},
{
"token": "brown",
"start_offset": 10,
"end_offset": 15,
"type": "<ALPHANUM>",
"position": 2
},
{
"token": "fox",
"start_offset": 16,
"end_offset": 19,
"type": "<ALPHANUM>",
"position": 3
}
]
}

可以看到,ES把这句话分成了四个词the/quick/brown/fox,这个分词器把大写的The,改成了小写,把fox之后的句号给去掉了。这样,以后我们搜索小写的the,也能搜出这句话。

ES有许多内置的分词器,默认的是英文的标准分词器,还有只分割空格的whitespace分词器。完整的分词器列表见这里

我们现在看看,使用whitespace分词器的结果是如何的:

1
2
3
4
5
POST _analyze
{
"tokenizer": "whitespace",
"text": "The quick brown fox."
}

返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
"tokens": [
{
"token": "The",
"start_offset": 0,
"end_offset": 3,
"type": "word",
"position": 0
},
{
"token": "quick",
"start_offset": 4,
"end_offset": 9,
"type": "word",
"position": 1
},
{
"token": "brown",
"start_offset": 10,
"end_offset": 15,
"type": "word",
"position": 2
},
{
"token": "fox.",
"start_offset": 16,
"end_offset": 20,
"type": "word",
"position": 3
}
]
}

可以看到,这个分词器比较暴力,效果不太好,首先没有把The小写化,同时,fox后面的句号也没去掉。

不同的业务场景要选择不同的分词器。

中文分词测试

接下来我们试一下中文:

1
2
3
4
POST _analyze
{
"text": "不列颠传说中的王"
}

返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
{
"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
},
{
"token": "传",
"start_offset": 3,
"end_offset": 4,
"type": "<IDEOGRAPHIC>",
"position": 3
},
{
"token": "说",
"start_offset": 4,
"end_offset": 5,
"type": "<IDEOGRAPHIC>",
"position": 4
},
{
"token": "中",
"start_offset": 5,
"end_offset": 6,
"type": "<IDEOGRAPHIC>",
"position": 5
},
{
"token": "的",
"start_offset": 6,
"end_offset": 7,
"type": "<IDEOGRAPHIC>",
"position": 6
},
{
"token": "王",
"start_offset": 7,
"end_offset": 8,
"type": "<IDEOGRAPHIC>",
"position": 7
}
]
}

因为我们用的是ES默认的英文分词器,所以分词器索性就暴力地把中文字一个个拆开了。这样不太好,我们去找一个支持中文的分词器吧。

Smart Chinese Analysis的安装

支持ES的中文分词器主要有两个,一个是官方提供的插件:Smart Chinese Analysis plugin,另外一个是比较热门的非官方分词器:IK Analysis for Elasticsearch。比较好的是IK那个,在这里我们为了简单起见,就使用官方插件。

ES的插件安装非常简单,使用命令行进入ElasticSearch的目录,然后输入以下命令:

1
bin/elasticsearch-plugin install analysis-smartcn

接下来等待安装完成即可。

安装完成后,重启ES服务器。如果看到启动log里面有这么一句:loaded plugin [analysis-smartcn],就说明插件安装一切正常。

使用Smart Chineses来进行分词

我们重新试一下之前测试的内容,采用smartcn分词器:

1
2
3
4
5
POST _analyze
{
"analyzer": "smartcn",
"text": "不列颠传说中的王"
}

注意,这里用analyzer或者tokenizer都可以,analyzer的选择包含了tokenizer。在这里不详细展开。比如上面的命令,和下面的命令返回结果是一样的:

1
2
3
4
5
POST _analyze
{
"tokenizer": "smartcn_tokenizer",
"text": "不列颠传说中的王"
}

返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
{
"tokens": [
{
"token": "不列颠",
"start_offset": 0,
"end_offset": 3,
"type": "word",
"position": 0
},
{
"token": "传说",
"start_offset": 3,
"end_offset": 5,
"type": "word",
"position": 1
},
{
"token": "中",
"start_offset": 5,
"end_offset": 6,
"type": "word",
"position": 2
},
{
"token": "的",
"start_offset": 6,
"end_offset": 7,
"type": "word",
"position": 3
},
{
"token": "王",
"start_offset": 7,
"end_offset": 8,
"type": "word",
"position": 4
}
]
}

这下好多了,smartcn把这句话分成了不列颠/传说/中/的/王,这代表什么呢?这代表如果我们单独搜索【不】,是无法和这句话里面的不列颠匹配的。这很对,【不】的意思显然和不列颠不一样。

把现有数据库改造成使用smartcn分词器

好,我们之前存的4条数据,都是使用默认的英文分词器分词的,效果堪忧。我们现在要改成中文分词器。

要改成中文分词器,必须把原来的数据全部删除,然后在index的设置中设置成采用中文分词器,再把数据全部重新录入。

为什么一定要这么做?因为在之前数据录入的那一刻,分词、索引之类的事情就已经做完了。有太多东西不可逆,所以必须这么做。这里要提醒大家,在实际录入数据之前,很多设置要先想好,这就要靠长期的经验了。

删除整个index

1
DELETE /fgo

重新新建index,同时设定默认analyzer

1
2
3
4
5
6
7
PUT /fgo
{
"settings": {
"analysis":{"analyzer":{"default":{"type":"smartcn"}}},
"number_of_shards": 1
}
}

这里除了设定默认的analyzer,我还把shards(分片)的数量设为了1。因为ES有一个小问题,在数据量比较小的情况下,多个shard会导致搜索结果的score不对。

注:关于index的更多设定,可以参见官方文档。

重新导入数据

1
2
3
4
5
6
7
8
9
POST /fgo/servant/_bulk
{"create": {"_id": 1}}
{"name": "阿尔托莉雅・潘德拉贡","class": "Saber","description": "不列颠传说中的王,被称为骑士王。阿尔托莉雅是幼名,从成为王的那一天起就被称为亚瑟王。在那个骑士道如花般凋零的时代,用手中的圣剑为不列颠带来了短暂的和平与最后的繁荣。虽然史实上是男性,但在这个世界似乎是男装的丽人。"}
{"create": {"_id": 5}}
{"name": "尼禄・克劳狄乌斯","class": "Saber","description": "自称是男装的丽人。既自我至上主义又自私任性,明朗豁达,像小孩子一样天真无邪、被万人爱戴的无所不能恣意妄为的皇帝。本名,尼禄・克劳狄乌斯・凯撒・奥古斯都・日耳曼尼库斯。罗马帝国的第5代皇帝。生涯被涂上谋略与毒之色彩的恶名昭彰的暴君。"}
{"create": {"_id": 11}}
{"name": "卫宫","class": "Archer","description": "由于不是和其他英灵一样出自典故,不能被称为正统的英灵。由于这个英灵被称为守护者,所以它就像是由人类“应该存活下去”这个集体无意识中诞生出的像是防卫装置的东西。这个防卫装置所在的那一方被称为人类立场的抑止力,要点在于要选择没有名字的人们,没有知名度的正义的代理人。"}
{"create": {"_id": 12}}
{"name": "吉尔伽美什","class": "Archer","description": "公元以前统治着苏美尔的都市国家乌鲁克的半神半人的王者。不仅仅是传说而是真实存在的人物,记述于人类最古的叙事诗《吉尔伽美什叙事诗》中的王。"}

重新测试搜索

1
2
3
4
5
6
7
8
GET /fgo/_search
{
"query": {
"match": {
"_all": "不列颠"
}
}
}

返回(部分):

1
2
3
4
5
{
"name": "阿尔托莉雅・潘德拉贡",
"class": "Saber",
"description": "不列颠传说中的王,被称为骑士王。阿尔托莉雅是幼名,从成为王的那一天起就被称为亚瑟王。在那个骑士道如花般凋零的时代,用手中的圣剑为不列颠带来了短暂的和平与最后的繁荣。虽然史实上是男性,但在这个世界似乎是男装的丽人。"
}

如果我们搜索【不】

1
2
3
4
5
6
7
8
GET /fgo/_search
{
"query": {
"match": {
"_all": "不"
}
}
}

返回(部分):

1
2
3
4
5
{
"name": "卫宫",
"class": "Archer",
"description": "由于不是和其他英灵一样出自典故,不能被称为正统的英灵。由于这个英灵被称为守护者,所以它就像是由人类“应该存活下去”这个集体无意识中诞生出的像是防卫装置的东西。这个防卫装置所在的那一方被称为人类立场的抑止力,要点在于要选择没有名字的人们,没有知名度的正义的代理人。"
}

可以看到,分词器达成了可以理解你的意思的效果。
到这里,我们已经完成了搜索这一部分。

搜索引擎的UI部分

说起搜索引擎,大家想到的是这样的:

baidu

这其实属于网页前端的代码开发,我们可以用一个AngularJS或者是React程序,调用ElasticSearch的API,从而达到返回搜索结果,并显示在网页上的目的。关于前端的开发本篇教程不做展开。如有兴趣这可以在这里留言询问。

接下去请看下一章:高亮


题图来源:Designed by Freepik

ElasticSearch官方教程:点此进入
Kibana官方教程:点此进入