入门

简介

Elasticsearch是一个基于Lucene库的开源搜索引擎,它提供分布式的实时文件存储和搜索,可扩展性好,并且支持通过HTTP网络接口交互,数据以JSON格式展示。

Elasticsearch因为其极快的搜索和聚合速度通常被应用在各种搜索应用中,比如在自己的app里面加一个搜索框或者分析实时日志(ELK系统)。

Elasticsearch会对所有输入的文本进行处理,建立索引放入内存中,从而提高搜索效率。在这一点上ES要优于MySQL的B+树的结构,MySQL需要将索引放入磁盘,每次读取需要先从磁盘读取索引然后寻找对应的数据节点,但是ES能够直接在内存中就找到目标文档对应的大致位置,最大化提高效率。并且在进行组合查询的时候MySQL的劣势更加明显,它不支持复杂的组合查询比如聚合操作,即使要组合查询也要事先建好索引,但是ES就可以完成这种复杂的操作,默认每个字段都是有索引的,在查询的时候可以各种互相组合。

为了省事,以下统一用ES来代替Elasticsearch,其实我们在公司里面也基本都说ES,全称比较难读。还有一点,因为ES使用了Lucene的库,下面说的很多优化思路都是Lucene里面的,但是为了讲解方便,我就用ES来代替Lucene。

部署

单机配置

1
2
3
4
5
6
cluster.name: "docker-cluster" # 集群名称
network.host: 0.0.0.0
cluster.initial_master_nodes: ["node-1"] # 指定初始节点,否则会报错:master_not_discovered_exception
node.name: node-1 # 节点名称
node.master: true # 主节点
node.data: true

索引

介绍

在ES中每个字段都是被索引的,所以不会像MySQL中那样需要对字段进行手动的建立索引。

ES在建立索引的时候采用了一种叫做倒排索引的机制,保证每次在搜索关键词的时候能够快速定位到这个关键词所属的文档。

Inverted Index

比如有三句话在数据库中:

  1. Winter is coming
  2. Ours is fury
  3. Ths choice is yours

如果没有倒排索引(Inverted Index),想要去找其中的choice,需要遍历整个文档,才能找到对应的文档的id,这样做效率是十分低的,所以为了提高效率,我们就给输入的所有数据的都建立索引,并且把这样的索引和对应的文档建立一个关联关系,相当于一个词典。当我们在寻找choice的时候就可以直接像查字典一样,直接找到所有包含这个数据的文档的id,然后找到数据。

Lucene在对文档建立索引的时候,会给词典的所有的元素排好序,在搜索的时候直接根据二分查找的方法进行筛选就能够快速找到数据。不过是不是感觉有点眼熟,这不就是MySQL的索引方式的,直接用B+树建立索引词典指向被索引的文档。

ES做的要更深一点,ES希望把这个词典“搬进”内存,直接从内存读取数据不就比从磁盘读数据要快很多吗!问题在于对于海量的数据,索引的空间消耗十分巨大,直接搬进来肯定不合适,所以需要进一步的处理,建立词典索引(term index)。通过词典索引可以直接找到搜索词在词典中的大致位置,然后从磁盘中取出词典数据再进行查找。所以大致的结构图就变成了这样:

ES通过有限状态转化器,把term dictionary变成了term index,放进了内存。

倒排索引

正排索引(传统)

通过文章id关联文章内容出现的keyword

id content
1001 my name is zhang san
1002 my name is li si

倒排索引

查keyword在哪几个文章中出现,并记录文章id,效率更高

keyword id
name 1001, 1002
zhang 1001

索引创建

对比关系型数据库,创建索引就等同于创建数据库。

在 Postman 中,向 ES 服务器发 PUT 请求 : http://192.168.33.200:9200/shopping

请求后,服务器返回响应:

1
2
3
4
5
{
"acknowledged": true,//响应结果
"shards_acknowledged": true,//分片结果
"index": "shopping"//索引名称
}

PUT具有幂等性,重复发 PUT 请求 : http://192.168.33.200:9200/shopping 添加索引,会返回错误信息 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"error": {
"root_cause": [
{
"type": "resource_already_exists_exception",
"reason": "index [shopping/J0WlEhh4R7aDrfIc3AkwWQ] already exists",
"index_uuid": "J0WlEhh4R7aDrfIc3AkwWQ",
"index": "shopping"
}
],
"type": "resource_already_exists_exception",
"reason": "index [shopping/J0WlEhh4R7aDrfIc3AkwWQ] already exists",
"index_uuid": "J0WlEhh4R7aDrfIc3AkwWQ",
"index": "shopping"
},
"status": 400
}

但是 POST 方式没有幂等性,所以不支持 POST 方式添加索引,否则报错:

1
2
3
4
{
"error": "Incorrect HTTP method for uri [/shopping] and method [POST], allowed: [PUT, HEAD, DELETE, GET]",
"status": 405
}

索引查询

查询:在 Postman 中,向 ES 服务器发 GET 请求 :http://192.168.33.200:9200/shopping

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"shopping": {//索引名
"aliases": {},//别名
"mappings": {},//映射
"settings": {//设置
"index": {//设置 - 索引
"creation_date": "1621688325308",//设置 - 索引 - 创建时间
"number_of_shards": "1",//设置 - 索引 - 主分片数量
"number_of_replicas": "1",//设置 - 索引 - 主分片数量
"uuid": "J0WlEhh4R7aDrfIc3AkwWQ",//设置 - 索引 - 主分片数量
"version": {//设置 - 索引 - 主分片数量
"created": "7080099"
},
"provided_name": "shopping"//设置 - 索引 - 主分片数量
}
}
}
}

查看所有

GET http://192.168.33.200:9200/_cat/indices?v,返回结果如下:

1
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size

索引删除

删除:在 Postman 中,向 ES 服务器发 DELETE 请求 : http://192.168.33.200:9200/shopping

1
2
3
{
"acknowledged": true
}

文档

介绍

通过使用 index API ,文档可以被 索引 —— 存储和使文档可被搜索。 但是首先,我们要确定文档的位置。正如我们刚刚讨论的,一个文档的 _index_type_id 唯一标识一个文档。 我们可以提供自定义的 _id 值,或者让 index API 自动生成。

文档创建

在 Postman 中,向 ES 服务器发 POST 请求 : http://192.168.33.200:9200/shopping/_doc,请求体JSON内容为:

1
2
3
4
5
6
{
"title":"小米手机",
"category":"小米",
"images":"http://www.coderblue.cn/blue.jpg",
"price":3999.00
}

等效于@PostMapping + @RequestBody,返回结果:

上面的数据创建后,由于没有指定数据唯一性标识(ID),默认情况下, ES 服务器每次都会随机生成一个。

如果想要自定义唯一性标识,需要在创建时指定: http://192.168.33.200:9200/shopping/_doc/20210522

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"_index": "shopping",
"_type": "_doc",
"_id": "20210522", // 不然随机生成,比如0wk5lHkBX7dGZfoqKmKk
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 5,
"_primary_term": 1
}

注意,此处发送请求的方式必须为 POST,不能是 PUT,否则会发生错误 。

1
2
3
4
{
"error": "Incorrect HTTP method for uri [/shopping/_doc] and method [PUT], allowed: [POST]",
"status": 405
}

除非数据时明确数据主键,那么请求方式也可以为 PUT。每次put后对应的 _version 都会加一。

文档查询

1、指定id查询文档

查看文档时,需要指明文档的唯一性标识,类似于 MySQL 中数据的主键查询

在 Postman 中,向 ES 服务器发 GET 请求 :http://192.168.33.200:9200/shopping/_doc/20210522

返回结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"_index": "shopping",
"_type": "_doc",
"_id": "20210522",
"_version": 3,
"_seq_no": 8,
"_primary_term": 1,
"found": true,
"_source": {
"title": "小米手机",
"category": "小米10",
"images": "http://www.coderblue.cn/blue.jpg",
"price": 4999.00
}
}

查找不存在的内容,向 ES 服务器发 GET 请求 http://192.168.33.200:9200/shopping/_doc/202105221,返回结果如下:

1
2
3
4
5
6
{
"_index": "shopping",
"_type": "_doc",
"_id": "202105221",
"found": false
}

2、查看文档下所有数据

查看文档下所有数据,向 ES 服务器发 GET 请求 :http://192.168.33.200:9200/shopping/_search。

返回结果如下:

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
{
"took": 79,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 7,
"relation": "eq"
},
"max_score": 1.0,
"hits": [
{
"_index": "shopping",
"_type": "_doc",
"_id": "zgk1lHkBX7dGZfoqFmJR", //先创建的
"_score": 1.0,
"_source": {
"title": "小米手机",
"category": "小米",
"images": "http://www.gulixueyuan.com/xm.jpg",
"price": 3999.00
}
},
{
"_index": "shopping",
"_type": "_doc",
"_id": "20210522", //后创建的
"_score": 1.0,
"_source": {
"title": "小米手机",
"category": "小米10",
"images": "http://www.coderblue.cn/blue.jpg",
"price": 4999.00
}
}
]
}
}

文档修改

1、全量修改

和新增文档一样,输入相同的 URL 地址请求,如果请求体变化,会将原有的数据内容覆盖

在 Postman 中,向 ES 服务器发 POST 请求 : http://127.0.0.1:9200/shopping/_doc/20210522

请求体JSON内容为:

1
2
3
4
5
6
{
"title":"Blue手机",
"category":"小米10",
"images":"http://www.coderblue.cn/blue.jpg",
"price":4999.00
}

修改成功后,服务器响应结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"_index": "shopping",
"_type": "_doc",
"_id": "20210522",
"_version": 5,
"result": "updated",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 10,
"_primary_term": 1
}

2、局部修改

修改数据时,也可以只修改某一给条数据的局部信息

在 Postman 中,向 ES 服务器发 POST 请求 : http://192.168.33.200:9200/shopping/_update/20210522。

请求体JSON内容为:

1
2
3
4
5
6
{
"doc": {
"title":"Blue",
"color":"pink"
}
}

查询修改结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"_index": "shopping",
"_type": "_doc",
"_id": "20210522",
"_version": 10,
"_seq_no": 15,
"_primary_term": 1,
"found": true,
"_source": {
"title": "Blue",
"category": "小米10",
"images": "http://www.coderblue.cn/blue.jpg",
"price": 4999.0,
"color": "pink"
}
}

文档删除

删除一个文档不会立即从磁盘上移除,它只是被标记成已删除(逻辑删除)

在 Postman 中,向 ES 服务器发 DELETE 请求 : http://192.168.33.200:9200/shopping/_doc/20210522

条件查询

1、URL带参查询(不推荐)

查找category为小米的文档,在 Postman 中,向 ES 服务器发 GET请求 :http://192.168.33.200:9200/shopping/_search?q=category:小米,返回结果如下:

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
{
"took": 65,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 2.041739,
"hits": [
{
"_index": "shopping",
"_type": "_doc",
"_id": "20210522",
"_score": 2.041739,
"_source": {
"title": "小米手机",
"category": "小米10",
"images": "http://www.coderblue.cn/blue.jpg",
"price": 4999.00
}
}
]
}
}

2、请求体带参查询

接下带JSON请求体,还是查找category为小米的文档,在 Postman 中,向 ES 服务器发 GET请求 : http://192.168.33.200:9200/shopping/_search

1
2
3
4
5
6
7
{
"query":{
"match":{
"category":"小 Mac"
}
}
}

此为分词查询,中间空格即可,如上会查找 分类含有 小 和 Mac的(不分大小写)

3、带请求体方式的查找所有内容

查找所有文档内容,也可以这样,在 Postman 中,向 ES 服务器发 GET请求 : http://192.168.33.200:9200/shopping/_search,附带JSON体如下:

1
2
3
4
5
{
"query":{
"match_all":{}
}
}

则返回所有文档内容。

4、查询指定字段

如果你想查询指定字段,在 Postman 中,向 ES 服务器发 GET请求 : http://192.168.33.200:9200/shopping/_search,附带JSON体如下:

1
2
3
4
5
6
{
"query":{
"match_all":{}
},
"_source":["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
{
"took": 11,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 7,
"relation": "eq"
},
"max_score": 1.0,
"hits": [
{
"_index": "shopping",
"_type": "_doc",
"_id": "1QlVlHkBX7dGZfoqI2Kb",
"_score": 1.0,
"_source": {
"title": "华为手机"
}
},
{
"_index": "shopping",
"_type": "_doc",
"_id": "1glVlHkBX7dGZfoqWmKN",
"_score": 1.0,
"_source": {
"title": "iPhone 12"
}
},
// ...
]
}
}

5、多条件查询

当我们既想要查询分类为小米又要查询价格为12999.05时:

在 Postman 中,向 ES 服务器发 GET请求 : http://192.168.33.200:9200/shopping/_search,附带JSON体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"query":{
"bool":{
"must":[{ // 相当于 &&
"match":{
"category":"小米"
}
},{
"match":{
"price":12999.05
}
}]
}
}
}

注意:must相当于 &&,should相当于 ||

6.范围查询

当我们既想要查询分类为小米或Mac,且price大于10000时:

在 Postman 中,向 ES 服务器发 GET请求 : http://192.168.33.200:9200/shopping/_search,附带JSON体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"query":{
"bool":{
"should":[{
"match":{
"category":"小米"
}
},{
"match":{
"category":"Mac"
}
}],
"filter":{
"range":{
"price":{
"gt":10000
}
}
}
}

}
}

分页查询

在 Postman 中,向 ES 服务器发 GET请求 : http://192.168.33.200:9200/shopping/_search,附带JSON体如下:

1
2
3
4
5
6
7
{
"query":{
"match_all":{}
},
"from":0,
"size":2
}

查询排序

在 Postman 中,向 ES 服务器发 GET请求 : http://192.168.33.200:9200/shopping/_search,附带JSON体如下:

1
2
3
4
5
6
7
8
9
10
{
"query":{
"match_all":{}
},
"sort":{
"price":{
"order":"desc"
}
}
}

全文检索

这功能像搜索引擎那样,如品牌输入“小华”,返回结果带回品牌有“小米”和华为的。

在 Postman 中,向 ES 服务器发 GET请求 : http://192.168.33.200:9200/shopping/_search,附带JSON体如下:

1
2
3
4
5
6
7
{
"query":{
"match":{
"category" : "小华"
}
}
}

完全匹配

在 Postman 中,向 ES 服务器发 GET请求 : http://192.168.33.200:9200/shopping/_search,附带JSON体如下:

1
2
3
4
5
6
7
{
"query":{
"match_phrase":{
"category" : "为"
}
}
}

高亮查询

在 Postman 中,向 ES 服务器发 GET请求 : http://192.168.33.200:9200/shopping/_search,附带JSON体如下:

1、指定字段高亮查询

1
2
3
4
5
6
7
8
9
10
11
12
{
"query":{
"match_phrase":{
"category" : "为"
}
},
"highlight":{
"fields":{
"category":{} //<----高亮这字段
}
}
}

返回结果部分JSON示下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"_index": "shopping",
"_type": "_doc",
"_id": "CdR7sHgBaKNfVnMbsJb9",
"_score": 0.6931471,
"_source": {
"title": "华为手机",
"category": "华为",
"images": "http://www.gulixueyuan.com/xm.jpg",
"price": 1999
},
"highlight": {
"category": [
"华<em>为</em>" //默认高亮拼接
]
}
}

2、自定义高亮查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"query":{
"match_phrase":{
"category" : "为"
}
},
"highlight":{
"fields":{
"pre_tags": [
"<em>"
],
"post_tags": [
"</em>"
]
}
}
}

聚合查询 TODO

聚合允许使用者对 es 文档进行统计分析,类似与关系型数据库中的 group by,当然还有很多其他的聚合,例如取最大值max、平均值avg等等。

接下来按price字段进行分组:

在 Postman 中,向 ES 服务器发 GET请求 : http://192.168.33.200:9200/shopping/_search,附带JSON体如下:

1
2
3
4
5
6
7
8
9
{
"aggs":{ //聚合操作
"price_group":{ //名称,随意起名
"terms":{ //分组
"field":"price"//分组字段
}
}
}
}

若想对所有手机价格求平均值

1
2
3
4
5
6
7
8
9
10
{
"aggs":{
"price_avg":{ //名称,随意起名
"avg":{ //求平均
"field":"price"
}
}
},
"size":0 // 不需要展示原始数据列表
}

映射关系

有了索引库,等于有了数据库中的 database。

接下来就需要建索引库(index)中的映射了,类似于数据库(database)中的表结构(table)。

创建数据库表需要设置字段名称,类型,长度,约束等;索引库也一样,需要知道这个类型下有哪些字段,每个字段有哪些约束信息,这就叫做映射(mapping)。

先创建一个索引:

1
#PUT http://192.168.33.200:9200/user

创建映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# PUT http://192.168.33.200:9200/user/_mapping

{
"properties": {
"name":{
"type": "text",
"index": true
},
"sex":{
"type": "keyword",
"index": true
},
"tel":{
"type": "keyword",
"index": false
}
}
}

查询映射

1
#GET http://192.168.33.200:9200/user/_mapping

返回结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"user": {
"mappings": {
"properties": {
"name": {
"type": "text" // 查询使用分词可以模糊匹配,比如小米,分别查查 小 和 米
},
"sex": {
"type": "keyword" // 查询需要完全匹配
},
"tel": {
"type": "keyword",
"index": false // 没有创建索引,不支持查询
}
}
}
}
}

添加数据

1
2
3
4
5
6
7
# PUT/POST http://192.168.33.200:9200/user/_create/1001

{
"name":"小米",
"sex":"M",
"tel":"1234"
}

查询数据

1、查找name含有”小“数据

1
2
3
4
5
6
7
8
9
#GET http://192.168.33.200:9200/user/_search

{
"query":{
"match":{
"name":"小"
}
}
}

返回结果:

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
{
"took": 25,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"max_score": 0.110377684,
"hits": [
{
"_index": "user",
"_type": "_doc",
"_id": "1002",
"_score": 0.110377684,
"_source": {
"name": "小米",
"sex": "Man",
"tel": "1234"
}
},
{
"_index": "user",
"_type": "_doc",
"_id": "1001",
"_score": 0.09271726,
"_source": {
"name": "小米的",
"sex": "Man 10",
"tel": "1000"
}
}
]
}
}

2、查找sex含有”Man“数据

1
2
3
4
5
6
7
8
9
#GET http://192.168.33.200:9200/user/_search

{
"query":{
"match":{
"sex":"Man"
}
}
}

返回结果:

1
2
3
4
5
6
7
{
"query":{
"match":{
"sex":"Man"
}
}
}

找不想要的结果,只因创建映射时"sex"的类型为"keyword"。"sex"只能完全为 "Man",才能得出原数据。

3、查询不存在索引的字段

1
2
3
4
5
6
7
8
9
#GET http://192.168.33.200:9200/user/_search

{
"query":{
"match":{
"tel":"11"
}
}
}

返回结果:

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
{
"error": {
"root_cause": [
{
"type": "query_shard_exception",
"reason": "failed to create query: Cannot search on field [tel] since it is not indexed.",
"index_uuid": "iE_MhYipT-uBAm58LZvKbQ",
"index": "user"
}
],
"type": "search_phase_execution_exception",
"reason": "all shards failed",
"phase": "query",
"grouped": true,
"failed_shards": [
{
"shard": 0,
"index": "user",
"node": "vk-SF-MdRaSNJUisxE6GOg",
"reason": {
"type": "query_shard_exception",
"reason": "failed to create query: Cannot search on field [tel] since it is not indexed.",
"index_uuid": "iE_MhYipT-uBAm58LZvKbQ",
"index": "user",
"caused_by": {
"type": "illegal_argument_exception",
"reason": "Cannot search on field [tel] since it is not indexed."
}
}
}
]
},
"status": 400
}

参考:

Elasticsearch: 权威指南

带你走进神一样的Elasticsearch索引机制

进阶

数据操作流程

数据写

数据读