ElasticSearch(三)--文档

面向对象编程语言流行的原因之一是,可以用对象表示和处理现实生活中那些有潜在关系和复杂结构的实体.到目前为止,这种方式还不错.

当我们存储这些实体时问题来了,以行和列的形式将数据存储在关系数据库中,相当于使用电子表格,这种方式使对象的灵活性不复存在.

如何能以对象的方式存储数据呢?使程序专注于使用数据,而不是围绕行列的表格建模.

对象Object是一种语言相关,记录在内存中的数据结构.为了在网络中发送或者存储它,一种标准化的格式JSON被采用.

JSON是一种可读的,以文本来表示对象的方式.当对象被序列化为JSON以后,它就成为JSON文档了.


ElasticSearch就是一个分布式的文档存储引擎,它可以实时存储并检索复杂数据结构-序列化的JSON文档.

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

通常认为对象和文档是等价相通的,在ES中,文档document这个术语有特殊的含义,它特指最顶层结构或者根对象序列化为的JSON数据,以唯一ID标识在ES中.

本章节探讨如何使用API创建,检索,更新,删除文档.

1. 文档元数据

一个文档不只有数据,它还包含了元数据Metadata-关于文档的信息.

三个必须的元数据元素:

_index :文档存储的地方;

    索引类似于关系数据库中的数据库,它是用来存储和索引关联数据的地方;

    事实上,数据被存储和索引在分片中,索引只是把一个或多个分片组织在逻辑空间,对于应用程序而言,只关心索引即可.

    命名必须是全部小写,不能以下划线开头.

_type:文档代表的对象的类;使用相同类型的文档表示相同的事物,它们的数据结构也是相同的;

   每个类型都有自己的映射mapping或结构定义,类型的映射告诉ES不同的文档如何被索引,后续会有关于映射的定义和管理,现在不需深入;

   命名大小写都可以,不能以下划线开头,不能包含逗号;

_id:文档的唯一标识;仅是一个字符串,与_index和_type组合,唯一标示确定一个文档.

还有一些其他的元数据,暂不考虑.

2. 索引indexing文档

文档通过index API被索引,通过指定_index,_type,_id唯一指定文档,_id可以自己指定,也可以由ES自动生成.

手动指定_id:

使用PUT请求方式索引一个文档:

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

例,索引一个文档:

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

{
  "_index": "website",
  "_type": "blog",
  "_id": "123",
  "_version": 7,
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "created": true
}
响应指出请求的结果已经被创建,created为true;文档的元数据_index,_type,_id;

_version是该文档的版本号,在ES中每个文档都有版本号,每当文档变化,包括删除时,版本号都会增加,其目的是确保你的程序一部分不会覆盖掉另一部分所做的修改,在版本控制章节深入学习.

_shards表示分片的分配情况.

自增_id

使用POST请求方式索引文档,让ES自动生成_id,与PUT方式的不同含义:

PUT是在这个URL中存储文档,把文档存储到某个ID对应的空间;

POST是在这个位置下存储文档,把文档添加到_type下.

POST /website/blog/
{
  "title": "My second blog entry",
  "text":  "Still trying this out...",
  "date":  "2014/01/01"
}
只需指定_index和_type

响应:

{
  "_index": "website",
  "_type": "blog",
  "_id": "AVSoCeLh4XBrQdKsrxp0",
  "_version": 1,
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "created": true
}
自动生成的ID是20字符长度,URL-safe, Base64-encoded GUID 的字符串,这些GUID(全局唯一标识符Globally Unique IDentifier)是由修改的FlakeID机制产生的,该机制可以使多个节点并行的产生唯一ID,零机会冲突.

3. 检索Retrieving文档

索引文档到ES集群之后,可以检索这些文档.

使用GET请求方式,指定_index,_type,_id三个元素.

GET /website/blog/123
响应:

{
  "_index": "website",
  "_type": "blog",
  "_id": "123",
  "_version": 7,
  "found": true,
  "_source": {
    "title": "My first blog entry",
    "text": "Just trying this out...",
    "date": "2014/01/01"
  }
}
响应中增加了_source字段,它包含了在创建索引时的原始文档.

_found字段表示文档是否被找到,如果不存在,响应依旧返回JSON数据,_found字段为false

{
  "_index": "website",
  "_type": "blog",
  "_id": "124",
  "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
}

检索文档是否存在,还可以使用HEAD方式,只能通过curl命令请求:

curl -i -XHEAD http://localhost:9200/website/blog/123
如果存在,响应为"200 OK"

HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8
Content-Length: 0
不存在,响应为"404 Not Found"
HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=UTF-8
Content-Length: 0

检索文档的一部分:

如果不需要得到文档的全部内容,只关心里边的某些字段数据,使用添加参数的方式_source=filed1,filed2...:

GET /website/blog/123?_source=title,text
响应结果:

{
  "_index": "website",
  "_type": "blog",
  "_id": "123",
  "_version": 7,
  "found": true,
  "_source": {
    "text": "Just trying this out...",
    "title": "My first blog entry"
  }
}
如果_source添加的字段field不存在,不会出错,只是在响应的_source字段没有该field

如果响应中只得到_source字段,而不需要其他元数据:

GET /website/blog/123/_source

注:实际中不起作用,还是返回包含元数据的所有信息,同GET  /website/blog/123效果一样

4. 更新

文档在ES中是不可变的,我们不能修改它们,更新的方式就是通过建立新的文档索引,然后替换以前的文档索引.

还是同创建索引一样,使用PUT方式:

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

每次更新,响应中,文档的元数据_version就会增加1

{
  "_index": "website",
  "_type": "blog",
  "_id": "123",
  "_version": 8,
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "created": false
}
ES集群中已经存在这个文档索引,那么响应中的created字段false
在内部,Elasticsearch已经标记旧文档为删除并添加了一个完整的新文档。旧版本文档不会立即消失,但你也不能去访问它。Elasticsearch会在你继续索引更多数据时清理被删除的文档

后边会学习到局部更新文档,其实也是这样的原理与过程.

5. 索引新文档

前边提到了索引文档的方法,索引一个新的文档和更新一个文档使用PUT方式是同样的,那如何避免索引的是一个新文档,而不是覆盖旧文档呢?

对于这个问题,可以使用POST方式,使ES自动生成文档的唯一_id.

如果需要手动指定id,那么就需要确保指定的索引,类型和id三个唯一指定一个文档,才能接受请求:

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

PUT /website/blog/123?op_type=create
{ ... }
第二种方法,在URL后添加/_create作为端点

PUT /website/blog/123/_create
{ ... }
这两种方法其实实质是一样的.

当索引一个新的文档成功后,将返回正常的元数据,且状态码是201 Created;如果ES中已经存在有该文档,那么返回409 Conflict状态码

curl -i -XPUT http://localhost:9200/website/blog/128/_create?pretty -d '
{
  "title": "My first blog entry",
  "text":  "Just trying this out...",
  "date":  "2014/01/01"
}'
响应:

HTTP/1.1 201 Created
Content-Type: application/json; charset=UTF-8
Content-Length: 179

{
  "_index" : "website",
  "_type" : "blog",
  "_id" : "128",
  "_version" : 1,
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "created" : true
}
再执行一次这个命令.得到响应:

HTTP/1.1 409 Conflict
Content-Type: application/json; charset=UTF-8
Content-Length: 376

{
  "error" : {
    "root_cause" : [ {
      "type" : "document_already_exists_exception",
      "reason" : "[blog][128]: document already exists",
      "shard" : "0",
      "index" : "website"
    } ],
    "type" : "document_already_exists_exception",
    "reason" : "[blog][128]: document already exists",
    "shard" : "0",
    "index" : "website"
  },
  "status" : 409
}

6. 删除文档

删除文档使用DELETE请求:

DELETE /website/blog/128
如果删除成功,响应:

{
  "found": true,
  "_index": "website",
  "_type": "blog",
  "_id": "128",
  "_version": 3,
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  }
}
响应状态码为200 OK,响应体中found为true,且_version加1

如果文档不存在,将返回404 Not Found的状态码,响应体为:

{
  "found": false,
  "_index": "website",
  "_type": "blog",
  "_id": "128",
  "_version": 4,
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  }
}
尽管found为false,响应体的_version依然增加1,这时内部记录的一部分,它确保在多节点不同操作可以有不同的顺序.

7. 版本控制

在使用index API更新文档时,将文档一次性重新索引,最近的请求索引会生效,也就是ES只存储最后任何被索引的文档.如果在这期间,其他人也对文档做了修改,那么这些修改就会丢失.

变化越是频繁,或者读取或更新的时间越长,越容易丢失我们的更改.

在数据库中有两种方法确保在更新时,修改步丢失:

悲观并发控制Pessimistic concurrency control

这种方式在关系数据库中经常使用,它假设更改经常发生,为解决冲突,把访问区块化,典型的例子是在访问一行数据前锁定该行,只有加锁的进程可以修改该行数数据.

乐观并发控制Optimistic concurrency control

在ES中使用的,假设冲突不经常发生,也不区块化访问,然而在读写过程中数据发生了变化,更新操作将失败.这时由程序决定在失败后如何解决冲突.实际中,可以重新尝试修改.

ES是分布式的,在文档被创建或者更新后,新的文档会被复制发送到集群中其他的节点上,这些复制请求都是平行发送的,会无序的到达节点上,会出现旧的版本在最新的版本之后到达同一节点,这就需要老版本的文档永远不会覆盖掉新的版本.这就是ES的版本控制.

每个文档都有一个_version元数据,当更修,修改,删除时,该元数据都会增加1,ES使用_version确保所有修改都被正确排序.

当一个旧版本出现在新版本之后,这个就版本会被简单忽略.

现在,创建一个新的文档:

PUT /website/blog/1/_create
{
  "title": "My first blog entry",
  "text":  "Just trying this out..."
}
响应体的_version是1,现在我们检索这个文档,并做修改之后重新提交:

PUT /website/blog/1?version=1
{
  "title": "My first blog entry",
  "text":  "Starting to get the hang of this..."
}
通过URL参数version=1的方式,指定更新的文档是版本号为1的,如果在这个过程中,该文档没有被其他进程更新过,那么文档版本号是与请求相符的,

请求成功,verison增加1到2.

如果重新执行一次上边的请求,此时文档的版本号为2,并不符合请求指定的1,响应结果:

{
   "error": {
      "root_cause": [
         {
            "type": "version_conflict_engine_exception",
            "reason": "[blog][1]: version conflict, current [2], provided [1]",
            "shard": "3",
            "index": "website"
         }
      ],
      "type": "version_conflict_engine_exception",
      "reason": "[blog][1]: version conflict, current [2], provided [1]",
      "shard": "3",
      "index": "website"
   },
   "status": 409
}
所有更新和删除文档的请求都接受version参数,它可以允许你在代码中增加乐观控制.

使用外部版本控制系统

一种常见的结构是使用一些其他的数据库做为主数据库,然后使用Elasticsearch搜索数据,这意味着所有主数据库发生变化,就要将其拷贝到Elasticsearch中.

ES可以使用外部软件提供的版本控制号,例如关系数据库提供的

外部版本号与内部版本的使用不同,内部版本号是在请求指定的版本号和文档的版本号一致时,才接受请求操作.而外部版本号,是判断文档中的版本号是否小于请求的版本号,才接受请求.(必须小于,相等也不可以)

外部版本号的使用,通过在查询字符串后加version_type=external来使用外部版本号.

可以在索引,更新,删除文档的时候,指定外部版本号:

索引一个文档,指定其版本号为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.

现在更新文档,指定新的版本号为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

现在再执行一次更新操作,版本号依旧指定为10,得到响应冲突.

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

8. 局部更新

在ES中,文档是不能修改的,只能被替换.前边学习的文档更新,是采用重新索引文档的方式实现文档更新.

局部更新使用update API,通过一个请求实现局部更新,例如增加数量.局部更新同更新文档的原理一样:检索-修改-重新索引

局部更新使用POST请求,在URL添加/_update的方式,接受一个局部文档参数doc,将参数携带的内容合并到现有文档中,已经存在的字段被覆盖,新的字段会被添加.

向元文档/website/blog/1中添加两个字段tags和views:

POST /website/blog/1/_update
{
   "doc" : {
      "tags" : [ "testing" ],
      "views": 0
   }
}
如果成功,返回与索引一个新文档得到的响应相同:

{
  "_index": "website",
  "_type": "blog",
  "_id": "1",
  "_version": 3,
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  }
}
重新索引文档,得到响应:

{
  "_index": "website",
  "_type": "blog",
  "_id": "1",
  "_version": 3,
  "found": true,
  "_source": {
    "title": "My first blog entry",
    "text": "Starting to get the hang of this...",
    "views": 0,
    "tags": [
      "testing"
    ]
  }
}
可以看出新添加的字段已经在_source字段中.

使用脚本局部更新

也可以通过脚本的方式局部更新文档,在请求体中使用script字段里写脚本代码,在脚本里ctx._source表示文档内容.

例如,修改views的数量加1:

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

不知到为什么一直使用不了脚本,关于脚本的使用,有待深入研究.下边是关于使用脚本时出现的错误:

{
   "error": {
      "root_cause": [
         {
            "type": "remote_transport_exception",
            "reason": "[Jamal Afari][127.0.0.1:9300][indices:data/write/update[s]]"
         }
      ],
      "type": "illegal_argument_exception",
      "reason": "failed to execute script",
      "caused_by": {
         "type": "script_exception",
         "reason": "scripts of type [inline], operation [update] and lang [groovy] are disabled"
      }
   },
   "status": 400
}

我已经安装了Groovy软件,并配置了环境变量,运行groovy -v得到的版本号:

Groovy Version: 2.4.6 JVM: 1.8.0_91 Vendor: Oracle Corporation OS: Linux

9. 检索多个文档

合并多个请求可以避免每个请求单独的网络开销,在ES中检索多个文档依旧非常快,使用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": [
    {
      "_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": 0
      }
    }
  ]
}
如果检索的多个文档在同一个index,type下,就可以指定默认的_index,_type:

GET /website/blog/_mget
{
   "docs" : [
      { "_id" : 2 },
      { "_type" : "pageviews", "_id" :   1 }
   ]
}
虽然指定了默认的type为blog,还可以在请求体docs参数中修改为pageviews.

事实上如果所有的文档具有相同的索引,类型,那么可以更加简化的检索文档,使用ids参数数组:

GET /website/blog/_mget
{
   "ids" : [ "2", "3" ]
}
虽然/website/blog/3文档不存在,还是会在响应中体现:

{
  "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": "3",
      "found": false
    }
  ]
}
虽然有个别文档检索不到,但是HTTP的响应码为200 OK,因为mget API是请求成功了的.对于找不到的文档,found字段为false.

每个文档的检索与报告都是独立的.

10. 批量操作

mget API可以使我们一次性检索多个文档,而bulk API允许我们在一个请求里实现多次create,index,update,delete操作,其实就是增删改操作.

bulk的请求体格式:

{ action: { metadata }}\n
{ request body        }\n
{ action: { metadata }}\n
{ request body        }\n
...
action行为行和reques请求体t行,每个都各成一行,不能分为两行,中间也不能隔开一行,否则出错.

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

1. 每行必须以"\n"符号结尾,包括最后一行.

2. 每行的数据都不能包含未转义的换行符

action/metadata这一行指定什么样的行为发生在哪一个文档上.

action必须是以下几种:

create:当文档不存在时创建

index:创建新文档或覆盖已有的文档

update:局部更新文档

delete:删除文档

在执行这些操作时,都必须指明文档的元数据_index,_type,_id,例如删除操作是这样的:

{ "delete": { "_index": "website", "_type": "blog", "_id": "123" }}
请求体包含文档的一些字段及其值,例如创建一个不存在的文档:

{ "create":  { "_index": "website", "_type": "blog", "_id": "123" }}
{ "title":    "My first blog post" }
如果不指定_id,ID会自动创建.

现在执行一个多操作的例子:

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操作没有请求体,它紧接着另一个行为;记得最后一个换行符

得到的响应:

{
  "took": 89,
  "errors": false,
  "items": [
    {
      "delete": {
        "_index": "website",
        "_type": "blog",
        "_id": "123",
        "_version": 1,
        "_shards": {
          "total": 2,
          "successful": 1,
          "failed": 0
        },
        "status": 404,
        "found": false
      }
    },
    {
      "create": {
        "_index": "website",
        "_type": "blog",
        "_id": "123",
        "_version": 2,
        "_shards": {
          "total": 2,
          "successful": 1,
          "failed": 0
        },
        "status": 201
      }
    },
    {
      "create": {
        "_index": "website",
        "_type": "blog",
        "_id": "AVSuUUI8IWAhZgGDHw0H",
        "_version": 1,
        "_shards": {
          "total": 2,
          "successful": 1,
          "failed": 0
        },
        "status": 201
      }
    },
    {
      "update": {
        "_index": "website",
        "_type": "blog",
        "_id": "123",
        "_version": 3,
        "_shards": {
          "total": 2,
          "successful": 1,
          "failed": 0
        },
        "status": 200
      }
    }
  ]
}
返回结果包含在一个items数组中,结果的顺序与请求的顺序相同;

每个子请求都独立的完成,互不影响,如果有一个子请求没成功,那么errors字段为true.

如果子请求失败,响应的结果中,子请求对应的数组元素会显示.

这说明bulk请求不是原子操作,它们不能实现事务.

同mget API一样,也可以指定默认的索引,类型,不至于在每个请求中都要写出:可以只指明索引,也可以同时指明索引和类型.

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

POST /website/log/_bulk
{ "index": {}}
{ "event": "User logged in" }
{ "index": { "_type": "blog" }}
{ "title": "Overriding the default type" }

那么执行bulk请求时,可以批量执行成百上千的操作,一次性执行的多少个请求算合适呢?

因为批量请求的文档操作,需要被加载到我们请求的节点内存中,受制硬件设备.

操作与文档的大小也有关系,一千个1kb的文档和一千个1mb的文档肯定是不同的.

有一个最佳点sweetspot:一个好的批次,最好保持在5-15M之间.

学习完这一章,就学会了如何将ES作为分布式存储系统使用了.

猜你喜欢

转载自blog.csdn.net/WuyZhen_CSDN/article/details/51388098