Elasticsearch学习笔记(5)

Elasticsearch文档基本操作

新建文档

首先新建一个索引,比如 blog。

然后向索引中添加一个文档。

1
2
3
4
5
6
PUT blog/_doc/1
{
"title":"Elasticsearch 文档基本操作",
"date":"2021-06-04",
"content":"Elasticsearch 文档基本操作,如何新建文档,删除文档,更新文档等"
}

Kibana添加成功响应结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"_index" : "blog",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 2,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1
}
  • _index表示文档索引。
  • _type表示文档类型。
  • _id 表示文档id。
  • _version 表示文档版本(更新文档,版本会自动+1,针对文档的)
  • result表示执行结果类型。
  • _shards表示分片信息。
  • _seq_no_primary_term也是版本控制使用的(针对当前索引的)。

当然,添加文档时也可以不指定id,此时系统会默认给出一个id。如果不指定id,则需要使用 POST 请求,而不能使用 PUT 请求。

1
2
3
4
5
6
POST blog/_doc
{
"title":"Elasticsearch 文档基本操作1",
"date":"2021-06-04",
"content":"Elasticsearch 文档基本操作,如何新建文档,删除文档,更新文档等1"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"_index" : "blog",
"_type" : "_doc",
"_id" : "8Ld91nkBirAhYR49g5lr",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 2,
"failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1
}

查询文档

Elasticsearch 提供了 GET API 来查看文档。

1
GET blog/_doc/1

返回信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"_index" : "blog",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"title" : "Elasticsearch 文档基本操作",
"date" : "2021-06-04",
"content" : "Elasticsearch 文档基本操作,如何新建文档,删除文档,更新文档等"
}
}

如果获取不存在的文档,会返回未找到。

1
GET blog/_doc/2
1
2
3
4
5
6
{
"_index" : "blog",
"_type" : "_doc",
"_id" : "2",
"found" : false
}

如果只是想知道某个文档是否存在,使用 HEAD 请求。

1
2
3
4
请求:
HEAD blog/_doc/1
返回:
200 - OK
1
2
3
4
请求:
HEAD blog/_doc/2
返回:
{"statusCode":404,"error":"Not Found","message":"404 - Not Found"}

当然也可以批量获取文档。

1
2
3
4
GET blog/_mget
{
"ids":["1","8Ld91nkBirAhYR49g5lr"]
}

GET 请求携带请求体的问题?

某些特定的语言,例如 JavaScript 的HTTP请求是不允许 GET请求有请求体的,实际上在 RFC7231 文档中,并没有规定GET 请求的请求体改如何处理,这样造成了一定程度的混乱,有的HTTP服务器支持GET请求携带请求体,有的HTTP服务器则不支持。虽然ES工程师倾向于使用GET做查询,但是为了保证兼容性,ES同时也支持使用POST查询。

1
2
3
4
POST blog/_mget
{
"ids":["1","8Ld91nkBirAhYR49g5lr"]
}

上面方法也是可以的。

更新文档

普通更新

注意,文档更新一次,_version就会加1。

1
2
3
4
PUT blog/_doc/8Ld91nkBirAhYR49g5lr
{
"title":"123456"
}

这种方式更新的文档会覆盖原文档内容,如果我们只想更新某个字段,这种方式需要把其他未变化的字段也带上。

大多数时候,我们只想更新文档字段,这个可以通过脚本来实现。

1
2
3
4
5
6
7
8
9
10
POST blog/_update/1
{
"script":{
"lang": "painless",
"source": "ctx._source.title=params.title",
"params": {
"title":"123456"
}
}
}

更新请求格式:POST {index}/_update/{id}

在脚本中,lang 表示脚本语言,painless 是ES 内置的一种脚本语言。source 表示具体执行的脚本,ctx是一个上下文对象,通过 ctx可以访问到 _source_title等字段。

也可以向文档中添加字段。

1
2
3
4
5
6
7
POST blog/_update/1
{
"script":{
"lang": "painless",
"source": "ctx._source.tags=[\"java\",\"php\"]"
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"_index" : "blog",
"_type" : "_doc",
"_id" : "1",
"_version" : 3,
"_seq_no" : 4,
"_primary_term" : 2,
"found" : true,
"_source" : {
"title" : "123456",
"date" : "2021-06-04",
"content" : "Elasticsearch 文档基本操作,如何新建文档,删除文档,更新文档等",
"tags" : [
"java",
"php"
]
}
}

通过脚本语言,也可以修改数组字段:

1
2
3
4
5
6
7
POST blog/_update/1
{
"script":{
"lang": "painless",
"source": "ctx._source.tags.add(\"javascript\")"
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"_index" : "blog",
"_type" : "_doc",
"_id" : "1",
"_version" : 4,
"_seq_no" : 5,
"_primary_term" : 2,
"found" : true,
"_source" : {
"title" : "123456",
"date" : "2021-06-04",
"content" : "Elasticsearch 文档基本操作,如何新建文档,删除文档,更新文档等",
"tags" : [
"java",
"php",
"javascript"
]
}
}

当然。也可以使用 if else 构造稍微复杂一点的逻辑。

1
2
3
4
5
6
7
POST blog/_update/1
{
"script":{
"lang": "painless",
"source": "if(ctx._source.tags.contains(\"java\")){ctx.op=\"delete\"}else{ctx.op=\"none\"}"
}
}

结果:

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

查询更新

通过条件查询找到文档,然后再去更新。

将title中包含12345的文档的content修改为 12345.

1
2
3
4
5
6
7
8
9
10
11
12
POST blog/_update_by_query
{
"script": {
"source": "ctx._source.content=\"12345\"",
"lang": "painless"
},
"query": {
"term": {
"title": "12345"
}
}
}

删除文档

根据id删除

从索引中删除一个文档

1
DELETE blog/_doc/1

结果

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

如果在添加文档时指定了路由,则删除文档时也需要指定路由,否则删除失败。

查询删除

查询删除是POST请求

1
2
3
4
5
6
7
8
POST blog/_delete_by_query
{
"query":{
"term":{
"title":"12345"
}
}
}

也可以删除某一个索引下的所有文档。

1
2
3
4
5
6
7
8
POST blog/_delete_by_query
{
"query":{
"match_all":{

}
}
}

批量操作

ES中通过bulk API 可以执行批量索引、批量删除、批量更新等操作。

首先需要将所有的批量操作写入到一个JSON文件中,然后通过POST请求将该JSON文件上传并执行。

例如新建一个名为aaa.json的文件,内容如下:

1
2
3
4
5
{"index":{"_index":"user","_id":"666"}}
{"name":"sakuratears"}
{"update":{"_index":"user","_id":"666"}}
{"doc":{"name":"sakura"}}

第一行:index表示执行索引操作(表示一个action,其他action还有 create、delete、update)。

第二行是第一行的操作参数。

第三行的update则表示更新操作。

第四行是第三行的操作参数。

注意,结尾要空出一行。

index创建索引和create创建索引的区别是create创建索引如果索引已经存在则会创建失败。

文件创建成功后,直接在该目录下执行请求命令:

1
curl -XPOST "http://localhost:9200/user/_bulk" -H "content-type:application/json" --data-binary @aaa.json

当然,我们如果不新建文件,直接在 Kibana 开发者工具里也是可以执行的。

1
2
3
4
5
POST _bulk
{"index":{"_index":"user","_id":"666"}}
{"name":"sakuratears"}
{"update":{"_index":"user","_id":"666"}}
{"doc":{"name":"sakura"}}

结果如下:

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
{
"took" : 2358,
"errors" : false,
"items" : [
{
"index" : {
"_index" : "user",
"_type" : "_doc",
"_id" : "666",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1,
"status" : 201
}
},
{
"update" : {
"_index" : "user",
"_type" : "_doc",
"_id" : "666",
"_version" : 2,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1,
"status" : 200
}
}
]
}

Elasticsearch 文档路由

ES 是一个分布式系统,当我们存储一个文档到 ES 上之后,这个文档实际是被存储到 master 节点中的某一个主分片上。

那么就存在一个问题,ES如何知道这个文档被存放到哪一个分片上?

例如,新建一个索引,该索引有两个分片,0个副本,如下:

接下来,向索引中保存一个文档。

1
2
3
4
PUT blog/_doc/a
{
"title":"a"
}

文档保存成功后,可以查看该文档被保存到哪个分片。

1
GET _cat/shards/blog?v

查看结果如下:

1
2
3
4
index shard prirep state   docs store ip        node
blog 1 p STARTED 0 208b 127.0.0.1 slave02
blog 0 p STARTED 1 4.7kb 127.0.0.1 slave01

从这个结果中可以看出,文档被保存在分片 0 上。

我们在保存一个文档。

1
2
3
4
PUT blog/_doc/b
{
"title":"b"
}

继续查看分片,可以看到它被保存在分片 1 上。

1
2
3
index shard prirep state   docs store ip        node
blog 1 p STARTED 1 4.7kb 127.0.0.1 slave02
blog 0 p STARTED 1 4.9kb 127.0.0.1 slave01

那么ES 是按照什么样的规则去分配分片的呢?

ES中的路由机制是通过哈希算法,将具有相同哈希值的文档放到同一个主分片中,分片位置的计算方式如下:

1
shard = hash(routing)%number_of_primary_shards

routing可以是一个任意字符串,ES默认是将文档的id作为routing值,通过哈希函数根据routing生成一个数字,然后将该数字和分片数取余。取余的结果就是分片的位置。

默认的这种路由模式,最大的优势在于负载均衡,这种方式可以保证数据平均分配在不同的分片上。但是有一个很大的劣势。就是查询时候无法确定文档位置,此时它会将请求广播到所有的分片上去执行。另一方面,使用默认的路由模式,后期修改分片数量十分不方便。

开发者也可以自定义routing的值,方式如下:

1
2
3
4
PUT blog/_doc/d?routing=sakura
{
"title":"d"
}

如果文档在添加时指定了routing,则查询、删除、更新是也需要指定routing。

1
GET blog/_doc/d?routing=sakura

自定义routing可能导致负载不均衡,这需要结合实际情况选择。

典型场景:

对于用户数据,我们可以将 userId 作为 routing,这样就能保证同一个用户的数据保存在同一个分片中,检索时,同样使用userId作为routing,这样就可以精确的从某一个分片中获取数据。

Elasticsearch 文档版本控制

当我们使用 ES API 进行文档更新时,它首先读取原文档,然后对文档进行更新,然后再重新索引整个文档。无论执行多少次更新,最终保存在 ES 中的是最后一次更新的文档。

但是如果有两个线程同时更新,就可能会出现问题。

要解决问题,就要用到锁。

悲观锁

每一次读取数据时,都认为数据可能会被修改,所以屏蔽一切可能破坏数据完整性的操作。关系型数据库中,悲观锁使用较多,例如行锁、表锁等。

乐观锁

每次读取数据时,都认为数据不会被修改,因此不锁定数据,只有在提交数据时,检查数据完整性。这种方式可以省去锁的开销,进而提高吞吐量。

在 ES 中,实际上使用的就是乐观锁。

版本控制

ES 6.7 版本之前,使用 _version + _version_type 来进行乐观并发控制。根据前面的介绍,文档每被修改一次,_version就会自增1次,ES 通过 _version 字段来确保所有的操作都有序进行。

version 分为内部版本控制和外部版本控制。

内部版本控制

ES 自己维护的就是内部版本,当创建一个文档时,ES 会给文档版本赋值为1。

每当用户修改一次文档,版本号就会自增1。

如果使用内部版本,ES 要求 _version 参数必须和 ES 文档中 _version的值相等才能操作成功。

外部版本控制

ES也可以通过外部版本进行版本控制。

添加文档时,PUT请求后添加参数 version 和 version_type。

version_type 有 external 和 external_gte 两种。

external 表示更新时版本号参数必须大于文档版本号。

external_gte 表示更新时版本号参数必须大于等于文档版本号。

1
2
3
4
PUT blog/_doc/1?version=200&version_type=external
{
"title":"2222"
}

新版本控制(ES 6.7之后)

现在使用 _seq_no_primary_term 两个参数来进行并发控制。

_seq_no 不属于某一个文档,它属于整个索引的。(_version 则是属于某一个文档的,每个文档的_version互不影响)

现在更想文档时,使用 _seq_no 来做并发。由于 _seq_no 是属于整个 索引的,所以索引下任何文档的修改或者新增,_seq_no 都会自增。

现在就可以通过 _seq_no_primary_term 来做并发控制。

1
2
3
4
PUT blog/_doc/e?if_seq_no=3&if_primary_term=1
{
"title":"1212121"
}




-------------文章结束啦 ~\(≧▽≦)/~ 感谢您的阅读-------------

您的支持就是我创作的动力!

欢迎关注我的其它发布渠道