ES1_4文档

常用操作 - 对象(文档 Document)

文档

  • ES是面向文档的,文档是所有可搜索数据的最小单位。
  • 文档会被序列化成JSON格式,保存在ES中;
    JSON对象由字段组成,每个字段都有对应的字段类型。
  • 每个文档都有一个Unique ID。
    这个Unique ID可以自己指定或由ES自动生成。

文档和字段 - Document、Field

一个文档是一个可被索引的基础信息单元,文档以 JSON 格式来表示。
在一个 index/type 里面,可以存储任意多的文档,每个文档都有唯一 id。
每个文档包含多个字段(fields),即 json 数据里的字段。

文档元数据

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

  • _index
    一个 索引 应该是因共同的特性被分组到一起的文档集合。
    索引名字必须小写,不能以下划线开头,不能包含逗号。
  • _type
    Lucene 没有文档类型的概念,而是使用一个元数据字段_type 文档表示的对象类别,数据可能在索引中只是松散的组合在一起,但是通常明确定义一些数据中的子分区是很有用的,不同 types 的文档可能有不同的字段,但最好能够非常相似。
    一个 _type 命名可以是大写或者小写,但是不能以下划线或者句号开头,不应该包含逗号, 并且长度限制为 256 个字符。
    当我们要检索某个类型的文档时, Elasticsearch 通过在 _type 字段上使用过滤器限制只返回这个类型的文档。
  • _id
    文档唯一标识,和 _index 以及 _type 组合就可以唯一确定 Elasticsearch 中的一个文档。
    id 也可以由 Elasticsearch 自动生成。
  • _version
    在 Elasticsearch 中每个文档都有一个版本号。当每次对文档进行修改时(包括删除), _version 的值会递增。这个字段用来确保这些改变在跨多节点时以正确的顺序执行。
    版本号——不管是内部的还是引用外部的——都必须是在(0, 9.2E+18)范围内的一个 long 类型的正数。
  • _source
    即索引数据时发送给 Elasticsearch 的原始 JSON 文档。
  • _score
    相关性打分
  • _all
    整合所有字段内容到该字段,已被废除。

文档属性

文档里有几个最重要的设置:

  • type
    字段的数据类型,例如 string 或 date
  • index
    字段是否应当被当成全文来搜索(analyzed),或被当成一个准确的值(not_analyzed),还是完全不可被搜索( no )
  • analyzer
    确定在索引和搜索时全文字段使用的 analyzer
  • _source
    存储代表文档体的 JSON 字符串,和所有被存储的字段一样, _source 字段在被写入磁盘之前先会被压缩。这个字段有以下作用:
    1. 搜索结果包括了整个可用的文档——不需要额外的从另一个的数据仓库来取文档。
    2. 如果没有 _source 字段,部分 update 请求不会生效。
    3. 当你的映射改变时,你需要重新索引你的数据,有了_source 字段你可以直接从 Elasticsearch 这样做,而不必从另一个(通常是速度更慢的)数据仓库取回你的所有文档。
    4. 当你不需要看到整个文档时,单个字段可以从 _source 字段提取和通过 get 或者 search 请求返回。
      1
      2
      3
      4
      5
      GET /_search
      {
      "query": { "match_all": {}},
      "_source": [ "title", "created" ]
      }
    5. 调试查询语句更加简单,因为你可以直接看到每个文档包括什么,而不是从一列 id 猜测它们的内容。
      也可以调用下面的映射来禁用_source 字段:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      PUT /my_index
      {
      "mappings": {
      "my_type": {
      "_source": {
      "enabled": false
      }
      }
      }
      }

对象和文档

通常情况下,我们使用的术语 对象 和 文档 是可以互相替换的。不过,有一个区别:
一个对象仅仅是类似于 hash 、 hashmap 、字典或者关联数组的 JSON 对象,对象中也可以嵌套其他的对象。 对象可能包含了另外一些对象。
文档指最顶层或者根对象,这个根对象被序列化成 JSON 并存储到 Elasticsearch 中,指定了唯一 ID 及一些必须的文档元数据。

根对象

映射的最高一层被称为 根对象 ,它可能包含下面几项:

  • 一个 properties 节点,列出了文档中可能包含的每个字段的映射
  • 各种元数据字段,它们都以一个下划线开头,例如 _type 、 _id 和 _source
  • 设置项,控制如何动态处理新的字段,例如 analyzer 、 dynamic_date_formats 和 dynamic_templates
  • 其他设置,可以同时应用在根对象和其他 object 类型的字段上,例如 enabled 、 dynamic 和 include_in_all

操作类型

文档的CRUD:

  • Index
    Index操作——如果ID不存在——则创建新的文档,否则删除现有的再创建新的,版本号会增加
    PUT my_index/_doc/1
  • Create
    Create操作——如果ID已经存在——会失败
    PUT my_index/_create/1
    不指定ID,自动生成
    POST my_index/_doc
  • Read
    GET my_index/_doc/1
  • Update
    文档必须已经存在,更新只会对相应字段做增量修改
    POST my_index/_update/1
  • Delete
    my_index/_doc/1

常见返回

问题 原因
无法连接 网络故障或集群挂了
连接无法关闭 网络故障或节点出错
429 集群过于繁忙
4xx 请求体格式有问题
500 集群内部错误

更新 - PUT

更新现有的对象需要自己指定对象的 id,如果不存在将自动创建一个,文档更新后_version 字段的值也会相应提高。在内部,Elasticsearch 已将旧文档标记为已删除,并增加一个全新的文档。 尽管你不能再对旧版本的文档进行访问,但它并不会立即消失。当继续索引更多的数据,Elasticsearch 会在后台清理这些已删除文档。
检索 和 重建索引 步骤的间隔越小,变更冲突的机会越小。 但是它并不能完全消除冲突的可能性。 还是有可能在 update 设法重新索引之前,来自另一进程的请求修改了文档。为了避免数据丢失, update API 在 检索 步骤时检索得到文档当前的 _version 号,并传递版本号到 重建索引 步骤的 index 请求。 如果另一个进程修改了处于检索和重新索引步骤之间的文档,那么 _version 号将不匹配,更新请求将会失败。为了实现版本号控制只需要在请求参数中加入 version(如上所示)。

1
2
3
4
5
6
PUT /website/blog/123
{
"title": "My first blog entry",
"text": "Just trying this out...",
"date": "2014/01/01"
}

如果已经有自己的 _id 、而又想执行创建,那么我们必须告诉 Elasticsearch ,只有在相同的 _index 、 _type 和 _id 不存在时才接受我们的索引请求——而不是覆盖掉,有两种方式:

1
2
3
4
5
6
# 指定ID的index操作,其实是个upsert操作
PUT /website/blog/123?op_type=create
{ ... }
# 创建一个文档
PUT /website/blog/123/_create
{ ... }

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

创建 - POST

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

1
2
3
4
5
6
POST /website/blog/
{
"title": "My second blog entry",
"text": "Still trying this out...",
"date": "2014/01/01"
}

部分更新 - POST

update 请求最简单的一种形式是接收文档的一部分作为 doc 的参数, 它只是与现有的文档进行合并。对象被合并到一起,覆盖现有的字段,增加新的字段。

1
2
3
4
5
6
7
8
# 文档必须已经存在
POST /website/blog/1/_update
{
"doc" : {
"tags" : [ "testing" ],
"views": 0
}
}

使用脚本部分更新文档:脚本可以在 update API 中用来改变 _source 的字段内容, 它在更新脚本中称为 ctx._source ,运行在一个沙盒内,默认使用 Painless 语言作为脚本语言。下面这个脚本在页面不存在时执行新增并初始化 views=1(第一次运行这个请求时, upsert 值作为新文档被索引,初始化 views 字段为 1 ;在后续的运行中,由于文档已经存在, script 更新操作将替代 upsert 进行应用,对 views 计数器进行累加)、页面被浏览 2 次后执行删除,其他情况浏览量+1 并添加一个新标签:

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /website/blog/zVmOW2EBsZ0GEqF92yf6/_update
{
"script" : {
"source" : "if(ctx._source.views == params.count) { ctx.op = 'delete'} ctx._source.views+=1; ctx._source.tags.add(params.new_tag)",
"params" : {
"new_tag" : "search",
"count": 2
}
},
"upsert": {
"views": 1
}
}

重试
正如之前所说,update 操作是检索-修改-重新索引的过程, 检索 和 重建索引 步骤的间隔越小,变更冲突的机会越小。 但是它并不能完全消除冲突的可能性。 还是有可能在 update 设法重新索引之前,来自另一进程的请求修改了文档。为了避免数据丢失, update API 在 检索 步骤时检索得到文档当前的 _version 号,并传递版本号到 重建索引 步骤的 index 请求。 如果另一个进程修改了处于检索和重新索引步骤之间的文档,那么 _version 号将不匹配,更新请求将会失败。
对于部分更新的很多使用场景,文档已经被改变也没有关系。 例如,如果两个进程都对页面访问量计数器进行递增操作,它们发生的先后顺序其实不太重要; 如果冲突发生了,我们唯一需要做的就是尝试再次更新。这可以通过 设置参数 retry_on_conflict 来自动完成, 这个参数规定了失败之前 update 应该重试的次数,它的默认值为 0

1
2
3
4
5
6
7
POST /website/blog/zVmOW2EBsZ0GEqF92yf6/_update?retry_on_conflict=5 
{
"script" : "ctx._source.views+=1",
"upsert": {
"views": 0
}
}

GET(搜索)

在请求的查询串参数中加上 pretty 参数,这将会调用 Elasticsearch 的 pretty-print 功能,该功能 使得 JSON 响应体更加可读,但其中的 _source 字段并不是被当成字符串打印出来,而是格式化成了 JSON 串:

1
2
3
GET /website/blog/123?pretty
GET /website/blog/123/_source
GET /website/blog/123?_source=title,text

将多个请求合并成一个,避免单独处理每个请求花费的网络延时和开销。 如果你需要从 Elasticsearch 检索很多文档,那么使用 multi-get 或者 mget API 来将这些检索请求放在一个请求中,将比逐个文档请求更快地检索到全部文档。
mget API 要求有一个 docs 数组作为参数,每个 元素包含需要检索文档的元数据, 包括 _index 、 _type 和 _id 。如果你想检索一个或者多个特定的字段,那么你可以通过 _source 参数来指定这些字段的名字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GET /_mget
{
"docs" : [
{
"_index" : "website",
"_type" : "blog",
"_id" : "zVmOW2EBsZ0GEqF92yf6"
},
{
"_index" : "website",
"_type" : "blog",
"_id" : 1,
"_source": "views"
}
]
}
GET /website/blog/_mget
{
"ids" : [ "2", "1" ]
}

HEAD(ping)

如果只想检查一个文档是否存在——根本不想关心内容——那么用 HEAD 方法来代替 GET 方法。

1
HEAD /website/blog/124

DELETE(删除)

1
DELETE /website/blog/123

bulk(批量操作)

每一行——包括最后一行——都必须以换行符结尾,格式如下所示:

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

action/metadata 行指定 哪一个文档 做 什么操作 。action 必须是以下选项之一:
create:如果文档不存在,那么就创建它。类似POSTPUT /_create
index:创建一个新文档或者替换一个现有的文档。类似POSTPUT
update:部分更新一个文档。类似POST /_update
delete:删除一个文档。类似DELETE
metadata 应该 指定被索引、创建、更新或者删除的文档的 _index 、 _type 和 _id ,每个请求的 metadata 都会覆盖请求 URL 中带上的默认元数据。
request body 行由文档的 _source 本身组成–文档包含的字段和值。它是 index、create、update 操作所必需的。
为什么不直接用一个 JSON 数组来保存?主要是考虑效率问题,解析为数组需要有更多的 RAM 空间,且 JVM 要花时间进行 gc。而直接使用原始数据只需要多注意每条数据之间的间隔(换行符)。
每个子请求都是独立执行,因此某个子请求的失败不会对其他子请求的成功与否造成影响。 如果其中任何子请求失败,最顶层的 error 标志被设置为 true ,并且在相应的请求报告出错误明细。这也意味着 bulk 请求不是原子的: 不能用它来实现事务控制。每个请求是单独处理的,因此一个请求的成功或失败不会影响其他的请求。

1
2
3
4
5
6
7
8
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": "webiite", "_type": "blog", "_id": "123", "_retry_on_conflict" : 3} }
{ "doc" : {"title" : "My updated blog post"} }

批量请求的大小有一个最佳值,大于这个值,性能将不再提升,甚至会下降。 但是最佳值不是一个固定的值,它完全取决于硬件、文档的大小和复杂度、索引和搜索的负载的整体情况
幸运的是,很容易找到这个 最佳点 :通过批量索引典型文档,并不断增加批量大小进行尝试。 当性能开始下降,那么你的批量大小就太大了。一个好的办法是开始时将 1,000 到 5,000 个文档作为一个批次, 如果你的文档非常大,那么就减少批量的文档个数。并且请求的文档也最好不要太大,一个好的批量大小在开始处理后所占用的物理大小约为 5-15 MB。