跳至主要內容

文档

Mr.Liu大约 13 分钟数据库ElasticsearchNoSQL

文档

在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 允许在单个步骤中进行多次 createindexupdatedelete 请求。

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":"开发部"}}