关于Elasticsearch文档的描述以及如何操作文档的详细总结

文档

什么是文档

在大多数应用中,多数实体或对象可以被序列化为包含键值对的 JSON 对象。 一个 键 可以是一个字段或字段的名称,一个 值 可以是一个字符串,一个数字,一个布尔值, 另一个对象,一些数组值,或一些其它特殊类型诸如表示日期的字符串,或代表一个地理位置的对象:

{
    "name":         "John Smith",
    "age":          42,
    "confirmed":    true,
    "join_date":    "2019-06-01",
    "home": {
        "lat":      51.5,
        "lon":      0.1
    },
    "accounts": [
        {
            "type": "facebook",
            "id":   "johnsmith"
        },
        {
            "type": "twitter",
            "id":   "johnsmith"
        }
    ]
}

通常情况下,我们使用的术语对象文档是可以互相替换的。不过,有一个区别: 一个对象仅仅是类似于hash、hashmap 、字典或者关联数组的JSON对象,对象中也可以嵌套其他的对象。 对象可能包含了另外一些对象。在Elasticsearch中,术语文档有着特定的含义。它是指最顶层或者根对象, 这个根对象被序列化成JSON并存储到Elasticsearch中,指定了唯一ID。
需要注意的是字段的名字可以是任何合法的字符串,但不可以包含英文句号(.)。

文档元数据

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

  • _index

    文档在哪存放

  • _type

    文档表示的对象类别

  • _id

    文档唯一标识

还有其他的元数据,后面会陆续说到。

索引文档

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

使用自定义的ID

如果你的文档有一个自然的唯一的标识符,应该使用如下方式的index API并提供你自己的_id

PUT /{index}/{type}/{id}
{
  "field": "value",
  ...
}

例如:

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":  "2019/01/01"
}
'

Elasticsearch 响应体如下:

{
   "_index":    "website",
   "_type":     "blog",
   "_id":       "123",
   "_version":  1,
   "created":   true
}

_version:

在 Elasticsearch中每个文档都有一个版本号。当每次对文档进行修改时(包括删除),_version的值会递增。_version确保你的应用程序中的一部分修改不会覆盖另一部分所做的修改。

使用Elasticsearch自动生成ID

请求的结构调整为:不再使用PUT谓词(“使用这个URL存储这个文档”),而是使用POST谓词(“存储文档在这个URL命名空间下”)。

现在该URL只需包含_index_type:

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

除了_id是Elasticsearch自动生成的,响应的其他部分和前面的类似:

{
   "_index":    "website",
   "_type":     "blog",
   "_id":       "AVFgSgVHUP18jI2wRx0w",
   "_version":  1,
   "created":   true
}

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

取回一个文档

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

curl -X GET "localhost:9200/website/blog/123?pretty"

响应体增加了_source字段,这个字段包含我们索引数据时发送给Elasticsearch的原始JSON文档:

{
  "_index" :   "website",
  "_type" :    "blog",
  "_id" :      "123",
  "_version" : 1,
  "found" :    true,
  "_source" :  {
      "title": "My first blog entry",
      "text":  "Just trying this out...",
      "date":  "2019/01/01"
  }
}

URL中增加pretty参数,将会调用Elasticsearch的pretty-print功能,该功能使JSON响应体更加可读。但是,_source字段不能被格式化打印出来。相反,我们得到的_source字段中的JSON串,刚好是和我们传给它的一样。

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

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
}

返回文档的一部分

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

curl -X GET "localhost:9200/website/blog/123?_source=title,text&pretty"

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

{
  "_index" :   "website",
  "_type" :    "blog",
  "_id" :      "123",
  "_version" : 1,
  "found" :   true,
  "_source" : {
      "title": "My first blog entry" ,
      "text":  "Just trying this out..."
  }
}

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

curl -X GET "localhost:9200/website/blog/123/_source?pretty"

返回内容:

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

文档存在:

HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8
Content-Length: 0

文档不存在:

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进行实现:

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

在响应体中,我们能看到Elasticsearch已经增加 _version字段值:

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

created:

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

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

与update API的区别

update API虽然它似乎对文档直接进行了修改,但实际上Elasticsearch按前述完全相同方式执行以下过程:

  1. 从旧文档构建JSON
  2. 更改该JSON
  3. 删除旧文档
  4. 索引一个新文档
    唯一的区别在于, update API仅仅通过一个客户端请求来实现这些步骤,而不需要单独的getindex请求。

创建新文档

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

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

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

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

删除文档

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

curl -X DELETE "localhost:9200/website/blog/123?pretty"

如果找到该文档,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 内部记录本的一部分,用来确保这些改变在跨多节点时以正确的顺序执行。

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

处理冲突

当我们使用 index API 更新文档 ,可以一次性读取原始文档,做我们的修改,然后重新索引 整个文档 。 最近的索引请求将获胜:无论最后哪一个文档被索引,都将被唯一存储在 Elasticsearch 中。如果其他人同时更改这个文档,他们的更改将丢失。

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

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

悲观并发控制

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

乐观并发控制

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

乐观并发控制

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

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

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

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

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

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

首先我们检索文档:

curl -X GET "localhost:9200/website/blog/1?pretty"

响应体包含相同的 _version 版本号 1 :

{
  "_index" :   "website",
  "_type" :    "blog",
  "_id" :      "1",
  "_version" : 1,
  "found" :    true,
  "_source" :  {
      "title": "My first blog entry",
      "text":  "Just trying this out..."
  }
}

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

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

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

此请求成功,并且响应体告诉我们 _version 已经递增到 2 :

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

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

{
   "error": {
      "root_cause": [
         {
            "type": "version_conflict_engine_exception",
            "reason": "[blog][1]: version conflict, current [2], provided [1]",
            "index": "website",
            "shard": "3"
         }
      ],
      "type": "version_conflict_engine_exception",
      "reason": "[blog][1]: version conflict, current [2], provided [1]",
      "index": "website",
      "shard": "3"
   },
   "status": 409
}

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

我们现在怎么做取决于我们的应用需求。我们可以告诉用户说其他人已经修改了文档,并且在再次保存之前检查这些修改内容。

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

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

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

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

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

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

例如,要创建一个新的具有外部版本号 5 的博客文章,我们可以按以下方法进行:

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

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

{
  "_index":   "website",
  "_type":    "blog",
  "_id":      "2",
  "_version": 5,
  "created":  true
}

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

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

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

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

如果你要重新运行此请求时,它将会失败,并返回像我们之前看到的同样的冲突错误, 因为指定的外部版本号不大于 Elasticsearch 的当前版本号。

文档的部分更新

使用 update API 我们可以部分更新文档。

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

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

curl -X POST "localhost:9200/website/blog/1/_update?pretty" -H 'Content-Type: application/json' -d'
{
   "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" ], 
      "views":  0 
   }
}

新的字段已被添加到 _source 中。

使用脚本部分更新文档

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

curl -X POST "localhost:9200/website/blog/1/_update?pretty" -H 'Content-Type: application/json' -d'
{
   "script" : "ctx._source.views+=1"
}
'

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

curl -X POST "localhost:9200/website/blog/1/_update?pretty" -H 'Content-Type: application/json' -d'
{
   "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"], 
      "views":  1 
   }
}

我们可以看到search标签已追加到tags数组中,views字段已递增。

通过设置 ctx.opdelete 来删除基于其内容的文档:

curl -X POST "localhost:9200/website/blog/1/_update?pretty" -H 'Content-Type: application/json' -d'
{
   "script" : "ctx.op = ctx._source.views == count ? \u0027delete\u0027 : \u0027none\u0027",
    "params" : {
        "count": 1
    }
}
'

更新的文档可能尚不存在

假设我们需要 在 Elasticsearch 中存储一个页面访问量计数器。 每当有用户浏览网页,我们对该页面的计数器进行累加。但是,如果它是一个新网页,我们不能确定计数器已经存在。 如果我们尝试更新一个不存在的文档,那么更新操作将会失败。

在这样的情况下,我们可以使用upsert参数,指定如果文档不存在就应该先创建它:

curl -X POST "localhost:9200/website/pageviews/1/_update?pretty" -H 'Content-Type: application/json' -d'
{
   "script" : "ctx._source.views+=1",
   "upsert": {
       "views": 1
   }
}
'

我们第一次运行这个请求时, upsert 值作为新文档被索引,初始化 views 字段为 1 。 在后续的运行中,由于文档已经存在, script 更新操作将替代 upsert 进行应用,对 views 计数器进行累加。

更新和冲突

检索重建索引 步骤的间隔越小,变更冲突的机会越小。 但是它并不能完全消除冲突的可能性。 还是有可能在 update 设法重新索引之前,来自另一进程的请求修改了文档。

为了避免数据丢失, update API 在 检索 步骤时检索得到文档当前的 _version 号,并传递版本号到 重建索引 步骤的 index 请求。 如果另一个进程修改了处于检索和重新索引步骤之间的文档,那么_version号将不匹配,更新请求将会失败。

对于部分更新的很多使用场景,文档已经被改变也没有关系。 例如,如果两个进程都对页面访问量计数器进行递增操作,它们发生的先后顺序其实不太重要; 如果冲突发生了,我们唯一需要做的就是尝试再次更新。

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

指定失败重试次数,例如失败之前重试该更新5次:

curl -X POST "localhost:9200/website/pageviews/1/_update?retry_on_conflict=5&pretty" -H 'Content-Type: application/json' -d'
{
   "script" : "ctx._source.views+=1",
   "upsert": {
       "views": 0
   }
}
'

在增量操作无关顺序的场景,例如递增计数器等这个方法十分有效,但是在其他情况下变更的顺序 是 非常重要的。 类似 index API , update API 默认采用 最终写入生效 的方案,但它也接受一个 version 参数来允许你使用 optimistic concurrency control 指定想要更新文档的版本。

取回多个文档

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

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

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 数组 , 对于每一个在请求中指定的文档,这个数组中都包含有一个对应的响应,且顺序与请求中的顺序相同。 其中的每一个响应都和使用单个 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

curl -X GET "localhost:9200/website/blog/_mget?pretty" -H 'Content-Type: application/json' -d'
{
   "docs" : [
      { "_id" : 2 },
      { "_type" : "pageviews", "_id" :   1 }
   ]
}
'

如果所有文档的 _index_type 都是相同的,你可以只传一个 ids 数组,而不是整个 docs 数组:

GET /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  
    }
  ]
}

found=false :未找到该文档。
第二个文档未能找到并不妨碍第一个文档被检索到。每个文档都是单独检索和报告的。

即使有某个文档没有找到,上述请求的HTTP状态码仍然是200。事实上,即使请求没有找到任何文档,它的状态码依然是200--因为mget请求本身已经成功执行。为了确定某个文档查找是成功或者失败,你需要检查found标记。

代价较小的批量操作

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

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 本身组成--文档包含的字段和值。它是 indexcreate 操作所必需的,你必须提供文档以索引。

它也是 update 操作所必需的,并且应该包含你传递给 update API 的相同请求体: docupsertscript 等等。 删除操作不需要 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 请求有以下形式:

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

需要注意两点:

  • 请注意delete动作不能有请求体,它后面跟着的是另外一个操作。
  • 谨记最后一个换行符不要落下。
    Elasticsearch 响应包含 items 数组, 这个数组的内容是以请求的顺序列出来的每个请求的结果。

    {
    "took": 4,
    "errors": false, 
    "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,并且在相应的请求报告出错误明细:

    curl -X POST "localhost:9200/_bulk?pretty" -H 'Content-Type: application/json' -d'
    { "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": 3,
    "errors": true, 
    "items": [
      {  "create": {
            "_index":   "website",
            "_type":    "blog",
            "_id":      "123",
            "status":   409, 
            "error":    "DocumentAlreadyExistsException 
                        [[website][4] [blog][123]:
                        document already exists]"
      }},
      {  "index": {
            "_index":   "website",
            "_type":    "blog",
            "_id":      "123",
            "_version": 5,
            "status":   200 
      }}
    ]
    }

    errors=true:一个或者多个请求失败。

status=409:这个请求的HTTP状态码报告为 409 CONFLICT

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

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

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

不要重复指定Index和Type

也许你正在批量索引日志数据到相同的 indextype 中。 但为每一个文档指定相同的元数据是一种浪费。相反,可以像 mget API 一样,在 bulk 请求的 URL 中接收默认的 /_index 或者 /_index/_type

curl -X POST "localhost:9200/website/_bulk?pretty" -H 'Content-Type: application/json' -d'
{ "index": { "_type": "log" }}
{ "event": "User logged in" }
'

可以覆盖元数据行中的 _index_type , 但是它将使用URL中的这些元数据值作为默认值:

curl -X POST "localhost:9200/website/log/_bulk?pretty" -H 'Content-Type: application/json' -d'
{ "index": {}}
{ "event": "User logged in" }
{ "index": { "_type": "blog" }}
{ "title": "Overriding the default type" }
'

多大是太大了?

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

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

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


除非注明,否则均为李锋镝的博客原创文章,转载必须以链接形式标明本文链接

本文链接:https://www.lifengdi.com/archives/article/tech/934

分享到:
打赏 赞(0)
微信
支付宝
微信二维码图片

微信扫描二维码打赏

支付宝二维码图片

支付宝扫描二维码打赏

   

说点什么

avatar
  订阅  
提醒