在 Elasticsearch 中,每一个文档都有一个版本号码。每当文档产生变化时(包括删除),_version
就会增大。在《版本控制》中,我们将会详细讲解如何使用_version
的数字来确认你的程序不会随意替换掉不想覆盖的数据。
自增 ID
如果我们的数据中没有天然的标示符,我们可以让 Elasticsearch 为我们自动生成一个。请求的结构发生了变化:我们把PUT
——“把文档存储在这个地址中”变量变成了POST
——“把文档存储在这个地址下”。
这样一来,请求中就只包含 _index
和_type
了:
POST /website/blog/
{
"title": "My second blog entry",
"text": "Still trying this out...",
"date": "2014/01/01"
}
这次的反馈和之前基本一样,只有_id
改成了系统生成的自增值:
{
"_index": "website",
"_type": "blog",
"_id": "wM0OSFhDQXGZAWDf0-drSA",
"_version": 1,
"created": true
}
自生成 ID 是由 22 个字母组成的,安全 universally unique identifiers 或者被称为UUIDs。
3.1 文档
文档是什么?
在很多程序中,大部分实体或者对象都被序列化为包含键和值的 JSON 对象。键是一个字段或者属性的名字,值可以是一个字符串、数字、布尔值、对象、数组或者是其他的特殊类型,比如代表日期的字符串或者代表地理位置的对象:
{
"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"
}
]
}
通常情况下,我们使用可以互换对象和文档。然而,还是有一个区别的。对象(object )仅仅是一个 JSON 对象,类似于哈希,哈希映射,字典或关联数组。对象(Objects)则可以包含其他对象(Objects)。
在 Elasticsearch 中,文档这个单词有特殊的含义。它指的是在 Elasticsearch 中被存储到唯一 ID 下的由最高级或者根对象 (root object )序列化而来的 JSON。
文档元数据
一个文档不只包含了数据。它还包含了元数据(metadata) —— 关于文档的信息。有三个元数据元素是必须存在的,它们是:
名字 | 说明 |
---|---|
_index | 文档存储的地方 |
_type | 文档代表的对象种类 |
_id | 文档的唯一编号 |
_index
索引 类似于传统数据库中的”数据库”——也就是我们存储并且索引相关数据的地方。
TIP:
在 Elasticsearch 中,我们的数据都在分片中被存储以及索引,索引只是一个逻辑命名空间,它可以将一个或多个分片组合在一起。然而,这只是一个内部的运作原理——我们的程序可以根本不用关心分片。对于我们的程序来说,我们的文档存储在索引中。剩下的交给 Elasticsearch 就可以了。
我们将会在《索引管理》章节中探讨如何创建并管理索引。但是现在,我们只需要让 Elasticsearch 帮助我们创建索引。我们只需要选择一个索引的名字。这个名称必须要全部小写,也不能以下划线开头,不能包含逗号。我们可以用website
作为我们索引的名字。
_type
在程序中,我们使用对象代表“物品”,比如一个用户、一篇博文、一条留言或者一个邮件。每一个对象都属于一种类型,类型定义了对象的属性或者与数据的关联。用户类的对象可能就会包含名字、性别、年龄以及邮箱地址等。
在传统的数据库中,我们总是将同类的数据存储在同一个表中,因为它们的数据格式是相同的。同理,在 Elasticsearch 中,我们使用同样类型的文档来代表同类“事物”,也是因为它们的数据结构是相同的。
每一个类型都拥有自己的映射(mapping)或者结构定义,它们定义了当前类型下的数据结构,类似于数据库表中的列。所有类型下的文档会被存储在同一个索引下,但是映射会告诉 Elasticsearch 不同的数据应该如何被索引。
我们将会在《映射》中探讨如何制定或者管理映射,但是目前为止,我们只需要依靠 Elasticsearch 来自动处理数据结构。
_id
id是一个字符串,当它与_index
以及_type
组合时,就可以来代表 Elasticsearch 中一个特定的文档。我们创建了一个新的文档时,你可以自己提供一个_id
,或者也可以让 Elasticsearch 帮你生成一个。
其他元数据
在文档中还有一些其他的元数据,我们将会在《映射》章节中详细讲解。使用上面罗列的元素,我们已经可以在 Elasticsearch 中存储文档或者通过 ID 来搜索已经保存的文档了。
3.2 索引
索引一个文档
文档通过索引
API 被索引——存储并使其可搜索。但是最开始我们需要决定我们将文档存储在哪里。正如之前提到的,一篇文档通过_index
, _type
以及_id
来确定它的唯一性。我们可以自己提供一个_id
,或者也使用index
API 帮我们生成一个。
使用自己的 ID
如果你的文档拥有天然的标示符(例如user_account
字段或者文档中其他的标识值),这时你就可以提供你自己的_id
,这样使用index
API:
PUT /{index}/{type}/{id}
{
"field": "value",
...
}
几个例子。如果我们的索引叫做"website"
,我们的类型叫做 "blog"
,然后我们选择"123"
作为 ID 的编号。这时,请求就是这样的:
PUT /website/blog/123
{
"title": "My first blog entry",
"text": "Just trying this out...",
"date": "2014/01/01"
}
Elasticsearch 返回内容:
{
"_index": "website",
"_type": "blog",
"_id": "123",
"_version": 1,
"created": true
}
这个返回值意味着我们的索引请求已经被成功创建,其中还包含了_index
, _type
以及_id
的元数据,以及一个新的元素_version
。
在 Elasticsearch 中,每一个文档都有一个版本号码。每当文档产生变化时(包括删除),_version
就会增大。在《版本控制》中,我们将会详细讲解如何使用_version
的数字来确认你的程序不会随意替换掉不想覆盖的数据。
自增 ID
如果我们的数据中没有天然的标示符,我们可以让 Elasticsearch 为我们自动生成一个。请求的结构发生了变化:我们把PUT
——“把文档存储在这个地址中”变量变成了POST
——“把文档存储在这个地址下”。
这样一来,请求中就只包含 _index
和_type
了:
POST /website/blog/
{
"title": "My second blog entry",
"text": "Still trying this out...",
"date": "2014/01/01"
}
这次的反馈和之前基本一样,只有_id
改成了系统生成的自增值:
{
"_index": "website",
"_type": "blog",
"_id": "wM0OSFhDQXGZAWDf0-drSA",
"_version": 1,
"created": true
}
自生成 ID 是由 22 个字母组成的,安全 universally unique identifiers 或者被称为UUIDs。
3.3 Get
搜索文档
要从 Elasticsearch 中获取文档,我们需要使用同样的_index
,_type
以及 _id
但是不同的 HTTP 变量GET
:
GET /website/blog/123?pretty
返回结果包含了之前提到的内容,以及一个新的字段_source
,它包含我们在最初创建索引时的原始 JSON 文档。
{
"_index" : "website",
"_type" : "blog",
"_id" : "123",
"_version" : 1,
"found" : true,
"_source" : {
"title": "My first blog entry",
"text": "Just trying this out..."
"date": "2014/01/01"
}
}
pretty
在任意的查询字符串中添加pretty
参数,类似上面的请求,Elasticsearch 就可以得到优美打印的更加易于识别的 JSON 结果。_source
字段不会执行优美打印,它的样子取决于我们录入的样子。
GET 请求的返回结果中包含{"found": true}
。这意味着这篇文档确实被找到了。如果我们请求了一个不存在的文档,我们依然会得到 JSON 反馈,只是found
的值会变为false
。
同样,HTTP 返回码也会由'200 OK'
变为'404 Not Found'
。我们可以在curl
后添加-i
,这样你就能得到反馈头文件:
curl -i -XGET /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
}
检索文档中的一部分
通常,GET
请求会将整个文档放入_source
字段中一并返回。但是可能你只需要title
字段。你可以使用_source
得到指定字段。如果需要多个字段你可以使用逗号分隔:
GET /website/blog/123?_source=title,text
现在_source
字段中就只会显示你指定的字段:
{
"_index" : "website",
"_type" : "blog",
"_id" : "123",
"_version" : 1,
"exists" : true,
"_source" : {
"title": "My first blog entry" ,
"text": "Just trying this out..."
}
}
或者你只想得到_source
字段而不要其他的元数据,你可以这样请求:
GET /website/blog/123/_source
这样结果就只返回:
{
"title": "My first blog entry",
"text": "Just trying this out...",
"date": "2014/01/01"
}
3.4 存在
检查文档是否存在
如果确实想检查一下文档是否存在,你可以试用HEAD
来替代GET
方法,这样就是会返回 HTTP 头文件:
curl -i -XHEAD /website/blog/123
如果文档存在,Elasticsearch 将会返回200 OK
的状态码:
HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8
Content-Length: 0
如果不存在将会返回404 Not Found
状态码:
curl -i -XHEAD /website/blog/124
HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=UTF-8
Content-Length: 0
当然,这个反馈只代表了你查询的那一刻文档不存在,但是不代表几毫秒后它不存在,很可能与此同时,另一个进程正在创建文档。
3.5 更新
更新整个文档
在 Documents 中的文档是不可改变的。所以如果我们需要改变已经存在的文档,我们可以使用《索引》中提到的index
API 来重新索引或者替换掉它:
PUT /website/blog/123
{
"title": "My first blog entry",
"text": "I am starting to get the hang of this...",
"date": "2014/01/02"
}
在反馈中,我们可以发现 Elasticsearch 已经将_version
数值增加了:
{
"_index" : "website",
"_type" : "blog",
"_id" : "123",
"_version" : 2,
"created": false <1>
}
created
被标记为false
是因为在同索引、同类型下已经存在同 ID 的文档。
在内部,Elasticsearch 已经将旧文档标记为删除并且添加了新的文档。旧的文档并不会立即消失,但是你也无法访问他。Elasticsearch 会在你继续添加更多数据的时候在后台清理已经删除的文件。
在本章的后面,我们将会在《局部更新》中介绍最新更新的 API。这个 API 允许你修改局部,但是原理和下方的完全一样:
- 从旧的文档中检索 JSON
- 修改它
- 删除修的文档
- 索引一个新的文档
唯一不同的是,使用了update
API 你就不需要使用get
然后再操作index
请求了。
3.6 创建
创建一个文档
当我们索引一个文档时,如何确定我们是创建了一个新的文档还是覆盖了一个已经存在的文档呢?
请牢记_index
,_type
以及_id
组成了唯一的文档标记,所以为了确定我们创建的是全新的内容,最简单的方法就是使用POST
方法,让 Elasticsearch 自动创建不同的_id
:
POST /website/blog/
{ ... }
然而,我们可能已经决定好了_id
,所以需要告诉 Elasticsearch 只有当_index
,_type
以及_id
这 3 个属性全部相同的文档不存在时才接受我们的请求。实现这个目的有两种方法,他们实质上是一样的,你可以选择你认为方便的那种:
第一种是在查询中添加op_type
参数:
PUT /website/blog/123?op_type=create
{ ... }
或者在请求最后添加 /_create
:
PUT /website/blog/123/_create
{ ... }
如果成功创建了新的文档,Elasticsearch 将会返回常见的元数据以及201 Created
的 HTTP 反馈码。
而如果存在同名文件,Elasticsearch 将会返回一个409 Conflict
的 HTTP 反馈码,以及如下方的错误信息:
{
"error" : "DocumentAlreadyExistsException[[website][4] [blog][123]:
document already exists]",
"status" : 409
}
3.7 删除
删除一个文档
删除文档的基本模式和之前的基本一样,只不过是需要更换成DELETE
方法:
DELETE /website/blog/123
如果文档存在,那么 Elasticsearch 就会返回一个200 OK
的 HTTP 响应码,返回的结果就会像下面展示的一样。请注意_version
的数字已经增加了。
{
"found" : true,
"_index" : "website",
"_type" : "blog",
"_id" : "123",
"_version" : 3
}
如果文档不存在,那么我们就会得到一个404 Not Found
的响应码,返回的内容就会是这样的:
{
"found" : false,
"_index" : "website",
"_type" : "blog",
"_id" : "123",
"_version" : 4
}
尽管文档并不存在("found"
值为false
),但是_version
的数值仍然增加了。这个就是内部管理的一部分,它保证了我们在多个节点间的不同操作的顺序都被正确标记了。
正如我在《更新》一章中提到的,删除一个文档也不会立即生效,它只是被标记成已删除。Elasticsearch 将会在你之后添加更多索引的时候才会在后台进行删除内容的清理。
3.8 版本控制
处理冲突
当你使用索引
API 来更新一个文档时,我们先看到了原始文档,然后修改它,最后一次性地将整个新文档进行再次索引处理。Elasticsearch 会根据请求发出的顺序来选择出最新的一个文档进行保存。但是,如果在你修改文档的同时其他人也发出了指令,那么他们的修改将会丢失。
很长时间以来,这其实都不是什么大问题。或许我们的主要数据还是存储在一个关系数据库中,而我们只是将为了可以搜索,才将这些数据拷贝到 Elasticsearch 中。或许发生多个人同时修改一个文件的概率很小,又或者这些偶然的数据丢失并不会影响到我们的正常使用。
但是有些时候如果我们丢失了数据就会出大问题。想象一下,如果我们使用 Elasticsearch 来存储一个网店的商品数量。每当我们卖出一件,我们就会将这个数量减少一个。
突然有一天,老板决定来个大促销。瞬间,每秒就产生了多笔交易。并行处理,多个进程来处理交易:
web_1
中库存量
的变化丢失的原因是web_2
并不知道它所得到的库存量
数据是是过期的。这样就会导致我们误认为还有很多货存,最终顾客就会对我们的行为感到失望。
当我们对数据修改得越频繁,或者在读取和更新数据间有越长的空闲时间,我们就越容易丢失掉我们的数据。
以下是两种能避免在并发更新时丢失数据的方法:
悲观并发控制(PCC)
这一点在关系数据库中被广泛使用。假设这种情况很容易发生,我们就可以阻止对这一资源的访问。典型的例子就是当我们在读取一个数据前先锁定这一行,然后确保只有读取到数据的这个线程可以修改这一行数据。
乐观并发控制(OCC)
Elasticsearch 所使用的。假设这种情况并不会经常发生,也不会去阻止某一数据的访问。然而,如果基础数据在我们读取和写入的间隔中发生了变化,更新就会失败。这时候就由程序来决定如何处理这个冲突。例如,它可以重新读取新数据来进行更新,又或者它可以将这一情况直接反馈给用户。
乐观并发控制
Elasticsearch 是分布式的。当文档被创建、更新或者删除时,新版本的文档就会被复制到集群中的其他节点上。Elasticsearch 即是同步的又是异步的,也就是说复制的请求被平行发送出去,然后可能会混乱地到达目的地。这就需要一种方法能够保证新的数据不会被旧数据所覆盖。
我们在上文提到每当有索引
、put
和删除
的操作时,无论文档有没有变化,它的_version
都会增加。Elasticsearch 使用_version
来确保所有的改变操作都被正确排序。如果一个旧的版本出现在新版本之后,它就会被忽略掉。
我们可以利用_version
的优点来确保我们程序修改的数据冲突不会造成数据丢失。我们可以按照我们的想法来指定_version
的数字。如果数字错误,请求就是失败。
我们来创建一个新的博文:
PUT /website/blog/1/_create
{
"title": "My first blog entry",
"text": "Just trying this out..."
}
反馈告诉我们这是一个新建的文档,它的_version
是1
。假设我们要编辑它,把这个数据加载到网页表单中,修改完毕然后保存新版本。
首先我们先要得到文档:
GET /website/blog/1
返回结果显示_version
为1
:
{
"_index" : "website",
"_type" : "blog",
"_id" : "1",
"_version" : 1,
"found" : true,
"_source" : {
"title": "My first blog entry",
"text": "Just trying this out..."
}
}
现在,我们试着重新索引文档以保存变化,我们这样指定了version
的数字:
PUT /website/blog/1?version=1 <1>
{
"title": "My first blog entry",
"text": "Starting to get the hang of this..."
}
- 我们只希望当索引中文档的
_version
是1
时,更新才生效。
请求成功相应,返回内容告诉我们_version
已经变成了2
:
{
"_index": "website",
"_type": "blog",
"_id": "1",
"_version": 2
"created": false
}
然而,当我们再执行同样的索引请求,并依旧指定version=1
时,Elasticsearch 就会返回一个409 Conflict
的响应码,返回内容如下:
{
"error" : "VersionConflictEngineException[[website][2] [blog][1]:
version conflict, current [2], provided [1]]",
"status" : 409
}
这里面指出了文档当前的_version
数字是2
,而我们要求的数字是1
。
我们需要做什么取决于我们程序的需求。比如我们可以告知用户已经有其它人修改了这个文档,你应该再保存之前看一下变化。而对于上文提到的库存量
问题,我们可能需要重新读取一下最新的文档,然后显示新的数据。
所有的有关于更新或者删除文档的 API 都支持version
这个参数,有了它你就通过修改你的程序来使用乐观并发控制。
使用外部系统的版本
还有一种常见的情况就是我们还是使用其他的数据库来存储数据,而 Elasticsearch 只是帮我们检索数据。这也就意味着主数据库只要发生的变更,就需要将其拷贝到 Elasticsearch 中。如果多个进程同时发生,就会产生上文提到的那些并发问题。
如果你的数据库已经存在了版本号码,或者也可以代表版本的时间戳
。这是你就可以在 Elasticsearch 的查询字符串后面添加version_type=external
来使用这些号码。版本号码必须要是大于零小于9.2e+18
(Java 中 long 的最大正值)的整数。
Elasticsearch 在处理外部版本号时会与对内部版本号的处理有些不同。它不再是检查_version
是否与请求中指定的数值相同,而是检查当前的_version
是否比指定的数值小。如果请求成功,那么外部的版本号就会被存储到文档中的_version
中。
外部版本号不仅可以在索引和删除请求时使用,还可以在创建时使用。
例如,创建一篇使用外部版本号为5
的博文,我们可以这样操作:
PUT /website/blog/2?version=5&version_type=external
{
"title": "My first external blog entry",
"text": "Starting to get the hang of this..."
}
在返回结果中,我们可以发现_version
是5
:
{
"_index": "website",
"_type": "blog",
"_id": "2",
"_version": 5,
"created": true
}
现在我们更新这个文档,并指定version
为10
:
PUT /website/blog/2?version=10&version_type=external
{
"title": "My first external blog entry",
"text": "This is a piece of cake..."
}
请求被成功执行并且version
也变成了10
:
{
"_index": "website",
"_type": "blog",
"_id": "2",
"_version": 10,
"created": false
}
如果你再次执行这个命令,你会得到之前的错误提示信息,因为你所指定的版本号并没有大于当前 Elasticsearch 中的版本号。
3.9 局部更新
更新文档中的一部分
在《更新》一章中,我们讲到了要是想更新一个文档,那么就需要去取回数据,更改数据然后将整个文档进行重新索引。当然,你还可以通过使用更新
API 来做部分更新,比如增加一个计数器。
正如我们提到的,文档不能被修改,它们只能被替换掉。更新
API 也必须遵循这一法则。从表面看来,貌似是文档被替换了。对内而言,它必须按照找回-修改-索引的流程来进行操作与管理。不同之处在于这个流程是在一个片(shard) 中完成的,因此可以节省多个请求所带来的网络开销。除了节省了步骤,同时我们也能减少多个进程造成冲突的可能性。
使用更新
请求最简单的一种用途就是添加新数据。新的数据会被合并到现有数据中,而如果存在相同的字段,就会被新的数据所替换。例如我们可以为我们的博客添加tags
和views
字段:
POST /website/blog/1/_update
{
"doc" : {
"tags" : [ "testing" ],
"views": 0
}
}
如果请求成功,我们就会收到一个类似于索引
时返回的内容:
{
"_index" : "website",
"_id" : "1",
"_type" : "blog",
"_version" : 3
}
再次取回数据,你可以在_source
中看到更新的结果:
{
"_index": "website",
"_type": "blog",
"_id": "1",
"_version": 3,
"found": true,
"_source": {
"title": "My first blog entry",
"text": "Starting to get the hang of this...",
"tags": [ "testing" ], <1>
"views": 0 <1>
}
}
- 新的数据已经添加到了字段
_source
中。
使用脚本进行更新
我们将会在《脚本》一章中学习更详细的内容,我们现在只需要了解一些在 Elasticsearch 中使用 API 无法直接完成的自定义行为。默认的脚本语言叫做 MVEL,但是 Elasticsearch 也支持 JavaScript, Groovy 以及 Python。
MVEL 是一个简单高效的 JAVA 基础动态脚本语言,它的语法类似于 Javascript。你可以在Elasticsearch scripting docs 以及 MVEL website了解更多关于 MVEL 的信息。
脚本语言可以在更新
API 中被用来修改_source
中的内容,而它在脚本中被称为ctx._source
。例如,我们可以使用脚本来增加博文中views
的数字:
POST /website/blog/1/_update
{
"script" : "ctx._source.views+=1"
}
我们同样可以使用脚本在tags
数组中添加新的 tag。在这个例子中,我们把新的 tag 声明为一个变量,而不是将他写死在脚本中。这样 Elasticsearch 就可以重新使用这个脚本进行 tag 的添加,而不用再次重新编写脚本了:
POST /website/blog/1/_update
{
"script" : "ctx._source.tags+=new_tag",
"params" : {
"new_tag" : "search"
}
}
获取文档,后两项发生了变化:
{
"_index": "website",
"_type": "blog",
"_id": "1",
"_version": 5,
"found": true,
"_source": {
"title": "My first blog entry",
"text": "Starting to get the hang of this...",
"tags": ["testing", "search"], <1>
"views": 1 <2>
}
}
tags
数组中出现了search
。views
字段增加了。
我们甚至可以使用ctx.op
来根据内容选择是否删除一个文档:
POST /website/blog/1/_update
{
"script" : "ctx.op = ctx._source.views == count ? 'delete' : 'none'",
"params" : {
"count": 1
}
}
更新一篇可能不存在的文档
想象一下,我们可能需要在 Elasticsearch 中存储一个页面计数器。每次用户访问这个页面,我们就增加一下当前页面的计数器。但是如果这是个新的页面,我们不能确保这个计数器已经存在。如果我们试着去更新一个不存在的文档,更新操作就会失败。
为了防止上述情况的发生,我们可以使用upsert
参数来设定文档不存在时,它应该被创建:
POST /website/pageviews/1/_update
{
"script" : "ctx._source.views+=1",
"upsert": {
"views": 1
}
}
首次运行这个请求时,upsert
的内容会被索引成新的文档,它将views
字段初始化为1
。当之后再请求时,文档已经存在,所以脚本
更新就会被执行,views
计数器就会增加。
更新和冲突
在本节的开篇我们提到了当取回与重新索引两个步骤间的时间越少,发生改变冲突的可能性就越小。但它并不能被完全消除,在更新
的过程中还可能存在另一个进程进行重新索引的可能性。
为了避免丢失数据,更新
API 会在获取步骤中获取当前文档中的_version
,然后将其传递给重新索引步骤中的索引
请求。如果其他的进程在这两部之间修改了这个文档,那么_version
就会不同,这样更新就会失败。
对于很多的局部更新来说,文档有没有发生变化实际上是不重要的。例如,两个进程都要增加页面浏览的计数器,谁先谁后其实并不重要 —— 发生冲突时只需要重新来过即可。
你可以通过设定retry_on_conflict
参数来设置自动完成这项请求的次数,它的默认值是0
。
POST /website/pageviews/1/_update?retry_on_conflict=5 <1>
{
"script" : "ctx._source.views+=1",
"upsert": {
"views": 0
}
}
- 失败前重新尝试 5 次
这个参数非常适用于类似于增加计数器这种无关顺序的请求,但是还有些情况的顺序就是很重要的。例如上一节提到的情况,你可以参考乐观并发控制以及悲观并发控制来设定文档的版本号。
3.10 Mget
获取多个文档
尽管 Elasticsearch 已经很快了,但是它依旧可以更快。你可以将多个请求合并到一个请求中以节省网络开销。如果你需要从 Elasticsearch 中获取多个文档,你可以使用multi-get 或者 mget
API 来取代一篇又一篇文档的获取。
mget
API 需要一个docs
数组,每一个元素包含你想要的文档的_index
, _type
以及_id
。你也可以指定_source
参数来设定你所需要的字段:
GET /_mget
{
"docs" : [
{
"_index" : "website",
"_type" : "blog",
"_id" : 2
},
{
"_index" : "website",
"_type" : "pageviews",
"_id" : 1,
"_source": "views"
}
]
}
返回值包含了一个docs
数组,这个数组以请求中指定的顺序每个文档包含一个响应。每一个响应都和独立的get
请求返回的响应相同:
{
"docs" : [
{
"_index" : "website",
"_id" : "2",
"_type" : "blog",
"found" : true,
"_source" : {
"text" : "This is a piece of cake...",
"title" : "My first external blog entry"
},
"_version" : 10
},
{
"_index" : "website",
"_id" : "1",
"_type" : "pageviews",
"found" : true,
"_version" : 2,
"_source" : {
"views" : 2
}
}
]
}
如果你所需要的文档都在同一个_index
或者同一个_type
中,你就可以在 URL 中指定一个默认的/_index
或是/_index/_type
。
你也可以在单独的请求中重写这个参数:
GET /website/blog/_mget
{
"docs" : [
{ "_id" : 2 },
{ "_type" : "pageviews", "_id" : 1 }
]
}
事实上,如果所有的文档拥有相同的_index
以及 _type
,直接在请求中添加ids
的数组即可:
GET /website/blog/_mget
{
"ids" : [ "2", "1" ]
}
请注意,我们所请求的第二篇文档不存在,这是就会返回如下内容:
{
"docs" : [
{
"_index" : "website",
"_type" : "blog",
"_id" : "2",
"_version" : 10,
"found" : true,
"_source" : {
"title": "My first external blog entry",
"text": "This is a piece of cake..."
}
},
{
"_index" : "website",
"_type" : "blog",
"_id" : "1",
"found" : false <1>
}
]
}
- 文档没有被找到。
当第二篇文档没有被找到的时候也不会影响到其它文档的获取结果。每一个文档都会被独立展示。
注意:上方请求的 HTTP 状态码依旧是200
,尽管有个文档没有找到。事实上,即使所有的文档都没有被找到,响应码也依旧是200
。这是因为mget
这个请求本身已经成功完成。要确定独立的文档是否被成功找到,你需要检查found
标识。
3.11 Bulk
批量更高效
与mget
能同时允许帮助我们获取多个文档相同,bulk
API 可以帮助我们同时完成执行多个请求,比如:create
,index
, update
以及delete
。当你在处理类似于 log 等海量数据的时候,你就可以一下处理成百上千的请求,这个操作将会极大提高效率。
bulk
的请求主体的格式稍微有些不同:
{ action: { metadata }}\n
{ request body }\n
{ action: { metadata }}\n
{ request body }\n
...
这种格式就类似于一个用"\n"
字符来连接的单行 json 一样。下面是两点注意事项:
- 每一行都结尾处都必须有换行字符
"\n"
,最后一行也要有。这些标记可以有效地分隔每行。
- 这些行里不能包含非转义字符,以免干扰数据的分析 — — 这也意味着 JSON不能是 pretty-printed 样式。
TIP
在《bulk 格式》一章中,我们将解释为何bulk
API 要使用这种格式。
action/metadata 行指定了将要在哪个文档中执行什么操作。
其中action必须是index
, create
, update
或者delete
。metadata 需要指明需要被操作文档的_index
,_type
以及_id
,例如删除命令就可以这样填写:
{ "delete": { "_index": "website", "_type": "blog", "_id": "123" }}
在你进行index
以及create
操作时,request body 行必须要包含文档的_source
数据——也就是文档的所有内容。
同样,在执行update
API: doc
, upsert
,script
的时候,也需要包含相关数据。而在删除的时候就不需要request body行。
{ "create": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "title": "My first blog post" }
如果没有指定_id
,那么系统就会自动生成一个 ID:
{ "index": { "_index": "website", "_type": "blog" }}
{ "title": "My second blog post" }
完成以上所有请求的bulk
如下:
POST /_bulk
{ "delete": { "_index": "website", "_type": "blog", "_id": "123" }} <1>
{ "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"} } <2>
- 注意
delete
操作是如何处理request body的,你可以在它之后直接执行新的操作。 - 请记住最后有换行符
Elasticsearch 会返回含有items
的列表、它的顺序和我们请求的顺序是相同的:
{
"took": 4,
"errors": false, <1>
"items": [
{ "delete": {
"_index": "website",
"_type": "blog",
"_id": "123",
"_version": 2,
"status": 200,
"found": true
}},
{ "create": {
"_index": "website",
"_type": "blog",
"_id": "123",
"_version": 3,
"status": 201
}},
{ "create": {
"_index": "website",
"_type": "blog",
"_id": "EiwfApScQiiy7TIKFxRCTw",
"_version": 1,
"status": 201
}},
{ "update": {
"_index": "website",
"_type": "blog",
"_id": "123",
"_version": 4,
"status": 200
}}
]
}}
- 所有的请求都被成功执行。
每一个子请求都会被单独执行,所以一旦有一个子请求失败了,并不会影响到其他请求的成功执行。如果一旦出现失败的请求,error
就会变为true
,详细的错误信息也会出现在返回内容的下方:
POST /_bulk
{ "create": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "title": "Cannot create - it already exists" }
{ "index": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "title": "But we can update it" }
请求中的create
操作失败,因为123
已经存在,但是之后针对文档123
的index
操作依旧被成功执行:
{
"took": 3,
"errors": true, <1>
"items": [
{ "create": {
"_index": "website",
"_type": "blog",
"_id": "123",
"status": 409, <2>
"error": "DocumentAlreadyExistsException <3>
[[website][4] [blog][123]:
document already exists]"
}},
{ "index": {
"_index": "website",
"_type": "blog",
"_id": "123",
"_version": 5,
"status": 200 <4>
}}
]
}
- 至少有一个请求错误发生。
- 这条请求的状态码为
409 CONFLICT
。 - 错误信息解释了导致错误的原因。
- 第二条请求的状态码为
200 OK
。
这也更好地解释了bulk
请求是独立的,每一条的失败与否 都不会影响到其他的请求。
能省就省
或许你在批量导入大量的数据到相同的index
以及type
中。每次都去指定每个文档的 metadata 是完全没有必要的。在mget
API 中,bulk
请求可以在 URL 中声明/_index
或者/_index/_type
:
POST /website/_bulk
{ "index": { "_type": "log" }}
{ "event": "User logged in" }
你依旧可以在 metadata 行中使用_index
以及_type
来重写数据,未声明的将会使用 URL 中的配置作为默认值:
POST /website/log/_bulk
{ "index": {}}
{ "event": "User logged in" }
{ "index": { "_type": "blog" }}
{ "title": "Overriding the default type" }
最大有多大?
整个数据将会被处理它的节点载入内存中,所以如果请求量很大的话,留给其他请求的内存空间将会很少。bulk
应该有一个最佳的限度。超过这个限制后,性能不但不会提升反而可能会造成宕机。
最佳的容量并不是一个确定的数值,它取决于你的硬件,你的文档大小以及复杂性,你的索引以及搜索的负载。幸运的是,这个平衡点 很容易确定:
试着去批量索引越来越多的文档。当性能开始下降的时候,就说明你的数据量太大了。一般比较好初始数量级是 1000 到 5000 个文档,或者你的文档很大,你就可以试着减小队列。 有的时候看看批量请求的物理大小是很有帮助的。1000 个 1KB 的文档和 1000 个 1MB 的文档的差距将会是天差地别的。比较好的初始批量容量是 5-15MB。
3.12 总结
现在你应该知道如何作为分布式文档存储来使用 Elasticsearch。你可以对文档进行存储,更新,获取,删除操作,而且你还知道该如何安全的执行这些操作。这已经非常有用处了,即使我们现在仍然没有尝试更激动人心的方面 — 在文档中进行查询操作。不过我们先探讨下分布式环境中 Elasticsearch 安全管理你的文档所使用的内部过程。