Elasticsearch文档基本操作
新建文档
首先新建一个索引,比如 blog。
然后向索引中添加一个文档。
1 | PUT blog/_doc/1 |
Kibana添加成功响应结果:
1 | { |
- _index表示文档索引。
- _type表示文档类型。
- _id 表示文档id。
- _version 表示文档版本(更新文档,版本会自动+1,针对文档的)
- result表示执行结果类型。
- _shards表示分片信息。
_seq_no
和_primary_term
也是版本控制使用的(针对当前索引的)。
当然,添加文档时也可以不指定id,此时系统会默认给出一个id。如果不指定id,则需要使用 POST 请求,而不能使用 PUT 请求。
1 | POST blog/_doc |
1 | { |
查询文档
Elasticsearch 提供了 GET API 来查看文档。
1 | GET blog/_doc/1 |
返回信息:
1 | { |
如果获取不存在的文档,会返回未找到。
1 | GET blog/_doc/2 |
1 | { |
如果只是想知道某个文档是否存在,使用 HEAD 请求。
1 | 请求: |
1 | 请求: |
当然也可以批量获取文档。
1 | GET blog/_mget |
GET 请求携带请求体的问题?
某些特定的语言,例如 JavaScript 的HTTP请求是不允许 GET请求有请求体的,实际上在 RFC7231 文档中,并没有规定GET 请求的请求体改如何处理,这样造成了一定程度的混乱,有的HTTP服务器支持GET请求携带请求体,有的HTTP服务器则不支持。虽然ES工程师倾向于使用GET做查询,但是为了保证兼容性,ES同时也支持使用POST查询。
1 | POST blog/_mget |
上面方法也是可以的。
更新文档
普通更新
注意,文档更新一次,_version就会加1。
1 | PUT blog/_doc/8Ld91nkBirAhYR49g5lr |
这种方式更新的文档会覆盖原文档内容,如果我们只想更新某个字段,这种方式需要把其他未变化的字段也带上。
大多数时候,我们只想更新文档字段,这个可以通过脚本来实现。
1 | POST blog/_update/1 |
更新请求格式:POST {index}/_update/{id}
在脚本中,lang 表示脚本语言,painless 是ES 内置的一种脚本语言。source 表示具体执行的脚本,ctx是一个上下文对象,通过 ctx可以访问到 _source
、_title
等字段。
也可以向文档中添加字段。
1 | POST blog/_update/1 |
结果:
1 | { |
通过脚本语言,也可以修改数组字段:
1 | POST blog/_update/1 |
结果:
1 | { |
当然。也可以使用 if else 构造稍微复杂一点的逻辑。
1 | POST blog/_update/1 |
结果:
1 | { |
查询更新
通过条件查询找到文档,然后再去更新。
将title中包含12345的文档的content修改为 12345.
1 | POST blog/_update_by_query |
删除文档
根据id删除
从索引中删除一个文档
1 | DELETE blog/_doc/1 |
结果
1 | { |
如果在添加文档时指定了路由,则删除文档时也需要指定路由,否则删除失败。
查询删除
查询删除是POST请求
1 | POST blog/_delete_by_query |
也可以删除某一个索引下的所有文档。
1 | POST blog/_delete_by_query |
批量操作
ES中通过bulk API 可以执行批量索引、批量删除、批量更新等操作。
首先需要将所有的批量操作写入到一个JSON文件中,然后通过POST请求将该JSON文件上传并执行。
例如新建一个名为aaa.json的文件,内容如下:
1 | {"index":{"_index":"user","_id":"666"}} |
第一行:index表示执行索引操作(表示一个action,其他action还有 create、delete、update)。
第二行是第一行的操作参数。
第三行的update则表示更新操作。
第四行是第三行的操作参数。
注意,结尾要空出一行。
index创建索引和create创建索引的区别是create创建索引如果索引已经存在则会创建失败。
文件创建成功后,直接在该目录下执行请求命令:
1 | curl -XPOST "http://localhost:9200/user/_bulk" -H "content-type:application/json" --data-binary @aaa.json |
当然,我们如果不新建文件,直接在 Kibana 开发者工具里也是可以执行的。
1 | POST _bulk |
结果如下:
1 | { |
Elasticsearch 文档路由
ES 是一个分布式系统,当我们存储一个文档到 ES 上之后,这个文档实际是被存储到 master 节点中的某一个主分片上。
那么就存在一个问题,ES如何知道这个文档被存放到哪一个分片上?
例如,新建一个索引,该索引有两个分片,0个副本,如下:
接下来,向索引中保存一个文档。
1 | PUT blog/_doc/a |
文档保存成功后,可以查看该文档被保存到哪个分片。
1 | GET _cat/shards/blog?v |
查看结果如下:
1 | index shard prirep state docs store ip node |
从这个结果中可以看出,文档被保存在分片 0 上。
我们在保存一个文档。
1 | PUT blog/_doc/b |
继续查看分片,可以看到它被保存在分片 1 上。
1 | index shard prirep state docs store ip node |
那么ES 是按照什么样的规则去分配分片的呢?
ES中的路由机制是通过哈希算法,将具有相同哈希值的文档放到同一个主分片中,分片位置的计算方式如下:
1 | shard = hash(routing)%number_of_primary_shards |
routing可以是一个任意字符串,ES默认是将文档的id作为routing值,通过哈希函数根据routing生成一个数字,然后将该数字和分片数取余。取余的结果就是分片的位置。
默认的这种路由模式,最大的优势在于负载均衡,这种方式可以保证数据平均分配在不同的分片上。但是有一个很大的劣势。就是查询时候无法确定文档位置,此时它会将请求广播到所有的分片上去执行。另一方面,使用默认的路由模式,后期修改分片数量十分不方便。
开发者也可以自定义routing的值,方式如下:
1 | PUT blog/_doc/d?routing=sakura |
如果文档在添加时指定了routing,则查询、删除、更新是也需要指定routing。
1 | GET blog/_doc/d?routing=sakura |
自定义routing可能导致负载不均衡,这需要结合实际情况选择。
典型场景:
对于用户数据,我们可以将 userId 作为 routing,这样就能保证同一个用户的数据保存在同一个分片中,检索时,同样使用userId作为routing,这样就可以精确的从某一个分片中获取数据。
Elasticsearch 文档版本控制
当我们使用 ES API 进行文档更新时,它首先读取原文档,然后对文档进行更新,然后再重新索引整个文档。无论执行多少次更新,最终保存在 ES 中的是最后一次更新的文档。
但是如果有两个线程同时更新,就可能会出现问题。
要解决问题,就要用到锁。
锁
悲观锁
每一次读取数据时,都认为数据可能会被修改,所以屏蔽一切可能破坏数据完整性的操作。关系型数据库中,悲观锁使用较多,例如行锁、表锁等。
乐观锁
每次读取数据时,都认为数据不会被修改,因此不锁定数据,只有在提交数据时,检查数据完整性。这种方式可以省去锁的开销,进而提高吞吐量。
在 ES 中,实际上使用的就是乐观锁。
版本控制
ES 6.7 版本之前,使用 _version
+ _version_type
来进行乐观并发控制。根据前面的介绍,文档每被修改一次,_version
就会自增1次,ES 通过 _version
字段来确保所有的操作都有序进行。
version 分为内部版本控制和外部版本控制。
内部版本控制
ES 自己维护的就是内部版本,当创建一个文档时,ES 会给文档版本赋值为1。
每当用户修改一次文档,版本号就会自增1。
如果使用内部版本,ES 要求 _version
参数必须和 ES 文档中 _version
的值相等才能操作成功。
外部版本控制
ES也可以通过外部版本进行版本控制。
添加文档时,PUT请求后添加参数 version 和 version_type。
version_type 有 external 和 external_gte 两种。
external 表示更新时版本号参数必须大于文档版本号。
external_gte 表示更新时版本号参数必须大于等于文档版本号。
1 | PUT blog/_doc/1?version=200&version_type=external |
新版本控制(ES 6.7之后)
现在使用 _seq_no
和 _primary_term
两个参数来进行并发控制。
_seq_no
不属于某一个文档,它属于整个索引的。(_version
则是属于某一个文档的,每个文档的_version
互不影响)
现在更想文档时,使用 _seq_no
来做并发。由于 _seq_no
是属于整个 索引的,所以索引下任何文档的修改或者新增,_seq_no
都会自增。
现在就可以通过 _seq_no
和 _primary_term
来做并发控制。
1 | PUT blog/_doc/e?if_seq_no=3&if_primary_term=1 |