文档(document)

大多数实体或对象(object)可以序列化位JSON格式的。在ES中,文档(document)特指最顶层的对象(root object)序列化成的JSON数据,并通过唯一标识存储在ES中。

文档元数据(metadata)

元数据(metadata)是关于文档的信息。

节点 意义
_index 文档存储的地方
_type 文档代表的对象的类型
_id 文档的唯一标识
  • index(索引)
    即索引,是存储和索引关联数据的地方。实际上数据真正存储在shards中,index只是一个把一个或多个shards分组在一起的逻辑空间。
    索引的创建只需选择一个索引名称:不可以"_"开头,全部小写,不包含逗号。
  • type(类型)
    相同类型(type)的文档表示相同的“事物”。
    每个类型(type)都有自己的映射(mapping)或者结构定义,所有类型下的文档被存储在同一个索引下,但是类型的mapping会告诉ES不同的文档如何被索引。
    _type 的名字可以是大写或小写,不能包含下划线或逗号。
  • _id
    id仅仅是一个字符串,它与 _index 和 _type 组合时,就可以在Elasticsearch中唯一标识一个文档。当创建一个文档,可以自定义 _id ,也可以让ES自动生成。

索引文档

文档通过其 _index,_type,_id 唯一确定。

PUT & POST 创建/更新 文档

这里要提到一个机制,在 restful api风格中,put一般表示更新资源,post表示对创建资源。在ES中,put可以理解为:若指定的index、type、id可以找到一个已存在的数据,则对其进行更新操作,否则进行创建。
而post因为其id是由ES自动生成的所有始终是创建。

请求格式如下:

1
2
3
4
5
6
7
8
9
10
11
PUT /{index}/{type}/{id}
{
"field": "value",
...
}

POST /{index}/{type}
{
"field": "value",
...
}

响应内容:

1
2
3
4
5
6
7
{
"_index": "index_name", <- index名称
"_type": "type_name", <- type 名称
"_id": "wM0OSFhDQXGZAWDf0-drSA", <- 如果是PUT 则是传入的ID,如果是POST 则是自动生成的ID字符串
"_version": 1, <- 版本号
"created": true <- true表示当前文档是新创建的。false表示已经存在只是更新了该文档,这时也伴随着_version的增加
}

Elasticsearch中每个文档都有版本号,每当文档变化(包括删除,以及PUT的数据已存在)都会使 _version 增加。

GET 检索文档

请求格式:

1
2
3
4
5
GET /{index}/{type}/{id}?pretty 

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

响应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"_index" : "index_name",
"_type" : "type_name",
"_id" : "wM0OSFhDQXGZAWDf0-drSA",
"_version" : 1,
"found" : true,
"_source" : {
"title": "My first blog entry",
"text": "Just trying this out...",
"date": "2014/01/01"
}
}

"found" : true, <- true 表示找到了数据,false表示未找到数据,此外,HTTP响应状态码也会变成 '404 Not Found' 代替 '200 OK'

GET 检索文档的一部分

  1. 可以使用 _source 参数,值可以是多个字段使用逗号分隔 如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
GET /website/blog/123?_source=title,text

{
"_index" : "website",
"_type" : "blog",
"_id" : "123",
"_version" : 1,
"exists" : true,
"_source" : {
"title": "My first blog entry" ,
"text": "Just trying this out..."
}
}
  1. 只想得到 _source 字段而不要其他的元数据 如下:
1
2
3
4
5
6
7
GET /website/blog/123/_source

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

HEAD 检查文档是否存在

HEAD 请求不会返回响应体,只有HTTP头,可以通过 Status Code 判断是否存在资源:200存在/404不存在

重谈:关于 PUT 更新/创建 文档

前边已经说过,在ES的机制中 PUT 同时具有创建和更新两种操作,但是为了保证对数据的管理与请求的意愿是一致的。可以通过两种方式保证。

  • 使用 op_type 查询参数

    1
    2
    PUT /{index}/{type}/{id}?op_type=create
    { ... }
  • 是在URL后加 /_create 做为端点

    1
    2
    PUT /{index}/{type}/{id}/_create
    { ... }

    此时,如果正常创建,ES会返回正常的元数据且响应状态码是 201 Created。如果包含相同的 _index 、 _type 和 _id 的文档已经存在,将返回 409 Conflict 响应状态码以及错误信息:

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

DELETE 删除文档

请求格式:

1
DELETE /{index}/{type}/{id}

返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
如果找到文档,返回状态 200 ok 响应体:
{
"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 依旧会增加。这是内部记录的一部分,它确保在多节点间不同操作可以有正确的顺序。

关于并发操作的问题 本版控制

类似关系型数据库,在高并发的场景下,会出现幻读进而造成数据丢失的情况。

ES使用乐观锁的机制,使用 _version 字段保证所有修改都被正确排序。当一个旧版本出现在新版本之后,它会被简单的忽略。并且可以指定文档的 version 来做想要的更改。如果那个版本号不是现在的,请求就失败了。

也可以使用外部的本版控制系统,比如使用timestamp,可以在查询字符串后边添加 version_type=external 来使用这些版本号。外部版本号与之前说的内部版本号在处理的时候有些不同。它不再检查 _version 是否与请求中指定的一致,而是检查是否小于指定的版本。如果请求成功,外部版本号就会被存储到 _version 中。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
创建一个包含外部版本号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
}

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

局部更新

  • 通过 /_update api 更新文档,请求表单接受一个局部文档参数 doc:

    1
    2
    3
    4
    5
    6
    7
    POST /website/blog/1/_update
    {
    "doc" : {
    "tags" : [ "testing" ],
    "views": 0
    }
    }
  • 使用脚本局部更新,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    1.
    views 字段 +1:
    POST /website/blog/1/_update
    {
    "script" : "ctx._source.views+=1"
    }

    2.
    tags数组字段 添加新元素 search:
    POST /website/blog/1/_update
    {
    "script" : "ctx._source.tags+=new_tag",
    "params" : {
    "new_tag" : "search"
    }
    }

    3.
    使用 upsert 参数定义文档来使其不存在时被创建
    POST /website/pageviews/1/_update
    {
    "script" : "ctx._source.views+=1",
    "upsert": {
    "views": 1
    }
    }

    4.
    在不关心执行顺序时可以通过 retry_on_conflict 参数设置重试次数来自动完成 值默认为0
    POST /website/pageviews/1/_update?retry_on_conflict=5
    {
    "script" : "ctx._source.views+=1",
    "upsert": {
    "views": 0
    }
    }
    对于需要保证顺序的更新操作,则需要指定version信息

检索多个文档 mget

为了减少网络开销,ES支持合并请求检索多个文档。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /_mget
{
"docs" : [
{
"_index" : "website",
"_type" : "blog",
"_id" : 2
},
{
"_index" : "website",
"_type" : "pageviews",
"_id" : 1,
"_source": "views"
}
]
}

其返回结果的顺序与请求的数组顺序一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
"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
}
}
]
}

每个文档的检索和报告都是独立的,如果其中某些文档不存在不会影响到其他可以查询到的文档。

注:尽管前面提到有一个文档没有被找到,但HTTP请求状态码还是 200 。事实上,就算所有文档都找不到,请求也还是返回 200 ,原因是 mget 请求本身成功了。如果想知道每个文档是否都成功了,你需要检查 found 标志。

更新时的批量操作 bulk

bulk API允许我们使用单一请求来实现多个文档的 create\index\update\delete。

bulk 请求体如下:

1
2
3
4
5
{ action: { metadata }}\n
{ request body }\n
{ action: { metadata }}\n
{ request body }\n
...

用 “\n” 符号连接起来的一行一行的JSON文档流(stream),并且:

  • 每行必须以 “\n” 符号结尾,包括最后一行。这些都是作为每行有效的分离而做的标记。
  • 每一行的数据不能包含未被转义的换行符,它们会干扰分析,这意味着JSON不能被美化打印。

bulk 请求不是原子操作——它们不能实现事务。每个请求操作时分开的,所以每个请求的成功与否不干扰其它操作。