文档
文档
在Elasticsearch中,文档这个词有特殊的含义。它指的是在Elasticsearch中被存储到唯一ID下的由最高级或者根对象(root object)序列化而来的JOSN。
一个文档不只包含了数据。它还包含了元数据(metadata)一关于文档的信息。有三个元数据元素是必须存在的,它们是:
名字 | 说明 |
---|---|
_index | 索引,文档存储的地方 |
_type | 类型,文档代表的对象种类 |
_id | 文档的唯一编号 |
在Elasticsearch中,文档属于一种类型,各种各样的类型存在于一个索引中。
在Elasticsearch7.x之前, 每类文档都需要定义一个类型对象,但是7.x之后,移除了_type
,所有的文档类型都是默认为_doc
。一个Elasticsearch集群包含多个索引,一个索引可以包含一个固定的_doc
类型,类型包含了很多的文档(行),然后每个文档中又包含了很多的字段(列)。
注意:Elasticsearch
索引
在Elasticsearch中,索引这个词汇有太多的涵义,下面简单区分:
索引(名词)
如上文所说,一个索引就类似于传统关系型数据库的数据库,这里就是存储相关文档的地方。
索引(动词)
为一个文档创建索引是把一个文档存储到一个索引(名称)中的过程,这样才能被检索。这个过程非常类似SQL中的Insert语句,如果已经存在文档,新的文档则会覆盖旧的文档。
反向索引
在更新型数据库中的某列添加一个索引,比如多路搜索树(B-Tree)索引,就可以加快数据的查询速度。Elasticsearch以及Lucene使用的是一个叫做反省索引(inverted index)的结构来实现相同的功能。
索引
文档通过 索引(名词)API被索引(动词),简单地说就是将文档数据存储到索引中并使其可搜索。但是最开始需要决定将文档存储到哪里。
正如之前所说的,一个文档通过_index
、_type
、_id
来确定它的唯一性。我们可以自己提供一个_id
,也可以让index自动生成一个。
自己指定ID
为了创建员工名单,需要以下操作:
- 为每一个员工的文档创建索引,每个文档包含了员工的所有信息
- 每个文档都会被标记为employee类型
- 这种类型存在于company这个索引中
在实际的操作中,这些操作是非常简单的。可以将多步骤合为一个命令来完成:
PUT /employee/_doc/1/
{
"firstName": "John",
"lastName": "Smith",
"age": 25,
"about": "I love Python",
"interests": [
"sports",
"music"
]
}
在/company/empToyee/1/
路径下,包含了三个部分:
名字 | 内容 |
---|---|
employee | 索引的名字 |
_doc | 默认的类型名字 |
1 | 当前员工的id |
请求部分,也就是json文档部分,这里包含了这个员工的所有信息。
怎么样?很简单吧!我们不需要在操作前进行任何管理操作,比如创建索引,或者为字段指定数据的类型。我们就这么直接的为一个文档创建了索引。ElasticSearch会在创建的时候,为它们设定默认值。
Elasticsearch返回内容:
{
"_index" : "employee",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1
}
响应指出请求的索引已经被成功创建,这个索引中包含_index
、_type
和_id
元数据,以及一个新元素:_version
。每当文档变化时(包括删除),_version
就会增大。
自动生成ID
如果数据中没有编号字段,就可以通过Elasticsearch生成一个。请求的结构发生了变化:把PUT请求换成POST请求。请求网址中也只有_index
和 _type
。
POST /employee/_doc/
{
"firstName": "Mily",
"lastName": "Smith",
"age": 22,
"about": "I love Python and Java",
"interests": [
"book",
"music"
]
}
这次的响应和之前基本一样,只有_id变成了系统自动生成的值:_
{
"_index" : "employee",
"_type" : "_doc",
"_id" : "rfZt238BJHOwMxbsnp3g",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1
}
_id
是由22个字母组成的,被称为UUIDs。
获取
检索全部字段
现在,我们已经在ElasticSearch中存储了一些数据,我们可以根据需求进行操作啦。
第一个需求就是可以搜索每个员工的信息。
这是非常简单的操作。只需要执行一次GET请求,然后指出文档的地址,也就是索引、类型、ID即可。通过这三部分,我们就可以得到原始的json文档:
GET /employee/_doc/1/?pretty
返回的内容包含了这个文档的元数据信息,而这名员工的信息包含在_source字段中。
{
"_index" : "employee",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"firstName" : "John",
"lastName" : "Smith",
"age" : 25,
"about" : "I love Python",
"interests" : [
"sports",
"music"
]
}
}
pretty
在任意的查询字符串中添加pretty参数,Elasticsearch就可以得到更加易于识别的JSON结果。_source
不会执行格式化输出,它的输出结果取决于我们录入的样子。
GET请求的响应中包含"found": true
。这意味着结果文档确实被找到了。如果请求了一个不存在的文档,我们依然会得到JSON响应,只是found
的值会变为false
。
GET /employee/_doc/111/?pretty
{
"_index" : "employee",
"_type" : "_doc",
"_id" : "111",
"found" : false
}
检索特定字段
通常,GET请求会将整个文档放入 _source
字段并返回。但是,可能你只是想要某一个字段。因此,你可以使用_source
得到指定字段。如果需要多个字段,可以使用逗号隔开。
GET /employee/_doc/1?_source=firstName,lastName,age
现在,_source
字段中就只会显示你指定的字段:
{
"_index" : "employee",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"firstName" : "John",
"lastName" : "Smith",
"age" : 25
}
}
创建
当我们索引一个文档时,如何确定是创建了一个新的文档还是覆盖了一个已经存在的文档呢?
请牢记_index
,_type
、_id
组成了唯一的文档标记,所以为了确定我们创建的是全新的内容,最简单的方法就是使用POST请求,让Elasticsearch创建不同的_id。
POST /employee/_doc/
{
"firstName": "Hilun",
"lastName": "Lucy",
"age": 25,
"about": "I love Js and Java",
"interests": [
"book"
]
}
然而,当我们已经确定了_id
,所以需要告诉Elasticsearch只有当_index
,_type
以及_id
这3个属性全部相同的文档不存在时,才接受我们的请求。实现这个目的的方法有两种:
第一种是在请求中添加op_type
参数
PUT /employee/_doc/2/?op_type=create
{
"firstName": "Mily",
"lastName": "Lucy",
"age": 27,
"about": "I love Js",
"interests": [
"sports"
]
}
第二种是在请求最后添加_create
:
PUT /employee/_doc/2/_create
{
"firstName": "Mily",
"lastName": "Lucy",
"age": 27,
"about": "I love Js",
"interests": [
"sports"
]
}
如果成功创建了新的文档,Elasticsearch将会返回正常的响应以及201 Created
的HTTP状态码。
{
"_index": "company",
"_type": "employee",
"_id": "2",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 2,
"_primary_term": 1
}
而如果已经存在文档,将会返回错误信息以及409 Conflict
的HTTP状态码。
{
"error": {
"root_cause": [
{
"type": "version_conflict_engine_exception",
"reason": "[2]: version conflict, document already exists (current version [1])",
"index_uuid": "8a2UuuM9S6arBWLDYAQZjQ",
"shard": "0",
"index": "company"
}
],
"type": "version_conflict_engine_exception",
"reason": "[2]: version conflict, document already exists (current version [1])",
"index_uuid": "8a2UuuM9S6arBWLDYAQZjQ",
"shard": "0",
"index": "company"
},
"status": 409
}
更新
更新全部字段
在Documents中的文档是不可变的。所以如果需要改变已经存在的文档,可以使用indexAP(来重新索引或者替换掉它:
PUT /employee/_doc/1/
{
"firstName": "John2",
"lastName": "Smith",
"age": 25,
"about": "I love Python",
"interests": [
"sports",
"music"
]
}
在响应中,可以发现Elasticsearch已经将_version
的值增加了:
{
"_index" : "employee",
"_type" : "_doc",
"_id" : "1",
"_version" : 2,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 3,
"_primary_term" : 1
}
更新特定字段
在之前的方法中,我们是将全部的数据取出,更改数据之后,才将整个文档进行重新索引。当然,Elasticsearch也提供了更新API来实现局部更新。
但正如之前提到的,文档不能被修改,它们只能被替换掉。更新API也必须遵守这一法则。从表面来看,貌似是文档被替换了。对内而言,它必须按照查询、修改、索引的流程来实现文档更新。不同之处在于这个流程是一个片(shard)中完成的,因此可以节省多个请求带来的网络开销。除了节省步骤,也可以减少多个进程造成的冲突可能。
使用更新API最简单的一种作用就是给文档添加字段。新的数据会被合并到现有数据中,而如果存在相同字段的数据,就会被新的字段数据所替代。
POST /employee/_update/1/
{
"doc": {
"gender": "man"
}
}
如果请求成功,就会得到一个类似于索引时的响应内容:
{
"_index" : "employee",
"_type" : "_doc",
"_id" : "1",
"_version" : 3,
"result" : "noop",
"_shards" : {
"total" : 0,
"successful" : 0,
"failed" : 0
},
"_seq_no" : 4,
"_primary_term" : 1
}
再次取出数据,可以在_source
中看到更新的数据:
{
"_index" : "employee",
"_type" : "_doc",
"_id" : "1",
"_version" : 3,
"_seq_no" : 4,
"_primary_term" : 1,
"found" : true,
"_source" : {
"firstName" : "John2",
"lastName" : "Smith",
"age" : 25,
"about" : "I love Python",
"interests" : [
"sports",
"music"
],
"gender" : "man"
}
}
删除
删除文档的基本模式和之前一样。只不过请求方式变为DELETE:
DELETE /employee/_doc/r_aB238BJHOwMxbsHJ1y/
如果文档存在,则会返回一个200 OK的状态码以及成功的响应信息。注意:_version
的数字已经增加了。
{
"_index" : "employee",
"_type" : "_doc",
"_id" : "r_aB238BJHOwMxbsHJ1y",
"_version" : 2,
"result" : "deleted",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 8,
"_primary_term" : 1
}
如果文档不存在,则会返回404 NotFound状态码以及失败的响应信息:
{
"_index" : "employee",
"_type" : "_doc",
"_id" : "r_aB238BJHOwMxbsHJ1y",
"_version" : 5,
"result" : "not_found",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 11,
"_primary_term" : 1
}
尽管文档不存在,但是_version
的值还是增加了。这就是内部管理的一部分,它保证了在多个节点的不同操作的顺序被正确的标记了。
批量操作
在批量操作中,可以不用指定
_doc
类型
查询
如果你需要从 Elasticsearch 检索很多文档,那么使用mget
API 来将这些检索请求放在一个请求中,将比逐个文档请求更快地检索到全部文档。
mget
API 要求有一个 docs
数组作为参数,每个元素包含需要检索文档的元数据, 包括 _index
、 _type
和 _id
。如果你想检索一个或者多个特定的字段,那么你可以通过 _source
参数来指定这些字段的名字:
GET /_mget
{
"docs": [
{
"_index": "employee",
"_id": 1
},
{
"_index": "department",
"_id": 1
}
]
}
该响应体也包含一个 docs
数组, 对于每一个在请求中指定的文档,这个数组中都包含有一个对应的响应,且顺序与请求中的顺序相同。 其中的每一个响应都和使用单个 get
请求所得到的响应体相同:
{
"docs" : [
{
"_index" : "employee",
"_type" : "_doc",
"_id" : "1",
"_version" : 3,
"_seq_no" : 4,
"_primary_term" : 1,
"found" : true,
"_source" : {
"firstName" : "John2",
"lastName" : "Smith",
"age" : 25,
"about" : "I love Python",
"interests" : [
"sports",
"music"
],
"gender" : "man"
}
},
{
"_index" : "department",
"_type" : "_doc",
"_id" : "1",
"found" : false
}
]
}
即使有某个文档没有找到,上述请求的 HTTP 状态码仍然是
200
。事实上,即使请求 没有 找到任何文档,它的状态码依然是200
--因为mget
请求本身已经成功执行。 每个文档都是单独检索和报告的。为了确定某个文档查找是成功或者失败,你需要检查found
标记。
如果想检索的数据都在相同的 _index
中(甚至相同的 _type
中),则可以在 URL 中指定默认的 /_index
或者默认的 /_index/_type
。
你仍然可以通过单独请求覆盖这些值:
GET /employee/_mget
{
"docs": [
{
"_id": 1
},
{
"_index": "department",
"_id": "sPaD238BJHOwMxbs_p0W"
}
]
}
事实上,如果所有文档的 _index
和 _type
都是相同的,你可以只传一个 ids
数组,而不是整个 docs
数组:
GET /employee/_mget
{
"ids": [
1,
2,
3
]
}
更改
与 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
必须是以下选项之一:
metadata
应该指定被索引、创建、更新或者删除的文档的 _index
、 _type
和 _id
。
为了把所有的操作组合在一起,一个完整的 bulk
请求 有以下形式:
POST /_bulk
{"delete":{"_index":"department","_id":"svaE238BJHOwMxbsSZ0N"}}
{"create":{"_index":"department","_id":"1"}}
{"title":"战略策划部"}
{"index":{"_index":"department"}}
{"title":"后勤部"}
{"update":{"_index":"department","_id":"sPaD238BJHOwMxbs_p0W"}}
{"doc":{"title":"开发部"}}
请注意
delete
动作不能有请求体,它后面跟着的是另外一个操作。谨记最后一个换行符不要落下。
这个 Elasticsearch 响应包含 items
数组,这个数组的内容是以请求的顺序列出来的每个请求的结果。
{
"took" : 79,
"errors" : true,
"items" : [
{
"delete" : {
"_index" : "department",
"_type" : "_doc",
"_id" : "svaE238BJHOwMxbsSZ0N",
"_version" : 3,
"result" : "not_found",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 7,
"_primary_term" : 1,
"status" : 404
}
},
{
"create" : {
"_index" : "department",
"_type" : "_doc",
"_id" : "1",
"status" : 409,
"error" : {
"type" : "version_conflict_engine_exception",
"reason" : "[1]: version conflict, document already exists (current version [1])",
"index_uuid" : "JYdpgZuYQv-aPXGL4YYb3g",
"shard" : "0",
"index" : "department"
}
}
},
{
"index" : {
"_index" : "department",
"_type" : "_doc",
"_id" : "tPaR238BJHOwMxbsJp3I",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 8,
"_primary_term" : 1,
"status" : 201
}
},
{
"update" : {
"_index" : "department",
"_type" : "_doc",
"_id" : "sPaD238BJHOwMxbs_p0W",
"_version" : 2,
"result" : "noop",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 6,
"_primary_term" : 1,
"status" : 200
}
}
]
}
每个子请求都是独立执行,因此某个子请求的失败不会对其他子请求的成功与否造成影响。 如果其中任何子请求失败,最顶层的 error
标志被设置为 true
,并且在相应的请求报告出错误明细,并设置对应的状态码。
这也意味着 bulk
请求不是原子的: 不能用它来实现事务控制。每个请求是单独处理的,因此一个请求的成功或失败不会影响其他的请求。
如果,
_bulk
操作中,类似于_mget
,也可以在url上指定默认索引,也可以在请求地址中覆盖默认索引。
POST /department/_bulk
{"delete":{"_id":"svaE238BJHOwMxbsSZ0N"}}
{"create":{}}
{"title":"企划部"}
{"update":{"_index":"department","_id":"sPaD238BJHOwMxbs_p0W"}}
{"doc":{"title":"开发部"}}