《Elasticsearch检索引擎学习指南》第三章 数据输入和输出

参考资料

数据输入和输出

ES是分布式的 文档 存储。它能存储和检索复杂的数据结构,序列化为JSON文档,以 实时 的方式。换句话说,一旦一个文档被存储在ES中,它就是可以被集群中的任意节点检索到。
尽管现存的NoSQL解决方案允许我们以文档形式存储对象,但是他们仍旧需要我们思考如何查询数据,以及确定哪些字段需要被索引以加快数据检索。
在ES中,每个字段的所有数据都是默认被索引的。即每个字段都有为了快速检索设置的专用倒排索引 。它可以在同一个查询中使用这些倒排索引,返回结果。
在本章中,展示用来创建、检索、更新和删除文档的API。

什么是文档

多数应用中,实体或对象可以被序列化为包含键值对的JSON对象。在ES中,文档与对象类似,但有一个区别,文档 特指最顶层或者根对象,这个根对象被序列化成JSON并存储到ES中,指定了唯一ID
注意: 字段的名字可以是任何合法的字符串,但不可以包含英文句号。

文档元数据

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

  • _index 文档存放位置
  • _type 文档表示的对象类别
  • _id 文档唯一标识

_index

一个索引应该是因共同的特性被分组到一起的文档集合
TIP 在ES中,数据是被存储和索引在分片中,而一个索引仅仅是逻辑上的命名空间,这个命名空间由一个或者多个分片组合在一起。

_type

数据可能在索引中只是松散的组合在一起,但是通常明确定义一些数据中的子分区是很有用的。
ES公开了一个称为types的特性,它允许在索引中对数据进行逻辑分区。不同types的文档可能有不同的字段,但最好能够非常相似。

_id

ID是一个字符串,当它和_index以及_type组合就可以唯一确定ES中的一个文档。当创建一个新的文档,可以提供资金的_id,或者让ES来生成。

除此之外,还有一些其他元数据元素。

索引文档

通过使用index API,文档可以被索引,存储和使文档可被检索。但是要先确定文档的位置。如上面所述,一个文档的index,type和id可以唯一标识一个文档。

使用自定义的ID

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

PUT /{index}/{type}/{id}
{
	“field”:"value",
	……
}

ES响应如下:

{
	"_index":""
	"_type":""
	"_id":""
	"_version":1
	"created":true
}

该响应表明文档已经成功创建
在ES中每个文档都有一个版本号。当每次对文档进行修改,_version的值会递增。版本号有助于解决文档修改导致的冲突。

自动生成ID

如果你的数据没有自然的ID,ES可以帮助自动生成。请求的结构调整为:不再使用PUT谓词。而是使用POST
URL只需包含_index _type

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

ES响应如下:_id为自动生成

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

自动生成的ID是URL-safe,Base64编码,长度为20字符的GUID字符串。

取回一个文档

为了从ES中检索出文档,仍然使用_index _type _id,使用GET谓词

GET /website/blog/123?pretty

响应体包括目前已经熟悉了的元数据元素,再加上_source字段,这个字段包含索引数据时发送给ES的原始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参数,将会调用ES的pretty-print功能,该功能使得JSON响应体更加可读。

返回文档的一部分

默认情况下,GET请求会返回整个文档,这个文档正如存储在_source字段中一样。如果只需要返回某个字段,可用_source参数请求得到,多个字段可以使用逗号分隔的列表来指定。

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

ES响应如下

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

如果只想得到_source字段,不需要返回任何元数据,可以使用如下url

GET /website/blog/123/_source

ES响应如下:

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

检查文档是否存在

如果只检查文档是否存在,不关心文档内容。需要用HEAD代替GET。HEAD请求没有返回体,只返回一个http请求报文头。返回200ok状态码。文档不存在ES将返回404not found

更新整个文档

在ES中文档是不可改变的,不能修改他们。相应的,如果要更新现有文档,需要重建索引或者进行替换,可以使用相同的index API进行实现覆盖。如下覆盖一个ID为123的文档

PUT /website/blog/123
{
  "title": "My first blog entry",
  "text":  "I am starting to get the hang of this...",
  "date":  "2014/01/02"
}

ES响应中,文档版本号_version自增,created则为false

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

在内部,ES已将旧文档标记为 已删除 ,并增加一个新文档。尽管不能对旧文档进行访问,但是旧文档不会立即消失。ES会在后台清理这些文档。

后面会讲到update API,这个API可以用于对文档部分更新。似乎直接修改了文档,但ES实际的执行过程如下:

  1. 从旧文档创建JSON
  2. 更改JSON
  3. 删除旧文档
  4. 索引一个新文档

唯一的区别在于,update API仅仅通过一个客户端请求来实现这些步骤,不需要单独的get和index请求。

创建新文档

当我们索引一个文档,怎么确认是在创建一个全新的文档,不是覆盖现有的?
确保创建一个新文档的最简单方法是,使用索引请求的POST形式让ES自动生成唯一_id

然而,如果已经有自己的_id,那么我们必须告诉ES,只有在相同的_index _type _id不存在时才接受索引请求。这里有两种方式,原理相同。

  • 使用op_type查询-字符串参数
PUT /website/blog/123?op_type=create
{ ... }
  • 在URL末端使用/_create
PUT /website/blog/123/_create
{ ... }

如果创建新文档的请求成功执行,ES会返回元数据和一个201 created的http状态码。
如果文档已存在,ES将会返回409 confict相应码,以及错误信息

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

DELETE /website/blog/123

如果找到该文档,ES将返回200OK,和一个如下结构的响应体。其中_version值已经增加

{
  "found" :    true,
  "_index" :   "website",
  "_type" :    "blog",
  "_id" :      "123",
  "_version" : 3
}

如果文档没有找到,将得到404 NOT FOUND的相应码和如下响应体:

{
  "found" :    false,
  "_index" :   "website",
  "_type" :    "blog",
  "_id" :      "123",
  "_version" : 4
}

即使文档不存在,_version的值仍然会增加。这是ES内部记录日志的一部分,用于确保这些改变在跨多节点时以正确的顺序执行。

处理冲突

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

有时这种冲突可能有严重后果。导致数据错误
在这里插入图片描述

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

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

乐观并发控制

ES是分布式的。当文档创建,更新或删除时,新版本的文档必须复制到集群中的其他节点。ES也是异步和并发的,这意味着这些复制请求被并行发送,并且到达目的地时也许顺序是乱的。ES需要一种方法确保文档的旧版本不会覆盖新版本。主要就是依靠_version元数据。在修改请求中添加版本号限制
例如修改文档的版本号实际为2,用版本号1标记的文档数据去修改

PUT /website/blog/1?version=1 
{
  "title": "My first blog entry",
  "text":  "Starting to get the hang of this..."
}

ES会返回409conflict,和如下响应体:

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

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

通过外部系统使用并发控制

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

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

外部版本号的处理方式和内部版本号的处理方式有些不同,ES不是检查当前_version和请求中指定的版本号是否相同,而是检查当前_versiion是否小于指定的版本号。如果请求成功,外部的版本号作为文档的新_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..."
}

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

文档的部分更新

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

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" ], 
      "views":  0 
   }
}

使用脚本部分更新文档

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

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

剩余内容参考 这里

取回多个文档

将多个请求合并成一个,避免单独处理每个请求花费的网络延时和开销。如果需要从ES检索很多文档,那么使用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 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 。
你仍然可以通过单独请求覆盖这些值:(第二条请求pageviews覆盖了blog的类型)

GET /website/blog/_mget
{
   "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  
    }
  ]
}

事实上第二个文档未能找到并不妨碍第一个文档被检索到。每个文档都是单独检索和报告的。

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

代价较小的批量操作

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

这个 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 ,并且在相应的请求报告出错误明细:

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

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

不要重复指定Index和Type

也许你正在批量索引日志数据到相同的 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。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值