读前声明
文中一些专有名词所对应的英文名称
英文名称 | 中文翻译 |
---|---|
token | 分词 |
Inverted Index | 倒排索引 |
Analyzer | 分析器 |
Character Filters | 字符过滤器 |
Tokenizer | 分词器 |
Token Filter | 分词过滤器 |
正式内容
如果想用 Elasticsearch创建一个好的搜索引擎,必须要了解分析器(Analyzer)的工作原理。一个好的搜索引擎能够返回用户查询相关的文档。 Elasticsearch 中的分析器负责处理用户要索引的文档,并且在查询时评估哪些文档与查询关键词更匹配。
倒排索引
由于分析器与倒排索引紧密相关,因此需要首先了解什么是倒排索引。
倒排索引是一种数据结构,用于存储分词与具有该分词的文档编号之间的映射。除了文档编号之外,倒排索引还存储该分词在文档中的位置。由于 Elasticsearch 将分词与文档编号进行映射,因此使用Elasticsearch 进行关键词查询,它可以快速获取关键词所匹配的文档编号并返回。
文档构建倒排索引
假设现在有两条文档
文档1:Elasticsearch is fast
文档2:I want to learn Elasticsearch
下图为倒排索引构建的结果:第一列为分词内容,第二列为分词出现的次数,第三列为分词所在的文档编号以及在文档中的位置。
分词统计后映射到文档编号并保存了其在文档中的位置。我们没有看到完整的文档是因为其经过分析器的分析,这也是本篇文章的主旨。
在倒排索引中查询
关于查询倒排索引时有一点需要注意:Elasticsearch 只会获取与查询关键词具有相同分词的文档,通过Elasticsearch的match query
和term query
两类查询可以非常简单进行验证。match query
会使用分析器进行分析,而term query
则不会。另一篇文档有详细描述这两类查询的区别。
如果使用term query
以"Elasticsearch"为关键词在上述索引中进行查询,不会得到任何结果,因为分词在倒排索引中存储的是"elasticsearch",其开头字母是小写的"e"。如果用相同的关键字使用match query
查询,Elasticsearch会首先使用分析器对查询关键字进行分析,得到"elasticsearch",再到倒排索引中进行检索,因此可以返回相应结果。
Elasticsearch中的分析器(Analyzer)是什么?
当在Elasticsearch中插入一篇文档时,Elasticsearch不会对文档内容进行原样保存,而是先经过分析器分析。在分析的过程中,分析器首先对文本转换并将其拆分为分词,然后在保存到倒排索引中。
例如:插入“Let’s build an Autocomplete!”到Elasticsearch中,文本转换后将分别划分为4个分词:“let’s”, “build”, “an”和“autocomplete”。
分析器会影响搜索文本的方式,但不会影响文本本身的内容。在前面的例子中,如果我们搜索“let”,Elasticsearch 仍然会返回全文“Let's build an autocomplete!”而不仅仅是“let”单个词。
Elasticsearch中分析器的API
Elasticsearch提供了API供用户查询分析器对文本进行分析后的效果, API 可以大大简化分析器的调试过程。其中本文是在kibana的Dev Tools使用命令查看。
GET /_analyze
{
"text": "Let’s build an Autocomplete!",
"analyzer": "standard"
}
响应结果
{
"tokens" : [
{
"token" : "let’s",
"start_offset" : 0,
"end_offset" : 5,
"type" : "<ALPHANUM>",
"position" : 0
},
{
"token" : "build",
"start_offset" : 6,
"end_offset" : 11,
"type" : "<ALPHANUM>",
"position" : 1
},
{
"token" : "an",
"start_offset" : 12,
"end_offset" : 14,
"type" : "<ALPHANUM>",
"position" : 2
},
{
"token" : "autocomplete",
"start_offset" : 15,
"end_offset" : 27,
"type" : "<ALPHANUM>",
"position" : 3
}
]
}
Elasticsearch中分析器的组成
Elasticsearch的分析器主要由三部分组成,用户可以对其自行调整
- 字符过滤器(Character Filters)
- 分词器(Tokenizer)
- 分词过滤器(Token Filter)
字符过滤器(Character Filters)
分析过程中的第一个阶段是字符过滤,用于删除、添加和替换文本中的字符。
Elasticsearch 中有三个内置的字符过滤器:
- HTML字符过滤器:去除html中的标签,如
<b>
,<i>
,<div>
,<br />
等。 - 映射字符过滤器:该滤器可将一个词映射到另一个词。例如,如果想让用户可以搜索表情符号,可以将“:)”映射到“smile”
- 正则替换字符过滤器:将正则表达式替换为另一个词。不过使用该过滤器会减慢文档索引的速度。
分词器(Tokenizer)
经过字符过滤器过滤后,分词器会对文本进行划分,分为若干个分词。例如,先前的"Let’s build an autocomplete"文本会被划分为“let’s”, “build”, “an”和“autocomplete”4个分词,这个过程就是分词器所负责的。Elasticsearch中有大量的分词器,可以参考:https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-tokenizers.html
常用的分词器有:
- 标准分词器(Standard Tokenizer):Elasticsearch 的默认分词器,按照空格和标点符号划分文本。
- 空格分词器(Whitespace Tokenizer):对文本仅按空格进行划分。
- Edge N-Gram Tokenizer:对于自动补全非常有用,会按照空格和字符对单词进行划分,例如:Hello划分为 -> “H”, “He”, “Hel”, “Hell”, “Hello”。
在使用分词器时需要注意,因为太多的分词器会减慢文档的插入速度。
分词过滤器(Token Filter)
分词过滤器是分析器分析过程中的第三个阶段,也是最后的阶段。此过程将根据使用的分词过滤器转换分词。在分词过滤的过程中,可以小写转换,删除停用词,并为分词添加同义词。
Elasticsearch有大量的分词过滤器,可以参考https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-tokenfilters.html
最常用的分词过滤器是小写分词过滤器,用于将分词转换为小写。
标准分析器(Standard Analyzer)
标准分析器是Elasticsearch默认的分析器,如果你在构建文档mapping的时候没有指明分析器,则会使用标准分析器。其语法可参考:https://unicode.org/reports/tr29/
标准分析器主要由以下3部分组成:
- 标准分词器(Standard Tokenizer)
- 小写分词过滤器(Lower Case Token Filter)
- 停用词过滤器(默认不启动)
其主要功能有:
- 按照空格和标点符号对文本进行分词
- 将分词转化为小写
- 如果启用停用词过滤器,将会移除文本中的停用词
以"Let’s learn about Analyzer!"为例,看一下标准分析器的分析结果。
GET /_analyze
{
"text": "Let’s build an Autocomplete!",
"analyzer": "standard"
}
响应结果
{
"tokens" : [
{
"token" : "let’s",
"start_offset" : 0,
"end_offset" : 5,
"type" : "<ALPHANUM>",
"position" : 0
},
{
"token" : "build",
"start_offset" : 6,
"end_offset" : 11,
"type" : "<ALPHANUM>",
"position" : 1
},
{
"token" : "an",
"start_offset" : 12,
"end_offset" : 14,
"type" : "<ALPHANUM>",
"position" : 2
},
{
"token" : "autocomplete",
"start_offset" : 15,
"end_offset" : 27,
"type" : "<ALPHANUM>",
"position" : 3
}
]
}
可以看到标准分析器按照空格将文本拆分为若干个分词,并且还删除了标点符号“!”,因为它之后没有更多的分词。所有的分词都是小写的,因为标准分析器使用小写分词过滤器。
只使用标准分析器就可以实现一个简单的自动补全功能。如果通过使用不同的字符过滤器、分词器和分词过滤器来自定义分析器,便可以实现更高级的自动补全,产生更相关的结果。
自定义分析器
自定义分析器可以在根据需要自行定义其中的名称和组件。为了创建自定义的分析器,可以在创建索引的时候,在Elasticsearch的settings中定义。
PUT /autocomplete-custom-analyzer
{
"settings": {
"analysis": {
"analyzer": {
"custom_analyzer":{
"type":"custom",
"tokenizer":"whitespace",
"char_filter":["html_strip"],
"filter":["lowercase"]
}
}
}
}
}
上述语句,自定义了一个分析器,包括:空白分词器,html字符过滤器,小写分词过滤器。
使用"<b>Let’s build an autocomplete!</b>"文本进行测试标准分析器和自定义分析器的分析结果。
- 标准分析器
GET /_analyze
{
"text": "<b>Let’s build an autocomplete!</b>",
"analyzer": "standard"
}
响应结果
{
"tokens" : [
{
"token" : "b",
"start_offset" : 1,
"end_offset" : 2,
"type" : "<ALPHANUM>",
"position" : 0
},
{
"token" : "let’s",
"start_offset" : 3,
"end_offset" : 8,
"type" : "<ALPHANUM>",
"position" : 1
},
{
"token" : "build",
"start_offset" : 9,
"end_offset" : 14,
"type" : "<ALPHANUM>",
"position" : 2
},
{
"token" : "an",
"start_offset" : 15,
"end_offset" : 17,
"type" : "<ALPHANUM>",
"position" : 3
},
{
"token" : "autocomplete",
"start_offset" : 18,
"end_offset" : 30,
"type" : "<ALPHANUM>",
"position" : 4
},
{
"token" : "b",
"start_offset" : 33,
"end_offset" : 34,
"type" : "<ALPHANUM>",
"position" : 5
}
]
}
- 自定义分析器
GET /autocomplete-custom-analyzer/_analyze
{
"text": "<b>Let’s build an autocomplete!</b>",
"analyzer": "custom_analyzer"
}
响应结果
{
"tokens" : [
{
"token" : "let’s",
"start_offset" : 3,
"end_offset" : 8,
"type" : "word",
"position" : 0
},
{
"token" : "build",
"start_offset" : 9,
"end_offset" : 14,
"type" : "word",
"position" : 1
},
{
"token" : "an",
"start_offset" : 15,
"end_offset" : 17,
"type" : "word",
"position" : 2
},
{
"token" : "autocomplete!",
"start_offset" : 18,
"end_offset" : 35,
"type" : "word",
"position" : 3
}
]
}
可以发现以下几点不同:
- 标准分析器有两个
b
分词,而自定义分析器没有,这是因为自定义分析器中的html字符过滤器会将html移除。 - 标准分析器按照空格或者具体的字符如
<
,>
和!
对文本进行划分,而自定义分析器仅按照空格对文本进行划分。 - 标准分析器会移除特殊字符,而自定义分析器不会。如标准分析器后的分词为
autocomplete
,而自定义分析器是autocomplete!
。
最后一步是设置索引的mapping,不同的字段使用不同的分析器:standard-text
字段使用标准分析器,cust_analyzer-text
字段使用自定义分析器。
PUT /autocomplete-custom-analyzer/_mapping
{
"properties": {
"standard-text": {
"type": "text",
"analyzer": "standard"
},
"cust_analyzer-text": {
"type": "text",
"analyzer": "custom_analyzer"
}
}
}
添加两条文档:
POST /autocomplete-custom-analyzer/_doc/1
{
"standard-text":"<b>Let's build an autocomplete!</b>"
}
POST /autocomplete-custom-analyzer/_doc/2
{
"cust_analyzer-text":"<b>Let's build an autocomplete!</b>"
}
bool查询
GET /autocomplete-custom-analyzer/_search
{
"query": {
"bool": {
"should": [
{
"term": {
"standard-text": {
"value": "b"
}
}
},
{
"term": {
"cust_analyzer-text": {
"value": "b"
}
}
}
]
}
}
}
响应结果
{
"took" : 35,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 0.25069216,
"hits" : [
{
"_index" : "autocomplete-custom-analyzer",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.25069216,
"_source" : {
"standard-text" : "<b>Let's build an autocomplete!</b>"
}
}
]
}
}
结果返回使用标准分析器索引的文档,因为在标准分析器中没有使用 html字符过滤器去除 html 标签,而在自定义分析器过滤了。
尝试另一条查询语句,使用autocomplete!
关键字查询
GET /autocomplete-custom-analyzer/_search
{
"query": {
"bool": {
"should": [
{
"term": {
"standard-text": {
"value": "autocomplete!"
}
}
},
{
"term": {
"cust_analyzer-text": {
"value": "autocomplete!"
}
}
}
]
}
}
}
响应结果
{
"took" : 2,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 0.2876821,
"hits" : [
{
"_index" : "autocomplete-custom-analyzer",
"_type" : "_doc",
"_id" : "2",
"_score" : 0.2876821,
"_source" : {
"cust_analyzer-text" : "<b>Let's build an autocomplete!</b>"
}
}
]
}
}
现在,结果返回使用自定义分析器索引的文档。由于标准分析器通过空格和标点符号对文档进行分词,因此它删除了!
符号。 自定义分析器仅按空格划分文档,所以该标点符号没有被删除。
结论
如果想要创建一个好的搜索引擎,分析器Analyzer是必须要学习的重要组件。它是控制在用户查询单词时返回哪些文档的第一步。
参考
https://codecurated.com/blog/introduction-to-analyzer-in-elasticsearch/#a-bit-about-inverted-index