摘要:介绍 Elasticsearch 索引创建和文档的增删改查操作。
索引相关
看集群节点
通过 _cat/nodes
,可以列出当前集群下的所有节点,前面在启动一个多节点集群时,我们已经过了使用该 API,不多介绍了。
列出所有索引
_cat/indices?v
能列出集群中的所有索引。说明一下,indices 是 index 的复数形式。
1 | GET _cat/indices |
输出如下:
1 | health status index uuid pri rep docs.count docs.deleted store.size pri.store.size |
我的本地集群经常拿来测试,所以这里会看到已经有很多 index,对于你刚搭建好的集群,输出肯定就是空的。
创建索引
接下来,我们学习下如何创建索引,直接看示例吧。尝试创建一个名为 “customer” 的索引,如下:
1 | PUT customer?pretty |
PUT 加上索引名称即可。 customer 索引成功创建后,查看下集群当前索引列表,如下:
1 | GET _cat/indices |
输出结果如下:
1 | health status index uuid pri rep docs.count docs.deleted store.size pri.store.size |
customer 成功创建,包含 5 个主分片和 1 个副本分片,索引中当前的文档数量为 0,索引状态为 green。
索引的状态和集群一样,也是 green、yellow 和 red 这 3 个值,并且含义与集群状态类似。green 是全功能正常,yellow 表示副本未完全分配,red 表示部分主分片不可用。我们可以测试一下,停止集群中的一个节点,这时 customer 的状态将会马上切换为 yellow 状态。
索引和查询文档
在 elasticsearch 中,”索引” 这个名词常常会搞晕我们。一般讲到索引,主要是指它的名词含义,比如我们说,创建一个 customer 索引。但有时,它又是动词,比如我们在增加和更新文档时,常会说索引一个文档,在这种情况下,可以把它理解为存储文档并使其可搜索。
那如何索引一个文档呢?比如,索引一个 ID 为 1 的 customer 文档到 customer 索引中,如下。
1 | PUT /customer/_doc/1?pretty |
响应返回:
1 | { |
结果表明,我们已经成功创建一个 ID 为 1 的文档。从这里也能看出,在索引一个文档之前,并不要求我们明确创建索引 customer,如果 customer 索引不存在,elasticsearch 将会自动帮助我们创建索引。
检索刚才索引的文档。
1 | GET /customer/_doc/1?pretty |
响应如下:
1 | { |
found 字段表明,我们已经成功检索到了一个 ID 为 1 的文档。_source
中的内容是检索文档的完整内容。
删除索引
让我们删除掉刚刚创建的索引。
1 | DELETE /customer?pretty |
如此,我们就成功删除了之前创建的 customer。如果不确定,可以使用 GET _cat/indices
检查下。
文档 API
回顾下前面介绍的那些 API,我们重新再走一遍。
1 | PUT /customer |
仔细观察上面的命令,你可能会发现一个比较通用的模式,如下:
1 | <HTTP Verb> /<Index>/<Type>/<ID> |
这个模式在 elasticsearch 中非常普遍,记住它,这将对 elasticsearch 学习之旅将会非常有帮助。
更新数据
区别于传统关系型数据库的实时性特点,elasticsearch 是近实时的,也就是说,在文档被 create/update/delete 与搜索结果中出现之间将会有 1 秒的延迟。
文档替换
创建和替换文档本质上都属于索引文档,因而,我们之前用来创建文档的命令同样适用于文档替换。
因为 customer 索引重建过,我们重新创建一个 ID 为 1 的文档。如下:
1 | PUT /customer/_doc/1?pretty |
同样的命令即可实现文档更新,我们只需要传递不同的文档内容即可,如下:
1 | PUT /customer/_doc/1?pretty |
将 ID 为 1 的文档中 name 由 Johh Doe 更新为 Jane Doe。如果指定 ID 文档不存在,将会创建新的文档,否则更新当前文档。
比如,ID 为 2 的文档不存在,通过如下命令创建。
1 | PUT /customer/_doc/2?pretty |
索引文档时,ID 不是必须的,如果不指定,elasticsearch 会自动为这个文档指定一个随机的 ID,并作为响应的一部分返回给你。
示例如下:
1 | POST /customer/_doc?pretty |
这种情况下,我们需要使用 POST 代替 PUT 提交请求。
响应结果:
1 | { |
上面可以看到 elasticsearch 为我们生成的文档 ID,Z-RnpGwBe6KTDC6t3MGV。
更新文档
说完文档的索引和替换,我们再来谈谈文档的更新。提前说明,elasticsearch 并非真的去更新文档,它的更新操作与替换类似,包含删除旧文档和索引新文档两个操作。
示例演示,更新前面创建的 ID 为 1 的文档,更新 name 字段为 Jane Doe。
1 | POST /customer/_doc/1/_update?pretty |
示例 2,更新 name 为 Jane Doe 的同时,增加一个字段 age。
1 | POST /customer/_doc/1/_update?pretty |
示例 3,使用脚本更新文档,比如将 ID 为 1 的文档的字段 age 加 5。
1 | POST /customer/_doc/1/_update?pretty |
上面的例子,ctx._source
表示的是我们将要更新的文档。这里是通过指定 ID 的方式查询要更新的文档,elasticsearch 也可以像 SQL 一样,通过复杂查询实现更新。
删除文档
删除文档最简单直接,HTTP 方法换成 DELETE,指定文档 ID 即可。如下:
1 | DELETE /customer/_doc/2?pretty |
和更新类似,删除也可以根据查询结果执行删除,API 是 _delete_by_query
。如果是删除索引中的所有文档,直接删除索引更直接点。
批处理
经过前面的学习,我们已经了解了 elasticsearch 一些基础 API 的使用,如文档的索引、更新、删除。这一小节介绍一个新的 API,_bulk API
,它支持将多个操作打包成一个请求,实现批处理。这样可以更加高效的执行操作,也能减少网络传递次数。
一个快速入门案例,如下:
1 | POST /customer/_doc/_bulk?pretty |
索引了 2 个文档,一个文档 ID 1,name 为 John Doe,另一个文档 ID 2,name 为 Jane Doe。
再看一个案例,如下:
1 | POST /customer/_doc/_bulk?pretty |
批处理包含 2 个操作,更新 ID 为 1 文档的 name 字段和删除 ID 为 2 的文档。
批处理中的一个操作失败并不会导致整个 bulk API 处理失败,如果一个操作失败,剩下来的其他操作仍会继续执行。bulk API 处理完成后,响应结果中会包含每个操作的处理结果。
数据探索
本节内容主要涉及两个方面:搜索与分析。
数据
–
搜索分析不可缺少数据,我们将使用 elastic 官方提供的数据样本,相对而言,应该比自己的生成更符合真实场景。
一个文档,示例如下:
1 | { |
官方的数据是用工具随机生成的,工具地址。有兴趣,可以设置自己的数据生成规则。
开始数据加载之前,要先下载数据,下载地址。
1 | $ wget https://raw.githubusercontent.com/elastic/elasticsearch/master/docs/src/test/resources/accounts.json |
下载完成后,执行如下命令加载数据:
1 | $ curl -H "Content-Type: application/json" -XPOST "localhost:9200/bank/_doc/_bulk?pretty&refresh" --data-binary "@accounts.json" |
查看索引信息
1 | $ curl "localhost:9200/_cat/indices?v" |
响应如下:
1 | health status index uuid pri rep docs.count docs.deleted store.size pri.store.size |
可以看出,我们已经为 bank 成功索引了 1000 个文档。
搜索 API
开始尝试一些简单的搜索。有两种基本的搜索方式:
- URI Search,通常 URI 参数指定搜索参数。
- Request Body,在请求内容包含在请求体中发送。
相对而言,Request Body 方式更灵活,包含了全部的搜索支持。而 URI Search 主要在测试时使用,比较方便。
搜索请求通过 _search
执行。一个示例,通过搜索返回 bank 索引中的所有文档。
1 | GET /bank/_search?q=*&sort=account_number:asc&pretty |
URI Search 方式实现搜索,通过 q=* 执行匹配全部文档,sort=account_number:asc 指定排序方式。
响应如下:
1 | { |
结果中的各个参数函数如下:
- took 表示搜索执行时间
- timed_out 搜索是否超时
- _shards 多少分片参与搜索,以及成功与失败的情况如何。
- hits 搜索结果
- hits.total 匹配搜索条件的文档数量
- hits.hits.sort 排序
hits.hits._score
和 max_score,相关度得分,指定排序,字段会被忽略。
通过 Reuqest Body 方式执行与上面相同的操作,如下:
1 | GET /bank/_search |
我们下面主要介绍 Request Body 的使用,毕竟它更加强大。
DSL 介绍
DSL,全称 Domain Special Language, 即特定领域语言,elasticsearch 制定了一套 JSON 风格的 DSL 语言。它支持的功能非常全面,刚学习它时,会让我们产生一种恐惧,因为它真的很难。我们可以先从一些简单的例子看起。
查询所有文档,如下:
1 | GET /bank/_search |
解剖下请求体。query 定义查询语句,match_all 就是我们将会执行的查询语句,表示匹配索引中的所有文档。
执行上面的查询语句默认只会返回 10 条文档,我们可以通过指定 size 参数改变默认获取文档数量。如下:
1 | GET /bank/_search |
通过 from 和 size 可以实现分页效果,如下:
1 | GET /bank/_search |
from 用于指定文档偏移的开始位置,相当于 SQL 中的 offset。
文档默认根据搜索相关度得分排序,不过我们这里是默认匹配全部,所以文档的相关度得分都是 1。除了相关度排序,还可以按其他字段,比如 balance 字段。如下:
1 | GET /bank/_search |
搜索查询语句
继续看下搜索返回文档字段,默认情况下,搜索将会返回文档的所有字段内容。我们可通过 _source
指定只返回部分内容。
1 | GET /bank/_search |
如此,搜索将只返回 account_number
和 balance
两个字段。 了解的 SQL 的朋友可以将其与 SELECT 指定列类比。
继续看查询部分吧!
前面,通过 match_all 查询匹配了全部文档。现在,我们再引入一个新的查询语句,match,它是基于一个字段的查询。
查询 account_number 为 20 的文档。示例如下:
1 | GET /bank/_search |
查询 address 包含 mill 的文档。示例如下:
1 | GET /bank/_search |
查询 address 包含 mill 或 lane 的文档。示例如下:
1 | GET /bank/_search |
match 基于分词查询,2 个查询单词是 or 的关系。如果我们就要搜索 “mill lane” 呢? 这时可以使用 match_pharse。示例如下:
查询 address 包含 “mill lane” 的文档。示例如下:
1 | GET /bank/_search |
继续介绍 bool 查询,它允许我们将上面这些基础查询组合形成一个复合查询。比如,查询同时包含 “mill” 和 “jane” 的文档。
示例如下:
1 | GET /bank/_search |
例子中的 bool must 表示文档必须同时满足两个 must 条件。如果是只要满足一个条件即可,我们可以使用 bool should,示例如下:
1 | GET /bank/_search |
如果要求两者必须不满足,我们可以使用 bool must_not,示例如下:
1 | GET /bank/_search |
bool 查询中还可以将上面的这几种查询同时组合起来,同时包含 must、must_not 和 should。
示例如下:
1 | GET /bank/_search |
注意一点,如果是 must、must_not 和 should 组合时,should 就缺少了必须满足一个才算匹配的限制,这时,我们可以通过 minimum_should_match 指定 should 匹配个数。
更复杂的,bool 查询中还可以包含其他的 bool 查询。这里先不介绍了。
过滤语句
谈到过滤之前,我们就不得不提文档相关度评分,相关度评分是用于衡量搜索语句与文档的匹配程度的一个指标。前面已经提过的查询语句都会参与到这个指标的计算。
但有时,查询仅仅是为了过滤一些不满足条件的文档,我们并不希望它们也参与到相关度评分的计算中,由此,我们并引入了 filter。
前面介绍的 bool 查询中便支持 filter 功能。它可以在不影响相关度评分的情况下,实现文档过滤。如下:
1 | GET /bank/_search |
bool 查询中包含了 must 和 filter 两部分。filter 部分通过 range query 实现只查询 blanace 满足指定区间的文档,must 中的 match_all 实现全部文档返回。
除了上面介绍的查询,如 match_all、match、bool、range,elasticsearch 还有很多其它查询可用,这里不详细介绍了。只要掌握了前面的知识,我们已经完全可以照猫画虎。
聚合功能
利用 elasticsearch 的聚合能力,我们可以实现分组统计,可以和 SQL 的 GROUP BY 分组和聚合函数类比。搜索和聚合都是通过 _search
请求实现,同一个请求可同时处理搜索与聚合的请求。这样也可以帮助我们节省必要的网络带宽。
一个例子,按银行卡账号状态(即 state)分组。默认是返回 top 10。如下:
1 | GET bank/_search |
如何 SQL 呢?如下:
1 | SELECT state, COUNT(*) FROM bank GROUP BY state ORDER BY COUNT(*) DESC LIMIT 10; |
响应如下:
1 | { |
从上面可以看出,ID 为 Idaho 的数量为 27,紧接着是 TX 数量 27,然后是 AL 共 25 个。上面设置 size 为 0,是为隐藏搜索结果内容,仅仅显示聚合结果。
我们可以在前面的聚合结果的基础上,计算 top 10 的账户余额平均值。如下:
1 | GET /bank/_search |
在 group_by_state 中加入子聚合 average_balance,同时在 group_by_state 中,通过 order 配置实现了按 balance 平均值大小排序的需求。
下面这个例子演示了如何按年龄区间分组,比如 20-29、30-39 和 40-49,并在基础上,继续按性别分组。最后,计算各个分组 balance 的平均值。
1 | GET /bank/_search |
例子中有两个分组 group_by_age 和 group_by_gener 以及一个聚合计算 average_balance。