开始使用 Elasticsearch (1)
在今天的这篇文章中,我们来主要介绍一下如何使用 REST 接口来对 Elasticsearch 进行操作。为了完成这项工作,我们必须完成如下的步骤:
- 安装 Elasticsearch。请参阅文章 “如何在 Linux,MacOS 及 Windows 上进行安装 Elasticsearch”。把 Elasticsearch 运行起来。
- 安装 Kibana。请参阅文章 “如何在 Linux,MacOS 及 Windows 上安装 Elastic 栈中的 Kibana”。把 Kibana 运行起来。
- 熟悉有关于 Elastic 栈的一些最基本的概念。请参阅文章 “Elasticsarch 中的一些重要概念: cluster, node, shards 及 replica”。这些概念对我们如下的练习有非常多的帮助。
有了上面最基本的一些安装及概念,我们就很容进行下面的讲解了。在如下所展示的所有的 scripts 可在地址 https://github.com/liu-xiao-guo/es-scripts-7.3 找到。
搜索引擎执行以下两个主要操作:
- 索引(indexing):此操作用于接收文档,对其进行处理,并将其存储在一个索引中。
- 搜索(searching):此操作用于根据查询从索引中检索数据。
除了上述的两个操作,作为数据库,通常它还有另外的两个操作:
- 更改(updating)
- 删除(deleting)
有关这两个操作的详述,请参阅我的另外一篇文章 “Elasticsearch:彻底理解 Elasticsearch 数据操作”。
本教程基于上面的两个操作来完成。这个教程是一个3篇文章的教程,它涵盖了最基本的 Elasticsearch 的一些最基本的点:
- 开始使用 Elasticsearch (1): 了解如何创建索引,添加,删除,更新文档
- 开始使用 Elasticsearch (2):了解如何进行搜索
- 开始使用 Elasticsearch (3):了解如何进行分析数据: analyze 及 aggregate 数据
开始使用Elasticsearch (1)
开始使用Elasticsearch (1)_哔哩哔哩_bilibili
什么是 JSON?
JSON (JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScript Programming Language, Standard ECMA-262 3rd Edition - December 1999 的一个子集。在 Elasticsearch 中,所以的数据都是以 JSON 的格式来进行表述的。这个和其它的有些数据库,比如 Solr,它支持更多格式的数据,比如 xml, csv 等。
我们来看一下一个简单的 JSON 格式的数据表达:
{
"name" : "Elastic",
"location" : {
"state" : "CA",
"zipcode" : 94123
}
}
这个看起来非常简单直接。如果大家熟悉 Javascript 的话,你会发现它和 Javascript 里的 Object 非常地相似。
什么是 REST 接口?
相信很多做过微服务架构的开发者来说,你们可能对 REST 接口再熟悉不过了。REST 即表述性状态传递(英文:Representational State Transfer,简称 REST)是 Roy Fielding 博士在2000年他的博士论文中提出来的一种软件架构风格。REST 是一种规范。即参数通过封装后进行传递,响应也是返回的一个封装对象。一个 REST 的接口就像如下的接口:
http://example.com/customers/1234
我们可以通过:
HTTP GET
HTTP POST
HTTP PUT
HTTP DELETE
HTTP PATCH
来对数据进行增加(Create),查询(Read),更新(Update)及删除(Delete)。也就是我们通常说是的 CRUD。
方法 | 用法 |
---|---|
GET | 读取数据 |
POST | 插入数据 |
PUT 或 PATCH | 更新数据,或如果是一个新的 id,则插入数据 |
DELETE | 删除数据 |
Elasticsearch 里的接口都是通过 REST 接口来实现的。我们在一下的章节里来重点介绍一下是如果使用 REST 接口来实现对数据的操作及查询的。
检查 Elasticsearch 及 Kibana 是否运行正常
我们首先在我们的浏览器中输入如下地址:http://elasticsearch_endpoint:9200。查看一下我们的输出:
如果你能看到如上的信息输出,表明我们的 Elasticsearch 是处于一个正常运行的状态。
同时,我们在浏览器中输入地址:http://kibana_endpoint:5601。在浏览器中,我们查看输出的信息:
上面显示了 Kibana 的界面。由于 Kibana 的功能有很多。我们在今天的培训中就不一一介绍了。我们着重使用在上面显示的 “Dev Tools” 菜单里的功能。当我们点击它的时候,我们可以看到如下的界面。
当我们执行命令时,我们必须点击左边窗口里的那个绿色的播放按钮。命令所执行显示的结果将在右边展示。在接下的所有练习中,我们都将使用这样的操作来进行。
在使用 Kibana 之前,最后先熟悉一下一些快捷键的使用,你可以点击 help 链接得到更多的帮助:
查看 Elasticsearch 信息
就像我们之前在浏览器其中打入地址 http://localhost:9200 看到的效果一样,我们直接打入
GET /
我们就可以看到如下的信息:
在这里我们可以看到 Elasticsearch 的版本信息及我们正在使用的 Elasticsearch 的 Cluster 名称等信息。
在很多时候,我们也可以直接在 terminal 中打入相应的指令来达到同样的效果,不过在 Kibana 中更加直接:
我们把上面的命令拷贝为 cURL,然后再粘贴到 terminal 中,你就会看到:
同样地,我们也可以直接把如下的 terminal 命令:
curl -XGET "http://localhost:9200/"
直接拷贝并粘贴到 Dev Tools 的 Console 里去,你会发现,它变成为:
GET /
我们可以使用 cURL 将请求从命令行提交到本地 Elasticsearch 实例。对 Elasticsearch 的请求包含与任何 HTTP 请求相同的部分:
curl -X<VERB> '<PROTOCOL>://<HOST>:<PORT>/<PATH>?<QUERY_STRING>' -d '<BODY>'
本示例使用以下变量:
- <VERB> :适当的 HTTP 方法或动词。 例如,GET,POST,PUT,HEAD 或 DELETE
- <PROTOCOL>:http 或 https。 如果你在 Elasticsearch 前面有一个 HTTPS 代理,或者你使用 Elasticsearch 安全功能来加密 HTTP 通信,请使用后者
- <HOST>:Elasticsearch 集群中任何节点的主机名。 或者,将 localhost 用于本地计算机上的节点
- <PORT>:运行 Elasticsearch HTTP 服务的端口,默认为9200
- <PATH>:API 端点,可以包含多个组件,例如 _cluster/stats 或 _nodes/stats/jvm
- <QUERY_STRING>:任何可选的查询字符串参数。 例如,?pretty 将漂亮地打印 JSON 响应以使其更易于阅读
- <BODY>:JSON 编码的请求正文(如有必要)
如果启用了 Elasticsearch 安全功能,则还必须提供有权运行 API 的有效用户名(和密码)。 例如,使用 -u 或 --u cURL 命令参数。比如:
curl -u elastic:password -X<VERB> '<PROTOCOL>://<HOST>:<PORT>/<PATH>?<QUERY_STRING>' -d '<BODY>'
这里的 elastic 及 password 代表用超级用户名 elastic 及其密码。你也可以可以使用其它用户账号,如果你的安装是带有安全的。比如一个写入文档到 Elasticsearch 的 curl 命令如下:
curl -XPUT "https://10.211.55.2:9200/twitter/_doc/1" -H "Content-Type: application/json" -d'
{
"content": "This is Xiaoguo from Elastic"
}'
我们发现当我们打入一个命令时,Kibana 会帮我们自动地显示可以输入的选择项,它具有 autocomplete 的功能。这个对我们打入我们所需要的命令非常有用。我们有时候不需要记那么多。
创建一个索引及文档
我们接下来创建一个叫做 twitter 的索引(index),并插入一个文档(document)。我们知道在 RDMS 中,我们通常需要有专用的语句来生产相应的数据库,表格,让后才可以让我们输入相应的记录,但是针对 Elasticsearch 来说,这个是不必须的。我们在左边的窗口中输入:
PUT twitter/_doc/1
{
"user": "GB",
"uid": 1,
"city": "Beijing",
"province": "Beijing",
"country": "China"
}
我们可以看到在 Kibana 右边的窗口中有下面的输出:
一旦一个文档被写入,它经历如下的一个过程:
在通常的情况下,新写入的文档并不能马上被用于搜索。新增的索引必须写入到 Segment 后才能被搜索到。需要等到 refresh 操作才可以。在默认的情况下每隔一秒的时间 refresh 一次。这就是我们通常所说的近实时。详细阅读请参阅文章 “Elasticsearch:Elasticsearch 中的 refresh 和 flush 操作指南”。在编程的时候,我们尤为需要注意这一点。比如我们通过 REST API 时写进一个文档,在写入的时候没有强制 refresh 操作,而是立即进行搜索。我们可能搜索不到刚写入的文档。
请注意:在上面创建文档的过程中,我们并没有像其他 RDMS 系统一样,在输入文档之前需要定义各个字段的类型及长度等。为了提高入门时的易用性,Elasticsearch 可以自动动态地为你创建索 mapping。当我们建立一个索引的第一个文档时,如果你没有创建它的 schema,那么 Elasticsearch 会根据所输入字段的数据进行猜测它的数据类型,比如上面的 user 被被认为是 text 类型,而 uid 将被猜测为整数类型。这种方式我们称之为 schema on write,也即当我们写入第一个文档时,Elasticsearch 会自动帮我们创建相应的 schema。在 Elasticsearch 的术语中,mapping 被称作为 Elasticsearch 的数据 schema。文档中的所有字段都需要映射到 Elasticsearch 中的数据类型。 mapping 指定每个字段的数据类型,并确定应如何索引和分析字段以进行搜索。 在 SQL 数据库中定义表时,mapping 类似于 schema。 mapping 可以显式声明或动态生成。一旦一个索引的某个字段的类型被确定下来之后,那么后续导入的文档的这个字段的类型必须是和之前的是一致,否则写入将导致错误。schema on write 可能在某些时候不是我们想要的,那么在这种情况下,我们可以事先创建一个索引的 schema。你将在文章的下面部分中看到如何创建这个 schema。在最新的 Elasticsearch 设计中,也出现了一种叫做 schema on read 的设计。如果你对这个感兴趣的话,请参阅我的另外一篇文章 “Elasticsearch:Runtime fields 入门, Elastic 的 schema on read 实现 - 7.11 发布”。
在写入文档时,如果该文档的 ID 已经存在,那么就更新现有的文档;如果该文档从来没有存在过,那么就创建新的文档。如果更新时该文档有新的字段并且这个字段在现有的 mapping 中没有出现,那么 Elasticsearch 会根据 schem on write 的策略来推测该字段的类型,并更新当前的 mapping 到最新的状态。
动态 mapping 还可能导致某些字段未映射到你的预期,从而导致索引请求失败。显式 mapping 允许更好地控制索引中的字段和数据类型。 一旦知道索引 schema,明确定义索引映射是一个好主意。我们在运行完上面的命令后,可以通过如下的命令来查看当前索引的 mapping:
GET twitter/_mapping
{
"twitter" : {
"mappings" : {
"properties" : {
"city" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"country" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"province" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"uid" : {
"type" : "long"
},
"user" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
}
}
}
如果我仅想得到某一个字段的类型,我们可以使用如的命令:
GET twitter/_mapping/field/city
上面的名仅返回 city 这个字段的属性:
{
"twitter": {
"mappings": {
"city": {
"full_name": "city",
"mapping": {
"city": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}
}
}
针对一些刚接触 Elasticsearch 的开发者来说,上面的这个类型的字段:
"city" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
它可能难以让人理解。究其实上面的这个字段是一个 mulit-field 字段。为不同目的以不同方式索引同一字段通常很有用。比如在上面,我们定义字段 city 为 text 类型。text 类型的数据在摄入的时候会分词,这样它可以实现搜索的功能。同时,这个字段也被定义为 keyword 类型的数据。这个类型的数据可以让我们针对它进行精确匹配(比如区分大小写,空格等符号),聚合和排序。我刚开始学习的时候,上面的两个 keyword 有时让人费解。其实,上面的第一个 keyword 可以是你定义的任何词,而第二个 keyword 才是它的类型定义。比如,我们可以这样来定义这个字段:
"city" : {
"type" : "text",
"fields" : {
"raw" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
这在早期的 Elasticsearch 发行版中比较常见,一般定义它为 raw。在现在的版本中,在默认的情况下,Elasticsearch 会自动选用 keyword 作为它的名称。在我们访问这个 multi-field 字段时,我们需要以这样的形式来进行访问:city.text 及 city.raw。在实际的使用中,我们有时可能对一个字段需要同时进行搜索和聚合,我们可以这样定义它为 multi-field,但是如果我们仅对搜索或者聚合感兴趣,我们只需要定义其中的一种类型。这样做的好处是,它可以提高数据摄入的速度,因为不必为两个类型进行索引,同时它也可以减少磁盘的使用。
Elasticsearch 的数据类型:
- text:全文搜索字符串
- keyword:用于精确字符串匹配和聚合
- date 及 date_nanos:格式化为日期或数字日期的字符串
- byte, short, integer, long:整数类型
- boolean:布尔类型
- float,double,half_float:浮点数类型
-
分级的类型:object 及 nested。你可以参考文章 “Elasticsearch: nested 对象”
有关 Elasticsearch 的数据类型,可以参考链接。
在默认的情况下,Elasticsearch 可以理解你正在索引的文档的结构并自动创建映射(mapping)定义。 这称为显式映射(Explicit mapping)创建。在绝大多数的情况下,它工作的非常好。使用显式映射可以开始使用无模式(schemaless)方法快速摄取数据,而无需担心字段类型。 因此,为了在索引中获得更好的结果和性能,我们有时需要需要手动定义映射。 微调映射带来了一些优势,例如:
- 减少磁盘上的索引大小(禁用自定义字段的功能)
- 仅索引感兴趣的字段(一般加速)
- 用于快速搜索或实时分析(例如聚合)
- 正确定义字段是否必须分词为多个 token 或单个 token
- 定义映射类型,例如地理点、suggester、向量等
假如,我们想创建一个索引 test,并且含有 id 及 message 字段。id 字段为 keyword 类型,而 message 字段为 text 类型,那么我们可以使用如下的方法来创建:
PUT test
{
"mappings": {
"properties": {
"id": {
"type": "keyword"
},
"message": {
"type": "text"
}
}
}
}
我们甚至可以使用如下的 API 来追加一个新的字段 age,并且它的类型为 long 类型:
PUT test/_mapping
{
"properties": {
"age": {
"type": "long"
}
}
}
我们可以使用如下的命令来查看索引 test 的最终 mapping:
GET test/_mapping
上面的命令显示的结果为:
{
"test" : {
"mappings" : {
"properties" : {
"age" : {
"type" : "long"
},
"id" : {
"type" : "keyword"
},
"message" : {
"type" : "text"
}
}
}
}
}
在上面,我们可以看出来我们已经成功地创建了一个叫做 twitter 的 index。通过这样的方法,我们可以自动创建一个 index。如果大家不喜欢自动创建一个 index,我们可以修改如下的一个设置:
PUT _cluster/settings
{
"persistent": {
"action.auto_create_index": "false"
}
}
请注意:在绝大多数情况下,我们并不需要这么做,除非你知道你要做什么。详细设置请参阅链接。如果你想禁止自动创建索引,你必须配置 action.auto_create_index 以允许这些创建以下索引的组件:
PUT _cluster/settings
{
"persistent": {
"action.auto_create_index": ".monitoring*,.watches,.triggered_watches,.watcher-history*,.ml*"
}
}
如果使用 Logstash 或 Beats,则应在上面的列表中添加其他索引名称。我们也可以在 elasticsearch.yml 里进行配置。
通常对一个通过上面方法写入到 Elasticsearch 的文档,在默认的情况下并不马上可以进行搜索。这是因为在 Elasticsearch 的设计中,有一个叫做 refresh 的操作。它可以使更改可见以进行搜索的操作。通常会有一个 refresh timer 来定时完成这个操作。这个周期为1秒。这也是我们通常所说的 Elasticsearch 可以实现秒级的搜索。当然这个 timer 的周期也可以在索引的设置中进行配置。如果我们想让我们的结果马上可以对搜索可见,我们可以用如下的方法:
PUT twitter/_doc/1?refresh=true
{
"user": "GB",
"uid": 1,
"city": "Beijing",
"province": "Beijing",
"country": "China"
}
上面的方式可以强制使 Elasticsearch 进行 refresh 的操作,当然这个是有代价的。频繁的进行这种操作,可以使我们的 Elasticsearch 变得非常慢。另外一种方式是通过设置 refresh=wait_for。这样相当于一个同步的操作,它等待下一个 refresh 周期发生完后,才返回。这样可以确保我们在调用上面的接口后,马上可以搜索到我们刚才录入的文档:
PUT twitter/_doc/1?refresh=wait_for
{
"user": "GB",
"uid": 1,
"city": "Beijing",
"province": "Beijing",
"country": "China"
}
如果你想对 refresh 有更多的了解,请参阅我的文章 “Elasticsearch 中的 refresh 和 flush 操作指南”。
它也创建了一个被叫做 _doc 的 type。自从 Elasticsearch 6.0 以后,一个 index 只能有一个 type。如果我们创建另外一个 type 的话,系统会告诉我们是错误的。这里我们也会发现有一个版本(_version)信息,它显示的是4。如果这个 _id 为 1 的 document 之前没有被创建过的话,它会显示为 1。之后如果我们更改这个 document,它的版本会每次自动增加1。比如,我们输入:
POST twitter/_doc/1
{
"user": "GB",
"uid": 1,
"city": "Shenzhen",
"province": "Guangdong",
"country": "China"
}
我们在左边修改了我们的数据,在右边,我们可以看到版本信息增加到6。这是因为我们把左边的命令执行了两次。同时,我们也可以看出来,我们也把左边的数据进行了修改,我们也看到了成功被修改的返回信息。在上面我们可以看出来,我们每次执行那个 POST 或者 PUT 接口时,如果文档已经存在,那么相应的版本(_version)就会自动加1,之前的版本抛弃。如果这个不是我们想要的,那么我们可以使 _create 端点接口来实现:
PUT twitter/_create/1
{
"user": "GB",
"uid": 1,
"city": "Shenzhen",
"province": "Guangdong",
"country": "China"
}
如果文档已经存在的话,我们会收到一个错误的信息:
上面的命令和如下的命令也是一样的效果:
PUT twitter/_doc/1?op_type=create
{
"user": "双榆树-张三",
"message": "今儿天气不错啊,出去转转去",
"uid": 2,
"age": 20,
"city": "北京",
"province": "北京",
"country": "中国",
"address": "中国北京市海淀区",
"location": {
"lat": "39.970718",
"lon": "116.325747"
}
}
在上面,我们在请求时带上 op_type。它可以有两种值:index 及 create。
我们必须指出的是,如果你是在 Linux 或 MacOS 机器上,我们也可以使用如下的命令行指令来达到同样的效果:
curl -XPUT 'http://localhost:9200/twitter/_doc/1?pretty' -H 'Content-Type: application/json' -d '
{
"user": "GB",
"uid": 1,
"city": "Shenzhen",
"province": "Guangdong",
"country": "China"
}'
本方法适用于一下所有的命令,如法炮制!
我们可以通过如下的命令来查看被修改的文档:
GET twitter/_doc/1
我们可以看到在右边显示了我们被修改的文档的结果。
如果我们只想得到这个文档的 _source 部分,我们可以使用如下的命令格式:
GET twitter/_doc/1/_source
在 Elasticsearch 7.0 之后,在 type 最终要被废除的情况下,我们建立使用如下的方法来获得 _source:
GET twitter/_source/1
自动 ID 生成
在上面,我特意为我们的文档分配了一个 ID。其实在实际的应用中,这个并不必要。相反,当我们分配一个 ID 时,在数据导入的时候会检查这个 ID 的文档是否存在,如果是已经存在,那么就更新到版本。如果不存在,就创建一个新的文档。如果我们不指定文档的 ID,转而让 Elasticsearch 自动帮我们生成一个 ID,这样的速度更快。在这种情况下,我们必须使用 POST,而不是 PUT,比如:
POST my_index/_doc
{
"content": "this is really cool"
}
返回的结果:
{
"_index" : "my_index",
"_type" : "_doc",
"_id" : "egiY4nEBQTokU_uEEGZz",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1
}
从上面我们可以看出来,系统会为我们自动分配一个 ID 啊。
在正常的情况下,当当前复制组的所有分片都执行了索引操作时,Elasticsearch 从索引操作返回。
设置异步复制允许我们在主分片上同步执行索引操作,在副本分片上异步执行。这样,API 调用会更快地返回响应操作。我们可以这样来进行调用:
POST my_index/_doc?replication=async
{
"content": "this is really cool"
}
注意:上面的 replication=async 在新版本中已经弃用,并不再支持。
如果我们只对 source 的内容感兴趣的话,我们可以使用:
GET twitter/_doc/1/_source
这样我们可以直接得到 source 的信息:
{
"user" : "双榆树-张三",
"message" : "今儿天气不错啊,出去转转去",
"uid" : 2,
"age" : 20,
"city" : "北京",
"province" : "北京",
"country" : "中国",
"address" : "中国北京市海淀区",
"location" : {
"lat" : "39.970718",
"lon" : "116.325747"
}
}
我们也可以只获取 source 的部分字段:
GET twitter/_doc/1?_source=city,age,province
如果你想一次请求查找多个文档,我们可以使用 _mget 接口:
GET _mget
{
"docs": [
{
"_index": "twitter",
"_id": 1
},
{
"_index": "twitter",
"_id": 2
}
]
}
我们也可以只获得部分字段:
GET _mget
{
"docs": [
{
"_index": "twitter",
"_id": 1,
"_source":["age", "city"]
},
{
"_index": "twitter",
"_id": 2,
"_source":["province", "address"]
}
]
}
在这里,我们同时请求 id 为 1 和 2 的两个文档。
我们也可以简单地写为:
GET twitter/_doc/_mget
{
"ids": ["1", "2"]
}
它和上面的做一个是一样的。使用一个命令同时获取 id 为 1 及 2 的文档。
在上面当我们写入数据时,我们有意识地把文档的 id 在命令中写了出来。如果我们不写这个 id 的话,ES 会帮我们自动生产一个 id:
POST twitter/_doc/
我可以看到右边的一个 id 像是一个随机的数值,同时我们可以看到它的一个版本信息为1。在实际的需要有大量导入数据的情况下,我们建议让系统自动帮我们生成一个 id,这样可以提高导入的速度。假如我们指定一个 id,通常 ES 会先查询这个 id 是否存在,然后在觉得是更新之前的文档还是创建一个新的文档。这里是分两步走。显然它比直接创建一个文档要慢!
我们也可以看出来系统所给出来的字段都是以下划线的形式给出来的,比如:_id, _shards, _index, _typed 等
修改一个文档
我们接下来看一下如何修改一个文档。在上面我们看到了可以使用 POST 的命令来修改改一个文档。通常我们使用 POST 来创建一个新的文档。在使用 POST 的时候,我们甚至不用去指定特定的 id,系统会帮我们自动生成。但是我们修改一个文档时,我们通常会使用 PUT 来进行操作,并且,我们需要指定一个特定的 id 来进行修改:
PUT twitter/_doc/1
{
"user": "GB",
"uid": 1,
"city": "北京",
"province": "北京",
"country": "中国",
"location":{
"lat":"29.084661",
"lon":"111.335210"
}
}
如上面所示,我们使用 PUT 命令来对我们的 id 为1的文档进行修改。我们也可以使用我们上面学过的 GET 来进行查询:
GET twitter/_doc/1
{
"_index" : "twitter",
"_type" : "_doc",
"_id" : "1",
"_version" : 8,
"_seq_no" : 13,
"_primary_term" : 1,
"found" : true,
"_source" : {
"user" : "GB",
"uid" : 1,
"city" : "北京",
"province" : "北京",
"country" : "中国",
"location" : {
"lat" : "29.084661",
"lon" : "111.335210"
}
}
}
显然,我们的这个文档已经被成功修改了。
我们使用 PUT 的这个方法,每次修改一个文档时,我们需要把文档的每一项都要写出来。这对于有些情况来说,并不方便,我们可以使用如下的方法来进行修改:
POST twitter/_update/1
{
"doc": {
"city": "成都",
"province": "四川"
}
}
我们可以使用如上的命令来修改我们的部分数据。同样我们可以使用 GET 来查询我们的修改是否成功:
从上面的显示中,我们可以看出来,我们的修改是成功的,虽然在我们修改时,我们只提供了部分的数据。
在关系数据库中,我们通常是对数据库进行搜索,让后才进行修改。在这种情况下,我们事先通常并不知道文档的 id。我们需要通过查询的方式来进行查询,让后进行修改。ES 也提供了相应的 REST 接口。
POST twitter/_update_by_query
{
"query": {
"match": {
"user": "GB"
}
},
"script": {
"source": "ctx._source.city = params.city;ctx._source.province = params.province;ctx._source.country = params.country",
"lang": "painless",
"params": {
"city": "上海",
"province": "上海",
"country": "中国"
}
}
}
对于那些名字是中文字段的文档来说,在 painless 语言中,直接打入中文字段名字,并不能被认可。我们可以使用如下的方式来操作:
POST edd/_update_by_query
{
"query": {
"match": {
"姓名": "张彬"
}
},
"script": {
"source": "ctx._source[\"签到状态\"] = params[\"签到状态\"]",
"lang": "painless",
"params" : {
"签到状态":"已签到"
}
}
}
在上面我们使用一个中括号并 escape 引号的方式来操作。有关 Painless 的编程,你可以参阅文章 “Elasticsearch: Painless script 编程”。
我们可以通过上面的方法搜寻 user 为 GB 的用户,并且把它的数据项修改为:
"city" : "上海",
"province": "上海",
"country": "中国"
我们也可以通过 update 接口,使用 script 的方法来进行修改。这个方法也是需要知道文档的 id:
POST twitter/_update/1
{
"script" : {
"source": "ctx._source.city=params.city",
"lang": "painless",
"params": {
"city": "长沙"
}
}
}
在我们使用上面的方法更新文档时,如果当前的文档 id 不存在,那么我们甚至可以使用 upsert 属性来创建一个文档:
POST twitter/_update/1
{
"script" : {
"source": "ctx._source.city=params.city",
"lang": "painless",
"params": {
"city": "长沙"
}
},
"upsert": {
"city": "长沙"
}
}
和前面的方法一下,我们可以使用 GET 来查询,我们的结果是否已经改变:
{
"_index" : "twitter",
"_type" : "_doc",
"_id" : "1",
"_version" : 18,
"_seq_no" : 39,
"_primary_term" : 1,
"found" : true,
"_source" : {
"uid" : 1,
"country" : "中国",
"province" : "上海",
"city" : "长沙",
"location" : {
"lon" : "111.335210",
"lat" : "29.084661"
},
"user" : "GB"
}
}
如果你涉及到多个客户端同时更新一个索引的情况,你需要阅读文章 “深刻理解文档中的 verision 及 乐观并发控制”。
我们甚至可以使用 _update 接口使用 ctx['_op'] 来达到删除一个文档的目的,比如:
POST twitter/_update/1
{
"script": {
"source": """
if(ctx._source.uid == 1) {
ctx.op = 'delete'
} else {
ctx.op = "none"
}
"""
}
}
当检测文档的 uid 是否为 1,如果为 1 的话,那么该文档将被删除,否则将不做任何事情。
我还可以充分使用 script 的一些高级操作,比如我们可以通过如下的方法来添加一个崭新的字段:
POST twitter/_update/1
{
"script" : {
"source": "ctx._source.newfield=4",
"lang": "painless"
}
}
通过上面的操作,我们可以发现,我们新增加了一个叫做 newfield 的字段。当然我们也可以使用如下的方法来删除一个字段:
POST twitter/_update/1
{
"script" : {
"source": "ctx._source.remove(\"newfield\")",
"lang": "painless"
}
}
在上面的命令中,我们通过 remove 删除了刚才被创建的 newfiled 字段。我们可以通过如下的命令来进行查看:
GET twitter/_doc/1
在这里请注意的是:一旦一个字段被创建,那么它就会存在于更新的 mapping 中。即便针对 id 为 1 的文档删除了 newfield,但是 newfield 还将继续存在于 twitter 的 mapping 中。我们可以使用如下的命令来查看 twitter 的 mapping:
GET twitter/_mapping
这里值得注意是:对于多用户,我们可以从各个客户端同时更新,这里可能会造成更新数据的一致性问题。为了避免这种现象的出现,请阅读我的另外一篇文章 “Elasticsearch:深刻理解文档中的 verision 及乐观并发控制”。
UPSERT 一个文档
仅在文档事先存在的情况下,我们在前面的代码中看到的部分更新才有效。 如果具有给定 id 的文档不存在,Elasticsearch 将返回一个错误,指出该文档丢失。 让我们了解如何使用更新 API 进行 upsert 操作。 术语 “upsert” 宽松地表示更新或插入,即更新文档(如果存在),否则,插入新文档。
doc_as_upsert 参数检查具有给定ID的文档是否已经存在,并将提供的 doc 与现有文档合并。 如果不存在具有给定 id 的文档,则会插入具有给定文档内容的新文档。
下面的示例使用 doc_as_upsert 合并到 id 为 3 的文档中,或者如果不存在则插入一个新文档:
POST /catalog/_update/3
{
"doc": {
"author": "Albert Paro",
"title": "Elasticsearch 5.0 Cookbook",
"description": "Elasticsearch 5.0 Cookbook Third Edition",
"price": "54.99"
},
"doc_as_upsert": true
}
检查一个文档是否存在
有时候我们想知道一个文档是否存在,我们可以使用如下的方法:
HEAD twitter/_doc/1
这个 HEAD 接口可以很方便地告诉我们在 twitter 的索引里是否有一 id 为1的文档:
上面的返回值表面 id 为1的文档时存在的。
删除一个文档
如果我们想删除一个文档的话,我们可以使用如下的命令:
DELETE twitter/_doc/1
在上面的命令中,我们删除了 id 为 1 的文档。
在关系数据库中,我们通常是对数据库进行搜索,让后才进行删除。在这种情况下,我们事先通常并不知道文档的 id。我们需要通过查询的方式来进行查询,让后进行删除。ES 也提供了相应的 REST 接口。
POST twitter/_delete_by_query
{
"query": {
"match": {
"city": "上海"
}
}
}
这样我们就把所有的 city 是上海的文档都删除了。
检查一个索引是否存在
我们可以使用如下的命令来检查一个索引是否存在:
HEAD twitter
如果 twitter 索引存在,那么上面的命令会返回:
200 - OK
否则就会返回:
{"statusCode":404,"error":"Not Found","message":"404 - Not Found"}
删除一个索引
删除一个索引 是非常直接的。我们可以直接使用如下的命令来进行删除:
DELETE twitter
当我们执行完这一条语句后,所有的在 twitter 中的所有的文档都将被删除。
批处理命令
上面我们已经了解了如何使用 REST 接口来创建一个 index,并为之创建(Create),读取(Read),修改(Update),删除文档(Delete)(CRUD)。因为每一次操作都是一个 REST 请求,对于大量的数据进行操作的话,这个显得比较慢。ES 创建一个批量处理的命令给我们使用。这样我们在一次的 REST 请求中,我们就可以完成很多的操作。这无疑是一个非常大的好处。下面,我们来介绍一下这个 _bulk 命令。
我们使用如下的命令来进行 bulk 操作:
POST _bulk
{ "index" : { "_index" : "twitter", "_id": 1} }
{"user":"双榆树-张三","message":"今儿天气不错啊,出去转转去","uid":2,"age":20,"city":"北京","province":"北京","country":"中国","address":"中国北京市海淀区","location":{"lat":"39.970718","lon":"116.325747"}}
{ "index" : { "_index" : "twitter", "_id": 2 }}
{"user":"东城区-老刘","message":"出发,下一站云南!","uid":3,"age":30,"city":"北京","province":"北京","country":"中国","address":"中国北京市东城区台基厂三条3号","location":{"lat":"39.904313","lon":"116.412754"}}
{ "index" : { "_index" : "twitter", "_id": 3} }
{"user":"东城区-李四","message":"happy birthday!","uid":4,"age":30,"city":"北京","province":"北京","country":"中国","address":"中国北京市东城区","location":{"lat":"39.893801","lon":"116.408986"}}
{ "index" : { "_index" : "twitter", "_id": 4} }
{"user":"朝阳区-老贾","message":"123,gogogo","uid":5,"age":35,"city":"北京","province":"北京","country":"中国","address":"中国北京市朝阳区建国门","location":{"lat":"39.718256","lon":"116.367910"}}
{ "index" : { "_index" : "twitter", "_id": 5} }
{"user":"朝阳区-老王","message":"Happy BirthDay My Friend!","uid":6,"age":50,"city":"北京","province":"北京","country":"中国","address":"中国北京市朝阳区国贸","location":{"lat":"39.918256","lon":"116.467910"}}
{ "index" : { "_index" : "twitter", "_id": 6} }
{"user":"虹桥-老吴","message":"好友来了都今天我生日,好友来了,什么 birthday happy 就成!","uid":7,"age":90,"city":"上海","province":"上海","country":"中国","address":"中国上海市闵行区","location":{"lat":"31.175927","lon":"121.383328"}}
在上面的命令中,我们使用了 bulk 指令来完成我们的操作。在输入命令时,我们需要特别的注意:千万不要添加除了换行以外的空格,否则会导致错误。在上面我们使用的 index 用来创建一个文档。为了说明问题的方便,我们在每一个文档里,特别指定了每个文档的 id。当执行完我们的批处理 bulk 命令后,我们可以看到:
显然,我们的创建时成功的。因为我运行了两遍的原因,所以你看到的是 version 为 2 的返回结果。bulk 指令是高效的,因为一个请求就可以处理很多个操作。在实际的使用中,我们必须注意的是:一个好的起点是批量处理 1,000 到 5,000 个文档,总有效负载在 5MB 到 15MB 之间。如果我们的 payload 过大,那么可能会造成请求的失败。如果你想更进一步探讨的话,你可以使用文件 accounts.json 来做实验。更多是有数据可以在地址 加载示例数据 | Kibana 用户手册 | Elastic 进行下载。
如果你想查询到所有的输入的文档,我们可以使用如下的命令来进行查询:
POST twitter/_search
这是一个查询的命令,在以后的章节中,我们将再详细介绍。通过上面的指令,我们可以看到所有的已经输入的文档。
上面的结果显示,我们已经有6条生产的文档记录已经生产了。
我们可以通过使用 _count 命令来查询有多少条数据:
GET twitter/_count
上面我们已经使用了 index 来创建6条文档记录。我也可以尝试其它的命令,比如 create:
POST _bulk
{ "create" : { "_index" : "twitter", "_id": 1} }
{"user":"双榆树-张三","message":"今儿天气不错啊,出去转转去","uid":2,"age":20,"city":"北京","province":"北京","country":"中国","address":"中国北京市海淀区","location":{"lat":"39.970718","lon":"116.325747"}}
{ "index" : { "_index" : "twitter", "_id": 2 }}
{"user":"东城区-老刘","message":"出发,下一站云南!","uid":3,"age":30,"city":"北京","province":"北京","country":"中国","address":"中国北京市东城区台基厂三条3号","location":{"lat":"39.904313","lon":"116.412754"}}
{ "index" : { "_index" : "twitter", "_id": 3} }
{"user":"东城区-李四","message":"happy birthday!","uid":4,"age":30,"city":"北京","province":"北京","country":"中国","address":"中国北京市东城区","location":{"lat":"39.893801","lon":"116.408986"}}
{ "index" : { "_index" : "twitter", "_id": 4} }
{"user":"朝阳区-老贾","message":"123,gogogo","uid":5,"age":35,"city":"北京","province":"北京","country":"中国","address":"中国北京市朝阳区建国门","location":{"lat":"39.718256","lon":"116.367910"}}
{ "index" : { "_index" : "twitter", "_id": 5} }
{"user":"朝阳区-老王","message":"Happy BirthDay My Friend!","uid":6,"age":50,"city":"北京","province":"北京","country":"中国","address":"中国北京市朝阳区国贸","location":{"lat":"39.918256","lon":"116.467910"}}
{ "index" : { "_index" : "twitter", "_id": 6} }
{"user":"虹桥-老吴","message":"好友来了都今天我生日,好友来了,什么 birthday happy 就成!","uid":7,"age":90,"city":"上海","province":"上海","country":"中国","address":"中国上海市闵行区","location":{"lat":"31.175927","lon":"121.383328"}}
在上面,我们的第一个记录里,我们使用了 create 来创建第一个 id 为1的记录。因为之前,我们已经创建过了,所以我们可以看到如下的信息:
从上面的信息,我们可以看出来 index 和 create 的区别。index 总是可以成功,它可以覆盖之前的已经创建的文档,但是 create 则不行,如果已经有以那个 id 为名义的文档,就不会成功。
我们可以使用 delete 来删除一个已经创建好的文档:
POST _bulk
{ "delete" : { "_index" : "twitter", "_id": 1 }}
我们可以看到 id 为1的文档已经被删除了。我可以通过如下的命令来查看一下:
显然,我们已经把 id 为1的文档已经成功删除了。
我们也可以是使用 update 来进行更新一个文档。
POST _bulk
{ "update" : { "_index" : "twitter", "_id": 2 }}
{"doc": { "city": "长沙"}}
运行的结果如下:
同样,我们可以使用如下的方法来查看我们修改的结果:
我们可以清楚地看到我们已经成功地把城市 city 修改为 “长沙”。
注意:通过 bulk API 为数据编制索引时,你不应在集群上进行任何查询/搜索。 这样做可能会导致严重的性能问题。
如果你对脚本编程比较熟悉的话,你可能更希望通过脚本的方法来把大量的数据通过脚本的方式来导入:
$ curl -s -H "Content-Type: application/x-ndjson" -XPOST localhost:9200/_bulk --data-binary @request_example.json
这里的 request_example.json 就是我们的 JSON 数据文件。我们可以做如下的实验:
下载测试数据:
wget https://github.com/liu-xiao-guo/elasticsearch-bulk-api-data/blob/master/es.json
然后在命令行中打入如下的命令:
curl -u elastic:123456 -s -H "Content-Type: application/x-ndjson" -XPOST localhost:9200/_bulk --data-binary @es.json
这里的 “elastic:123456” 是我们的 Elasticsearch 的用户名及密码,如果我们没有为我们的 Elasticsearch 设置安全,那么可以把 “-u elastic:123456” 整个去掉。正对配置有 https 的 Elasticsearch 服务器,我们可以使用如下格式的命令来进行操作:
curl --cacert /home/elastic/ca.crt -u elastic:123456 -s -H "Content-Type: application/x-ndjson" -XPOST localhost:9200/_bulk --data-binary @es.json
在上面, 我们使用 --cacert /home/elastic/ca.crt 来定义证书的地址。
等我们运行完上面的指令后,我们可以在 Kibana 中查看到我们的叫做 “bank_account” 的索引。
索引统计
Elasticsearch 提供有关进入索引的数据以及提取的数据的详细统计信息。 它提供 API 来生成报告,例如索引包含的文档数、已删除的文档、合并和刷新统计信息等。每个索引都会生成统计信息,例如它拥有的文档总数、已删除文档的计数、分片的内存、获取和搜索请求数据等。_stats API 帮助我们检索索引的统计信息,包括主分片和副本分片。
GET twitter/_stats
响应指示 total 属性,它是与该索引关联的总分片(包括主分片和副本分片)的数量。 由于我们只有一个主分片,所以 successful 属性指向这个分片号。
我们甚至可以同时获得多个索引的统计数据:
GET twitter1,twitter2,twitter3/_stats
我们也可以使用通配符来匹配多个索引:
GET twitter*/_stats
Open/close Index
Elasticsearch 支持索引的在线/离线模式。 使用脱机模式时,在群集上几乎没有任何开销地维护数据。 关闭索引后,将阻止读/写操作。 当你希望索引重新联机时,只需打开它即可。 但是,关闭索引会占用大量磁盘空间。 你可以通过将 cluster.indices.close.enable 的默认值从 true 更改为 false 来禁用关闭索引功能,以避免发生意外。
一旦 twitter 索引被关闭了,那么我们再访问时会出现如下的错误:
我们可以通过 _open 接口来重新打开这个 index:
关于关闭索引有很多用例:
- 它可以禁用基于日期的索引(按日期存储其记录的索引)— 例如,当你将索引保留一周、一个月或一天,并且你希望保留固定数量的旧索引(即 2 个月 旧)在线和一些离线(即从 2 个月到 6 个月)。
- 当你搜索集群的所有 active 索引并且不想搜索某些索引时(在这种情况下,使用 alias 是最好的解决方案,但你可以使用具有关闭索引的 alias 来实现相同的效果)。
Freeze/unfreeze index
冻结索引(freeze index)在群集上几乎没有开销(除了将其元数据保留在内存中),并且是只读的。 只读索引被阻止进行写操作,例如 docs-index 或 force merge。 请参阅冻结索引和取消冻结索引。
冻结索引受到限制,以限制每个节点的内存消耗。 每个节点的并发加载的冻结索引数受 search_throttled 线程池中的线程数限制,默认情况下为1。 默认情况下,即使已明确命名冻结索引,也不会针对冻结索引执行搜索请求。 这是为了防止由于误将冻结的索引作为目标而导致的意外减速。 如果要包含冻结索引做搜索,必须使用查询参数 ignore_throttled = false 来执行搜索请求。
我们可以使用如下的命令来对 twitter 索引来冻结:
POST twitter/_freeze
在执行上面的命令后,我们再对 twitter 进行搜索:
我们搜索不到任何的结果。按照我们上面所说的,我们必须加上 ignore_throttled=false 参数来进行搜索:
显然对于一个 frozen 的索引来说,我们是可以对它进行搜索的。我们可以通过如下的命令来对这个已经冻结的索引来进行解冻:
POST twitter/_unfreeze
一旦我们的索引被成功解冻,那么它就可以像我们正常的索引来进行操作了,而不用添加参数 ignore_throttled=false 来进行访问。
总结
在这篇文章中,我们详细地介绍了如果在 Elasticserch 中创建我们的索引,文档,并对他们进行更改,删除,查询的操作。希望对大家有所帮助。在接下来的文章里,我们将重点介绍如何对 Elasticsearch 里的 index 进行搜索和分析。
如果你想了解更多关于 Elastic Stack 相关的知识,请参阅我们的官方网站:Elastic Stack and Product Documentation | Elastic
下一步
接下来,我们可以学习教程: