elasticsearch数据输入和输出

2017-10-13 1783点热度 0人点赞 0条评论

官方文档地址

https://elasticsearch.cn/book/elasticsearch_definitive_guide_2.x/data-in-data-out.html

文档元数据

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

_index
文档在哪存放
_type
文档表示的对象类别
_id
文档唯一标识

_index

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

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

我们将在 索引管理 介绍如何自行创建和管理索引,但现在我们将让 Elasticsearch 帮我们创建索引。 所有需要我们做的就是选择一个索引名,这个名字必须小写,不能以下划线开头,不能包含逗号。我们用 website 作为索引名举例。
_type

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

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

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

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

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

还有一些其他的元数据元素,他们在 类型和映射 进行了介绍。通过前面已经列出的元数据元素, 我们已经能存储文档到 Elasticsearch 中并通过 ID 检索它--换句话说,使用 Elasticsearch 作为文档的存储介质。

索引文档

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

如果你的文档有一个自然的 标识符 (例如,一个 user_account 字段或其他标识文档的值),你应该使用如下方式的 index API 并提供你自己 _id :

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

Autogenerating IDs

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

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

POST /website/blog/
{
  "title": "My second blog entry",
  "text":  "Still trying this out...",
  "date":  "2014/01/01"
}

取回一个文档

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

GET /website/blog/123?pretty

响应体包括目前已经熟悉了的元数据元素,再加上 _source 字段,这个字段包含我们索引数据时发送给 Elasticsearch 的原始 JSON 文档:

{
  "_index": "website",
  "_type": "blog",
  "_id": "123",
  "_version": 2,
  "found": true,
  "_source": {
    "title": "My first blog entry",
    "text": "I am starting to get the hang of this...",
    "date": "2014/01/02"
  }
}

GET 请求的响应体包括 {"found": true} ,这证实了文档已经被找到。 如果我们请求一个不存在的文档,我们仍旧会得到一个 JSON 响应体,但是 found 将会是 false 。 此外, HTTP 响应码将会是 404 Not Found ,而不是 200 OK 。

返回文档的一部分

默认情况下, GET 请求 会返回整个文档,这个文档正如存储在 _source 字段中的一样。但是也许你只对其中的 title 字段感兴趣。单个字段能用 _source 参数请求得到,多个字段也能使用逗号分隔的列表来指定。

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

该 _source 字段现在包含的只是我们请求的那些字段,并且已经将 date 字段过滤掉了。

{
  "_index": "website",
  "_type": "blog",
  "_id": "123",
  "_version": 2,
  "found": true,
  "_source": {
    "text": "I am starting to get the hang of this...",
    "title": "My first blog entry"
  }
}

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

GET /website/blog/123/_source
{
   "title": "My first blog entry",
   "text":  "Just trying this out...",
   "date":  "2014/01/01"
}

检查文档是否存在

如果只想检查一个文档是否存在 --根本不想关心内容--那么用 HEAD 方法来代替 GET 方法.
当然,一个文档仅仅是在检查的时候不存在,并不意味着一毫秒之后它也不存在:也许同时正好另一个进程就创建了该文档。

更新整个文档

在 Elasticsearch 中文档是 不可改变 的,不能修改它们。 相反,如果想要更新现有的文档,需要 重建索引 或者进行替换, 我们可以使用相同的 index API 进行实现,在 索引文档 中已经进行了讨论。

创建新文档

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

请记住, _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 的 HTTP 响应码。

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

{
  "error": {
    "root_cause": [
      {
        "type": "version_conflict_engine_exception",
        "reason": "[blog][123]: version conflict, document already exists (current version [2])",
        "index_uuid": "EKI3cdV7RMa7-qCDS-p1Dg",
        "shard": "0",
        "index": "website"
      }
    ],
    "type": "version_conflict_engine_exception",
    "reason": "[blog][123]: version conflict, document already exists (current version [2])",
    "index_uuid": "EKI3cdV7RMa7-qCDS-p1Dg",
    "shard": "0",
    "index": "website"
  },
  "status": 409
}

删除文档

删除文档 的语法和我们所知道的规则相同,只是 使用 DELETE 方法:

DELETE /website/blog/123

如果找到该文档,Elasticsearch 将要返回一个 200 ok 的 HTTP 响应码,和一个类似以下结构的响应体。注意,字段 _version 值已经增加:

{
  "found": true,
  "_index": "website",
  "_type": "blog",
  "_id": "123",
  "_version": 3,
  "result": "deleted",
  "_shards": {
    "total": 2,
    "successful": 2,
    "failed": 0
  }
}

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

{
  "found": false,
  "_index": "website",
  "_type": "blog",
  "_id": "123",
  "_version": 4,
  "result": "not_found",
  "_shards": {
    "total": 2,
    "successful": 2,
    "failed": 0
  }
}

即使文档不存在( Found 是 false ), _version 值仍然会增加。这是 Elasticsearch 内部记录本的一部分,用来确保这些改变在跨多节点时以正确的顺序执行。
Note

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

处理冲突

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

悲观并发控制
这种方法被关系型数据库广泛使用,它假定有变更冲突可能发生,因此阻塞访问资源以防止冲突。 一个典型的例子是读取一行数据之前先将其锁住,确保只有放置锁的线程能够对这行数据进行修改。
乐观并发控制
Elasticsearch 中使用的这种方法假定冲突是不可能发生的,并且不会阻塞正在尝试的操作。 然而,如果源数据在读写当中被修改,更新将会失败。应用程序接下来将决定该如何解决冲突。 例如,可以重试更新、使用新的数据、或者将相关情况报告给用户。

乐观并发控制

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

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

我们可以利用 _version 号来确保 应用中相互冲突的变更不会导致数据丢失。我们通过指定想要修改文档的 version 号来达到这个目的。 如果该版本不是当前版本号,我们的请求将会失败。

让我们创建一个新的博客文章:

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

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

首先我们检索文档:

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
{
  "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,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 2,
    "failed": 0
  },
  "created": false
}

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

{
  "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": "EKI3cdV7RMa7-qCDS-p1Dg",
        "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": "EKI3cdV7RMa7-qCDS-p1Dg",
    "shard": "3",
    "index": "website"
  },
  "status": 409
}

这告诉我们在 Elasticsearch 中这个文档的当前 _version 号是 2 ,但我们指定的更新版本号为 1 。

我们现在怎么做取决于我们的应用需求。我们可以告诉用户说其他人已经修改了文档,并且在再次保存之前检查这些修改内容。 或者,在之前的商品 stock_count 场景,我们可以获取到最新的文档并尝试重新应用这些修改。

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

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

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

如果你的主数据库已经有了版本号 — 或一个能作为版本号的字段值比如 timestamp — 那么你就可以在 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,
  "result": "created",
  "_shards": {
    "total": 2,
    "successful": 2,
    "failed": 0
  },
  "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,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 2,
    "failed": 0
  },
  "created": false
}

文档的部分更新

在 更新整个文档 , 我们已经介绍过 更新一个文档的方法是检索并修改它,然后重新索引整个文档,这的确如此。然而,使用 update API 我们还可以部分更新文档,例如在某个请求时对计数器进行累加。

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

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

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

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

{
  "_index": "website",
  "_type": "blog",
  "_id": "1",
  "_version": 3,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 2,
    "failed": 0
  }
}

检索文档显示了更新后的 _source 字段:

{
  "_index": "website",
  "_type": "blog",
  "_id": "1",
  "_version": 3,
  "found": true,
  "_source": {
    "title": "My first blog entry",
    "text": "Just trying this out again",
    "views": 0,
    "tags": [
      "testing"
    ]
  }
}

使用脚本部分更新文档

脚本可以在 update API中用来改变 _source 的字段内容, 它在更新脚本中称为 ctx._source 。 例如,我们可以使用脚本来增加博客文章中 views 的数量:

POST /website/blog/1/_update
{
"script" : "ctx._source.views+=1"
}
返回结果:

{
  "_index": "website",
  "_type": "blog",
  "_id": "1",
  "_version": 4,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 2,
    "failed": 0
  }
}

我们也可以通过使用脚本给 tags 数组添加一个新的标签。在这个例子中,我们指定新的标签作为参数,而不是硬编码到脚本内部。 这使得 Elasticsearch 可以重用这个脚本,而不是每次我们想添加标签时都要对新脚本重新编译:

POST /website/blog/1/_update
{
   "script" : "ctx._source.tags+=\"search\""
}

更新成功:

{
  "_index": "website",
  "_type": "blog",
  "_id": "1",
  "_version": 5,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 2,
    "failed": 0
  }
}

获取文档结果,貌似不是想要的结果,search不是以数组的形式加入到tags,而是以字符串的形式加入的:

{
  "_index": "website",
  "_type": "blog",
  "_id": "1",
  "_version": 5,
  "found": true,
  "_source": {
    "title": "My first blog entry",
    "text": "Just trying this out again",
    "views": 1,
    "tags": "[testing]search"
  }
}

按照官方文档进行脚本更新,报错:

POST /website/blog/1/_update
{
   "script" : "ctx._source.tags+=new_tag",
   "params" : {
      "new_tag" : "search"
   }
}

报错信息:

{
  "error": {
    "root_cause": [
      {
        "type": "remote_transport_exception",
        "reason": "[_7Llp2c][172.20.0.3:9300][indices:data/write/update[s]]"
      }
    ],
    "type": "illegal_argument_exception",
    "reason": "failed to execute script",
    "caused_by": {
      "type": "script_exception",
      "reason": "compile error",
      "script_stack": [
        "ctx._source.tags+=new_tag",
        "                  ^---- HERE"
      ],
      "script": "ctx._source.tags+=new_tag",
      "lang": "painless",
      "caused_by": {
        "type": "illegal_argument_exception",
        "reason": "Variable [new_tag] is not defined."
      }
    }
  },
  "status": 400
}

这种用法不常用等用到的时候再深究吧,不管了。

取回多个文档

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

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

首先添加要查询的数据:

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

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

{
  "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": "pageviews",
      "_id": "1",
      "_version": 1,
      "found": true,
      "_source": {
        "views": 2
      }
    }
  ]
}

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

你仍然可以通过单独请求覆盖这些值:

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

事实上,如果所有文档的 _index 和 _type 都是相同的,你可以只传一个 ids 数组,而不是整个 docs 数组:

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

代价较小的批量操作

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

bulk 与其他的请求体格式稍有不同,如下所示:

{ 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 。

例如,一个 delete 请求看起来是这样的:

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

请注意 delete 动作不能有请求体,它后面跟着的是另外一个操作。

谨记最后一个换行符不要落下。

这个 Elasticsearch 响应包含 items 数组, 这个数组的内容是以请求的顺序列出来的每个请求的结果。

{
  "took": 367,
  "errors": false,
  "items": [
    {
      "delete": {
        "found": false,
        "_index": "website",
        "_type": "blog",
        "_id": "123",
        "_version": 1,
        "result": "not_found",
        "_shards": {
          "total": 2,
          "successful": 2,
          "failed": 0
        },
        "status": 404
      }
    },
    {
      "create": {
        "_index": "website",
        "_type": "blog",
        "_id": "123",
        "_version": 2,
        "result": "created",
        "_shards": {
          "total": 2,
          "successful": 2,
          "failed": 0
        },
        "created": true,
        "status": 201
      }
    },
    {
      "index": {
        "_index": "website",
        "_type": "blog",
        "_id": "AV8U-qSBGKNfpXtpnwbH",
        "_version": 1,
        "result": "created",
        "_shards": {
          "total": 2,
          "successful": 2,
          "failed": 0
        },
        "created": true,
        "status": 201
      }
    },
    {
      "update": {
        "_index": "website",
        "_type": "blog",
        "_id": "123",
        "_version": 3,
        "result": "updated",
        "_shards": {
          "total": 2,
          "successful": 2,
          "failed": 0
        },
        "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 失败,因为它已经存在。但是随后的 index 请求,也是对文档 123 操作,就成功了:

{
  "took": 186,
  "errors": true,
  "items": [
    {
      "create": {
        "_index": "website",
        "_type": "blog",
        "_id": "123",
        "status": 409,
        "error": {
          "type": "version_conflict_engine_exception",
          "reason": "[blog][123]: version conflict, document already exists (current version [3])",
          "index_uuid": "EKI3cdV7RMa7-qCDS-p1Dg",
          "shard": "0",
          "index": "website"
        }
      }
    },
    {
      "index": {
        "_index": "website",
        "_type": "blog",
        "_id": "123",
        "_version": 4,
        "result": "updated",
        "_shards": {
          "total": 2,
          "successful": 2,
          "failed": 0
        },
        "created": false,
        "status": 200
      }
    }
  ]
}

一个或者多个请求失败。

这个请求的HTTP状态码报告为 409 CONFLICT 。

解释为什么请求失败的错误信息。

第二个请求成功,返回 HTTP 状态码 200 OK 。

这也意味着 bulk 请求不是原子的: 不能用它来实现事务控制。每个请求是单独处理的,因此一个请求的成功或失败不会影响其他的请求。
不要重复指定Index和Type

{
  "_index": "website",
  "_type": "blog",
  "_id": "123",
  "_version": 4,
  "found": true,
  "_source": {
    "title": "But we can update it"
  }
}

也许你正在批量索引日志数据到相同的 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,000 到 5,000 个文档作为一个批次, 如果你的文档非常大,那么就减少批量的文档个数。

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

王显锋

激情工作,快乐生活!

文章评论