Elasticsearch

一、介绍

1 Elasticsearch介绍

当项目的数据规模极大时,数据检索会面临很多问题,比如数据库的选型,如何保证数据安全性,如何定位故障等等,这些就是 Elasticsearch的产生原因。

Elasticsearch是一个基于Lucene库的搜索引擎。它提供了一个分布式、RESTful 风格的搜索和数据分析引擎,具有HTTP Web接口和无模式JSON文档。Elasticsearch是用Java开发的,并在Apache许可证下作为开源软件发布。它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理PB级别的数据。官方客户端在Java、.NET(C#)、PHP、Python、Apache Groovy、Ruby和许多其他语言中都是可用的。

就 Elasticsearch 而言,起步很简单。对于初学者来说,它预设了一些适当的默认值,并隐藏了复杂的搜索理论知识。 它开箱即用。只需最少的理解,你很快就能具有生产力。

2 solr介绍

Solr(读作“solar”)是Apache Lucene项目的开源企业搜索平台,基于java开发。其主要功能包括全文检索、命中标示、分面搜索、动态聚类、数据库集成,以及富文本(如Word、PDF)的处理。Solr是用Java编写、运行在Servlet容器(如Apache Tomcat或Jetty)的一个独立的全文搜索服务器。与Elasticsearch都是提供数据检索的软件。

3 Elasticsearch的相关概念

3.1 Cluster集群

集群是一个或多个节点(服务器)的集合, 这些节点共同保存整个数据,并在所有节点上提供联合索引和搜索功能。一个集群由一个唯一集群ID确定,并指定一个集群名(默认为“elasticsearch”)。该集群名非常重要,因为节点可以通过这个集群名加入群集,一个节点只能是群集的一部分。

3.2 Node节点

形成集群的每个服务器称为节点。节点的名称默认为一个随机的通用唯一标识符(UUID),确定在启动时分配给该节点。如果不希望默认,可以定义任何节点名。

3.3 Index索引

索引是具有相似特性的文档集合。例如,可以为客户数据提供索引,为产品目录建立另一个索引,以及为订单数据建立另一个索引。索引由名称(必须全部为小写)标识,该名称用于在对其中的文档执行索引、搜索、更新和删除操作时引用索引。在单个群集中,您可以定义尽可能多的索引。

3.4 Type类型

在索引中,可以定义一个或多个类型。类型是索引的逻辑类别/分区,其语义完全取决于您。一般来说,类型定义为具有公共字段集的文档。例如,假设你运行一个博客平台,并将所有数据存储在一个索引中。在这个索引中,您可以为用户数据定义一种类型,为博客数据定义另一种类型,以及为注释数据定义另一类型。

3.5 Document文档

索引和搜索的最小单位就是文档。

3.6 Shards分片

当有大量的文档时,由于内存的限制、磁盘处理能力不足、无法足够快的响应客户端的请求等,一个节点可能不够。这种情况下,数据可以分为较小的分片。每个分片放到不同的服务器上。当你查询的索引分布在多个分片上时,ES会把查询发送给每个相关的分片,并将结果组合在一起,而应用程序并不知道分片的存在。即:这个过程对用户来说是透明的。

3.7 Replia副本

Elasticsearch允许你创建一个或多个拷贝,你的索引分片进入所谓的副本或称作复制品的分片,实现高可用性。需要注意的是,一个副本的分片不会分配在同一个节点作为原始的或主分片,副本是从主分片那里复制过来的。当主分片丢失时,如:该分片所在的数据不可用时,集群将副本提升为新的主分片。

4 关系型数据库和ElasticSearch中的概念对比

MySQL Elasticsearch
Database(数据库) Index(索引)
Table(数据表) Type(类型)
Row(行) Dcoument(文档)
column(列) Field(字段)
Schema Mapping
CURD(增删改查) GET,PUT,POST,UPDATE…
  • 关系型数据库中的数据库(DataBase),等价于ES中的索引(Index)
  • 一个数据库下面有N张表(Table),等价于1个索引Index下面有N多类型(Type),
  • 一个数据库表(Table)下的数据由多行(ROW)多列(column,属性)组成,等价于1个Type由多个文档(Document)和多Field组成。
  • 在一个关系型数据库里面,schema定义了表、每个表的字段,还有表和字段之间的关系。 与之对应的,在ES中:Mapping定义索引下的Type的字段处理规则,即索引如何建立、索引类型、是否保存原始索引JSON文档、是否压缩原始JSON文档、是否需要分词处理、如何进行分词处理等。
  • 在数据库中的增删改查操作,等价于ES中的GET,PUT,POST,UPDATE

5 ElasticSearch结构图

一个集群包含至少一个节点,节点内可以有多个索引。索引内有很多分片,分片有主分片,同时每个分片又有一个副本,如下图中有3个节点的集群,可以看到主分片和副本分别存放在不同的节点,即使有一个节点挂了也不会影响整体的数据完整。

image-20200306181921343

二、下载和安装

1 ElasticSearch安装和启动

首先安装java环境:下载地址,下载安装JDK。

ElasticSearch:下载地址,下载之后是一个压缩包,解压即安装。

进入到解压文件夹的bin路径下,执行启动文件。

1
2
elasticsearch.bat # windows	
./elasticsearch # linux

如果你想要后台运行,可以在后面添加参数 -d

等待一会之后,打开浏览器输入以下地址:http://127.0.0.1:9200/ ,如果看到如下json格式的数据就说明启动成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"name" : "QCFA-CL6GV5",
"cluster_name" : "elasticsearch",
"cluster_uuid" : "OmiT4TA-Rmqj8aS3ajAc_g",
"version" : {
"number" : "7.16.1",
"build_flavor" : "default",
"build_type" : "zip",
"build_hash" : "5b38441bc6b1eeb16a27c107a4c3865776e20c53",
"build_date" : "2021-10-12T00:29:38.865893768Z",
"build_snapshot" : false,
"lucene_version" : "8.10.1",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}

要删除ElasticSearch,直接删除该文件夹。

2 ElasticSearch插件

es支持安装一些官方或第三方的增强插件,安装插件是要注意对应版本,如果不兼容则无法启动。es提供了三种安装方式。

第一种,通过插件名安装,只能安装官方的插件:

所有官方插件的下载地址:Github

1
2
bin/elasticsearch-plugin install [plugin_name]
# bin/elasticsearch-plugin install analysis-smartcn 安装官方的中文分词器

第二种,通过url安装,提供一个插件的zip压缩文件的url地址:

1
2
bin/elasticsearch-plugin install [url]
# /bin/elasticsearch-plugin install https://github.com/NLPchina/elasticsearch-sql/releases/download/7.8.0.0/elasticsearch-sql-7.8.0.0.zip

第三种,通过源码安装,将zip下载到本地,将压缩文件解压到ElasticSearch安装目录下的plugins目录下,然后重启ElasticSearch服务。

2.1 IK分词器

IK Analyzer是一个开源的,基于java语言开发的轻量级的中文分词工具包。最初,它是以开源项目Luence为应用主体的,我们下载的是集成到ElasticSearch的IK分词器。

首先下载zip压缩包,下载地址:Github。然后进入es安装目录的plugins目录下,新建一个文件夹ik,解压zip压缩包到ik目录下,最后重启es。

3 Kibana安装和启动

下载地址,解压即安装,要注意Kibana的版本要与ElasticSearch对应。

之后修改配置参数,更多配置详见官方文档

1
2
3
4
5
server.port: 5601  # kibana服务的端口
server.host: "127.0.0.1" # kibana服务的ip地址
server.name: Kibana
elasticsearch.hosts: ["http://localhost:9200/"] # 要连接的 Elasticsearch 地址
i18n.locale: "zh-CN" # 7.0版本后,配置中文界面

要通过Kibana连接es,首先进入到Kibana的bin路径下,执行启动文件

1
2
kibana.bat # windows
./kibana # linux

等待一会之后,打开浏览器输入以下地址:http://127.0.0.1:5601/ ,如果看到下图界面说明Kibana启动成功。

image-20211215025646972

要删除Kibana,直接删除该文件夹。

4 Elasticsearch-head安装和启动

Elasticsearch-head也是针对Elasticsearch的web前端,基于node.js。

安装nodejs:下载地址

下载地址:Github

1
2
3
4
git clone git://github.com/mobz/elasticsearch-head.git # 克隆代码
cd elasticsearch-head # 进入目录
npm install # 安装依赖
npm run start # 启动服务

打开浏览器输入地址:http://localhost:9100/ ,如果出现下图界面说明启动成功。

image-20211215032251357

点击连接,如果连接不进去说明存在跨域问题。打开Elasticsearch安装目录的config/elasticsearch.yml文件,按照如下配置:

1
2
http.cors.enabled: true
http.cors.allow-origin: "*"

然后重启Elasticsearch,再连接就可以成功了。

image-20211215032743424

三、索引操作

索引就相当于关系型数据库的database。

1 新建索引

直接向ES服务器发出 PUT 请求,新建索引:

1
2
3
4
5
6
7
8
9
PUT foo 
{
"settings": {
"index":{
"number_of_shards":5,
"number_of_replicas":1
}
}
}

上面代码创建一个名为foo的索引。其中,参数number_of_shards表示分片个数,默认是5个分片,创建后不可修改,number_of_replicas是每个分片创建几个副本,默认是1个副本,可以后续修改。

收到如下响应说明创建成功:

image-20211215234849765

2 查询索引

1
2
3
4
5
6
7
8
#查询foo索引的配置信息
GET foo/_settings
#获取所有索引的配置信息
GET _all/_settings
#或者
GET _settings
#获取foo和abc索引的配置信息
GET foo,abc/_settings

3 更新索引

1
2
3
4
5
#修改foo索引的副本数量为2
PUT foo/_settings
{
"number_of_replicas": 2
}

如果出现cluster_block_exception错误,这是由于ES新节点的数据目录data存储空间不足,导致从master主节点接收同步数据的时候失败,此时ES集群为了保护数据,会自动把索引分片index置为只读read-only。解决方法:磁盘扩容,或者放开索引只读设置

1
2
3
4
5
6
7
8
PUT  _all/_settings
{
"index": {
"blocks": {
"read_only_allow_delete": false
}
}
}

4 删除索引

1
2
#删除foo索引
DELETE foo

四、映射与文档操作

索引创建之后,等于有了关系型数据库中的database。Type相当于关系型数据库的表, 在5.x及以前创建的索引可以有多个Type, 在Elasticsearch 6.x以后创建一个索引只有一个Type,但是仍然兼容以前版本。7.x之后取消了索引type类型的设置,不允许指定类型,默认为_doc,并且不兼容旧版本多个Type。但字段仍然是有的,我们需要设置字段的约束信息,叫做字段映射(mapping)。

1 数据类型

string类型:text,keyword

数字类型:long,integer,short,byte,double,float

日期类型:date

布尔类型:boolean

binary类型:binary

复杂类型:object(实体,对象),nested(列表)

geo类型:geo-point,geo-shape(地理位置)

专业类型:ip,competion(搜索建议)

2 映射参数

属性 描述 适合类型
store 值为yes表示存储,no表示不存储,默认为no all
index yes表示分析,no表示不分析,默认为true text
null_value 如果字段为空,可以设置一个默认值,比如”NA”(传过来为空,不能搜索,na可以搜索) all
analyzer 可以设置索引和搜索时用的分析器,默认使用的是standard分析器,还可以使用whitespace,simple。都是英文分析器 all
include_in_all 默认es为每个文档定义一个特殊域_all,它的作用是让每个字段都被搜索到,如果想让某个字段不被搜索到,可以设置为false all
format 时间格式字符串模式 date

3 创建映射

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
PUT books
{
"mappings": {
"properties":{
"title":{
"type":"text",
"analyzer": "ik_max_word"
},
"price":{
"type":"integer"
},
"address":{
"type":"keyword"
},
"company":{
"properties":{
"name":{"type":"text"},
"company_addr":{"type":"text"},
"employee_count":{"type":"integer"}
}
},
"publish_date":{"type":"date","format":"yyy-MM-dd"}

}

}
}

我们处理中文一般会选择ik分词器ik_max_word

4 插入文档

可以向books/_doc/1插入一条数据(文档)

1
2
3
4
5
6
7
8
9
10
11
12
PUT books/_doc/1
{
"title":"红楼梦",
"price":50,
"address":"xxxxx",
"company":{
"name":"xxxxxx",
"company_addr":"xxxxxxxxx",
"employee_count":20
},
"publish_date":"2020-08-19"
}

image-20211216163743769

插入文档数据不一定要和映射对应,你也可以插入原来没有的字段,这样的话es会自动创建。所以映射可以不创建,插入文档时,会根据字段自动创建。

如果索引也不存在,在插入文档时同样会自动创建一个。比如直接用语句PUT book1111/_doc/1插入文档,会直接创建出book1111的索引,并且自动创建映射。

5 查询文档

查看全部:

1
2
3
4
#查看books索引的mapping
GET books/_mapping
#获取所有的mapping
GET _all/_mapping

查看某条:

1
2
3
4
5
6
7
8
#查询books索引下id为7的文档
GET books/_doc/7
#查询books索引下id为3的文档,只要title字段
GET books/_doc/3?_source=title
#查询books索引下id为6的文档,只要title和price字段
GET books/_doc/6?_source=title,price
#查询books索引下id为6的文档,要全部字段
GET books/_doc/6?_source

除了通过字段查询之外,还可以通过字符串查询。还是使用GET命令,通过_serarch查询,查询条件是price属性是50的书都有哪些。最后,别忘了_search后要加英文分隔符?

1
2
# 查询books中price=50的所有字段
GET books/_doc/_search?q=price:50

另外一种是通过DSL语句来进行查询,把?后面的字段,放在括号里查询:

1
2
3
4
5
6
7
8
GET books/_doc/_search
{
"query": {
"match": {
"price": 50
}
}
}

关于高级查询,后面单独一节介绍。

6 修改文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#直接覆盖修改 当执行`PUT`命令时,如果数据不存在,则新增该条数据,如果数据存在则修改该条数据。
PUT books/_doc/10
{
"title":"xxxx",
"price":333,
"publish_addr":{
"province":"黑龙江",
"city":"福州"
}
}

#`POST`命令局部修改,这里可用来执行修改操作(还有其他的功能),`POST`命令配合`_update`完成修改操作,指定修改的内容放到`doc`中,必须要放到`doc`里,不能直接修改
POST books/_update/1
{
"doc":{
"title":"aaaaa"
}
}

7 删除文档

1
2
# 删除books下id为1的数据
DELETE books/doc/1

五、高级查询

查询文档中,已经介绍了基本的查询。下面是高级查询:

1 DSL查询之match

DSL分为matchtrem两种模式。

1.1 match

在前面简单介绍了match的查询:

1
2
3
4
5
6
7
8
GET books/_doc/_search
{
"query": {
"match": {
"price": 50
}
}
}

将查询条件添加到match中即可,而match则是查询所有price字段的值中为50的结果。

1.2 match_all

除了按条件查询之外,我们还可以查询books索引下的doc类型中的所有文档,查询全部match_all

1
2
3
4
5
6
GET books/_doc/_search
{
"query": {
"match_all": {}
}
}

1.3 match_phrase

前面的查询中,我们只能获取文档中的关键字,首先创建一些示例:

1
2
3
4
5
6
7
8
9
10
11
12
PUT t1/_doc/1
{
"title": "中国是世界上人口最多的国家"
}
PUT t1/_doc/2
{
"title": "美国是世界上军事实力最强大的国家"
}
PUT t1/_doc/3
{
"title": "北京是中国的首都"
}

现在,当我们以中国作为搜索条件,我们希望只返回和中国相关的文档。我们首先来使用match查询:

1
2
3
4
5
6
7
8
GET t1/_doc/_search
{
"query": {
"match": {
"title": "中国"
}
}
}

image-20211216184304693

虽然如期的返回了中国的文档。但是却把和美国的文档也返回了,这并不是我们想要的。是怎么回事呢?因为这是elasticsearch在内部对文档做分词的时候,对于中文来说,就是一个字一个字分的,所以,我们搜中国都符合条件,返回,而美国的也符合。
而我们认为中国是个短语,是一个有具体含义的词。所以elasticsearch在处理中文分词方面比较弱势。我们可以用中文分词器来解决。

另外还有另一种办法解决,那就是使用短语查询:

1
2
3
4
5
6
7
8
9
10
GET t1/_doc/_search
{
"query": {
"match_phrase": {
"title": {
"query": "中国"
}
}
}
}

这里match_phrase是在文档中搜索指定的词组,而中国则正是一个词组,所以能获取到。

比如我们要想搜索中国世界相关的文档,但又忘记其余部分了,此时可以使用match_phrase指定分词之间的间隔:

1
2
3
4
5
6
7
8
9
10
11
GET t1/_doc/_search
{
"query": {
"match_phrase": {
"title": {
"query": "中国世界",
"slop": 2
}
}
}
}

这里的slop相当于正则中的中国.*?世界。这个间隔默认为0,如果不加会导致查询不到,指定为2就可以查到了。

1.4 match_phrase_prefix

凌晨2点半,单身狗小黑为了缓解寂寞,就准备搜索几个beautiful girl来陪伴自己。但是由于英语没过2级,但单词beautiful拼到bea就不知道往下怎么拼了。这个时候,我们的智能搜索要帮他啊,elasticsearch就看自己的词库有啥是bea开头的词,结果还真发现了两个:

1
2
3
4
5
6
7
8
9
10
PUT t3/_doc/1
{
"title": "maggie",
"desc": "beautiful girl you are beautiful so"
}
PUT t3/_doc/2
{
"title": "sun and beach",
"desc": "I like basking on the beach"
}

但这里用matchmatch_phrase都不太合适,因为小黑输入的不是完整的词。那怎么办呢?我们用match_phrase_prefix来搞:

1
2
3
4
5
6
7
8
GET t3/_doc/_search
{
"query": {
"match_phrase_prefix": {
"desc": "bea"
}
}
}

这样就搜索到了前面插入的两条数据。

前缀查询是短语查询类似,但前缀查询可以更进一步的搜索词组,只不过它是和词组中最后一个词条进行前缀匹配(如搜这样的you are bea)。应用也非常的广泛,比如搜索框的提示信息,当使用这种行为进行搜索时,最好通过max_expansions来设置最大的前缀扩展数量,因为产生的结果会是一个很大的集合,不加限制的话,影响查询性能。

1
2
3
4
5
6
7
8
9
10
11
GET t3/_doc/_search
{
"query": {
"match_phrase_prefix": {
"desc": {
"query": "bea",
"max_expansions": 1
}
}
}
}

我们只需要记住,使用前缀查询会非常的影响性能,要对结果集进行限制,就加上这个max_expansions参数。

1.5 multi_match

有时候需要多字段查询:现在,我们有一个50个字段的索引,我们要在多个字段中查询同一个关键字,该怎么做呢?

1
2
3
4
5
6
7
8
9
10
PUT t3/_doc/1
{
"title": "maggie is beautiful girl",
"desc": "beautiful girl you are beautiful so"
}
PUT t3/_doc/2
{
"title": "beautiful beach",
"desc": "I like basking on the beach,and you? beautiful girl"
}

我们先用原来的方法查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GET t3/_doc/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"title": "beautiful"
}
},
{
"match": {
"desc": "beautiful"
}
}
]
}
}
}

bool是布尔查询,后面会介绍。使用must来限制两个字段(值)中必须同时含有关键字。这样虽然能达到目的,但是当有很多的字段,我们可以用multi_match来做:

1
2
3
4
5
6
7
8
9
GET t3/_doc/_search
{
"query": {
"multi_match": {
"query": "beautiful",
"fields": ["title", "desc"]
}
}
}

我们将多个字段放到fields列表中即可。以达到匹配多个字段的目的。也就是在titledesc中查询beautiful关键字。

除此之外,multi_match甚至可以当做match_phrasematch_phrase_prefix使用,只需要指定type类型即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GET t3/_doc/_search
{
"query": {
"multi_match": {
"query": "gi",
"fields": ["title"],
"type": "phrase_prefix"
}
}
}
GET t3/_doc/_search
{
"query": {
"multi_match": {
"query": "girl",
"fields": ["title"],
"type": "phrase"
}
}
}

2 DSL查询之term

默认情况下,es在对文档分析期间(将文档分词后保存到倒排索引中),会对文档进行分词,比如默认的标准分析器会对文档进行:

  • 删除大多数的标点符号。
  • 将文档分解为单个词条,我们称为token。
  • 将token转为小写。

完事再保存到倒排索引上,当然,原文件还是要保存一份的,而倒排索引使用来查询的。

例如Beautiful girl!,在经过分析后是这样的了:

1
2
3
4
5
6
7
POST _analyze
{
"analyzer": "standard",
"text": "Beautiful girl!"
}
# 结果
["beautiful", "girl"]

而当在使用match查询时,elasticsearch同样会对查询关键字进行分析,

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
PUT w10
{
"mappings": {
"_doc":{
"properties":{
"t1":{
"type": "text"
}
}
}
}
}
PUT w10/_doc/1
{
"t1": "Beautiful girl!"
}
PUT w10/_doc/2
{
"t1": "sexy girl!"
}

GET w10/_doc/_search
{
"query": {
"match": {
"t1": "Beautiful girl!"
}
}
}

也就是对查询关键字Beautiful girl!进行分析,得到["beautiful", "girl"],然后分别将这两个单独的token去索引中进行查询,结果就是将两篇文档都返回。这在有些情况下是非常好用的,但是,如果我们想查询确切的词怎么办?也就是精确查询,将Beautiful girl!当成一个token而不是分词后的两个token。这就要用到了term查询了,term查询的是没有经过分析的查询关键字。

如果你要查询的字段类型(如上例中的字段t1类型是text)是text(因为elasticsearch会对文档进行分析,上面说过),那么你得到的可能是不尽如人意的结果或者压根没有结果:

1
2
3
4
5
6
7
8
GET w10/_doc/_search
{
"query": {
"term": {
"t1": "Beautiful girl!"
}
}
}

如上面的查询,将不会有结果返回,因为索引w10中的两篇文档在经过elasticsearch分析后没有一个分词是Beautiful girl!,那此次查询结果为空也就好理解了。

所以,我们这里得到一个论证结果:不要使用term对类型是text的字段进行查询,要查询text类型的字段,请改用match查询。

再来一个示例:

1
2
3
4
5
6
7
8
GET w10/_doc/_search
{
"query": {
"term": {
"t1": "Beautiful"
}
}
}

答案是,没有结果返回!因为elasticsearch在对文档进行分析时,会经过小写!人家倒排索引上存的是小写的beautiful,而我们查询的是大写的Beautiful

所以,想有结果要这样:

1
2
3
4
5
6
7
8
GET w10/_doc/_search
{
"query": {
"term": {
"t1": "beautiful"
}
}
}

那term查询可以查询哪些类型的字段呢,例如elasticsearch会将keyword类型的字段当成一个token保存到倒排索引上,你可以将term和keyword结合使用。

最后,要想使用term查询多个精确的值使用terms(term+s)查询:

1
2
3
4
5
6
7
8
GET w10/_doc/_search
{
"query": {
"terms": {
"t1": ["beautiful", "sexy"]
}
}
}

3 排序sort

3.1 降序desc

比如想要根据age字段按照降序排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET foo/_doc/_search
{
"query": {
"match": {
"title": "xxx"
}
},
"sort": [
{
"age": {
"order": "desc"
}
}
]
}

3.2 升序asc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET foo/_doc/_search
{
"query": {
"match": {
"title": "xxx"
}
},
"sort": [
{
"age": {
"order": "asc"
}
}
]
}

注意:在排序的过程中,只能使用可排序的属性进行排序。可以排序的属性有数字和日期,使用其它的都不行。

4 分页from/size

将结果分页:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET foo/_doc/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"age": {
"order": "desc"
}
}
],
"from": 2,
"size": 1
}

上例,首先以age降序排序,查询所有。并且在查询的时候,添加两个属性fromsize来控制查询结果集的数据条数。

  • from:从哪开始查
  • size:返回几条结果

你可以这样:from:2,size:2意为从第2条开始返回两条数据。

如果这样写:siez:0那么就返回0条结果。

学到这里,我们也可以看到,查询条件越来越多,开始仅是简单查询,慢慢增加条件查询,增加排序,对返回结果进行限制。所以,我们可以说:对于elasticsearch来说,所有的条件都是可插拔的,彼此之间用,分割。

5 布尔查询

布尔查询是最常用的组合查询,根据子查询的规则,只有当文档满足所有子查询条件时,elasticsearch引擎才将结果返回。布尔查询支持的子查询条件共4种:

  • must(and)
  • should(or)
  • must_not(not)
  • filter(条件过滤)

5.1 must

逻辑与(and)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GET foo/_doc/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"from": "gu"
}
}
]
}
}
}

上例中,我们通过在bool属性(字段)内使用must来作为查询条件,那么条件是什么呢?条件同样被match包围,就是fromgu的所有数据。这里需要注意的是must字段对应的是个列表[],也就是说可以有多个并列的查询条件,一个文档满足各个子条件后才最终返回。

比如想要查询fromgu,并且age30的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GET foo/_doc/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"from": "gu"
}
},
{
"match": {
"age": 30
}
}
]
}
}
}

5.2 should

逻辑或(or)。

那么,如果要查询只要是fromgu或者tags闭月的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GET foo/_doc/_search
{
"query": {
"bool": {
"should": [
{
"match": {
"from": "gu"
}
},
{
"match": {
"tags": "闭月"
}
}
]
}
}
}

5.3 must_not

逻辑非(not)。

查询from既不是gu并且tags也不是可爱,还有age不是18的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
GET foo/_doc/_search
{
"query": {
"bool": {
"must_not": [
{
"match": {
"from": "gu"
}
},
{
"match": {
"tags": "可爱"
}
},
{
"match": {
"age": 18
}
}
]
}
}
}

5.4 filter

查询fromguage大于25的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
GET foo/_doc/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"from": "gu"
}
}
],
"filter": {
"range": {
"age": {
"gt": 25
}
}
}
}
}
}

这里就用到了filter条件过滤查询,过滤条件的范围用range表示,gt表示大于。

过滤条件有:

  • gt:大于
  • lt:小于
  • gte:大于等于
  • lte:小于等于

想要查询一个范围内的数据,比如要查询fromguage25~30之间的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
GET foo/_doc/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"from": "gu"
}
}
],
"filter": {
"range": {
"age": {
"gte": 25,
"lte": 30
}
}
}
}
}
}

同时使用ltegte来限定范围。

另外,如果在filter过滤条件中使用should的话,结果可能不会尽如人意!建议使用must代替。并且注意filter工作于bool查询内。比如我们将刚才的查询条件改一下,把filterbool中挪出来,就会报错。

6 查询结果高亮显示

如果返回的结果集中很多符合条件的结果,我们想要让某些结果高亮显示。比如下面网站所示的那样,我们搜索elasticsearch,在结果集中,将所有elasticsearch高亮显示。

06119F24-7838-43D8-84EE-F20B929C16B7

我们这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
GET foo/_doc/_search
{
"query": {
"match": {
"name": "石头"
}
},
"highlight": {
"fields": {
"name": {}
}
}
}

使用highlight属性来实现结果高亮显示,需要的字段名称添加到fields内即可,es会自动将检索结果用标签包裹起来,用于在页面中渲染。返回的结果会变成这样

1
2
3
4
5
"highlight" : {
"name" : [
"<em>石</em><em>头</em>"
]
}

结果被<em></em>标签包裹。如果不想用em标签,也可以自定义标签。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET foo/_doc/_search
{
"query": {
"match": {
"from": "gu"
}
},
"highlight": {
"pre_tags": "<b class='key' style='color:red'>",
"post_tags": "</b>",
"fields": {
"from": {}
}
}
}

上例中,在highlight中,pre_tags用来实现我们的自定义标签的前半部分,在这里,我们也可以为自定义的标签添加属性和样式。post_tags实现标签的后半部分,组成一个完整的标签。至于标签中的内容,则还是交给fields来完成。

需要注意的是:自定义标签中属性或样式中的逗号一律用英文状态的单引号表示,应该与外部elasticsearch语法的双引号区分开。

7 查询结果过滤

对查询结果进行过滤,使用_source,比如只需要查看nameage两个属性,其他的不要:

1
2
3
4
5
6
7
8
9
GET foo/_doc/_search
{
"query": {
"match": {
"name": "xxx"
}
},
"_source": ["name", "age"]
}

8 聚合查询

聚合函数大家都不陌生,es中也没玩出新花样:

  • avg
  • max
  • min
  • sum

8.1 avg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GET foo/_doc/_search
{
"query": {
"match": {
"from": "gu"
}
},
"aggs": {
"my_avg": {
"avg": {
"field": "age"
}
}
},
"_source": ["name", "age"]
}

上例中,首先匹配查询fromgu的数据。在此基础上做查询平均值的操作,这里就用到了聚合函数,其语法被封装在aggs中,而my_avg则是为查询结果起个别名,封装了计算出的平均值。那么,要查age字段的平均值。最后对结果进行过滤,只返回nameage字段的数据以及年龄的平均值。

如果不想看都有哪些数据,只想看平均值,用size即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GET foo/_doc/_search
{
"query": {
"match": {
"from": "gu"
}
},
"aggs": {
"my_avg": {
"avg": {
"field": "age"
}
}
},
"size": 0,
"_source": ["name", "age"]
}

只需要在原来的查询基础上,增加一个size就可以了,我们写上0,就是输出0条查询结果。

8.2 max

查最大值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GET foo/_doc/_search
{
"query": {
"match": {
"from": "gu"
}
},
"aggs": {
"my_max": {
"max": {
"field": "age"
}
}
},
"size": 0
}

8.3 min

查最小值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GET foo/_doc/_search
{
"query": {
"match": {
"from": "gu"
}
},
"aggs": {
"my_min": {
"min": {
"field": "age"
}
}
},
"size": 0
}

8.4 sum

求总和:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GET foo/_doc/_search
{
"query": {
"match": {
"from": "gu"
}
},
"aggs": {
"my_sum": {
"sum": {
"field": "age"
}
}
},
"size": 0
}

8.5 分组查询

假如想要查询所有人的年龄段,并且按照15~20,20~25,25~30分组,并且算出每组的平均年龄。

首先做出分组:

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
GET foo/_doc/_search
{
"size": 0,
"query": {
"match_all": {}
},
"aggs": {
"age_group": {
"range": {
"field": "age",
"ranges": [
{
"from": 15,
"to": 20
},
{
"from": 20,
"to": 25
},
{
"from": 25,
"to": 30
}
]
}
}
}
}

上例中,在aggs的自定义别名age_group中,使用range来做分组,field是以age为分组,分组使用ranges来做,fromto是范围,我们根据需求做出三组。接下来,我们就要对每个小组内的数据做平均年龄处理。

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
GET foo/_doc/_search
{
"size": 0,
"query": {
"match_all": {}
},
"aggs": {
"age_group": {
"range": {
"field": "age",
"ranges": [
{
"from": 15,
"to": 20
},
{
"from": 20,
"to": 25
},
{
"from": 25,
"to": 30
}
]
},
"aggs": {
"my_avg": {
"avg": {
"field": "age"
}
}
}
}
}
}

上例中,在分组下面,我们使用aggsage做平均数处理,这样就可以了。

注意:一定要先查出结果,然后对结果使用聚合函数做处理。

六、IK分词器的使用

之前已经介绍了IK分词器的下载和安装,下面就来验证一下:

1
2
3
4
5
GET _analyze
{
"analyzer": "ik_max_word",
"text": "上海自来水来自海上"
}

如果返回如下数据就说明安装成功了:

image-20211216233947041

1 ik_max_word

ik_max_word参数会将文档做最细粒度的拆分,会穷尽各种可能的组合。

1
2
3
4
5
6
7
8
9
10
11
PUT ik1
{
"mappings": {
"properties":{
"title":{
"type":"text",
"analyzer": "ik_max_word"
}
}
}
}

我们创建一个索引名为ik1,指定使用ik_max_word分词器,然后插入几条数据:

1
2
3
4
5
6
7
8
9
10
11
12
PUT ik1/_doc/1
{
"content":"真开心今天吃了三顿饭"
}
PUT ik1/_doc/2
{
"content":"今天你也在烦恼吗?笑一笑吧"
}
PUT ik1/_doc/3
{
"content":"爱笑的人运气都不会太差"
}

现在让我们开始查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GET ik1/_search
{
"query": {
"match": {
"content": "今天"
}
}
}

GET ik1/_search
{
"query": {
"match": {
"content": "笑"
}
}
}

2 ik_smart

ik_smart是另一种分词方式,它将文档作粗粒度的拆分。比如,爱笑的人运气都不会太差这句话:

  • ik_max_word会拆分为:爱笑,的人,运气,都不会,都不,不会,太差
  • ik_smart会拆分为:爱笑,的人,运气,都不会,太差

由上面的对比可以发现,两个参数的不同,所以查询结果也肯定不一样,视情况而定用什么粒度。

至于查询,与之前介绍的查询方法完全一样。

3 ik目录简介

ik/config目录下有ik分词配置文件:

  • IKAnalyzer.cfg.xml,用来配置自定义的词库
  • main.dic,ik原生内置的中文词库,只要是这些单词,都会被分在一起。
  • surname.dic,中国的姓氏。
  • suffix.dic,特殊(后缀)名词,例如乡、江、所、省等等。
  • preposition.dic,中文介词,例如不、也、了、仍等等。
  • stopword.dic,英文停用词库,例如a、an、and、the等。
  • quantifier.dic,单位名词,如厘米、件、倍、像素等。
  • extra开头的文件,是额外的词库。

image-20211217005243429

4 扩展词库

ik/config目录下的IKAnalyzer.cfg.xml中可以扩展词库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict"></entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords"></entry>
<!--用户可以在这里配置远程扩展字典 -->
<!-- <entry key="remote_ext_dict">words_location</entry> -->
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>

比如我们添加一个词典,在config目录下新建一个new.dic

1
<entry key="ext_dict">new.dic</entry>

然后重启es,再使用分词器试试:

1
2
3
4
5
GET _analyze
{
"analyzer": "ik_smart",
"text": "奥利给干了兄弟们"
}

image-20211217010344962

注意词库的编码必须是utf-8

IK插件还支持热更新,在配置文件中的如下配置

1
2
3
4
<!--用户可以在这里配置远程扩展字典 -->
<!-- <entry key="remote_ext_dict">words_location</entry> -->
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->

其中 words_location 是指一个 url,比如 http://yoursite.com/getCustomDict,该请求只需满足以下两点即可完成分词热更新。

  1. 该 http 请求需要返回两个头部(header),一个是 Last-Modified,一个是 ETag,这两者都是字符串类型,只要有一个发生变化,该插件就会去抓取新的分词进而更新词库。
  2. 该 http 请求返回的内容格式是一行一个分词,换行符用 \n 即可。

满足上面两点要求就可以实现热更新分词了,不需要重启es 。

可以将需自动更新的热词放在一个 UTF-8 编码的 .txt 文件里,放在 nginx 或其他简易 http server 下,当 .txt 文件修改时,http server 会在客户端请求该文件时自动返回相应的 Last-Modified 和 ETag。可以另外做一个工具来从业务系统提取相关词汇,并更新这个 .txt文件。

七、Elasticsearch集群搭建

Elasticsearch搭建集群的方式有广播和单播,一般都是使用单播方式,需要我们在elasticsearch.yml配置文件中设置。

下面假设要搭建一个四个节点的集群,配置如下:

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
#1 elasticsearch1节点,,集群名称是my_es1,集群端口是9300;节点名称是node1,监听本地9200端口,可以有权限成为主节点和读写磁盘
cluster.name: my_es1
node.name: node1
network.host: 127.0.0.1
http.port: 9200
transport.tcp.port: 9300
discovery.zen.ping.unicast.hosts: ["127.0.0.1:9300", "127.0.0.1:9302", "127.0.0.1:9303", "127.0.0.1:9304"]

# 2 elasticsearch2节点,集群名称是my_es1,集群端口是9302;节点名称是node2,监听本地9202端口,可以有权限成为主节点和读写磁盘。

cluster.name: my_es1
node.name: node2
network.host: 127.0.0.1
http.port: 9202
transport.tcp.port: 9302
node.master: true
node.data: true
discovery.zen.ping.unicast.hosts: ["127.0.0.1:9300", "127.0.0.1:9302", "127.0.0.1:9303", "127.0.0.1:9304"]

# 3 elasticsearch3节点,集群名称是my_es1,集群端口是9303;节点名称是node3,监听本地9203端口,可以有权限成为主节点和读写磁盘。

cluster.name: my_es1
node.name: node3
network.host: 127.0.0.1
http.port: 9203
transport.tcp.port: 9303
discovery.zen.ping.unicast.hosts: ["127.0.0.1:9300", "127.0.0.1:9302", "127.0.0.1:9303", "127.0.0.1:9304"]

# 4 elasticsearch4节点,集群名称是my_es1,集群端口是9304;节点名称是node4,监听本地9204端口,仅能读写磁盘而不能被选举为主节点。

cluster.name: my_es1
node.name: node4
network.host: 127.0.0.1
http.port: 9204
transport.tcp.port: 9304
node.master: false
node.data: true
discovery.zen.ping.unicast.hosts: ["127.0.0.1:9300", "127.0.0.1:9302", "127.0.0.1:9303", "127.0.0.1:9304"]

由上例的配置可以看到,各节点有一个共同的名字my_es1,但由于是本地环境,所以各节点的名字不能一致,我们分别启动它们,它们通过单播列表相互介绍,发现彼此,然后组成一个my_es1集群。谁是主节点则是要看谁先启动了。

另外,我们需要设置主节点的最小数量以防止脑裂问题。一般的规则是集群节点数除以2(向下取整)再加一,我们这里就是4/2+1=3

1
discovery.zen.minimum_master_nodes: 3

八、Elasticsearch配置讲解

es有三个配置文件,都在es目录下的config目录下:

  • config/elasticsearch.yml 主配置文件

  • config/jvm.options jvm参数配置文件(一般不作调整)

  • cofnig/log4j2.properties 日志配置文件(一般不作调整)

平时主要配置的是主配置文件config/elasticsearch.yml

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# 配置es的集群名称,默认是elasticsearch,es会自动发现在同一网段下的es,如果在同一网段下有多个集群,就可以用这个属性来区分不同的集群。
cluster.name: elasticsearch

#节点名称,默认随机指定一个name列表中名字,该列表在es的jar包中config文件夹里name.txt文件中,其中有很多作者添加的有趣名字。
node.name: es1

# 指定该节点是否有资格被选举成为主节点,默认是true,es是默认集群中的第一台机器为master,如果这台机挂了就会重新选举master。
node.master: true

# 指定该节点是否存储索引数据,默认为true。
node.data: true

# 设置默认索引分片个数,默认为5片。
index.number_of_shards: 5

# 设置默认索引副本个数,默认为1个副本。
index.number_of_replicas: 1

# 设置配置文件的存储路径,默认是es根目录下的config文件夹。
path.conf: /path/to/conf

# 设置索引数据的存储路径,默认是es根目录下的data文件夹,可以设置多个存储路径,用逗号隔开,例:
# path.data: /path/to/data1,/path/to/data2
path.data: /path/to/data

# 设置临时文件的存储路径,默认是es根目录下的work文件夹。
path.work: /path/to/work

# 设置日志文件的存储路径,默认是es根目录下的logs文件夹,es采用log4j2记录日志
path.logs: /path/to/logs

# 设置插件的存放路径,默认是es根目录下的plugins文件夹
path.plugins: /path/to/plugins

# 设置为true来锁住内存。因为当jvm开始swapping时es的效率会降低,所以要保证它不swap,可以把ES_MIN_MEM和 ES_MAX_MEM两个环境变量设置成同一个值,并且保证机器有足够的内存分配给es。同时也要允许elasticsearch的进程可以锁住内存,linux下可以通过`ulimit -l unlimited`命令。
bootstrap.mlockall: true

# 设置绑定的ip地址,可以是ipv4或ipv6的,默认为0.0.0.0。
network.bind_host: 192.168.0.1

# 设置其它节点和该节点交互的ip地址,如果不设置它会自动判断,值必须是个真实的ip地址。
network.publish_host: 192.168.0.1

# 这个参数是用来同时设置bind_host和publish_host上面两个参数。
network.host: 192.168.0.1

# 设置节点间交互的tcp端口,默认是9300。
transport.tcp.port: 9300

# 设置是否压缩tcp传输时的数据,默认为false,不压缩。
transport.tcp.compress: true

# 设置对外服务的http端口,默认为9200。
http.port: 9200

# 设置内容的最大容量,默认100mb
http.max_content_length: 100mb

# 是否使用http协议对外提供服务,默认为true,开启。
http.enabled: false

# gateway的类型,默认为local即为本地文件系统,可以设置为本地文件系统,分布式文件系统,Hadoop的HDFS,和amazon的s3服务器。
gateway.type: local

# 设置集群中N个节点启动时进行数据恢复,默认为1。
gateway.recover_after_nodes: 1

# 设置初始化数据恢复进程的超时时间,默认是5分钟。
gateway.recover_after_time: 5m

# 设置这个集群中节点的数量,默认为2,一旦这N个节点启动,就会立即进行数据恢复
# 将master节点(有master资格的节点)和data节点都算在内
gateway.expected_nodes: 2

# 初始化数据恢复时,并发恢复线程的个数,默认为4。
cluster.routing.allocation.node_initial_primaries_recoveries: 4

# 添加删除节点或负载均衡时并发恢复线程的个数,默认为4。
cluster.routing.allocation.node_concurrent_recoveries: 2

# 设置数据恢复时限制的带宽,如入100mb,默认为0,即无限制。
indices.recovery.max_size_per_sec: 0

# 设置这个参数来限制从其它分片恢复数据时最大同时打开并发流的个数,默认为5。
indices.recovery.concurrent_streams: 5

# 设置这个参数来保证集群中的节点可以知道其它N个有master资格的节点。默认为1,一般的规则是集群节点数除以2(向下取整)再加一。比如3个节点集群要设置为2,这样设置是为了防止脑裂问题。
discovery.zen.minimum_master_nodes: 1

# 设置集群中自动发现其它节点时ping连接超时时间,默认为3秒,对于比较差的网络环境可以高点的值来防止自动发现时出错。
discovery.zen.ping.timeout: 3s

# 设置是否打开多播发现节点,默认是true。
discovery.zen.ping.multicast.enabled: false

# 设置集群中master节点的初始列表,可以通过这些节点来自动发现新加入集群的节点。
# 举例:discovery.zen.ping.unicast.hosts: ["10.0.0.1", "10.0.0.3:9300", "10.0.0.6[9300-9400]"]
discovery.zen.ping.unicast.hosts: ["host1", "host2:port", "host3[portX-portY]"]

# 下面是一些查询时的慢日志参数设置
index.search.slowlog.level: TRACE
index.search.slowlog.threshold.query.warn: 10s
index.search.slowlog.threshold.query.info: 5s
index.search.slowlog.threshold.query.debug: 2s
index.search.slowlog.threshold.query.trace: 500ms
index.search.slowlog.threshold.fetch.warn: 1s
index.search.slowlog.threshold.fetch.info: 800ms
index.search.slowlog.threshold.fetch.debug:500ms
index.search.slowlog.threshold.fetch.trace: 200ms

九、脑裂

1 解决脑裂问题

脑裂这个词描述的是这样的一个场景:(通常是在重负荷或网络存在问题时)elasticsearch集群中一个或者多个节点失去和主节点的通信,然后各节点就开始选举新的主节点,继续处理请求。这个时候,可能有两个不同的集群在相互运行着,这就是脑裂一词的由来,因为单一集群被分成了两部分。为了防止这种情况的发生,我们就需要设置集群节点的总数,规则就是节点总数除以2再加一(半数以上)。这样,当一个或者多个节点失去通信,小老弟们就无法选举出新的主节点来形成新的集群。因为这些小老弟们无法满足设置的规则数量。
我们通过下图来说明如何防止脑裂。比如现在,有这样一个5个节点的集群,并且都有资格成为主节点:

img

为了防止脑裂,我们对该集群设置参数:

1
discovery.zen.minimum_master_nodes: 3   # 3=5/2+1

之前原集群的主节点是node1,由于网络和负荷等原因,原集群被分为了两个switchnode1 、2node3、4、5。因为minimum_master_nodes参数是3,所以node3、4、5可以组成集群,并且选举出了主节点node3。而node1、2节点因为不满足minimum_master_nodes条件而无法选举,只能一直寻求加入集群,要么网络和负荷恢复正常后加入node3、4、5组成的集群中,要么就是一直处于寻找集群状态,这样就防止了集群的脑裂问题。
除了设置minimum_master_nodes参数,有时候还需要设置node_master参数,比如有两个节点的集群,如果出现脑裂问题,那么它们自己都无法选举,因为都不符合半数以上。这时我们可以指定node_master,让其中一个节点有资格成为主节点,另外一个节点只能做存储用。当然这是特殊情况。

那么,主节点是如何知道某个节点还活着呢?这就要说到错误识别了。

2 错误识别

其实错误识别,就是当主节点被确定后,建立起内部的ping机制来确保每个节点在集群中保持活跃和健康,这就是错误识别。主节点ping集群中的其他节点,而且每个节点也会ping主节点来确认主节点还活着,如果没有响应,则宣布该节点失联。想象一下,老大要时不常的看看(循环)小弟们是否还活着,而小老弟们也要时不常的看看老大还在不在,不在了就赶紧再选举一个出来!

img

但是,多久没联系算是失联?这些细节都是可以设置的,不是一拍脑门子,就说某个小老弟挂了。在配置文件中,可以设置:

1
2
3
discovery.zen.fd.ping_interval: 1
discovery.zen.fd.ping_timeout: 30
discovery_zen.fd.ping_retries: 3

每个节点每隔discovery.zen.fd.ping_interval的时间(默认1秒)发送一个ping请求,等待discovery.zen.fd.ping_timeout的时间(默认30秒),并尝试最多discovery.zen.fd.ping_retries次(默认3次),无果的话,宣布节点失联,并且在需要的时候进行新的分片和主节点选举。根据开发环境,适当修改这些值。

十、recovery

在elasticsearch中,recovery指的是一个索引的分片分配到另外一个节点的过程,一般在快照恢复、索引复制分片的变更、节点故障或重启时发生,由于master节点保存整个集群相关的状态信息,因此可以判断哪些分片需要再分配及分配到哪个节点,例如:

  • 如果某个主分片在,而复制分片所在的节点挂掉了,那么master需要另行选择一个可用节点,将这个主分片的复制分片分配到可用节点上,然后进行主从分片的数据复制。
    如果某个主分片所在的节点挂掉了,复制分片还在,那么master会主导将复制分片升级为主分片,然后再做主从分片数据复制。
  • 如果某个分片的主副分片都挂掉了,则暂时无法恢复,而是要等持有相关数据的节点重新加入集群后,master才能主持数据恢复相关操作。

但是,recovery过程要消耗额外的资源,CPU、内存、节点间的网络带宽等。可能导致集群的服务性能下降,甚至部分功能暂时无法使用,所以,有必要了解在recovery的过程和其相关的配置,来减少不必要的消耗和问题。

1 减少集群full restart造成的数据来回拷贝

有时候,可能会遇到es集群整体重启的情况,比如硬件升级、不可抗力的意外等,那么再次重启集群会带来一个问题:某些节点优先起来,并优先选举出了主节点,有了主节点,该主节点会立刻主持recovery的过程。但此时,这个集群数据还不完整(还有其他的节点没有起来),例如A节点的主分片对应的复制分片所在的B节点还没起来,但主节点会将A节点的几个没有复制分片的主分片重新拷贝到可用的C节点上。而当B节点成功起来了,自检时发现在自己节点存储的A节点主分片对应的复制分片已经在C节点上出现了,就会直接删除自己节点中“失效”的数据(A节点的那几个复制分片),这种情况很可能频繁出现在有多个节点的集群中。而当整个集群恢复后,其各个节点的数据分布,显然是不均衡的(先启动的节点把数据恢复了,后起来的节点内删除了无效的数据),这时,master就会触发Rebalance的过程,将数据在各个节点之间挪动,这个过程又消耗了大量的网络流量。所以,我们需要合理的设置recovery相关参数来优化recovery过程。

在配置文件中:

  • 在集群启动过程中,一旦有了多少个节点成功启动,就执行recovery过程,这个命令将master节点(有master资格的节点)和data节点都算在内。
1
gateway.expected_nodes: 3
  • 有几个master节点启动成功,就执行recovery的过程。
1
gateway.expected_master_nodes: 3
  • 有几个data节点启动成功,就执行recovery的过程。
1
gateway.expected_data_nodes: 3

当集群在期待的节点数条件满足之前,recovery过程会等待gateway.recover_after_time指定的时间,一旦等待超时,则会根据以下条件判断是否执行recovery的过程:

1
2
3
gateway.recover_after_nodes: 3    # 3个节点(master和data节点都算)启动成功
gateway.recover_after_master_nodes: 3 # 3个有master资格的节点启动成功
gateway.recover_after_data_nodes: 3 # 3个有data资格的节点启动成功

上面三个配置满足一个就会执行recovery的过程。
如果有以下配置的集群:

1
2
3
gateway.expected_data_nodes: 10
gateway.recover_after_time: 5m
gateway.recover_after_data_nodes: 8

此时的集群在5分钟内,有10个data节点都加入集群,或者5分钟后有8个以上的data节点加入集群,都会启动recovery的过程。

2 减少主副本之间的数据复制

如果不是full restart,而是重启单个节点,也会造成不同节点之间来复制,为了避免这个问题,可以在重启之前,关闭集群的shard allocation

1
2
3
4
5
6
PUT _cluster/settings
{
"transient": {
"cluster.routing.allocation.enable":"none"
}
}

当节点重启后,再重新打开:

1
2
3
4
5
6
PUT _cluster/settings
{
"transient": {
"cluster.routing.allocation.enable":"all"
}
}

这样,节点重启后,尽可能的从本节点直接恢复数据。但是在es1.6版本之前,即使做了以上措施,仍然会出现大量主副分片之间的数据拷贝,从面上看,这点让人很不理解,主副分片数据是完全一致的,在节点重启后,直接从本节点的副本重恢复数据就好了呀,为什么还要再从主分片再复制一遍呢?原因是在于recovery是简单的对比主副分片的segment file(分段文件)来判断哪些数据一致是可以本地恢复,哪些不一致的需要重新拷贝的。而不同节点的segment file是完全独立运行的,这可能导致主副本merge的深度不完全一致,从而造成即使文档集完全一样,而产生的segment file却不完全一样。

为了解决这个问题,在es1.6版本之后,加入了synced flush(同步刷新)新特性,对于5分钟没有更新过的shard,会自动synced flush一下,其实就是为对应的shard加入一个synced flush id,这样在节点重启后,先对比主副shard的synced flush id,就可以知道两个shard是否完全相同,避免了不必要的segment file拷贝。
需要注意的是synced flush只对冷索引有效,对于热索引(5分钟内有更新的索引)无效,如果重启的节点包含有热索引,那还是免不了大量的拷贝。如果要重启一个包含大量热索引的节点,可以按照以下步骤执行重启过程,可以让recovery过程瞬间完成:

  • 暂停数据写入
  • 关闭集群的shard allocation
  • 手动执行 POST /_flush/synced
  • 重启节点
  • 重新开启集群的shard allocation
  • 等待recovery完成,当集群的health status是green后
  • 重新开启数据写入

3 特大热索引为何恢复慢

对于冷索引,由于数据不再更新(对于elasticsearch来说,5分钟,很久了),利用synced flush可以快速的从本地恢复数据,而对于热索引,特别是shard很大的热索引,除了synced flush派不上用场,从而需要大量跨节点拷贝segment file以外,translog recovery可能是导致慢的更重要的原因。
我们来研究下这个translog recovery是什么鬼!
当节点重启后,从主分片恢复数据到复制分片需要经历3个阶段:

  • 第一阶段,对于主分片上的segment file做一个快照,然后拷贝到复制分片所在的节点,在数据拷贝期间,不会阻塞索引请求,新增的索引操作会记录到translog中(理解为于临时文件)。
  • 第二阶段,对于translog做一个快照,此快照包含第一阶段新增的索引请求,然后重放快照里的索引操作,这个阶段仍然不会阻塞索引请求,新增索引操作记录到translog中。
  • 第三阶段,为了能达到主副分片完全同步,阻塞新索引请求,然后重放上一阶段新增的translog操作。

由此可见,在recovery过程完成之前,translog是不能被清除掉的。如果shard比较大,第一阶段会耗时很长,会导致此阶段产生的translog很大,重放translog要比简单的文件拷贝耗时更长,因此第二阶段的translog耗时也显著的增加了。等到了第三阶段,需要重放的translog可能会比第二阶段更多。要命的是,第三阶段是会阻塞新索引(写入)请求的,在对写入实时性要求很高的场合,这就会导致性能下降,非常影响用户体验。因此,要加快特大热索引恢复速度,最好是参照上一节中的方式:

  • 暂停数据写入
  • 手动执行 POST /_flush/synced
  • 等待数据恢复完成后
  • 重新恢复数据写入

这样就会把数据延迟影响降到最低。

万一遇到Recovery慢,想知道进度怎么办呢? CAT Recovery API可以显示详细的recovery各个阶段的状态。 这个API怎么用就不在这里赘述了,参考:CAT Recovery

4 其他设置

还有其他一些专家级的设置(参见: recovery)可以影响recovery的速度,但提升速度的代价是更多的资源消耗,因此在生产集群上调整这些参数需要结合实际情况谨慎调整,一旦影响应用要立即调整回来。 对于搜索并发量要求高,延迟要求低的场合,默认设置一般就不要去动了。 对于日志实时分析类对于搜索延迟要求不高,但对于数据写入延迟期望比较低的场合,可以适当调大indices.recovery.max_bytes_per_sec,提升recovery速度,减少数据写入被阻塞的时长。

十一、打分机制

确定文档和查询有多么相关的过程被称为打分(scoring)。一个例子就是搜索引擎搜索结果的排行,从上到下的词条分数也从高到低。

1 文档打分的运作机制:TF-IDF

Lucenees的打分机制是一个公式。将查询作为输入,使用不同的手段来确定每一篇文档的得分,将每一个因素最后通过公式综合起来,返回该文档的最终得分。这个综合考量的过程,就是我们希望相关的文档被优先返回的考量过程。在Lucenees中这种相关性称为得分。

在开始计算得分之前,es使用了被搜索词条的频率和它有多常见来影响得分,从两个方面理解:

  • 一个词条在某篇文档中出现的次数越多,该文档就越相关
  • 一个词条如果在不同的文档中出现的次数越多,它就越不相关

我们称之为TF-IDFTF是词频(term frequency),而IDF是逆文档频率(inverse document frequency)。

1.1 词频:TF

考虑一篇文档得分的首要方式,是查看一个词条在文档中出现的次数,比如某篇文章围绕es的打分展开的,那么文章中肯定会多次出现相关字眼,当查询时,我们认为该篇文档更符合,所以,这篇文档的得分会更高。
可以Ctrl + f搜一下相关的关键词(es,得分、打分)之类的试试。

1.2 逆文档频率:IDF

相对于词频,逆文档频率稍显复杂,如果一个词条在索引中的不同文档中出现的次数越多,那么它就越不重要。
来个例子,示例地址

1
2
3
The rules-which require employees to work from 9 am to 9 pm
In the weeks that followed the creation of 996.ICU in March
The 996.ICU page was soon blocked on multiple platforms including the messaging tool WeChat and the UC Browser.

假如es索引中,有上述3篇文档:

  • 词条ICU的文档频率是2,因为它出现在2篇文档中,文档的逆源自得分乘以1/DFDF是该词条的文档频率,这就意味着,由于ICU词条拥有更高的文档频率,所以,它的权重会降低。
  • 词条the的文档频率是3,它在3篇文档中都出现了,注意:尽管the在后两篇文档出都出现两次,但是它的词频是还是3,因为,逆文档词频只检查词条是否出现在某篇文档中,而不检查它在这篇文档中出现了多少次,那是词频该干的事儿。

逆文档词频是一个重要的因素,用来平衡词条的词频。比如我们搜索the 996.ICU。单词the几乎出现在所有的文档中(中文中比如),如果这个鬼东西要不被均衡一下,那么the的频率将完全淹没996.ICU。所以,逆文档词频就有效的均衡了the这个常见词的相关性影响。以达到实际的相关性得分将会对查询的词条有一个更准确地描述。
当词频和逆文档词频计算完成。就可以使用TF-IDF公式来计算文档的得分了。

2 Lucene评分公式

Lucene默认评分公式被称为TF-IDF,一个基于词频和逆文档词频的公式。Lucene实用评分公式如下:

img

词条的词频越高,得分越高;相似地,索引中词条越罕见,逆文档频率越高,其中再加上调和因子和查询标准化,调和因子考虑了搜索过多少文档以及发现了多少词条;查询标准化,是试图让不同的查询结果具有可比性,这显然很困难。

我们称这种默认的打分方法是TF-IDF和向量空间模型(vector space model)的结合。

3 其他的打分方法

除了TF-IDF结合向量空间模型的实用评分模式,是esLucene最为主流的评分机制,但这并不是唯一的,除了TF-IDF这种实用模型之外,其他的模型包括:

  • Okapi BM25。
  • 随机性分歧(Divergence from randomness),即DFR相似度。
  • LM Dirichlet相似度。
  • LM Jelinek Mercer相似度。

这里简要的介绍BM25几种主要设置,即k1bdiscount_overlaps

  • k1和b是数值的设置,用于调整得分是如何计算的。
  • k1控制对于得分而言词频(TF)的重要性。
  • b是介于0 ~ 1之间的数值,它控制了文档篇幅对于得分的影响程度。
  • 默认情况下,k1设置为1.2,而b则被设置为0.75
  • discount_overlaps的设置用于告诉es,在某个字段中,多少个分词出现在同一位置,是否应该影响长度的标准化,默认值是true

4 配置打分模型

4.1 简要配置BM25打分模型

BM25是一种基于概率的打分框架。我们来简要配置一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
PUT w2
{
"mappings": {
"doc": {
"properties": {
"title": {
"type": "text",
"similarity": "BM25"
}
}
}
}
}

上例是通过similarity参数来指定打分模型。

4.2 为BM25配置高级的settings

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
PUT w3
{
"settings": {
"index": {
"analysis": {
"analyzer":"ik_smart"
}
},
"similarity": {
"my_custom_similarity": {
"type": "BM25",
"k1": 1.2,
"b": 0.75,
"discount_overlaps": false
}
}
},
"mappings": {
"doc": {
"properties": {
"title": {
"type": "text",
"similarity":"my_custom_similarity"
}
}
}
}
}

PUT w3/doc/1
{
"title":"The rules-which require employees to work from 9 am to 9 pm"
}

PUT w3/doc/2
{
"title":"In the weeks that followed the creation of 996.ICU in March"
}

PUT w3/doc/3
{
"title":"The 996.ICU page was soon blocked on multiple platforms including the messaging tool WeChat and the UC Browser."
}

GET w3/doc/_search
{
"query": {
"match": {
"title": "the 996"
}
}
}

4.3 配置全局打分模型

如果我们要使用某种特定的打分模型,并且希望应用到全局,那么就在elasticsearch.yml配置文件中加入:

1
index.similarity.default.type: BM25

5 使用“explain”来理解文档是如何评分的

es中,一个文档要比另一个文档更符合某个查询很可能跟我们想象的不太一样。这一小节,我们来研究下esLucene内部使用了怎样的公式来计算得分。我们通过explain=true来让es解释一下为什么这个得分是这样的?

比如我们来查询:

1
2
3
4
5
6
7
8
9
10
11
GET py1/doc/_search
{
"query": {
"match": {
"title": "北京"
}
},
"explain": true,
"_source": "title",
"size": 1
}

由于结果太长,我们这里对结果进行了过滤("size": 1返回一篇文档),只查看指定的字段("_source": "title"只返回title字段)。看结果:

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 24,
"max_score" : 4.9223156,
"hits" : [
{
"_shard" : "[py1][1]",
"_node" : "NRwiP9PLRFCTJA7w3H9eqA",
"_index" : "py1",
"_type" : "doc",
"_id" : "NIjS1mkBuoj17MYtV-dX",
"_score" : 4.9223156,
"_source" : {
"title" : "大写的尴尬 插混为啥在北京不受待见?"
},
"_explanation" : {
"value" : 4.9223156,
"description" : "weight(title:北京 in 36) [PerFieldSimilarity], result of:",
"details" : [
{
"value" : 4.9223156,
"description" : "score(doc=36,freq=1.0 = termFreq=1.0\n), product of:",
"details" : [
{
"value" : 4.562031,
"description" : "idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:",
"details" : [
{
"value" : 4.0,
"description" : "docFreq",
"details" : [ ]
},
{
"value" : 430.0,
"description" : "docCount",
"details" : [ ]
}
]
},
{
"value" : 1.0789746,
"description" : "tfNorm, computed as (freq * (k1 + 1)) / (freq + k1 * (1 - b + b * fieldLength / avgFieldLength)) from:",
"details" : [
{
"value" : 1.0,
"description" : "termFreq=1.0",
"details" : [ ]
},
{
"value" : 1.2,
"description" : "parameter k1",
"details" : [ ]
},
{
"value" : 0.75,
"description" : "parameter b",
"details" : [ ]
},
{
"value" : 12.1790695,
"description" : "avgFieldLength",
"details" : [ ]
},
{
"value" : 10.0,
"description" : "fieldLength",
"details" : [ ]
}
]
}
]
}
]
}
}
]
}
}

在新增的_explanation字段中,可以看到value值是4.9223156,那么是怎么算出来的呢?
来分析,分词北京在描述字段(title)出现了1次,所以TF的综合得分经过"description" : "tfNorm, computed as (freq * (k1 + 1)) / (freq + k1 * (1 - b + b * fieldLength / avgFieldLength)) from:"计算,得分是1.0789746
那么逆文档词频呢?根据"description" : "idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:"计算得分是4.562031
所以最终得分是:

1
1.0789746 * 4.562031 = 4.9223155734126

结果在四舍五入后就是4.9223156
需要注意的是,explain的特性会给es带来额外的性能开销。所以,除了在调试时可以使用,生产环境下,应避免使用explain

十二、其他语言连接ES

1 python

Elasticsearch:官方提供版本

Elasticsearch DSL:在官方基础上封装的版本

2 Go

Elasticsearch:官方提供版本

3 JavaScript

Elasticsearch:官方提供版本

4 其它

官方提供的Client API:https://www.elastic.co/guide/en/elasticsearch/client/index.html

十三、倒排索引

随着央视诗词大会的热播,小史开始对诗词感兴趣,最喜欢的就是飞花令的环节。

img

但是由于小史很久没有背过诗词了,飞一个字很难说出一句,很多之前很熟悉的诗句也想不起来。

img

img

img

img

img

img

img

img

img

img

倒排索引

img

img

img

img

img

img

吕老师:但是我让你说出带“前”字的诗句,由于没有索引,你只能遍历脑海中所有诗词,当你的脑海中诗词量大的时候,就很难在短时间内得到结果了。

img

img

img

img

img

img

img

索引量爆炸

img

img

img

img

img

img

img

img

img

img

img

img

img

img

img

img

img

img

img

搜索引擎原理

img

img

img

img

img

img

img

img

img

img

img

img

img

img

img

Elasticsearch 简介

img

img

img

img

img

吕老师:但是 Lucene 还是一个库,必须要懂一点搜索引擎原理的人才能用的好,所以后来又有人基于 Lucene 进行封装,写出了 Elasticsearch。

img

img

img

img

img

img

Elasticsearch 基本概念

img

img

img

img

img

吕老师:类型是用来定义数据结构的,你可以认为是 MySQL 中的一张表。文档就是最终的数据了,你可以认为一个文档就是一条记录。

img

img

img

吕老师:比如一首诗,有诗题、作者、朝代、字数、诗内容等字段,那么首先,我们可以建立一个名叫 Poems 的索引,然后创建一个名叫 Poem 的类型,类型是通过 Mapping 来定义每个字段的类型。

比如诗题、作者、朝代都是 Keyword 类型,诗内容是 Text 类型,而字数是 Integer 类型,最后就是把数据组织成 Json 格式存放进去了。

img

img

img

吕老师:这个问题问得好,这涉及到分词的问题,Keyword 类型是不会分词的,直接根据字符串内容建立反向索引,Text 类型在存入 Elasticsearch 的时候,会先分词,然后根据分词后的内容建立反向索引。

img

img

吕老师:之前我们说过,Elasticsearch 把操作都封装成了 HTTP 的 API,我们只要给 Elasticsearch 发送 HTTP 请求就行。

img

Elasticsearch 分布式原理

img

img

吕老师:没错,Elasticsearch 也是会对数据进行切分,同时每一个分片会保存多个副本,其原因和 HDFS 是一样的,都是为了保证分布式环境下的高可用。

img

img

img

吕老师:没错,在 Elasticsearch 中,节点是对等的,节点间会通过自己的一些规则选取集群的 Master,Master 会负责集群状态信息的改变,并同步给其他节点。

img

img

img

img

img

吕老师:注意,只有建立索引和类型需要经过 Master,数据的写入有一个简单的 Routing 规则,可以 Route 到集群中的任意节点,所以数据写入压力是分散在整个集群的。

img

ELK 系统

img

img

吕老师:其实很多公司都用 Elasticsearch 搭建 ELK 系统,也就是日志分析系统。其中 E 就是 Elasticsearch,L 是 Logstash,是一个日志收集系统,K 是 Kibana,是一个数据可视化平台。

img

img

img

吕老师:分析日志的用处可大了,你想,假如一个分布式系统有 1000 台机器,系统出现故障时,我要看下日志,还得一台一台登录上去查看,是不是非常麻烦?

img

img

吕老师:但是如果日志接入了 ELK 系统就不一样。比如系统运行过程中,突然出现了异常,在日志中就能及时反馈,日志进入 ELK 系统中,我们直接在 Kibana 就能看到日志情况。如果再接入一些实时计算模块,还能做实时报警功能。

img

img

img

小史学完了 Elasticsearch,在笔记本上写下了如下记录:

  • 反向索引又叫倒排索引,是根据文章内容中的关键字建立索引。
  • 搜索引擎原理就是建立反向索引。
  • Elasticsearch 在 Lucene 的基础上进行封装,实现了分布式搜索引擎。
  • Elasticsearch 中的索引、类型和文档的概念比较重要,类似于 MySQL 中的数据库、表和行。
  • Elasticsearch 也是 Master-slave 架构,也实现了数据的分片和备份。
  • Elasticsearch 一个典型应用就是 ELK 日志分析系统。

写完,又高高兴兴背诗去了。

十四、Elasticsearch分析流程

1 前言

现在,我们已经了解了如何建立索引和搜索数据了。
那么,是时候来探索背后的故事了!当数据传递到elasticsearch后,到底发生了什么?

2 分析过程

当数据被发送到elasticsearch后并加入到倒排索引之前,elasticsearch会对该文档的进行一系列的处理步骤:

  • 字符过滤:使用字符过滤器转变字符。
  • 文本切分为分词:将文本(档)分为单个或多个分词。
  • 分词过滤:使用分词过滤器转变每个分词。
  • 分词索引:最终将分词存储在Lucene倒排索引中。

整体流程如下图所示:

image-20200310003447410

接下来,我们简要的介绍elasticsearch中的分析器、分词器和分词过滤器。它们配置简单,灵活好用,我们可以通过不同的组合来获取我们想要的分词。

是的,无论多么复杂的分析过程,都是为了获取更加人性化的分词!接下来,我们来看看其中,在整个分析过程的各个组件吧。

3 分析器

在elasticsearch中,一个分析器可以包括:

  • 可选的字符过滤器
  • 一个分词器
  • 0个或多个分词过滤器

接下来简要的介绍各内置分词的大致情况。在介绍之前,为了方便演示。如果你已经安装了ik analysis,现在请暂时将该插件移出plugins目录。

3.1 标准分析器:standard analyzer

标准分析器(standard analyzer):是elasticsearch的默认分析器,该分析器综合了大多数欧洲语言来说合理的默认模块,包括标准分词器、标准分词过滤器、小写转换分词过滤器和停用词分词过滤器。

1
2
3
4
5
POST _analyze
{
"analyzer": "standard",
"text":"To be or not to be, That is a question ———— 莎士比亚"
}

分词结果如下:

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
{
"tokens" : [
{
"token" : "to",
"start_offset" : 0,
"end_offset" : 2,
"type" : "<ALPHANUM>",
"position" : 0
},
{
"token" : "be",
"start_offset" : 3,
"end_offset" : 5,
"type" : "<ALPHANUM>",
"position" : 1
},
{
"token" : "or",
"start_offset" : 6,
"end_offset" : 8,
"type" : "<ALPHANUM>",
"position" : 2
},
{
"token" : "not",
"start_offset" : 9,
"end_offset" : 12,
"type" : "<ALPHANUM>",
"position" : 3
},
{
"token" : "to",
"start_offset" : 13,
"end_offset" : 15,
"type" : "<ALPHANUM>",
"position" : 4
},
{
"token" : "be",
"start_offset" : 16,
"end_offset" : 18,
"type" : "<ALPHANUM>",
"position" : 5
},
{
"token" : "that",
"start_offset" : 21,
"end_offset" : 25,
"type" : "<ALPHANUM>",
"position" : 6
},
{
"token" : "is",
"start_offset" : 26,
"end_offset" : 28,
"type" : "<ALPHANUM>",
"position" : 7
},
{
"token" : "a",
"start_offset" : 29,
"end_offset" : 30,
"type" : "<ALPHANUM>",
"position" : 8
},
{
"token" : "question",
"start_offset" : 31,
"end_offset" : 39,
"type" : "<ALPHANUM>",
"position" : 9
},
{
"token" : "莎",
"start_offset" : 45,
"end_offset" : 46,
"type" : "<IDEOGRAPHIC>",
"position" : 10
},
{
"token" : "士",
"start_offset" : 46,
"end_offset" : 47,
"type" : "<IDEOGRAPHIC>",
"position" : 11
},
{
"token" : "比",
"start_offset" : 47,
"end_offset" : 48,
"type" : "<IDEOGRAPHIC>",
"position" : 12
},
{
"token" : "亚",
"start_offset" : 48,
"end_offset" : 49,
"type" : "<IDEOGRAPHIC>",
"position" : 13
}
]
}

3.2 简单分析器:simple analyzer

简单分析器(simple analyzer):简单分析器仅使用了小写转换分词,这意味着在非字母处进行分词,并将分词自动转换为小写。这个分词器对于亚种语言来说效果不佳,因为亚洲语言不是根据空白来分词的,所以一般用于欧洲言中。

1
2
3
4
5
POST _analyze
{
"analyzer": "simple",
"text":"To be or not to be, That is a question ———— 莎士比亚"
}

分词结果如下:

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
{
"tokens" : [
{
"token" : "to",
"start_offset" : 0,
"end_offset" : 2,
"type" : "word",
"position" : 0
},
{
"token" : "be",
"start_offset" : 3,
"end_offset" : 5,
"type" : "word",
"position" : 1
},
{
"token" : "or",
"start_offset" : 6,
"end_offset" : 8,
"type" : "word",
"position" : 2
},
{
"token" : "not",
"start_offset" : 9,
"end_offset" : 12,
"type" : "word",
"position" : 3
},
{
"token" : "to",
"start_offset" : 13,
"end_offset" : 15,
"type" : "word",
"position" : 4
},
{
"token" : "be",
"start_offset" : 16,
"end_offset" : 18,
"type" : "word",
"position" : 5
},
{
"token" : "that",
"start_offset" : 21,
"end_offset" : 25,
"type" : "word",
"position" : 6
},
{
"token" : "is",
"start_offset" : 26,
"end_offset" : 28,
"type" : "word",
"position" : 7
},
{
"token" : "a",
"start_offset" : 29,
"end_offset" : 30,
"type" : "word",
"position" : 8
},
{
"token" : "question",
"start_offset" : 31,
"end_offset" : 39,
"type" : "word",
"position" : 9
},
{
"token" : "莎士比亚",
"start_offset" : 45,
"end_offset" : 49,
"type" : "word",
"position" : 10
}
]
}

3.3 空白分析器:whitespace analyzer

空白(格)分析器(whitespace analyzer):这玩意儿只是根据空白将文本切分为若干分词,真是有够偷懒!

1
2
3
4
5
POST _analyze
{
"analyzer": "whitespace",
"text":"To be or not to be, That is a question ———— 莎士比亚"
}

分词结果如下:

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
{
"tokens" : [
{
"token" : "To",
"start_offset" : 0,
"end_offset" : 2,
"type" : "word",
"position" : 0
},
{
"token" : "be",
"start_offset" : 3,
"end_offset" : 5,
"type" : "word",
"position" : 1
},
{
"token" : "or",
"start_offset" : 6,
"end_offset" : 8,
"type" : "word",
"position" : 2
},
{
"token" : "not",
"start_offset" : 9,
"end_offset" : 12,
"type" : "word",
"position" : 3
},
{
"token" : "to",
"start_offset" : 13,
"end_offset" : 15,
"type" : "word",
"position" : 4
},
{
"token" : "be,",
"start_offset" : 16,
"end_offset" : 19,
"type" : "word",
"position" : 5
},
{
"token" : "That",
"start_offset" : 21,
"end_offset" : 25,
"type" : "word",
"position" : 6
},
{
"token" : "is",
"start_offset" : 26,
"end_offset" : 28,
"type" : "word",
"position" : 7
},
{
"token" : "a",
"start_offset" : 29,
"end_offset" : 30,
"type" : "word",
"position" : 8
},
{
"token" : "question",
"start_offset" : 31,
"end_offset" : 39,
"type" : "word",
"position" : 9
},
{
"token" : "————",
"start_offset" : 40,
"end_offset" : 44,
"type" : "word",
"position" : 10
},
{
"token" : "莎士比亚",
"start_offset" : 45,
"end_offset" : 49,
"type" : "word",
"position" : 11
}
]
}

3.4 停用词分析器:stop analyzer

停用词分析(stop analyzer)和简单分析器的行为很像,只是在分词流中额外的过滤了停用词。

1
2
3
4
5
POST _analyze
{
"analyzer": "stop",
"text":"To be or not to be, That is a question ———— 莎士比亚"
}

结果也很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"tokens" : [
{
"token" : "question",
"start_offset" : 31,
"end_offset" : 39,
"type" : "word",
"position" : 9
},
{
"token" : "莎士比亚",
"start_offset" : 45,
"end_offset" : 49,
"type" : "word",
"position" : 10
}
]
}

3.5 关键词分析器:keyword analyzer

关键词分析器(keyword analyzer)将整个字段当做单独的分词,如无必要,我们不在映射中使用关键词分析器。

1
2
3
4
5
POST _analyze
{
"analyzer": "keyword",
"text":"To be or not to be, That is a question ———— 莎士比亚"
}

结果如下:

1
2
3
4
5
6
7
8
9
10
11
{
"tokens" : [
{
"token" : "To be or not to be, That is a question ———— 莎士比亚",
"start_offset" : 0,
"end_offset" : 49,
"type" : "word",
"position" : 0
}
]
}

说的一点没错,分析结果是将整段当做单独的分词。

3.6 模式分析器:pattern analyzer

模式分析器(pattern analyzer)允许我们指定一个分词切分模式。但是通常更佳的方案是使用定制的分析器,组合现有的模式分词器和所需要的分词过滤器更加合适。

1
2
3
4
5
6
POST _analyze
{
"analyzer": "pattern",
"explain": false,
"text":"To be or not to be, That is a question ———— 莎士比亚"
}

结果如下:

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
{
"tokens" : [
{
"token" : "to",
"start_offset" : 0,
"end_offset" : 2,
"type" : "word",
"position" : 0
},
{
"token" : "be",
"start_offset" : 3,
"end_offset" : 5,
"type" : "word",
"position" : 1
},
{
"token" : "or",
"start_offset" : 6,
"end_offset" : 8,
"type" : "word",
"position" : 2
},
{
"token" : "not",
"start_offset" : 9,
"end_offset" : 12,
"type" : "word",
"position" : 3
},
{
"token" : "to",
"start_offset" : 13,
"end_offset" : 15,
"type" : "word",
"position" : 4
},
{
"token" : "be",
"start_offset" : 16,
"end_offset" : 18,
"type" : "word",
"position" : 5
},
{
"token" : "that",
"start_offset" : 21,
"end_offset" : 25,
"type" : "word",
"position" : 6
},
{
"token" : "is",
"start_offset" : 26,
"end_offset" : 28,
"type" : "word",
"position" : 7
},
{
"token" : "a",
"start_offset" : 29,
"end_offset" : 30,
"type" : "word",
"position" : 8
},
{
"token" : "question",
"start_offset" : 31,
"end_offset" : 39,
"type" : "word",
"position" : 9
}
]
}

我们来自定制一个模式分析器,比如我们写匹配邮箱的正则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PUT pattern_test
{
"settings": {
"analysis": {
"analyzer": {
"my_email_analyzer":{
"type":"pattern",
"pattern":"\\W|_",
"lowercase":true
}
}
}
}
}

上例中,我们在创建一条索引的时候,配置分析器为自定义的分析器。

需要注意的是,在json字符串中,正则的斜杠需要转义。

我们使用自定义的分析器来查询。

1
2
3
4
5
POST pattern_test/_analyze
{
"analyzer": "my_email_analyzer",
"text": "John_Smith@foo-bar.com"
}

结果如下:

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" : "john",
"start_offset" : 0,
"end_offset" : 4,
"type" : "word",
"position" : 0
},
{
"token" : "smith",
"start_offset" : 5,
"end_offset" : 10,
"type" : "word",
"position" : 1
},
{
"token" : "foo",
"start_offset" : 11,
"end_offset" : 14,
"type" : "word",
"position" : 2
},
{
"token" : "bar",
"start_offset" : 15,
"end_offset" : 18,
"type" : "word",
"position" : 3
},
{
"token" : "com",
"start_offset" : 19,
"end_offset" : 22,
"type" : "word",
"position" : 4
}
]
}

3.7 语言和多语言分析器:chinese

elasticsearch为很多世界流行语言提供良好的、简单的、开箱即用的语言分析器集合:阿拉伯语、亚美尼亚语、巴斯克语、巴西语、保加利亚语、加泰罗尼亚语、中文、捷克语、丹麦、荷兰语、英语、芬兰语、法语、加里西亚语、德语、希腊语、北印度语、匈牙利语、印度尼西亚、爱尔兰语、意大利语、日语、韩国语、库尔德语、挪威语、波斯语、葡萄牙语、罗马尼亚语、俄语、西班牙语、瑞典语、土耳其语和泰语等。

我们可以指定其中之一的语言来指定特定的语言分析器,但必须是小写的名字。如果你要分析的语言不在上述集合中,可能还需要搭配相应的插件支持。

1
2
3
4
5
POST _analyze
{
"analyzer": "chinese",
"text":"To be or not to be, That is a question ———— 莎士比亚"
}

结果如下:

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" : "question",
"start_offset" : 31,
"end_offset" : 39,
"type" : "<ALPHANUM>",
"position" : 9
},
{
"token" : "莎",
"start_offset" : 45,
"end_offset" : 46,
"type" : "<IDEOGRAPHIC>",
"position" : 10
},
{
"token" : "士",
"start_offset" : 46,
"end_offset" : 47,
"type" : "<IDEOGRAPHIC>",
"position" : 11
},
{
"token" : "比",
"start_offset" : 47,
"end_offset" : 48,
"type" : "<IDEOGRAPHIC>",
"position" : 12
},
{
"token" : "亚",
"start_offset" : 48,
"end_offset" : 49,
"type" : "<IDEOGRAPHIC>",
"position" : 13
}
]
}

也可以是别语言:

1
2
3
4
5
6
7
8
9
10
POST _analyze
{
"analyzer": "french",
"text":"Je suis ton père"
}
POST _analyze
{
"analyzer": "german",
"text":"Ich bin dein vater"
}

3.8 雪球分析器:snowball analyzer

雪球分析器(snowball analyzer)除了使用标准的分词和分词过滤器(和标准分析器一样)也是用了小写分词过滤器和停用词过滤器,除此之外,它还是用了雪球词干器对文本进行词干提取。

1
2
3
4
5
POST _analyze
{
"analyzer": "snowball",
"text":"To be or not to be, That is a question ———— 莎士比亚"
}

结果如下:

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" : "question",
"start_offset" : 31,
"end_offset" : 39,
"type" : "<ALPHANUM>",
"position" : 9
},
{
"token" : "莎",
"start_offset" : 45,
"end_offset" : 46,
"type" : "<IDEOGRAPHIC>",
"position" : 10
},
{
"token" : "士",
"start_offset" : 46,
"end_offset" : 47,
"type" : "<IDEOGRAPHIC>",
"position" : 11
},
{
"token" : "比",
"start_offset" : 47,
"end_offset" : 48,
"type" : "<IDEOGRAPHIC>",
"position" : 12
},
{
"token" : "亚",
"start_offset" : 48,
"end_offset" : 49,
"type" : "<IDEOGRAPHIC>",
"position" : 13
}
]
}

4 字符过滤器

字符过滤器在char_filter属性中定义,它是对字符流进行处理。字符过滤器种类不多。elasticearch只提供了三种字符过滤器:

  • HTML字符过滤器(HTML Strip Char Filter)
  • 映射字符过滤器(Mapping Char Filter)
  • 模式替换过滤器(Pattern Replace Char Filter)

我们来分别看看吧。

4.1 HTML字符过滤器

HTML字符过滤器(HTML Strip Char Filter)从文本中去除HTML元素。

1
2
3
4
5
6
POST _analyze
{
"tokenizer": "keyword",
"char_filter": ["html_strip"],
"text":"<p>I&apos;m so <b>happy</b>!</p>"
}

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"tokens" : [
{
"token" : """

I'm so happy!

""",
"start_offset" : 0,
"end_offset" : 32,
"type" : "word",
"position" : 0
}
]
}

4.2 映射字符过滤器

映射字符过滤器(Mapping Char Filter)接收键值的映射,每当遇到与键相同的字符串时,它就用该键关联的值替换它们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
PUT pattern_test4
{
"settings": {
"analysis": {
"analyzer": {
"my_analyzer":{
"tokenizer":"keyword",
"char_filter":["my_char_filter"]
}
},
"char_filter":{
"my_char_filter":{
"type":"mapping",
"mappings":["aaa => 666","bbb => 888"]
}
}
}
}
}

上例中,我们自定义了一个分析器,其内的分词器使用关键字分词器,字符过滤器则是自定制的,将字符中的aaa替换为666,bbb替换为888。

1
2
3
4
5
POST pattern_test4/_analyze
{
"analyzer": "my_analyzer",
"text": "aaa热爱bbb,可惜后来aaa结婚了"
}

结果如下:

1
2
3
4
5
6
7
8
9
10
11
{
"tokens" : [
{
"token" : "666热爱888,可惜后来666结婚了",
"start_offset" : 0,
"end_offset" : 19,
"type" : "word",
"position" : 0
}
]
}

4.3 模式替换过滤器

模式替换过滤器(Pattern Replace Char Filter)使用正则表达式匹配并替换字符串中的字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
PUT pattern_test5
{
"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_"
}
}
}
}
}

上例中,我们自定义了一个正则规则。

1
2
3
4
5
POST pattern_test5/_analyze
{
"analyzer": "my_analyzer",
"text": "My credit card is 123-456-789"
}

结果如下:

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" : "My",
"start_offset" : 0,
"end_offset" : 2,
"type" : "<ALPHANUM>",
"position" : 0
},
{
"token" : "credit",
"start_offset" : 3,
"end_offset" : 9,
"type" : "<ALPHANUM>",
"position" : 1
},
{
"token" : "card",
"start_offset" : 10,
"end_offset" : 14,
"type" : "<ALPHANUM>",
"position" : 2
},
{
"token" : "is",
"start_offset" : 15,
"end_offset" : 17,
"type" : "<ALPHANUM>",
"position" : 3
},
{
"token" : "123_456_789",
"start_offset" : 18,
"end_offset" : 29,
"type" : "<NUM>",
"position" : 4
}
]
}

我们大致的了解elasticsearch分析处理数据的流程。但可以看到的是,我们极少地在例子中演示中文处理。因为elasticsearch内置的分析器处理起来中文不是很好。

5 分词器

由于elasticsearch内置了分析器,它同样也包含了分词器。分词器,顾名思义,主要的操作是将文本字符串分解为小块,而这些小块这被称为分词token

5.1 标准分词器:standard tokenizer

标准分词器(standard tokenizer)是一个基于语法的分词器,对于大多数欧洲语言来说还是不错的,它同时还处理了Unicode文本的分词,但分词默认的最大长度是255字节,它也移除了逗号和句号这样的标点符号。

1
2
3
4
5
POST _analyze
{
"tokenizer": "standard",
"text":"To be or not to be, That is a question ———— 莎士比亚"
}

结果如下:

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
{
"tokens" : [
{
"token" : "To",
"start_offset" : 0,
"end_offset" : 2,
"type" : "<ALPHANUM>",
"position" : 0
},
{
"token" : "be",
"start_offset" : 3,
"end_offset" : 5,
"type" : "<ALPHANUM>",
"position" : 1
},
{
"token" : "or",
"start_offset" : 6,
"end_offset" : 8,
"type" : "<ALPHANUM>",
"position" : 2
},
{
"token" : "not",
"start_offset" : 9,
"end_offset" : 12,
"type" : "<ALPHANUM>",
"position" : 3
},
{
"token" : "to",
"start_offset" : 13,
"end_offset" : 15,
"type" : "<ALPHANUM>",
"position" : 4
},
{
"token" : "be",
"start_offset" : 16,
"end_offset" : 18,
"type" : "<ALPHANUM>",
"position" : 5
},
{
"token" : "That",
"start_offset" : 21,
"end_offset" : 25,
"type" : "<ALPHANUM>",
"position" : 6
},
{
"token" : "is",
"start_offset" : 26,
"end_offset" : 28,
"type" : "<ALPHANUM>",
"position" : 7
},
{
"token" : "a",
"start_offset" : 29,
"end_offset" : 30,
"type" : "<ALPHANUM>",
"position" : 8
},
{
"token" : "question",
"start_offset" : 31,
"end_offset" : 39,
"type" : "<ALPHANUM>",
"position" : 9
},
{
"token" : "莎",
"start_offset" : 45,
"end_offset" : 46,
"type" : "<IDEOGRAPHIC>",
"position" : 10
},
{
"token" : "士",
"start_offset" : 46,
"end_offset" : 47,
"type" : "<IDEOGRAPHIC>",
"position" : 11
},
{
"token" : "比",
"start_offset" : 47,
"end_offset" : 48,
"type" : "<IDEOGRAPHIC>",
"position" : 12
},
{
"token" : "亚",
"start_offset" : 48,
"end_offset" : 49,
"type" : "<IDEOGRAPHIC>",
"position" : 13
}
]
}

5.2 关键词分词器:keyword tokenizer

关键词分词器(keyword tokenizer)是一种简单的分词器,将整个文本作为单个的分词,提供给分词过滤器,当你只想用分词过滤器,而不做分词操作时,它是不错的选择。

1
2
3
4
5
POST _analyze
{
"tokenizer": "keyword",
"text":"To be or not to be, That is a question ———— 莎士比亚"
}

结果如下:

1
2
3
4
5
6
7
8
9
10
11
{
"tokens" : [
{
"token" : "To be or not to be, That is a question ———— 莎士比亚",
"start_offset" : 0,
"end_offset" : 49,
"type" : "word",
"position" : 0
}
]
}

5.3 字母分词器:letter tokenizer

字母分词器(letter tokenizer)根据非字母的符号,将文本切分成分词。

1
2
3
4
5
POST _analyze
{
"tokenizer": "letter",
"text":"To be or not to be, That is a question ———— 莎士比亚"
}

结果如下:

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
{
"tokens" : [
{
"token" : "To",
"start_offset" : 0,
"end_offset" : 2,
"type" : "word",
"position" : 0
},
{
"token" : "be",
"start_offset" : 3,
"end_offset" : 5,
"type" : "word",
"position" : 1
},
{
"token" : "or",
"start_offset" : 6,
"end_offset" : 8,
"type" : "word",
"position" : 2
},
{
"token" : "not",
"start_offset" : 9,
"end_offset" : 12,
"type" : "word",
"position" : 3
},
{
"token" : "to",
"start_offset" : 13,
"end_offset" : 15,
"type" : "word",
"position" : 4
},
{
"token" : "be",
"start_offset" : 16,
"end_offset" : 18,
"type" : "word",
"position" : 5
},
{
"token" : "That",
"start_offset" : 21,
"end_offset" : 25,
"type" : "word",
"position" : 6
},
{
"token" : "is",
"start_offset" : 26,
"end_offset" : 28,
"type" : "word",
"position" : 7
},
{
"token" : "a",
"start_offset" : 29,
"end_offset" : 30,
"type" : "word",
"position" : 8
},
{
"token" : "question",
"start_offset" : 31,
"end_offset" : 39,
"type" : "word",
"position" : 9
},
{
"token" : "莎士比亚",
"start_offset" : 45,
"end_offset" : 49,
"type" : "word",
"position" : 10
}
]
}

5.4 小写分词器:lowercase tokenizer

小写分词器(lowercase tokenizer)结合了常规的字母分词器和小写分词过滤器(跟你想的一样,就是将所有的分词转化为小写)的行为。通过一个单独的分词器来实现的主要原因是,一次进行两项操作会获得更好的性能。

1
2
3
4
5
POST _analyze
{
"tokenizer": "lowercase",
"text":"To be or not to be, That is a question ———— 莎士比亚"
}

结果如下:

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
{
"tokens" : [
{
"token" : "to",
"start_offset" : 0,
"end_offset" : 2,
"type" : "word",
"position" : 0
},
{
"token" : "be",
"start_offset" : 3,
"end_offset" : 5,
"type" : "word",
"position" : 1
},
{
"token" : "or",
"start_offset" : 6,
"end_offset" : 8,
"type" : "word",
"position" : 2
},
{
"token" : "not",
"start_offset" : 9,
"end_offset" : 12,
"type" : "word",
"position" : 3
},
{
"token" : "to",
"start_offset" : 13,
"end_offset" : 15,
"type" : "word",
"position" : 4
},
{
"token" : "be",
"start_offset" : 16,
"end_offset" : 18,
"type" : "word",
"position" : 5
},
{
"token" : "that",
"start_offset" : 21,
"end_offset" : 25,
"type" : "word",
"position" : 6
},
{
"token" : "is",
"start_offset" : 26,
"end_offset" : 28,
"type" : "word",
"position" : 7
},
{
"token" : "a",
"start_offset" : 29,
"end_offset" : 30,
"type" : "word",
"position" : 8
},
{
"token" : "question",
"start_offset" : 31,
"end_offset" : 39,
"type" : "word",
"position" : 9
},
{
"token" : "莎士比亚",
"start_offset" : 45,
"end_offset" : 49,
"type" : "word",
"position" : 10
}
]
}

5.5 空白分词器:whitespace tokenizer

空白分词器(whitespace tokenizer)通过空白来分隔不同的分词,空白包括空格、制表符、换行等。但是,我们需要注意的是,空白分词器不会删除任何标点符号。

1
2
3
4
5
POST _analyze
{
"tokenizer": "whitespace",
"text":"To be or not to be, That is a question ———— 莎士比亚"
}

结果如下:

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
{
"tokens" : [
{
"token" : "To",
"start_offset" : 0,
"end_offset" : 2,
"type" : "word",
"position" : 0
},
{
"token" : "be",
"start_offset" : 3,
"end_offset" : 5,
"type" : "word",
"position" : 1
},
{
"token" : "or",
"start_offset" : 6,
"end_offset" : 8,
"type" : "word",
"position" : 2
},
{
"token" : "not",
"start_offset" : 9,
"end_offset" : 12,
"type" : "word",
"position" : 3
},
{
"token" : "to",
"start_offset" : 13,
"end_offset" : 15,
"type" : "word",
"position" : 4
},
{
"token" : "be,",
"start_offset" : 16,
"end_offset" : 19,
"type" : "word",
"position" : 5
},
{
"token" : "That",
"start_offset" : 21,
"end_offset" : 25,
"type" : "word",
"position" : 6
},
{
"token" : "is",
"start_offset" : 26,
"end_offset" : 28,
"type" : "word",
"position" : 7
},
{
"token" : "a",
"start_offset" : 29,
"end_offset" : 30,
"type" : "word",
"position" : 8
},
{
"token" : "question",
"start_offset" : 31,
"end_offset" : 39,
"type" : "word",
"position" : 9
},
{
"token" : "————",
"start_offset" : 40,
"end_offset" : 44,
"type" : "word",
"position" : 10
},
{
"token" : "莎士比亚",
"start_offset" : 45,
"end_offset" : 49,
"type" : "word",
"position" : 11
}
]
}

5.6 模式分词器:pattern tokenizer

模式分词器(pattern tokenizer)允许指定一个任意的模式,将文本切分为分词。

1
2
3
4
5
POST _analyze
{
"tokenizer": "pattern",
"text":"To be or not to be, That is a question ———— 莎士比亚"
}

现在让我们手动定制一个以逗号分隔的分词器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
PUT pattern_test2
{
"settings": {
"analysis": {
"analyzer": {
"my_analyzer":{
"tokenizer":"my_tokenizer"
}
},
"tokenizer": {
"my_tokenizer":{
"type":"pattern",
"pattern":","
}
}
}
}
}

上例中,在settings下的自定义分析器my_analyzer中,自定义的模式分词器名叫my_tokenizer;在与自定义分析器同级,为新建的自定义模式分词器设置一些属性,比如以逗号分隔。

1
2
3
4
5
POST pattern_test2/_analyze
{
"tokenizer": "my_tokenizer",
"text":"To be or not to be, That is a question ———— 莎士比亚"
}

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"tokens" : [
{
"token" : "To be or not to be",
"start_offset" : 0,
"end_offset" : 18,
"type" : "word",
"position" : 0
},
{
"token" : " That is a question ———— 莎士比亚",
"start_offset" : 19,
"end_offset" : 49,
"type" : "word",
"position" : 1
}
]
}

根据结果可以看到,文档被逗号分割为两部分。

5.7 UAX URL电子邮件分词器:UAX RUL email tokenizer

在处理单个的英文单词的情况下,标准分词器是个非常好的选择,但是现在很多的网站以网址或电子邮件作为结尾,比如我们现在有这样的一个文本:

1
2
3
4
5
作者:张开
来源:未知
原文:https://www.cnblogs.com/Neeo/articles/10402742.html
邮箱:xxxxxxx@xx.com
版权声明:本文为博主原创文章,转载请附上博文链接!

现在让我们使用标准分词器查看一下:

1
2
3
4
5
POST _analyze
{
"tokenizer": "standard",
"text":"作者:张开来源:未知原文:https://www.cnblogs.com/Neeo/articles/10402742.html邮箱:xxxxxxx@xx.com版权声明:本文为博主原创文章,转载请附上博文链接!"
}

结果很长:

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
{
"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" : 3,
"end_offset" : 4,
"type" : "<IDEOGRAPHIC>",
"position" : 2
},
{
"token" : "开",
"start_offset" : 4,
"end_offset" : 5,
"type" : "<IDEOGRAPHIC>",
"position" : 3
},
{
"token" : "来",
"start_offset" : 5,
"end_offset" : 6,
"type" : "<IDEOGRAPHIC>",
"position" : 4
},
{
"token" : "源",
"start_offset" : 6,
"end_offset" : 7,
"type" : "<IDEOGRAPHIC>",
"position" : 5
},
{
"token" : "未",
"start_offset" : 8,
"end_offset" : 9,
"type" : "<IDEOGRAPHIC>",
"position" : 6
},
{
"token" : "知",
"start_offset" : 9,
"end_offset" : 10,
"type" : "<IDEOGRAPHIC>",
"position" : 7
},
{
"token" : "原",
"start_offset" : 10,
"end_offset" : 11,
"type" : "<IDEOGRAPHIC>",
"position" : 8
},
{
"token" : "文",
"start_offset" : 11,
"end_offset" : 12,
"type" : "<IDEOGRAPHIC>",
"position" : 9
},
{
"token" : "https",
"start_offset" : 13,
"end_offset" : 18,
"type" : "<ALPHANUM>",
"position" : 10
},
{
"token" : "www.cnblogs.com",
"start_offset" : 21,
"end_offset" : 36,
"type" : "<ALPHANUM>",
"position" : 11
},
{
"token" : "Neeo",
"start_offset" : 37,
"end_offset" : 41,
"type" : "<ALPHANUM>",
"position" : 12
},
{
"token" : "articles",
"start_offset" : 42,
"end_offset" : 50,
"type" : "<ALPHANUM>",
"position" : 13
},
{
"token" : "10402742",
"start_offset" : 51,
"end_offset" : 59,
"type" : "<NUM>",
"position" : 14
},
{
"token" : "html",
"start_offset" : 60,
"end_offset" : 64,
"type" : "<ALPHANUM>",
"position" : 15
},
{
"token" : "邮",
"start_offset" : 64,
"end_offset" : 65,
"type" : "<IDEOGRAPHIC>",
"position" : 16
},
{
"token" : "箱",
"start_offset" : 65,
"end_offset" : 66,
"type" : "<IDEOGRAPHIC>",
"position" : 17
},
{
"token" : "xxxxxxx",
"start_offset" : 67,
"end_offset" : 74,
"type" : "<ALPHANUM>",
"position" : 18
},
{
"token" : "xx.com",
"start_offset" : 75,
"end_offset" : 81,
"type" : "<ALPHANUM>",
"position" : 19
},
{
"token" : "版",
"start_offset" : 81,
"end_offset" : 82,
"type" : "<IDEOGRAPHIC>",
"position" : 20
},
{
"token" : "权",
"start_offset" : 82,
"end_offset" : 83,
"type" : "<IDEOGRAPHIC>",
"position" : 21
},
{
"token" : "声",
"start_offset" : 83,
"end_offset" : 84,
"type" : "<IDEOGRAPHIC>",
"position" : 22
},
{
"token" : "明",
"start_offset" : 84,
"end_offset" : 85,
"type" : "<IDEOGRAPHIC>",
"position" : 23
},
{
"token" : "本",
"start_offset" : 86,
"end_offset" : 87,
"type" : "<IDEOGRAPHIC>",
"position" : 24
},
{
"token" : "文",
"start_offset" : 87,
"end_offset" : 88,
"type" : "<IDEOGRAPHIC>",
"position" : 25
},
{
"token" : "为",
"start_offset" : 88,
"end_offset" : 89,
"type" : "<IDEOGRAPHIC>",
"position" : 26
},
{
"token" : "博",
"start_offset" : 89,
"end_offset" : 90,
"type" : "<IDEOGRAPHIC>",
"position" : 27
},
{
"token" : "主",
"start_offset" : 90,
"end_offset" : 91,
"type" : "<IDEOGRAPHIC>",
"position" : 28
},
{
"token" : "原",
"start_offset" : 91,
"end_offset" : 92,
"type" : "<IDEOGRAPHIC>",
"position" : 29
},
{
"token" : "创",
"start_offset" : 92,
"end_offset" : 93,
"type" : "<IDEOGRAPHIC>",
"position" : 30
},
{
"token" : "文",
"start_offset" : 93,
"end_offset" : 94,
"type" : "<IDEOGRAPHIC>",
"position" : 31
},
{
"token" : "章",
"start_offset" : 94,
"end_offset" : 95,
"type" : "<IDEOGRAPHIC>",
"position" : 32
},
{
"token" : "转",
"start_offset" : 96,
"end_offset" : 97,
"type" : "<IDEOGRAPHIC>",
"position" : 33
},
{
"token" : "载",
"start_offset" : 97,
"end_offset" : 98,
"type" : "<IDEOGRAPHIC>",
"position" : 34
},
{
"token" : "请",
"start_offset" : 98,
"end_offset" : 99,
"type" : "<IDEOGRAPHIC>",
"position" : 35
},
{
"token" : "附",
"start_offset" : 99,
"end_offset" : 100,
"type" : "<IDEOGRAPHIC>",
"position" : 36
},
{
"token" : "上",
"start_offset" : 100,
"end_offset" : 101,
"type" : "<IDEOGRAPHIC>",
"position" : 37
},
{
"token" : "博",
"start_offset" : 101,
"end_offset" : 102,
"type" : "<IDEOGRAPHIC>",
"position" : 38
},
{
"token" : "文",
"start_offset" : 102,
"end_offset" : 103,
"type" : "<IDEOGRAPHIC>",
"position" : 39
},
{
"token" : "链",
"start_offset" : 103,
"end_offset" : 104,
"type" : "<IDEOGRAPHIC>",
"position" : 40
},
{
"token" : "接",
"start_offset" : 104,
"end_offset" : 105,
"type" : "<IDEOGRAPHIC>",
"position" : 41
}
]
}

无论如何,这个结果不符合我们的预期,因为把我们的邮箱和网址分的乱七八糟!那么针对这种情况,我们应该使用UAX URL电子邮件分词器(UAX RUL email tokenizer),该分词器将电子邮件和URL都作为单独的分词进行保留。

1
2
3
4
5
POST _analyze
{
"tokenizer": "uax_url_email",
"text":"作者:张开来源:未知原文:https://www.cnblogs.com/Neeo/articles/10402742.html邮箱:xxxxxxx@xx.com版权声明:本文为博主原创文章,转载请附上博文链接!"
}

结果如下:

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
{
"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" : 3,
"end_offset" : 4,
"type" : "<IDEOGRAPHIC>",
"position" : 2
},
{
"token" : "开",
"start_offset" : 4,
"end_offset" : 5,
"type" : "<IDEOGRAPHIC>",
"position" : 3
},
{
"token" : "来",
"start_offset" : 5,
"end_offset" : 6,
"type" : "<IDEOGRAPHIC>",
"position" : 4
},
{
"token" : "源",
"start_offset" : 6,
"end_offset" : 7,
"type" : "<IDEOGRAPHIC>",
"position" : 5
},
{
"token" : "未",
"start_offset" : 8,
"end_offset" : 9,
"type" : "<IDEOGRAPHIC>",
"position" : 6
},
{
"token" : "知",
"start_offset" : 9,
"end_offset" : 10,
"type" : "<IDEOGRAPHIC>",
"position" : 7
},
{
"token" : "原",
"start_offset" : 10,
"end_offset" : 11,
"type" : "<IDEOGRAPHIC>",
"position" : 8
},
{
"token" : "文",
"start_offset" : 11,
"end_offset" : 12,
"type" : "<IDEOGRAPHIC>",
"position" : 9
},
{
"token" : "https://www.cnblogs.com/Neeo/articles/10402742.html",
"start_offset" : 13,
"end_offset" : 64,
"type" : "<URL>",
"position" : 10
},
{
"token" : "邮",
"start_offset" : 64,
"end_offset" : 65,
"type" : "<IDEOGRAPHIC>",
"position" : 11
},
{
"token" : "箱",
"start_offset" : 65,
"end_offset" : 66,
"type" : "<IDEOGRAPHIC>",
"position" : 12
},
{
"token" : "xxxxxxx@xx.com",
"start_offset" : 67,
"end_offset" : 81,
"type" : "<EMAIL>",
"position" : 13
},
{
"token" : "版",
"start_offset" : 81,
"end_offset" : 82,
"type" : "<IDEOGRAPHIC>",
"position" : 14
},
{
"token" : "权",
"start_offset" : 82,
"end_offset" : 83,
"type" : "<IDEOGRAPHIC>",
"position" : 15
},
{
"token" : "声",
"start_offset" : 83,
"end_offset" : 84,
"type" : "<IDEOGRAPHIC>",
"position" : 16
},
{
"token" : "明",
"start_offset" : 84,
"end_offset" : 85,
"type" : "<IDEOGRAPHIC>",
"position" : 17
},
{
"token" : "本",
"start_offset" : 86,
"end_offset" : 87,
"type" : "<IDEOGRAPHIC>",
"position" : 18
},
{
"token" : "文",
"start_offset" : 87,
"end_offset" : 88,
"type" : "<IDEOGRAPHIC>",
"position" : 19
},
{
"token" : "为",
"start_offset" : 88,
"end_offset" : 89,
"type" : "<IDEOGRAPHIC>",
"position" : 20
},
{
"token" : "博",
"start_offset" : 89,
"end_offset" : 90,
"type" : "<IDEOGRAPHIC>",
"position" : 21
},
{
"token" : "主",
"start_offset" : 90,
"end_offset" : 91,
"type" : "<IDEOGRAPHIC>",
"position" : 22
},
{
"token" : "原",
"start_offset" : 91,
"end_offset" : 92,
"type" : "<IDEOGRAPHIC>",
"position" : 23
},
{
"token" : "创",
"start_offset" : 92,
"end_offset" : 93,
"type" : "<IDEOGRAPHIC>",
"position" : 24
},
{
"token" : "文",
"start_offset" : 93,
"end_offset" : 94,
"type" : "<IDEOGRAPHIC>",
"position" : 25
},
{
"token" : "章",
"start_offset" : 94,
"end_offset" : 95,
"type" : "<IDEOGRAPHIC>",
"position" : 26
},
{
"token" : "转",
"start_offset" : 96,
"end_offset" : 97,
"type" : "<IDEOGRAPHIC>",
"position" : 27
},
{
"token" : "载",
"start_offset" : 97,
"end_offset" : 98,
"type" : "<IDEOGRAPHIC>",
"position" : 28
},
{
"token" : "请",
"start_offset" : 98,
"end_offset" : 99,
"type" : "<IDEOGRAPHIC>",
"position" : 29
},
{
"token" : "附",
"start_offset" : 99,
"end_offset" : 100,
"type" : "<IDEOGRAPHIC>",
"position" : 30
},
{
"token" : "上",
"start_offset" : 100,
"end_offset" : 101,
"type" : "<IDEOGRAPHIC>",
"position" : 31
},
{
"token" : "博",
"start_offset" : 101,
"end_offset" : 102,
"type" : "<IDEOGRAPHIC>",
"position" : 32
},
{
"token" : "文",
"start_offset" : 102,
"end_offset" : 103,
"type" : "<IDEOGRAPHIC>",
"position" : 33
},
{
"token" : "链",
"start_offset" : 103,
"end_offset" : 104,
"type" : "<IDEOGRAPHIC>",
"position" : 34
},
{
"token" : "接",
"start_offset" : 104,
"end_offset" : 105,
"type" : "<IDEOGRAPHIC>",
"position" : 35
}
]
}

5.8 路径层次分词器:path hierarchy tokenizer

路径层次分词器(path hierarchy tokenizer)允许以特定的方式索引文件系统的路径,这样在搜索时,共享同样路径的文件将被作为结果返回。

1
2
3
4
5
POST _analyze
{
"tokenizer": "path_hierarchy",
"text":"/usr/local/python/python2.7"
}

返回结果如下:

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" : "/usr",
"start_offset" : 0,
"end_offset" : 4,
"type" : "word",
"position" : 0
},
{
"token" : "/usr/local",
"start_offset" : 0,
"end_offset" : 10,
"type" : "word",
"position" : 0
},
{
"token" : "/usr/local/python",
"start_offset" : 0,
"end_offset" : 17,
"type" : "word",
"position" : 0
},
{
"token" : "/usr/local/python/python2.7",
"start_offset" : 0,
"end_offset" : 27,
"type" : "word",
"position" : 0
}
]
}

6 分词过滤器

elasticsearch内置很多的分词过滤器。其中包含分词过滤器和字符过滤器。
这里仅列举几个常见的分词过滤器(token filter)包括:

  • ASCII折叠分词过滤器(ASCII Folding Token Filter)将前127个ASCII字符(基本拉丁语的Unicode块)中不包含的字母、数字和符号Unicode字符转换为对应的ASCII字符(如果存在的话)。
  • 扁平图形分词过滤器(Flatten Graph Token Filter)接受任意图形标记流。例如由同义词图形标记过滤器生成的标记流,并将其展平为适合索引的单个线性标记链。这是一个有损的过程,因为单独的侧路径被压扁在彼此之上,但是如果在索引期间使用图形令牌流是必要的,因为Lucene索引当前不能表示图形。 出于这个原因,最好只在搜索时应用图形分析器,因为这样可以保留完整的图形结构,并为邻近查询提供正确的匹配。该功能在Lucene中为实验性功能。
  • 长度标记过滤器(Length Token Filter)会移除分词流中太长或者太短的标记,它是可配置的,我们可以在settings中设置。
  • 小写分词过滤器(Lowercase Token Filter)将分词规范化为小写,它通过language参数支持希腊语、爱尔兰语和土耳其语小写标记过滤器。
  • 大写分词过滤器(Uppercase Token Filter)将分词规范为大写。

其余分词过滤器不一一列举。详情参见官网