Elasticsearch 数据

438 阅读22分钟
  1. 前面已经介绍过对象 (object) 是一种语言相关,记录在内存中的的数据结构。为了在网络间发送,或者存储它,我们需要一些标准的格式来表示它。JSON (JavaScript Object Notation) 是一种可读的以文本来表示对象的方式。它已经成为 NoSQL 世界中数据交换的一种事实标准。当对象被序列化为 JSON ,它就成为 JSON 文档 (JSON document) 了。

  2. Elasticsearch 是一个分布式的文档 (document) 存储引擎。它可以实时存储并检索复杂数据结构——序列化的 JSON 文档。换言说,一旦文档被存储在 Elasticsearch 中,它就可以在集群的任一节点上被检索。

  3. 我们不仅需要存储数据,还要快速的批量查询。虽然已经有很多 NoSQL 的解决方案允许我们以文档的形式存储对象,但它们依旧需要考虑如何查询这些数据,以及哪些字段需要被索引以便检索时更加快速。

  4. 在 Elasticsearch 中,每一个字段的数据都是默认被索引的。也就是说,每个字段专门有一个反向索引用于快速检索。而且,与其它数据库不同,它可以在同一个查询中利用所有的这些反向索引,以惊人的速度返回结果。

文档

  1. 程序中大多的实体或对象能够被序列化为包含键值对的 JSON 对象,键 (key) 是字段 (field) 或属性 (property) 的名字,值 (value) 可以是字符串、数字、布尔类型、另一个对象、值数组或者其他特殊类型,比如表示日期的字符串或者表示地理位置的对象。

  2. 通常可以认为对象 (object) 和文档 (document) 是等价相通的。不过,他们还是有所差别:对象 (Object) 是一个 JSON 结构体——类似于哈希、hashmap、字典或者关联数组;对象 (Object) 中还可能包含其他对象 (Object) 。 在 Elasticsearch 中,文档 (document) 这个术语有着特殊含义。它特指最顶层结构或者根对象 (root object) 序列化成的 JSON 数据(以唯一 ID 标识并存储于 Elasticsearch 中)。

  3. 一个文档不只有数据。它还包含了元数据(metadata)——关于文档的信息。三个必须的元数据节点是:

    节点 说明
    _index 文档存储的地方
    _type 文档代表的对象的类
    _id 文档的唯一标识
  4. 索引 (_index) 类似于关系型数据库里的“数据库”——它是我们存储和索引关联数据的地方。

     事实上,我们的数据被存储和索引在分片 (shards) 中,索引只是一个把一个或多个分片分组在一起的逻辑空间。然而,这只是一些内部细节——我们的程序完全不用关心分片。对于我们的程序而言,文档存储在索引 (index) 中。剩下的细节由 Elasticsearch 关心既可。
    

    选择一个索引名,这个名字必须是全部小写,不能以下划线开头,不能包含逗号。让我们使用 website 做为索引名。

  5. 在关系型数据库中,我们经常将相同类的对象存储在一个表里,因为它们有着相同的结构。同理,在 Elasticsearch 中,我们使用相同类型 (_type) 的文档表示相同的“事物”,因为他们的数据结构也是相同的。

    每个类型 (_type) 都有自己的映射 (mapping) 或者结构定义,就像传统数据库表中的列一样。所有类型下的文档被存储在同一个索引下,但是类型的映射 (mapping) 会告诉 Elasticsearch 不同的文档如何被索引。

    _type 的名字可以是大写或小写,不能包含下划线或逗号。我们将使用 blog 做为类型名。

  6. _id 仅仅是一个字符串,它与 _index 和 _type 组合时,就可以在 Elasticsearch 中唯一标识一个文档。当创建一个文档,你可以自定义 _id ,也可以让 Elasticsearch 帮你自动生成。

索引

  1. 文档通过 index API 被索引——使数据可以被存储和搜索。但是首先我们需要决定文档所在。正如我们讨论的,文档通过其 _index、_type、_id 唯一确定。们可以自己提供一个 _id ,或者也使用 index API 去生成一个。

     例如:我们的索引叫做 “website” ,类型叫做 “blog” ,我们选择的 ID 是 “123” ,那么这个索引请求就像这样:
     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 增加。

  2. 如果我们的数据没有自然 ID ,我们可以让 Elasticsearch 自动为我们生成。请求结构发生了变化:PUT 方法——“在这个 URL 中存储文档”变成了 POST 方法——"在这个类型下存储文档"。

     URL现在只包含 _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个字符长,URL-safe, Base64-encoded string universally unique identifiers, 或者叫 UUIDs。

获取

  1. 想要从 Elasticsearch 中获取文档,我们使用同样的 _index、_type、_id ,但是 HTTP 方法改为 GET :

     GET /website/blog/123?pretty
    

    响应包含了现在熟悉的元数据节点,增加了 _source 字段,它包含了在创建索引时我们发送给 Elasticsearch 的原始文档。

     	{
       "_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"
       }
     }
    

    GET 请求返回的响应内容包括 {"found": true} 。这意味着文档已经找到。如果我们请求一个不存在的文档,依旧会得到一个 JSON ,不过 found值变成了 false。

    在任意的查询字符串中增加 pretty 参数,类似于上面的例子。会让 Elasticsearch 美化输出 (pretty-print)JSON 响应以便更加容易阅读。 _source 字段不会被美化,它的样子与我们输入的一致。

    此外,HTTP响应状态码也会变成'404 Not Found'代替'200 OK'。我们可以在curl后加-i参数得到响应头:

     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
     }
    
  2. 通常,GET 请求将返回文档的全部,存储在 _source 参数中。但是可能你感兴趣的字段只是 title 。请求个别字段可以使用 _source 参数。多个字段可以使用逗号分隔:

     GET /website/blog/123?_source=title,text
    

    _source 字段现在只包含我们请求的字段,而且过滤了 date 字段:

     {
       "_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"
	}

存在

如果想做的只是检查文档是否存在——使用 HEAD 方法来代替 GET 。 HEAD 请求不会返回响应体,只有 HTTP 头:

curl -i -XHEAD http://localhost:9200/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 http://localhost:9200/website/blog/124

HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=UTF-8
Content-Length: 0

当然,这只表示你在查询的那一刻文档不存在,但并不表示几毫秒后依旧不存在。另一个进程在这期间可能创建新文档。

更新

文档在 Elasticsearch 中是不可变的——我们不能修改他们。如果需要更新已存在的文档,我们可以使用 index API 重建索引 (reindex) 或者替换掉它。

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>
}
<1> created 标识为 false 因为同索引、同类型下已经存在同ID的文档。

在内部,Elasticsearch 已经标记旧文档为删除并添加了一个完整的新文档。旧版本文档不会立即消失,但你也不能去访问它。 Elasticsearch 会在你继续索引更多数据时清理被删除的文档。

update API 似乎允许修改文档的局部,但事实上 Elasticsearch 遵循与之前所说完全相同的过程,这个过程如下:

从旧文档中检索JSON
修改它
删除旧文档
索引新文档

唯一的不同是 update API 完成这一过程只需要一个客户端请求既可,不再需要 get 和 index 请求了。

创建

当索引一个文档,我们如何确定是完全创建了一个新的还是覆盖了一个已经存在的呢? 因为 _index、_type、_id 三者唯一确定一个文档。所以要想保证文档是新加入的,最简单的方式是使用 POST 方法让 Elasticsearch 自动生成唯一 _id :

POST /website/blog/
{ ... }

然而,如果想使用自定义的 _id ,我们必须告诉 Elasticsearch 应该在 _index、_type、_id 三者都不同时才接受请求。为了做到这点有两种方法,它们其实做的是同一件事情。你可以选择适合自己的方式:

第一种方法使用 op_type 查询参数:

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

第二种方法是在 URL 后加 /_create 做为端点:

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

如果请求成功的创建了一个新文档,Elasticsearch 将返回正常的元数据且响应状态码是 201 Created 。

另一方面,如果包含相同的 _index、_type 和 _id 的文档已经存在,Elasticsearch 将返回 409 Conflict 响应状态码,错误信息类似如下:

{
  "error" : "DocumentAlreadyExistsException[[website][4] [blog][123]:
             document already exists]",
  "status" : 409
}

删除

删除文档的语法模式与之前基本一致,只不过要使用 DELETE 方法:

DELETE /website/blog/123

如果文档被找到, Elasticsearch 将返回 200 OK 状态码和以下响应体。注意 _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将会在你之后添加更多索引的时候才会在后台进行删除内容的清理。

版本控制

  1. 当使用 index API 更新文档的时候,我们读取原始文档,做修改,然后将整个文档 (whole document) 一次性重新索引。最近的索引请求会生效—— Elasticsearch 中只存储最后被索引的任何文档。如果其他人同时也修改了这个文档,他们的修改将会丢失。

  2. Elasticsearch 是分布式的。当文档被创建、更新或删除,文档的新版本会被复制到集群的其它节点。Elasticsearch 即是同步的又是异步的,意思是这些复制请求都是平行发送的,并无序 (out of sequence) 的到达目的地。这就需要一种方法确保老版本的文档永远不会覆盖新的版本。

    上文我们提到 index、get、delete 请求时,我们指出每个文档都有一个 _version 号码,这个号码在文档被改变时加一。 Elasticsearch 使用这个 _version 保证所有修改都被正确排序。当一个旧版本出现在新版本之后,它会被简单的忽略。

    我们利用 _version 的这一优点确保数据不会因为修改冲突而丢失。我们可以指定文档的 version 来做想要的更改。如果那个版本号不是现在的,我们的请求就失败了。

    让我们创建一个新案例:

     PUT /website/blog/1/_create
     {
       "title": "My first blog entry",
       "text":  "Just trying this out..."
     }
    

    首先我们检索文档:

     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..."
     }
     <1> 我们只希望文档的 _version 是 1 时更新才生效。
    

    请求成功,响应体告诉我们 _version 已经增加到 2 :

     {
       "_index":   "website",
       "_type":    "blog",
       "_id":      "1",
       "_version": 2
       "created":  false
     }
    

    然而,如果我们重新运行相同的索引请求,依旧指定 version=1 , Elasticsearch 将返回 409 Conflict 状态的 HTTP 响应。响应体类似这样:

     {
       "error" : "VersionConflictEngineException[[website][2] [blog][1]:
                  version conflict, current [2], provided [1]]",
       "status" : 409
     }
     这告诉我们当前 _version 是 2,但是我们指定想要更新的版本是 1。
    

    我们需要做什么取决于程序的需求。我们可以告知用户其他人修改了文档,你应该在保存前再看一下。所有更新和删除文档的请求都接受 version 参数,它可以允许在你的代码中增加乐观锁控制。

  3. 一种常见的结构是使用一些其他的数据库做为主数据库,然后使用 Elasticsearch 搜索数据,这意味着所有主数据库发生变化,就要将其拷贝到 Elasticsearch 中。如果有多个进程负责这些数据的同步,就会遇到上面提到的并发问题。

    如果主数据库有版本字段——或一些类似于 timestamp 等可以用于版本控制的字段——是你就可以在 Elasticsearch 的查询字符串后面添加 version_type=external 来使用这些版本号。版本号必须是整数,大于零小于 9.2e+18 —— Java 中的正的 long 。

    外部版本号与之前说的内部版本号在处理的时候有些不同。它不再检查 _version 是否与请求中指定的一致,而是检查是否小于指定的版本。如果请求成功,外部版本号就会被存储到 _version 中。

    外部版本号不仅在索引和删除请求中指定,也可以在创建 (create) 新文档中指定。

    创建一个包含外部版本号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
     }
    

    如果你重新运行这个请求,就会返回一个像之前一样的冲突错误,因为指定的外部版本号不大于当前在 Elasticsearc h中的版本。

局部更新

  1. 文档是不可变的——它们不能被更改,只能被替换。update API必须遵循相同的规则。表面看来,我们似乎是局部更新了文档的位置,内部却是像我们之前说的一样简单的使用update API处理相同的检索-修改-重建索引流程,我们也减少了其他进程可能导致冲突的修改。

  2. 最简单的 update 请求表单接受一个局部文档参数 doc ,它会合并到现有文档中——对象合并在一起,存在的标量字段被覆盖,新字段被添加。举个例子,我们可以使用以下请求为博客添加一个 tags 字段和一个 views 字段:

     POST /website/blog/1/_update
     {
        "doc" : {
           "tags" : [ "testing" ],
           "views": 0
        }
     }
    

    如果请求成功,我们将看到类似 index 请求的响应结果:

     {
        "_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>
        }
     }
     <1> 我们新添加的字段已经被添加到 _source 字段中
    
  3. 当 API 不能满足要求时,Elasticsearch 允许你使用脚本实现自己的逻辑。脚本支持非常多的 API ,例如搜索、排序、聚合和文档更新。脚本可以通过请求的一部分、检索特殊的 .scripts 索引或者从磁盘加载方式执行。

    默认的脚本语言是 Groovy ,一个快速且功能丰富的脚本语言,语法类似于 Javascript 。它在一个沙盒 (sandbox) 中运行,以防止恶意用户毁坏 Elasticsearch 或攻击服务器。

  4. 想象我们要在 Elasticsearch 中存储浏览量计数器。每当有用户访问页面,我们增加这个页面的浏览量。但如果这是个新页面,我们并不确定这个计数器存在与否。当我们试图更新一个不存在的文档,更新将失败。

    在这种情况下,我们可以使用 upsert 参数定义文档来使其不存在时被创建。

     POST /website/pageviews/1/_update
     {
        "script" : "ctx._source.views+=1",
        "upsert": {
            "views": 1
        }
     }
    

    第一次执行这个请求,upsert值 被索引为一个新文档,初始化 views 字段为 1 .接下来文档已经存在,所以 script 被更新代替,增加 views 数量。

  5. 在检索 (retrieve) 和重建索引 (reindex) 中保持更小的窗口,可以减少冲突性变更发生的概率,不过这些无法被完全避免,像一个其他进程在 update 进行重建索引时修改了文档这种情况依旧可能发生。

    为了避免丢失数据,update API 在检索 (retrieve) 阶段检索文档的当前 _version ,然后在重建索引 (reindex) 阶段通过 index 请求提交。如果其他进程在检索 (retrieve) 和重建索引 (reindex) 阶段修改了文档, _version 将不能被匹配,然后更新失败。

    对于多用户的局部更新,文档被修改了并不要紧。例如,两个进程都要增加页面浏览量,增加的顺序我们并不关心——如果冲突发生,我们唯一要做的仅仅是重新尝试更新既可。这些可以通过 retry_on_conflict 参数设置重试次数来自动完成,这样 update 操作将会在发生错误前重试——这个值默认为 0。

     POST /website/pageviews/1/_update?retry_on_conflict=5 <1>
     {
        "script" : "ctx._source.views+=1",
        "upsert": {
            "views": 0
        }
     }
     <1> 在错误发生前重试更新5次
    

    这适用于像增加计数这种顺序无关的操作,但是还有一种顺序非常重要的情况。例如 index API ,使用“保留最后更新 (last-write-wins) ” 的 update API ,但它依旧接受一个 version 参数以允许你使用乐观并发控制 (optimistic concurrency control) 来指定你要更细文档的版本。

Mget

合并多个请求可以避免每个请求单独的网络开销。如果你需要从 Elasticsearch 中检索多个文档,相对于一个一个的检索,更快的方式是在一个请求中使用 multi-get 或者 mget API 。

mget API 参数是一个 docs 数组,数组的每个节点定义一个文档的 _index、_type、_id 元数据。如果你只想检索一个或几个确定的字段,也可以定义一个 _source 参数:

 POST /_mget
{
   "docs" : [
      {
         "_index" : "website",
         "_type" :  "blog",
         "_id" :    2
      },
      {
         "_index" : "website",
         "_type" :  "pageviews",
         "_id" :    1,
         "_source": "views"
      }
   ]
}

响应体也包含一个 docs 数组,每个文档还包含一个响应,它们按照请求定义的顺序排列。每个这样的响应与单独使用 get request 响应体相同:

{
   "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。依旧可以在单独的请求中使用这些值:

POST /website/blog/_mget
{
   "docs" : [
      { "_id" : 2 },
      { "_type" : "pageviews", "_id" :   1 }
   ]
}

事实上,如果所有文档具有相同 _index 和 _type ,你可以通过简单的 ids 数组来代替完整的 docs 数组:

POST /website/blog/_mget
{
   "ids" : [ "2", "1" ]
}

注意到我们请求的第二个文档并不存在。我们定义了类型为 blog ,但是 ID 为 1 的文档类型为 pageviews 。这个不存在的文档会在响应体中被告知。

{
  "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>
    }
  ]
}
<1> 这个文档不存在,事实上第二个文档不存在并不影响第一个文档的检索。每个文档的检索和报告都是独立的。
尽管前面提到有一个文档没有被找到,但 HTTP 请求状态码还是 200 。事实上,就算所有文档都找不到,请求也还是返回 200 ,原因是 mget 请求本身成功了。如果想知道每个文档是否都成功了,你需要检查 found 标志。

批量

  1. 就像 mget 允许我们一次性检索多个文档一样,bulk API 允许我们使用单一请求来实现多个文档的 create、index、update 或 delete 。这对索引类似于日志活动这样的数据流非常有用,它们可以以成百上千的数据为一个批次按序进行索引。

    bulk请求体如下,它有一点不同寻常:

     { action: { metadata }}\n
     { request body        }\n
     { action: { metadata }}\n
     { request body        }\n
     ...
    

    这种格式类似于用 "\n" 符号连接起来的一行一行的 JSON 文档流 (stream) 。两个重要的点需要注意:

     每行必须以 "\n" 符号结尾,包括最后一行。这些都是作为每行有效的分离而做的标记。
     每一行的数据不能包含未被转义的换行符,它们会干扰分析——这意味着 JSON 不能被美化打印。
    
  2. action/metadata 这一行定义了文档行为 (what action) 发生在哪个文档 (which document) 之上。 行为 (action) 必须是以下几种:

    行为 解释
    create 当文档不存在时创建之
    index 创建新文档或替换已有文档
    update 局部更新文档
    delete 删除一个文档

    在索引、创建、更新或删除时必须指定文档的 _index、_type、_id 这些元数据 (metadata)。例如:

     { "delete": { "_index": "website", "_type": "blog", "_id": "123" }}
    

    请求体 (request body) 由文档的 _source 组成——文档所包含的一些字段以及其值。它被 index 和 create 操作所必须,这是有道理的:你必须提供文档用来索引。 这些还被 update 操作所必需,而且请求体的组成应该与 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>
     <1> 注意 delete 行为 (action) 没有请求体,它紧接着另一个行为 (action)
     <2> 记得最后一个换行符
    

    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
           }}
        ]
     }}
     <1> 所有子请求都成功完成。
    

    每个子请求都被独立的执行,所以一个子请求的错误并不影响其它请求。如果任何一个请求失败,顶层的 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>
           }}
        ]
     }
     <1> 一个或多个请求失败。
     <2> 这个请求的 HTTP 状态码被报告为 409 CONFLICT。
     <3> 错误消息说明了什么请求错误。
     <4> 第二个请求成功了,状态码是 200 OK。
     这些说明 bulk 请求不是原子操作——它们不能实现事务。每个请求操作时分开的,所以每个请求的成功与否不干扰其它操作。
    
  3. 可能在同一个 index 下的同一个 type 里批量索引日志数据。为每个文档指定相同的元数据是多余的。就像 mget API ,bulk 请求也可以在 URL 中使用 /_index 或 /_index/_type:

     POST /website/_bulk
     { "index": { "_type": "log" }}
     { "event": "User logged in" }
    

你依旧可以覆盖元数据行的 _index 和 _type,在没有覆盖时它会使用 URL 中的值作为默认值:

POST /website/log/_bulk
{ "index": {}}
{ "event": "User logged in" }
{ "index": { "_type": "blog" }}
{ "title": "Overriding the default type" }
  1. 整个批量请求需要被加载到接受我们请求节点的内存里,所以请求越大,给其它请求可用的内存就越小。有一个最佳的 bulk 请求大小。超过这个大小,性能不再提升而且可能降低。 最佳大小完全取决于硬件、文档的大小和复杂度以及索引和搜索的负载。幸运的是,这个最佳点(sweetspot)还是容易找到的:

    试着批量索引标准的文档,随着大小的增长,当性能开始降低,说明你每个批次的大小太大了。开始的数量可以在 1000~5000 个文档之间,如果你的文档非常大,可以使用较小的批次。通常着眼于你请求批次的物理大小是非常有用的。一千个 1kB 的文档和一千个 1MB 的文档大不相同。一个好的批次最好保持在 5-15MB 大小间。