Elasticsearch数据的存储格式
Elastcisearch 是分布式的 文档 存储。它能存储和检索复杂的数据结构—序列化成为JSON文档—以 实时 的方式。 换句话说,一旦一个文档被存储在 Elasticsearch 中,它就是可以被集群中的任意节点检索到
在 Elasticsearch 中, 每个字段的所有数据 都是 默认被索引的 。 即每个字段都有为了快速检索设置的专用倒排索引。而且,不像其他多数的数据库,它能在 同一个查询中 使用所有这些倒排索引,并以惊人的速度返回结果
Elasticsearch 文档
在 Elasticsearch 中,术语 文档 有着特定的含义。它是指最顶层或者根对象, 这个根对象被序列化成 JSON 并存储到 Elasticsearch 中,指定了唯一 ID
这个JSON中的字段的名字可以是任何合法的字符串,但 不可以 包含英文句号(.),ES中的每一个文档就是一个JSON对象,其中包含了我们定义的数据字段,其中可以每个字段可以存储任意类型的值。
元数据
一个文档不光是包括文档本身的数据,同时也包含了元数据(元数据就是有关文档本身的信息)
三个元数据必须的元素如下:
_index
//文档在哪存放
_type
//文档表示的对象类别
_id
//文档唯一标识
_index
这个表示文档存储对应的索引,一个索引对应多个分片,文档则存储在索引对应的分片中,通过分片计算公式可以计算出文档应该存入那个分片中,公式为:
shard = hash(routing) % number_of_primary_shards
//就是索引的ID取hash然后对主分片数量取余
实际上,在 Elasticsearch 中,我们的数据是被存储和索引在 分片 中,而一个索引仅仅是逻辑上的命名空间, 这个命名空间由一个或者多个分片组合在一起。 然而,这是一个内部细节,我们的应用程序根本不应该关心分片,对于应用程序而言,只需知道文档位于一个 索引 内。 Elasticsearch 会处理所有的细节
_type
数据可能在索引中只是松散的组合在一起,但是通常明确定义一些数据中的子分区是很有用的。 例如,所有的产品都放在一个索引中,但是你有许多不同的产品类别,比如 "electronics" 、 "kitchen" 和 "lawn-care"。
这些文档共享一种相同的(或非常相似)的模式:他们有一个标题、描述、产品代码和价格。他们只是正好属于“产品”下的一些子类。
Elasticsearch 公开了一个称为 types (类型)的特性,它允许您在索引中对数据进行逻辑分区。不同 types 的文档可能有不同的字段,但最好能够非常相似。 详情参考 类型和映射 中更多的讨论关于 types 的一些应用和限制。
一个 _type
命名可以是大写或者小写,但是不能以下划线或者句号开头,不应该包含逗号, 并且长度限制为256个字符.
_id
ID 是一个字符串,当它和 _index 以及 _type 组合就可以唯一确定 Elasticsearch 中的一个文档。 当你创建一个新的文档,要么提供自己的 _id ,要么让 Elasticsearch 帮你生成,如果你是通过ELK将数据存入ES时,在logstash中指定document_id字段为你文档数据中的ID字段就可以自行指定ID了
文档索引
通过使用 index API ,文档可以被 索引 —— 存储和使文档可被搜索。 但是首先,我们要确定文档的位置。正如我们刚刚讨论的,一个文档的 _index 、 _type 和 _id 唯一标识一个文档。 我们可以提供自定义的 _id 值,或者让 index API 自动生成
我们可以直接通过命令:
PUT /{INDEX}/{TYPE}/{ID}
{
//SOURCE
}
指定索引名、类型、id插入一个文档
插入成功后的响应是如下的格式
{
"_index": {INDEX}
"_type": {TYPE}
"_id": {ID}
"_version": 1
"created" : true
}
其中有一个_version,这也是元数据中的一个元素,标识着这个文档的版本,每次对这个文档做修改时,包括删除,都会递增,这样可以标识文档的修改,保证文档的修改部分不会影响未修改部分
如果你未指定ID,则ES会自行随机生成一个ID,生成规则如下:
自动生成的 ID 是 URL-safe、 基于 Base64 编码且长度为20个字符的 GUID 字符串。 这些 GUID 字符串由可修改的 FlakeID 模式生成,这种模式允许多个节点并行生成唯一 ID ,且互相之间的冲突概率几乎为零。
ES文档的查询(数据的查询)
我们可以通过一下语句进行数据的查询
GET /{INDEX}/{TYPE}/{ID}?pretty
的到的响应如下:
{
"_index": {INDEX}
"_type": {TYPE}
"_id": {ID}
"_version": 1
"_source":{//元数据字段
//我们存入ES文档时的原数据
}
}
在请求的查询串参数中加上 pretty 参数,正如前面的例子中看到的,这将会调用 Elasticsearch 的 pretty-print 功能,该功能 使得 JSON 响应体更加可读。但是, _source 字段不能被格式化打印出来。相反,我们得到的 _source 字段中的 JSON 串,刚好是和我们传给它的一样
GET 请求的响应体包括 {"found": true} ,这证实了文档已经被找到。 如果我们请求一个不存在的文档,我们仍旧会得到一个 JSON 响应体,但是 found 将会是 false 。 此外, HTTP 响应码将会是 404 Not Found ,而不是 200 OK
我们可以通过传递 -i 参数给 curl 命令,该参数能够显示响应的头部
curl -i XGET http://ip:port/{INDEX}/{TYPE}/{ID}?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
}
默认情况下, GET 请求会返回整个文档,这个文档正如存储在 _source 字段中的一样。但是也许你只对其中的 title 字段感兴趣。单个字段能用 _source 参数请求得到,多个字段也能使用逗号分隔的列表来指定
GET /{INDEX}/{TYPE}/{ID}?_source=title,text
这样返回的文档只会返回你指定的字段,如tittle和text
检查文档是否存在
如果只想检查一个文档是否存在--根本不想关心内容—那么用 HEAD 方法来代替 GET 方法。 HEAD 请求没有返回体,只返回一个 HTTP 请求报头
curl -i -XHEAD http://localhost:9200/{INDEX}/{TYPE}/{ID}
如果ES中存在该文档则返回
HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8
Content-Length: 0
如果ES中不存在该文档则返回
HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=UTF-8
Content-Length: 0
文档的更新操作
ES的文档一旦建立则不可更改,虽然ES有uopdate这种API,看似是对ES中的文档进行了更新操作,但是实际上ES中进行了先对旧文档的JSON数据提取出来,然后进行修改,然后将旧文档删除,在相同的索引相同的位置存入一个新文档
唯一的区别在于, update API 仅仅通过一个客户端请求来实现这些步骤,而不需要单独的 get 和 POST 请求对index进行操作
创建一个新文档
ES中的文档的唯一标识是由几个元数据的元素组成的,由_index,_type,_id等三个元素标识了一个文档的唯一性,如果我们需要指定id插入一个文档的话,如果是通过API自动插入的话,API会先检索一下ES中是否存在这个id对应的文档,如果存在则进行覆盖,如果不存在则插入新的,但是如果通过命令进行插入的话,我们可以通过以下命令进行校验
PUT /{INDEX}/{TYPE}/{ID}?op_type=create
{ ... }
//或者
PUT /{INDEX}/{TYPE}/{ID}/_create
{ ... }
通过这样的方式进行插入会对ID进行index,type和id组成的唯一标识进行校验,如果存在的话,则会返回一个409的报错信息,如下所示:
{
"error": {
"root_cause": [
{
"type": "document_already_exists_exception",
"reason": "[{TYPE}][{ID}]: document already exists",
"shard": "0",
"index": "website"
}
],
"type": "document_already_exists_exception",
"reason": "[{TYPE}][{ID}]: document already exists",
"shard": "0",
"index": "website"
},
"status": 409
}
如果ES中不存在这个文档,则会进行向其中插入新文档,返回201,也就是成功的状态码和http响应
文档删除
删除文档的语法和我们所知道的规则相同,只是使用 DELETE 方法:
DELETE /{INDEX}/{TYPE}/{ID}
如果ES中存在改文档,则会给该文档打上一个删除标记,然后返回成功的状态码,如果不存在该文档,则会返回一个404,结果如下:
{//200
"found" : true,
"_index" : "{INDEX}",
"_type" : "{TYPE}",
"_id" : "{ID}",
"_version" : 3
}
//404
{
"found" : false,
"_index" : "website",
"_type" : "blog",
"_id" : "123",
"_version" : 4
}
无论该文档在ES中是否存在,都会对version进行一个自增操作。
而已经打上删除标记的删除文档不会立即将文档从磁盘中删除,只是将文档标记为已删除状态。随着你不断的索引更多的数据,Elasticsearch 将会在后台清理标记为已删除的文档
ES处理冲突
当我们对数据进行操作时,如果出现了同时两个人对数据进行操作的话,那么只有最近的一次操作会起作用,而对于其他的操作则会被覆盖,这样就出现了数据处理冲突,我们可以通过并发控制来控制这些操作,处理冲突
悲观控制并发
这种方法广泛运用于关系型数据库中,这种方法控制的前提是假定所有资源都有可能被并发访问,都有可能会发生冲突,所以在,访问之前就给他锁住,阻塞其余访问这个资源的线程以防止冲突,确保只有加锁的对象能够对资源进行访问(synchronized就是一个典型的悲观锁)
乐观控制并发
ES中假定冲突是不会发生的并不会阻塞正在尝试的操作,但是如果源数据在读写中被修改,则会更新失败。之后会尝试去解决这个操作失败的问题,也就是解决冲突。 例如,可以重试更新、使用新的数据、或者将相关情况报告给用户。
Elasticsearch 是分布式的。当文档创建、更新或删除时, 新版本的文档必须复制到集群中的其他节点。Elasticsearch 也是异步和并发的,这意味着这些复制请求被并行发送,并且到达目的地时也许 顺序是乱的 。 Elasticsearch 需要一种方法确保文档的旧版本不会覆盖新的版本。
当我们之前讨论 index , GET 和 delete 请求时,我们指出每个文档都有一个 _version (版本)号,当文档被修改时版本号递增。 Elasticsearch 使用这个 _version 号来确保变更以正确顺序得到执行。如果旧版本的文档在新版本之后到达,它可以被简单的忽略。
我们可以利用 _version 号来确保 应用中相互冲突的变更不会导致数据丢失。我们通过指定想要修改文档的 version 号来达到这个目的。 如果该版本不是当前版本号,我们的请求将会失败。
更新和冲突
在本节的介绍中,我们说明 检索 和 重建索引 步骤的间隔越小,变更冲突的机会越小。 但是它并不能完全消除冲突的可能性。 还是有可能在 update 设法重新索引之前,来自另一进程的请求修改了文档。
为了避免数据丢失, update API 在 检索 步骤时检索得到文档当前的 _version 号,并传递版本号到 重建索引 步骤的 index 请求。 如果另一个进程修改了处于检索和重新索引步骤之间的文档,那么 _version 号将不匹配,更新请求将会失败。
对于部分更新的很多使用场景,文档已经被改变也没有关系。 例如,如果两个进程都对页面访问量计数器进行递增操作,它们发生的先后顺序其实不太重要; 如果冲突发生了,我们唯一需要做的就是尝试再次更新。
这可以通过设置参数 retry_on_conflict 来自动完成, 这个参数规定了失败之前 update 应该重试的次数,它的默认值为 0 。
在增量操作无关顺序的场景,例如递增计数器等这个方法十分有效,但是在其他情况下变更的顺序 是 非常重要的。 类似 index
API , update
API 默认采用 最终写入生效 的方案,但它也接受一个 version
参数来允许你使用 optimistic concurrency control 指定想要更新文档的版本。
取回多个文档
Elasticsearch 的速度已经很快了,但甚至能更快。 将多个请求合并成一个,避免单独处理每个请求花费的网络延时和开销。 如果你需要从 Elasticsearch 检索很多文档,那么使用 multi-get 或者 mget API 来将这些检索请求放在一个请求中,将比逐个文档请求更快地检索到全部文档。
例如:
GET /_mget
{
"docs" : [
{
"_index" : "index",
"_type" : "type",
"_id" : id
},
{
"_index" : "index",
"_type" : "type",
"_id" : id
"_source": "字段名"
}
]
}
可以指定字段名进行查询
代价比较小的批量操作
和mget一样,我们同样有一次性提交大量操作的API(bulk),bulk API 允许在单个步骤中进行多次 create 、 index 、 update 或 delete 请求。 如果你需要索引一个数据流比如日志事件,它可以排队和索引数百或数千批次。
bulk 与其他的请求体格式稍有不同,如下所示
{ action: { metadata }}\n
{ request body }\n
{ action: { metadata }}\n
{ request body }\n
这种格式类似一个有效的单行 JSON 文档 流 ,它通过换行符(\n)连接到一起。注意两个要点:
每行一定要以换行符(\n)结尾, 包括最后一行 。这些换行符被用作一个标记,可以有效分隔行。
这些行不能包含未转义的换行符,因为他们将会对解析造成干扰。这意味着这个 JSON 不 能使用 pretty 参数打印。
ction/metadata
行指定 哪一个文档 做 什么操作 。
action
必须是以下选项之一:
create: 如果文档不存在,那么就创建它。详情请见 创建新文档。
index: 创建一个新文档或者替换一个现有的文档。详情请见 索引文档 和 更新整个文档。
update: 部分更新一个文档。详情请见 文档的部分更新。
delete: 删除一个文档。详情请见 删除文档
metadata
应该指定被索引、创建、更新或者删除的文档的 _index
、 _type
和 _id
。
request body 行由文档的 _source 本身组成—文档包含的字段和值。它是 index 和 create 操作所必需的,这是有道理的:你必须提供文档以索引。
它也是 update 操作所必需的,并且应该包含你传递给 update API 的相同请求体: doc 、 upsert 、 script 等等。 删除操作不需要 request body 行。
如下列操作:
POST /_bulk
{ "delete": { "_index": "website", "_type": "blog", "_id": "123" }}//1
{ "create": { "_index": "website", "_type": "blog", "_id": "123" }}//2
{ "title": "My first blog post" }
{ "index": { "_index": "website", "_type": "blog" }}//3
{ "title": "My second blog post" }
{ "update": { "_index": "website", "_type": "blog", "_id": "123", "_retry_on_conflict" : 3} } //4
{ "doc" : {"title" : "My updated blog post"} }
上述操作,1号操作,delete操作后面不需要跟详细的操作语句,因为delete本身就是直接进行文档删除的。
2号操作执行的是create操作,后续接的title语句就是操作建立的详细字段,如果这个这个指定条件的文档已经存在了,则会返回 一个文档已经存在的错误信息
3号操作是建立或者覆盖一个文档,如果没有直接指定ID的话,那么就是新建一个文档,会自动随机生成一个新的ID
4号操作执行的是更新操作,后续接的doc就是操作的字段。
bulk操作d的每个子请求都是独立执行,因此某个子请求的失败不会对其他子请求的成功与否造成影响。 如果其中任何子请求失败,最顶层的 error 标志被设置为 true ,并且在相应的请求报告出错误明细
这也意味着 bulk 请求不是原子的: 不能用它来实现事务控制。每个请求是单独处理的,因此一个请求的成功或失败不会影响其他的请求
如果你是使用bulk对相同的索引(_index)和类型(_type)下的文档进行操作的话,则不需要重复指定这两个元数据,只需要指定ID就行了。
批量请求的大小
整个批量请求都需要由接收到请求的节点加载到内存中,因此该请求越大,其他请求所能获得的内存就越少。 批量请求的大小有一个最佳值,大于这个值,性能将不再提升,甚至会下降。 但是最佳值不是一个固定的值。它完全取决于硬件、文档的大小和复杂度、索引和搜索的负载的整体情况。
幸运的是,很容易找到这个 最佳点 :通过批量索引典型文档,并不断增加批量大小进行尝试。 当性能开始下降,那么你的批量大小就太大了。一个好的办法是开始时将 1,000 到 5,000 个文档作为一个批次, 如果你的文档非常大,那么就减少批量的文档个数。
密切关注你的批量请求的物理大小往往非常有用,一千个 1KB 的文档是完全不同于一千个 1MB 文档所占的物理大小。 一个好的批量大小在开始处理后所占用的物理大小约为 5-15 MB。