ES1_2使用索引和文档
这篇文档总结ES中的基础数据结构,并介绍如何操作它们。
ES存储的基础概念 - 索引、映射和文档
几个概念的映射关系:
Relational DB -> Databases -> Tables -> Rows -> Columns
Elasticsearch -> Indices -> Types -> Documents -> Fields
将 Type 类比为 Table 并不恰当,因为 ES 中一个索引下的多个类型共用相同的空间。
文档
- 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 字段在被写入磁盘之前先会被压缩。这个字段有以下作用:- 搜索结果包括了整个可用的文档——不需要额外的从另一个的数据仓库来取文档。
- 如果没有 _source 字段,部分 update 请求不会生效。
- 当你的映射改变时,你需要重新索引你的数据,有了_source 字段你可以直接从 Elasticsearch 这样做,而不必从另一个(通常是速度更慢的)数据仓库取回你的所有文档。
- 当你不需要看到整个文档时,单个字段可以从 _source 字段提取和通过 get 或者 search 请求返回。
1
2
3
4
5GET /_search
{
"query": { "match_all": {}},
"_source": [ "title", "created" ]
} - 调试查询语句更加简单,因为你可以直接看到每个文档包括什么,而不是从一列 id 猜测它们的内容。
也可以调用下面的映射来禁用_source 字段:1
2
3
4
5
6
7
8
9
10PUT /my_index
{
"mappings": {
"my_type": {
"_source": {
"enabled": false
}
}
}
}
对象和文档
通常情况下,我们使用的术语 对象 和 文档 是可以互相替换的。不过,有一个区别:
一个对象仅仅是类似于 hash 、 hashmap 、字典或者关联数组的 JSON 对象,对象中也可以嵌套其他的对象。 对象可能包含了另外一些对象。
文档指最顶层或者根对象,这个根对象被序列化成 JSON 并存储到 Elasticsearch 中,指定了唯一 ID 及一些必须的文档元数据。
索引 - Index
一个索引(index)就像是传统关系数据库中的数据库,它是相关文档存储的地方,实际上是组织数据的逻辑命名空间。
在一个索引中,可以定义一种或多种类型。
作为名词,一个 索引 类似于传统关系数据库中的一个 数据库,是一个存储关系型文档的地方;
作为动词,索引一个文档 就是存储一个文档到一个 索引 (名词)中以便它可以被检索和查询到,文档已存在时会被覆盖掉。
Index体现了逻辑空间的概念,每个索引都有自己的Mapping定义,用于定义包含的文档的字段名和字段类型;
Shard体现了物理空间的概念,索引中的数据分散在Shard上。
倒排索引
关系型数据库通过增加一个 索引,比如一个 B 树(B-tree)索引 到指定的列上,以便提升数据检索速度。Elasticsearch 和 Lucene 使用了一个叫做 倒排索引 的结构来达到相同的目的。
倒排索引包含两个部分:
- 单词词典(Term Dictionary)
记录所有文档的单词,记录单词到倒排列表的关联关系
单词词典一般比较大,可以通过B+树或哈希拉链法来实现,以实现高性能的插入和查询 - 倒排列表(Posting List)记录了单词对应的文档结合,由倒排索引项组成
倒排索引项由文档ID、词频TF(该单词在文档中出现的次数,用于相关性评分)、位置Position(单词在文档中分词的位置,用于语句搜索)、偏移Offset(记录单词的开始和结束位置,实现高亮显示)
类型 - Type
一个类型是索引的一个逻辑上的分类,代表一类相似的文档,类型由 名称(比如 user 或 blogpost)和 映射 组成。但是在 ES 6.0.0 以后,这个概念会被废弃。
类型可以很好的抽象划分相似但不相同的数据,但由于 Lucene 的处理方式,类型的使用有些限制。Lucene 没有文档类型的概念,每个文档的类型名被存储在一个叫 _type 的元数据字段上。 当我们要检索某个类型的文档时, Elasticsearch 通过在 _type 字段上使用过滤器限制只返回这个类型的文档。
每个 Lucene 索引中的所有字段都包含一个单一的、扁平的模式。一个特定字段可以映射成 string 类型也可以是 number 类型,但是不能两者兼具(比如两个类型都有一个 name 字段,但是他们映射到不同的数据类型)。
映射 - Mapping
映射就像数据库中的 schema ,描述了数据在每个字段内如何存储,包括文档可能具有的字段或 属性 、 每个字段的数据类型—比如 string, integer 或 date —以及 Lucene 是如何索引和存储这些字段的。
Lucene 也没有映射的概念,映射是 Elasticsearch 将复杂 JSON 文档 映射 成 Lucene 需要的扁平化数据的方式。
- 比如下面的索引名叫 data,其中定义了 people 和 transactions 类型:会被转换为类似下面的映射保存:
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{
"data": {
"mappings": {
"people": {
"properties": {
"name": {
"type": "string",
},
"address": {
"type": "string"
}
}
},
"transactions": {
"properties": {
"timestamp": {
"type": "date",
"format": "strict_date_optional_time"
},
"message": {
"type": "string"
}
}
}
}
}
}所以虽然创建一个文档后其类型就确定了,但是实际上这个文档所占用的空间是该索引内所有字段的总和。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22{
"data": {
"mappings": {
"_type": {
"type": "string",
"index": "not_analyzed"
},
"name": {
"type": "string"
}
"address": {
"type": "string"
}
"timestamp": {
"type": "long"
}
"message": {
"type": "string"
}
}
}
}
所以有一条建议:一个索引中的类型应当都是相似的,他们有类似的字段,比如 man 和 woman 共享 name 属性;如果两个类型的字段集互不相同,创建一个 类型的文档后将浪费很多空间,而是应该将他们分到不同的索引中。
动态映射机制
在索引一个新的文档时,es 会自动为每个字段推断类型,这个过程称为动态映射。这意味着如果你通过引号( “123” )索引一个数字,它会被映射为 string 类型,而不是 long 。但是,如果这个域已经映射为 long ,那么 Elasticsearch 会尝试将这个字符串转化为 long ,如果无法转化,则抛出一个异常。
分析和搜索 - Analysis、Search
分析表示全文是如何处理使之可以被搜索的。
Elasticsearch 除了支持在各种字段上的结构化查询,还支持排序、全文检索并分析相关性。
Query DSL
特定的查询语言,查询主要分为评分查询(query)和不评分查询(filter),前者在查询完毕后还需要为文档进行评分,主要用于全文搜索,而后者只需要决定是否采用结果,所以速度会快一些。
es 提供的查询 DSL 将语句分为叶子语句(如 match)和复合语句(如 bool),通过组合可以表达复杂的语义。
根对象
映射的最高一层被称为 根对象 ,它可能包含下面几项:
- 一个 properties 节点,列出了文档中可能包含的每个字段的映射
- 各种元数据字段,它们都以一个下划线开头,例如 _type 、 _id 和 _source
- 设置项,控制如何动态处理新的字段,例如 analyzer 、 dynamic_date_formats 和 dynamic_templates
- 其他设置,可以同时应用在根对象和其他 object 类型的字段上,例如 enabled 、 dynamic 和 include_in_all
精确值和全文
精确值是结构化的,如日期或者用户 ID,字符串也可以表示精确值,例如用户名或邮箱地址,对于精确值来讲,Foo 和 foo 是不同的,2014 和 2014-09-15 也是不同的。
查询精确值很容易,结果是二进制的:要么匹配查询,要么不匹配。
全文是指文本数据(通常以人类容易识别的语言书写),例如一个推文的内容或一封邮件的内容。
查询全文数据要微妙的多。我们问的不只是“这个文档匹配查询吗”,而是“该文档匹配查询的程度有多大?”换句话说,该文档与给定查询的相关性如何?es 解决这个需求的办法是为文档建立倒排索引。
常用操作 - 索引(Index)
查询
1 | GET _cat/indices |
创建
在 Elasticsearch 中,我们的数据是被存储和索引在 分片 中,而一个索引仅仅是逻辑上的命名空间, 这个命名空间由一个或者多个分片组合在一起。
1 | PUT /megacorp/employee/1 |
第一行指定了索引名/类型名/特定雇员id,其他所有的创建索引、指定每个属性的数据类型等工作都由后台默认设置完成。
如果你想禁止自动创建索引,你 可以通过在 config/elasticsearch.yml 的每个节点下添加下面的配置:
1 | action.auto_create_index: false |
删除
1 | DELETE /index_one,index_two |
删除所有索引:
1 | DELETE /_all |
如果想要避免意外删除所有数据带来的风险,可以在配置文件 elasticsearch.yml 中加入下面配置来禁止使用_all 和通配符删除索引:
1 | action.destructive_requires_name: true |
常用操作 - 映射(Mapping)
一些默认的映射
布尔型: true 或者 false | boolean
整数: 123 | long
浮点数: 123.45 | double
字符串,有效日期: 2014-09-15 | date
字符串: foo bar | string
整数 : byte, short, integer
浮点数: float
自定义映射
TODO
全文字符串域和精确值字符串域的区别
使用特定语言分析器
优化域以适应部分匹配
指定自定义数据格式
查看映射
1 | GET /megacrp/_mapping |
返回属性包括:
- index 属性控制怎样索引字符串
- analyzed 分析字符串再索引(全文索引),字符串且只有字符串可以取这个属性
- not_analyzed 不分析、直接索引精确值
- no 不索引、不能被搜索到
- analyzer 对于 analyzed 字符串域,用 analyzer 属性指定在搜索和索引时使用的分析器,默认时 standard
更新映射
- 可以通过更新一个映射来添加一个新域,并为其设置映射(后来版本取消了 string 类型,改成了text,要注意)
- 不能将一个存在的域从 analyzed 改为 not_analyzed。因为如果一个域的映射已经存在,那么该域的数据可能已经被索引。如果你意图修改这个域的映射,索引的数据可能会出错,不能被正常的搜索。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15PUT /gb
{
"mappings": {
"testmapping" : {
"properties" : {
"tweetgjghjggh" : {
"type" : "text",
"analyzer": "english"
},
"date" : {
"type" : "date"
},
"user_id" : {
"type" : "long"
}}}}}
常用操作 - 对象(文档 Document)
操作类型
文档的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 | PUT /website/blog/123 |
如果已经有自己的 _id 、而又想执行创建,那么我们必须告诉 Elasticsearch ,只有在相同的 _index 、 _type 和 _id 不存在时才接受我们的索引请求——而不是覆盖掉,有两种方式:
1 | # 指定ID的index操作,其实是个upsert操作 |
文档是不可变的:他们不能被修改,只能被替换。 update API 必须遵循同样的规则。 从外部来看,我们在一个文档的某个位置进行部分更新。然而在内部, update API 简单使用与之前描述相同的 检索-修改-重建索引 的处理过程。 区别在于这个过程发生在分片内部,这样就避免了多次请求的网络开销。通过减少检索和重建索引步骤之间的时间,我们也减少了其他进程的变更带来冲突的可能性。
创建 - POST
不需要指定对象 id,由 Elasticsearch 自动生成,自动生成的 ID 是 URL-safe、 基于 Base64 编码且长度为 20 个字符的 GUID 字符串。 这些 GUID 字符串由可修改的 FlakeID 模式生成,这种模式允许多个节点并行生成唯一 ID ,且互相之间的冲突概率几乎为零。
1 | POST /website/blog/ |
部分更新 - POST
update 请求最简单的一种形式是接收文档的一部分作为 doc 的参数, 它只是与现有的文档进行合并。对象被合并到一起,覆盖现有的字段,增加新的字段。
1 | # 文档必须已经存在 |
使用脚本部分更新文档:脚本可以在 update API 中用来改变 _source 的字段内容, 它在更新脚本中称为 ctx._source ,运行在一个沙盒内,默认使用 Painless 语言作为脚本语言。下面这个脚本在页面不存在时执行新增并初始化 views=1(第一次运行这个请求时, upsert 值作为新文档被索引,初始化 views 字段为 1 ;在后续的运行中,由于文档已经存在, script 更新操作将替代 upsert 进行应用,对 views 计数器进行累加)、页面被浏览 2 次后执行删除,其他情况浏览量+1 并添加一个新标签:
1 | POST /website/blog/zVmOW2EBsZ0GEqF92yf6/_update |
重试:
正如之前所说,update 操作是检索-修改-重新索引的过程, 检索 和 重建索引 步骤的间隔越小,变更冲突的机会越小。 但是它并不能完全消除冲突的可能性。 还是有可能在 update 设法重新索引之前,来自另一进程的请求修改了文档。为了避免数据丢失, update API 在 检索 步骤时检索得到文档当前的 _version 号,并传递版本号到 重建索引 步骤的 index 请求。 如果另一个进程修改了处于检索和重新索引步骤之间的文档,那么 _version 号将不匹配,更新请求将会失败。
对于部分更新的很多使用场景,文档已经被改变也没有关系。 例如,如果两个进程都对页面访问量计数器进行递增操作,它们发生的先后顺序其实不太重要; 如果冲突发生了,我们唯一需要做的就是尝试再次更新。这可以通过 设置参数 retry_on_conflict 来自动完成, 这个参数规定了失败之前 update 应该重试的次数,它的默认值为 0
1 | POST /website/blog/zVmOW2EBsZ0GEqF92yf6/_update?retry_on_conflict=5 |
GET(搜索)
在请求的查询串参数中加上 pretty 参数,这将会调用 Elasticsearch 的 pretty-print 功能,该功能 使得 JSON 响应体更加可读,但其中的 _source 字段并不是被当成字符串打印出来,而是格式化成了 JSON 串:
1 | GET /website/blog/123?pretty |
将多个请求合并成一个,避免单独处理每个请求花费的网络延时和开销。 如果你需要从 Elasticsearch 检索很多文档,那么使用 multi-get 或者 mget API 来将这些检索请求放在一个请求中,将比逐个文档请求更快地检索到全部文档。
mget API 要求有一个 docs 数组作为参数,每个 元素包含需要检索文档的元数据, 包括 _index 、 _type 和 _id 。如果你想检索一个或者多个特定的字段,那么你可以通过 _source 参数来指定这些字段的名字:
1 | GET /_mget |
HEAD(ping)
如果只想检查一个文档是否存在——根本不想关心内容——那么用 HEAD 方法来代替 GET 方法。
1 | HEAD /website/blog/124 |
DELETE(删除)
1 | DELETE /website/blog/123 |
bulk(批量操作)
每一行——包括最后一行——都必须以换行符结尾,格式如下所示:
1 | { action: { metadata }}\n |
action/metadata 行指定 哪一个文档 做 什么操作 。action 必须是以下选项之一:
create:如果文档不存在,那么就创建它。类似POST
或PUT /_create
。
index:创建一个新文档或者替换一个现有的文档。类似POST
或PUT
。
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 | POST /_bulk |
批量请求的大小有一个最佳值,大于这个值,性能将不再提升,甚至会下降。 但是最佳值不是一个固定的值,它完全取决于硬件、文档的大小和复杂度、索引和搜索的负载的整体情况。
幸运的是,很容易找到这个 最佳点 :通过批量索引典型文档,并不断增加批量大小进行尝试。 当性能开始下降,那么你的批量大小就太大了。一个好的办法是开始时将 1,000 到 5,000 个文档作为一个批次, 如果你的文档非常大,那么就减少批量的文档个数。并且请求的文档也最好不要太大,一个好的批量大小在开始处理后所占用的物理大小约为 5-15 MB。