elasticsearch 数据输入和输出

Elastcisearch 是分布式的 文档 存储。它能存储和检索复杂的数据结构—​序列化成为JSON文档—​以 实时 的方式。 换句话说,一旦一个文档被存储在 Elasticsearch 中,它就是可以被集群中的任意节点检索到。

在 Elasticsearch 中, 每个字段的所有数据 都是 默认被索引的 。 即每个字段都有为了快速检索设置的专用倒排索引。而且,不像其他多数的数据库,它能在 同一个查询中 使用所有这些倒排索引,并以惊人的速度返回结果。

ElasticSearch 和 MySQL 结构对照

ES MySQL
Node/Cluster Cluster
Index Database
Type table
Document row (一行)
field field (一列)

(1) 什么是文档?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"name": "John Smith",
"age": 42,
"confirmed": true,
"join_date": "2014-06-01",
"home": {
"lat": 51.5,
"lon": 0.1
},
"accounts": [
{
"type": "facebook",
"id": "johnsmith"
},
{
"type": "twitter",
"id": "johnsmith"
}
]
}

在 Elasticsearch 中,术语 文档 有着特定的含义。它是指最顶层或者根对象, 这个根对象被序列化成 JSON 并存储到 Elasticsearch 中,指定了唯一 ID。

字段的名字可以是任何合法的字符串,但 不可以 包含英文句号(.)。

(2) 文档元数据

一个文档不仅仅包含它的数据 ,也包含 元数据 —— 有关 文档的信息。 三个必须的元数据元素如下:

_index 文档在哪存放

_type 文档表示的对象类别
_id 文档唯一标识

(2.1) _index

一个 索引 应该是因共同的特性被分组到一起的文档集合。 例如,你可能存储所有的产品在索引 products 中,而存储所有销售的交易到索引 sales 中。 虽然也允许存储不相关的数据到一个索引中,但这通常看作是一个反模式的做法。

实际上,在 Elasticsearch 中,我们的数据是被存储和索引在 分片 中,而一个索引仅仅是逻辑上的命名空间, 这个命名空间由一个或者多个分片组合在一起。 然而,这是一个内部细节,我们的应用程序根本不应该关心分片,对于应用程序而言,只需知道文档位于一个 索引 内。 Elasticsearch 会处理所有的细节。

(2.2) _type

数据可能在索引中只是松散的组合在一起,但是通常明确定义一些数据中的子分区是很有用的。 例如,所有的产品都放在一个索引中,但是你有许多不同的产品类别,比如 “electronics” 、 “kitchen” 和 “lawn-care”。

这些文档共享一种相同的(或非常相似)的模式:他们有一个标题、描述、产品代码和价格。他们只是正好属于“产品”下的一些子类。

Elasticsearch 公开了一个称为 types (类型)的特性,它允许您在索引中对数据进行逻辑分区。不同 types 的文档可能有不同的字段,但最好能够非常相似。 我们将在 类型和映射 中更多的讨论关于 types 的一些应用和限制。

一个 _type 命名可以是大写或者小写,但是不能以下划线或者句号开头,不应该包含逗号, 并且长度限制为256个字符. 我们使用 blog 作为类型名举例。

(2.3) _id

ID 是一个字符串,当它和 _index 以及 _type 组合就可以唯一确定 Elasticsearch 中的一个文档。 当你创建一个新的文档,要么提供自己的 _id ,要么让 Elasticsearch 帮你生成。

(3) 索引文档

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

(3.1) 使用自定义id

1
2
3
4
5
PUT ip:port/{index}/{type}/{id}
{
"field": "value",
...
}

如果我们的索引称为 website ,类型称为 blog ,并且选择 123 作为 ID ,那么索引请求应该是下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ curl -X PUT "localhost:9200/website/blog/123?pretty" -H 'Content-Type: application/json' -d'
> {
> "title": "My first blog entry",
> "text": "Just trying this out...",
> "date": "2014/01/01"
> }
> '
{
"_index" : "website",
"_type" : "blog",
"_id" : "123",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1
}

该响应表明文档已经成功创建,该索引包括 _index 、 _type 和 _id 元数据, 以及一个新元素: _version 。

在 Elasticsearch 中每个文档都有一个版本号。当每次对文档进行修改时(包括删除), _version 的值会递增。 在 处理冲突 中,我们讨论了怎样使用 _version 号码确保你的应用程序中的一部分修改不会覆盖另一部分所做的修改。

(3.2) Autogenerating IDs

如果你的数据没有自然的 ID, Elasticsearch 可以帮我们自动生成 ID 。 请求的结构调整为: 不再使用 PUT 谓词(“使用这个 URL 存储这个文档”), 而是使用 POST 谓词(“存储文档在这个 URL 命名空间下”)。

现在该 URL 只需包含 _index 和 _type :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ curl -X POST "localhost:9200/website/blog/?pretty" -H 'Content-Type: application/json' -d'
> {
> "title": "My second blog entry",
> "text": "Still trying this out...",
> "date": "2014/01/01"
> }
> '
{
"_index" : "website",
"_type" : "blog",
"_id" : "1aMWwW8BXjhxAah6xIse",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1
}

自动生成的 ID 是 URL-safe、 基于 Base64 编码且长度为20个字符的 GUID 字符串。 这些 GUID 字符串由可修改的 FlakeID 模式生成,这种模式允许多个节点并行生成唯一 ID ,且互相之间的冲突概率几乎为零。

(4) 取回一个文档

为了从 Elasticsearch 中检索出文档,我们仍然使用相同的 _index , _type , 和 _id ,但是 HTTP 谓词更改为 GET :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ curl -X GET "localhost:9200/website/blog/123?pretty&pretty"
{
"_index" : "website",
"_type" : "blog",
"_id" : "123",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"title" : "My first blog entry",
"text" : "Just trying this out...",
"date" : "2014/01/01"
}
}

在请求的查询串参数中加上 pretty 参数,正如前面的例子中看到的,这将会调用 Elasticsearch 的 pretty-print 功能,该功能 使得 JSON 响应体更加可读。但是, _source 字段不能被格式化打印出来。相反,我们得到的 _source 字段中的 JSON 串,刚好是和我们传给它的一样。

1
2
3
4
5
6
7
8
9
10
11
$ curl -i -XGET http://localhost:9200/website/blog/124?pretty
HTTP/1.1 404 Not Found
content-type: application/json; charset=UTF-8
content-length: 83

{
"_index" : "website",
"_type" : "blog",
"_id" : "124",
"found" : false
}

返回文档的一部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ curl -X GET "localhost:9200/website/blog/123?_source=title,text&pretty"
{
"_index" : "website",
"_type" : "blog",
"_id" : "123",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"text" : "Just trying this out...",
"title" : "My first blog entry"
}
}

只想得到 _source 字段,不需要任何元数据,你能使用 _source 端点

1
2
3
4
5
6
$ curl -X GET "localhost:9200/website/blog/123/_source?pretty"
{
"title" : "My first blog entry",
"text" : "Just trying this out...",
"date" : "2014/01/01"
}

(5) 检查文档是否存在

如果只想检查一个文档是否存在–根本不想关心内容—​那么用 HEAD 方法来代替 GET 方法。 HEAD 请求没有返回体,只返回一个 HTTP 请求报头

1
2
3
4
$ curl -i -XHEAD http://localhost:9200/website/blog/123
HTTP/1.1 200 OK
content-type: application/json; charset=UTF-8
content-length: 215

果文档存在, Elasticsearch 将返回一个 200 ok 的状态码

1
2
3
4
$ curl -i -XHEAD http://localhost:9200/website/blog/124
HTTP/1.1 404 Not Found
content-type: application/json; charset=UTF-8
content-length: 61

文档不存在, Elasticsearch 将返回一个 404 Not Found 的状态码

(6) 更新整个文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ curl -X PUT "localhost:9200/website/blog/123?pretty" -H 'Content-Type: application/json' -d'
> {
> "title": "My first blog entry",
> "text": "I am starting to get the hang of this...",
> "date": "2014/01/02"
> }
> '
{
"_index" : "website",
"_type" : "blog",
"_id" : "123",
"_version" : 2,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1
}

created 标志设置成 false ,是因为相同的索引、类型和 ID 的文档已经存在。

在内部,Elasticsearch 已将旧文档标记为已删除,并增加一个全新的文档。 尽管你不能再对旧版本的文档进行访问,但它并不会立即消失。当继续索引更多的数据,Elasticsearch 会在后台清理这些已删除文档。

(7) 创建新文档

当我们索引一个文档,怎么确认我们正在创建一个完全新的文档,而不是覆盖现有的呢?

请记住, _index 、 _type 和 _id 的组合可以唯一标识一个文档。所以,确保创建一个新文档的最简单办法是,使用索引请求的 POST 形式让 Elasticsearch 自动生成唯一 _id :

1
2
3
4
5
$ curl POST "localhost:9200//website/blog/" -H 'Content-Type: application/json' -d'
{
...
}
'

如果已经有自己的 _id ,那么我们必须告诉 Elasticsearch ,只有在相同的 _index 、 _type 和 _id 不存在时才接受我们的索引请求。

第一种方法使用 op_type 查询-字符串参数:

1
2
PUT /website/blog/123?op_type=create
{ ... }

第二种方法是在 URL 末端使用 /_create :

1
2
PUT /website/blog/123/_create
{ ... }

如果创建新文档的请求成功执行,Elasticsearch 会返回元数据和一个 201 Created 的 HTTP 响应码。

另一方面,如果具有相同的 _index 、 _type 和 _id 的文档已经存在,Elasticsearch 将会返回 409 Conflict 响应码,以及如下的错误信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"error": {
"root_cause": [
{
"type": "document_already_exists_exception",
"reason": "[blog][123]: document already exists",
"shard": "0",
"index": "website"
}
],
"type": "document_already_exists_exception",
"reason": "[blog][123]: document already exists",
"shard": "0",
"index": "website"
},
"status": 409
}

(8) 删除文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ curl -X DELETE "localhost:9200/website/blog/123?pretty"
{
"_index" : "website",
"_type" : "blog",
"_id" : "123",
"_version" : 3,
"result" : "deleted",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 2,
"_primary_term" : 1
}

删除文档不会立即将文档从磁盘中删除,只是将文档标记为已删除状态。随着你不断的索引更多的数据,Elasticsearch 将会在后台清理标记为已删除的文档。

如果文档没有找到,我们将得到 404 Not Found 的响应码和类似这样的响应体:

1
2
3
4
5
6
7
{
"found" : false,
"_index" : "website",
"_type" : "blog",
"_id" : "123",
"_version" : 4
}

即使文档不存在( Found 是 false ), _version 值仍然会增加。

(9) 处理冲突

丢失了一个变更就是 非常严重的 。

变更越频繁,读数据和更新数据的间隙越长,也就越可能丢失变更。

(9.1) 冲突解决

在数据库领域中,有两种方法通常被用来确保并发更新时变更不会丢失:

(9.1.1) 悲观并发控制

这种方法被关系型数据库广泛使用,它假定有变更冲突可能发生,因此阻塞访问资源以防止冲突。 一个典型的例子是读取一行数据之前先将其锁住,确保只有放置锁的线程能够对这行数据进行修改。 

(9.1.2) 乐观并发控制

Elasticsearch 中使用的这种方法假定冲突是不可能发生的,并且不会阻塞正在尝试的操作。 然而,如果源数据在读写当中被修改,更新将会失败。应用程序接下来将决定该如何解决冲突。 例如,可以重试更新、使用新的数据、或者将相关情况报告给用户。 

(10) 乐观并发控制

Elasticsearch 是分布式的。当文档创建、更新或删除时, 新版本的文档必须复制到集群中的其他节点。Elasticsearch 也是异步和并发的,这意味着这些复制请求被并行发送,并且到达目的地时也许 顺序是乱的 。 Elasticsearch 需要一种方法确保文档的旧版本不会覆盖新的版本。

当我们之前讨论 index , GET 和 delete 请求时,我们指出每个文档都有一个 _version (版本)号,当文档被修改时版本号递增。 Elasticsearch 使用这个 _version 号来确保变更以正确顺序得到执行。如果旧版本的文档在新版本之后到达,它可以被简单的忽略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ curl -X PUT "localhost:9200/website/blog/1/_create?pretty" -H 'Content-Type: application/json' -d'
> {
> "title": "My first blog entry",
> "text": "Just trying this out..."
> }
> '
{
"_index" : "website",
"_type" : "blog",
"_id" : "1",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1
}

响应体告诉我们,这个新创建的文档 _version 版本号是 1 。现在假设我们想编辑这个文档:我们加载其数据到 web 表单中, 做一些修改,然后保存新的版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ curl -X GET "localhost:9200/website/blog/1?pretty"
{
"_index" : "website",
"_type" : "blog",
"_id" : "1",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"title" : "My first blog entry",
"text" : "Just trying this out..."
}
}

当我们尝试通过重建文档的索引来保存修改,我们指定 version 为我们的修改会被应用的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ curl -X PUT "localhost:9200/website/blog/1?version=1&pretty" -H 'Content-Type: application/json' -d'
> {
> "title": "My first blog entry",
> "text": "Starting to get the hang of this..."
> }
> '
{
"_index" : "website",
"_type" : "blog",
"_id" : "1",
"_version" : 2,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1
}

我们想这个在我们索引中的文档只有现在的 _version 为 1 时,本次更新才能成功。

重新运行相同的索引请求,仍然指定 version=1 , Elasticsearch 返回 409 Conflict HTTP 响应码,和一个如下所示的响应体:

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
$ curl -X PUT "localhost:9200/website/blog/1?version=1&pretty" -H 'Content-Type: application/json' -d'
> {
> "title": "My first blog entry",
> "text": "Starting to get the hang of this..."
> }
> '
{
"error" : {
"root_cause" : [
{
"type" : "version_conflict_engine_exception",
"reason" : "[blog][1]: version conflict, current version [2] is different than the one provided [1]",
"index_uuid" : "1Ptx5N-iTR2nDNtVgVMEpw",
"shard" : "3",
"index" : "website"
}
],
"type" : "version_conflict_engine_exception",
"reason" : "[blog][1]: version conflict, current version [2] is different than the one provided [1]",
"index_uuid" : "1Ptx5N-iTR2nDNtVgVMEpw",
"shard" : "3",
"index" : "website"
},
"status" : 409
}

所有文档的更新或删除 API,都可以接受 version 参数,这允许你在代码中使用乐观的并发控制,这是一种明智的做法。

(10.2) 通过外部系统使用版本控制

一个常见的设置是使用其它数据库作为主要的数据存储,使用 Elasticsearch 做数据检索, 这意味着主数据库的所有更改发生时都需要被复制到 Elasticsearch ,如果多个进程负责这一数据同步,你可能遇到类似于之前描述的并发问题。

如果你的主数据库已经有了版本号 — 或一个能作为版本号的字段值比如 timestamp — 那么你就可以在 Elasticsearch 中通过增加 version_type=external 到查询字符串的方式重用这些相同的版本号, 版本号必须是大于零的整数, 且小于 9.2E+18 — 一个 Java 中 long 类型的正值。

外部版本号的处理方式和我们之前讨论的内部版本号的处理方式有些不同, Elasticsearch 不是检查当前 _version 和请求中指定的版本号是否相同, 而是检查当前 _version 是否 小于 指定的版本号。 如果请求成功,外部的版本号作为文档的新 _version 进行存储。

外部版本号不仅在索引和删除请求是可以指定,而且在 创建 新文档时也可以指定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ curl -X PUT "localhost:9200/website/blog/2?version=5&version_type=external&pretty" -H 'Content-Type: application/json' -d'
> {
> "title": "My first external blog entry",
> "text": "Starting to get the hang of this..."
> }
> '
{
"_index" : "website",
"_type" : "blog",
"_id" : "2",
"_version" : 5,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1
}

在响应中,我们能看到当前的 _version 版本号是 5

现在我们更新这个文档,指定一个新的 version 号是 10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ curl -X PUT "localhost:9200/website/blog/2?version=10&version_type=external&pretty" -H 'Content-Type: application/json' -d'
> {
> "title": "My first external blog entry",
> "text": "This is a piece of cake..."
> }
> '
{
"_index" : "website",
"_type" : "blog",
"_id" : "2",
"_version" : 10,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1
}

请求成功并将当前 _version 设为 10

(11) 文档的部分更新

我们也介绍过文档是不可变的:他们不能被修改,只能被替换。 update API 必须遵循同样的规则。 从外部来看,我们在一个文档的某个位置进行部分更新。然而在内部, update API 简单使用与之前描述相同的 检索-修改-重建索引 的处理过程。 区别在于这个过程发生在分片内部,这样就避免了多次请求的网络开销。通过减少检索和重建索引步骤之间的时间,我们也减少了其他进程的变更带来冲突的可能性。

update 请求最简单的一种形式是接收文档的一部分作为 doc 的参数, 它只是与现有的文档进行合并。对象被合并到一起,覆盖现有的字段,增加新的字段。 例如,我们增加字段 tags 和 views 到我们的博客文章,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ curl -X POST "localhost:9200/website/blog/1/_update?pretty" -H 'Content-Type: application/json' -d'
> {
> "doc" : {
> "tags" : [ "testing" ],
> "views": 0
> }
> }
> '
{
"_index" : "website",
"_type" : "blog",
"_id" : "1",
"_version" : 3,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 2,
"_primary_term" : 1
}

查询更新后的文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ curl -X GET "localhost:9200/website/blog/1?pretty"
{
"_index" : "website",
"_type" : "blog",
"_id" : "1",
"_version" : 3,
"_seq_no" : 2,
"_primary_term" : 1,
"found" : true,
"_source" : {
"title" : "My first blog entry",
"text" : "Starting to get the hang of this...",
"views" : 0,
"tags" : [
"testing"
]
}
}

(11.1) 使用脚本部分更新文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ curl -X POST "localhost:9200/website/blog/1/_update?pretty" -H 'Content-Type: application/json' -d'
> {
> "script" : "ctx._source.views+=1"
> }
> '
{
"_index" : "website",
"_type" : "blog",
"_id" : "1",
"_version" : 4,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 3,
"_primary_term" : 1
}

(11.2) 使用脚本部分更新文档

对于那些 API 不能满足需求的情况,Elasticsearch 允许你使用脚本编写自定义的逻辑。 许多API都支持脚本的使用,包括搜索、排序、聚合和文档更新。 脚本可以作为请求的一部分被传递,从特殊的 .scripts 索引中检索,或者从磁盘加载脚本。

默认的脚本语言 是 Groovy,一种快速表达的脚本语言,在语法上与 JavaScript 类似。 它在 Elasticsearch V1.3.0 版本首次引入并运行在 沙盒 中,然而 Groovy 脚本引擎存在漏洞, 允许攻击者通过构建 Groovy 脚本,在 Elasticsearch Java VM 运行时脱离沙盒并执行 shell 命令。

因此,在版本 v1.3.8 、 1.4.3 和 V1.5.0 及更高的版本中,它已经被默认禁用。 此外,您可以通过设置集群中的所有节点的 config/elasticsearch.yml 文件来禁用动态 Groovy 脚本:

script.groovy.sandbox.enabled: false

这将关闭 Groovy 沙盒,从而防止动态 Groovy 脚本作为请求的一部分被接受, 或者从特殊的 .scripts 索引中被检索。当然,你仍然可以使用存储在每个节点的 config/scripts/ 目录下的 Groovy 脚本。

如果你的架构和安全性不需要担心漏洞攻击,例如你的 Elasticsearch 终端仅暴露和提供给可信赖的应用, 当它是你的应用需要的特性时,你可以选择重新启用动态脚本。

你可以在 scripting reference documentation 获取更多关于脚本的资料。

通过设置参数 retry_on_conflict 来自动完成, 这个参数规定了失败之前 update 应该重试的次数,它的默认值为 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ curl -X POST "localhost:9200/website/blog/1/_update?retry_on_conflict=5&pretty" -H 'Content-Type: application/json' -d'
> {
> "script" : "ctx._source.views+=1",
> "upsert": {
> "views": 0
> }
> }
> '
{
"_index" : "website",
"_type" : "blog",
"_id" : "1",
"_version" : 6,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 5,
"_primary_term" : 1
}

(12) 取回多个文档

Elasticsearch 的速度已经很快了,但甚至能更快。 将多个请求合并成一个,避免单独处理每个请求花费的网络延时和开销。 如果你需要从 Elasticsearch 检索很多文档,那么使用 multi-get 或者 mget API 来将这些检索请求放在一个请求中,将比逐个文档请求更快地检索到全部文档。

mget API 要求有一个 docs 数组作为参数,每个元素包含需要检索文档的元数据, 包括 _index 、 _type 和 _id 。如果你想检索一个或者多个特定的字段,那么你可以通过 _source 参数来指定这些字段的名字:

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
$ curl -X GET "localhost:9200/_mget?pretty" -H 'Content-Type: application/json' -d'
> {
> "docs" : [
> {
> "_index" : "website",
> "_type" : "blog",
> "_id" : 2
> },
> {
> "_index" : "website",
> "_type" : "pageviews",
> "_id" : 1,
> "_source": "views"
> }
> ]
> }
> '
{
"docs" : [
{
"_index" : "website",
"_type" : "blog",
"_id" : "2",
"_version" : 10,
"_seq_no" : 1,
"_primary_term" : 1,
"found" : true,
"_source" : {
"title" : "My first external blog entry",
"text" : "This is a piece of cake..."
}
},
{
"_index" : "website",
"_type" : "pageviews",
"_id" : "1",
"found" : false
}
]
}

该响应体也包含一个 docs 数组, 对于每一个在请求中指定的文档,这个数组中都包含有一个对应的响应,且顺序与请求中的顺序相同。 其中的每一个响应都和使用单个 get request 请求所得到的响应体相同

如果想检索的数据都在相同的 _index 中(甚至相同的 _type 中),则可以在 URL 中指定默认的 /_index 或者默认的 /_index/_type 。

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
$ curl -X GET "localhost:9200/website/blog/_mget?pretty" -H 'Content-Type: application/json' -d'
> {
> "docs" : [
> { "_id" : 2 },
> { "_type" : "pageviews", "_id" : 1 }
> ]
> }
> '
{
"docs" : [
{
"_index" : "website",
"_type" : "blog",
"_id" : "2",
"_version" : 10,
"_seq_no" : 1,
"_primary_term" : 1,
"found" : true,
"_source" : {
"title" : "My first external blog entry",
"text" : "This is a piece of cake..."
}
},
{
"_index" : "website",
"_type" : "pageviews",
"_id" : "1",
"found" : false
}
]
}
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
$ curl -X GET "localhost:9200/website/blog/_mget?pretty" -H 'Content-Type: application/json' -d'
> {
> "ids" : [ "2", "1" ]
> }
> '
{
"docs" : [
{
"_index" : "website",
"_type" : "blog",
"_id" : "2",
"_version" : 10,
"_seq_no" : 1,
"_primary_term" : 1,
"found" : true,
"_source" : {
"title" : "My first external blog entry",
"text" : "This is a piece of cake..."
}
},
{
"_index" : "website",
"_type" : "blog",
"_id" : "1",
"_version" : 6,
"_seq_no" : 5,
"_primary_term" : 1,
"found" : true,
"_source" : {
"title" : "My first blog entry",
"text" : "Starting to get the hang of this...",
"views" : 3,
"tags" : [
"testing"
]
}
}
]
}

(13) 代价较小的批量操作

与 mget 可以使我们一次取回多个文档同样的方式, bulk API 允许在单个步骤中进行多次 create 、 index 、 update 或 delete 请求。 如果你需要索引一个数据流比如日志事件,它可以排队和索引数百或数千批次。

1
2
3
4
5
{ action: { metadata }}\n
{ request body }\n
{ action: { metadata }}\n
{ request body }\n
...

这种格式类似一个有效的单行 JSON 文档 流 ,它通过换行符(\n)连接到一起。注意两个要点:
每行一定要以换行符(\n)结尾, 包括最后一行 。这些换行符被用作一个标记,可以有效分隔行。
这些行不能包含未转义的换行符,因为他们将会对解析造成干扰。这意味着这个 JSON 不 能使用 pretty 参数打印。

action/metadata 行指定 哪一个文档 做 什么操作 。

action 必须是以下选项之一:
create 如果文档不存在,那么就创建它。详情请见 创建新文档。
index 创建一个新文档或者替换一个现有的文档。详情请见 索引文档 和 更新整个文档。
update 部分更新一个文档。详情请见 文档的部分更新。
delete 删除一个文档。详情请见 删除文档。
metadata 应该指定被索引、创建、更新或者删除的文档的 _index 、 _type 和 _id 。

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
$ curl -X POST "localhost:9200/_bulk?pretty" -H 'Content-Type: application/json' -d'
> { "delete": { "_index": "website", "_type": "blog", "_id": "123" }}
> { "create": { "_index": "website", "_type": "blog", "_id": "123" }}
> { "title": "My first blog post" }
> { "index": { "_index": "website", "_type": "blog" }}
> { "title": "My second blog post" }
> { "update": { "_index": "website", "_type": "blog", "_id": "123", "_retry_on_conflict" : 3} }
> { "doc" : {"title" : "My updated blog post"} }
> '
{
"took" : 69,
"errors" : false,
"items" : [
{
"delete" : {
"_index" : "website",
"_type" : "blog",
"_id" : "123",
"_version" : 1,
"result" : "not_found",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 3,
"_primary_term" : 1,
"status" : 404
}
},
{
"create" : {
"_index" : "website",
"_type" : "blog",
"_id" : "123",
"_version" : 2,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 4,
"_primary_term" : 1,
"status" : 201
}
},
{
"index" : {
"_index" : "website",
"_type" : "blog",
"_id" : "1qMXwm8BXjhxAah64Ysk",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1,
"status" : 201
}
},
{
"update" : {
"_index" : "website",
"_type" : "blog",
"_id" : "123",
"_version" : 3,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 5,
"_primary_term" : 1,
"status" : 200
}
}
]
}

每个子请求都是独立执行,因此某个子请求的失败不会对其他子请求的成功与否造成影响。 如果其中任何子请求失败,最顶层的 error 标志被设置为 true ,并且在相应的请求报告出错误明细

(13.1) 批量执行时多大效率最好

整个批量请求都需要由接收到请求的节点加载到内存中,因此该请求越大,其他请求所能获得的内存就越少。 批量请求的大小有一个最佳值,大于这个值,性能将不再提升,甚至会下降。 但是最佳值不是一个固定的值。它完全取决于硬件、文档的大小和复杂度、索引和搜索的负载的整体情况。

幸运的是,很容易找到这个 最佳点 :通过批量索引典型文档,并不断增加批量大小进行尝试。 当性能开始下降,那么你的批量大小就太大了。一个好的办法是开始时将 1,000 到 5,000 个文档作为一个批次, 如果你的文档非常大,那么就减少批量的文档个数。

密切关注你的批量请求的物理大小往往非常有用,一千个 1KB 的文档是完全不同于一千个 1MB 文档所占的物理大小。 一个好的批量大小在开始处理后所占用的物理大小约为 5-15 MB。

References

[0] 数据输入和输出
[1] 什么是文档?
[2] 文档元数据
[3] 索引文档
[4] 取回一个文档
[5] 检查文档是否存在
[6] 更新整个文档
[7] 创建新文档
[8] 删除文档
[9] 处理冲突
[10] 乐观并发控制
[11] 文档的部分更新
[12] 取回多个文档
[13] 代价较小的批量操作