Elasticsearch用户指南(二)
本文于338天之前发表,文中内容可能已经过时。
书接上文《Elasticsearch用户指南(一)》,翻译作品,水平有限,如有错误,烦请留言指正。原文请见 官网英文文档
5 探索数据
样本数据集
现在我们已经了解了一些基本知识,让我们研究一下更真实的数据集。我已经准备好了一个虚构的JSON文档示例,它是一个客户的银行账户信息。每个文档都有下面的模式:
{
"account_number": 0,
"balance": 16623,
"firstname": "Bradshaw",
"lastname": "Mckenzie",
"age": 29,
"gender": "F",
"address": "244 Columbus Place",
"employer": "Euron",
"email": "bradshawmckenzie@euron.com",
"city": "Hobucken",
"state": "CO"
}
处于好奇,这个数据我是从这个 www.json-generator.com/ 网站上生成的,因此请忽略其中的数值和语义,这些都是随机生成的。
加载样本数据集
你可以从这里(accounts.json)下载样本数据集,将其解压到当前目录,然后使用下面的命令加载到集群中:
curl -H "Content-Type: application/json" -XPOST 'localhost:9200/bank/account/_bulk?pretty&refresh' --data-binary "@accounts.json"
curl 'localhost:9200/_cat/indices?v'
然后,响应如下:
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
yellow open bank l7sSYV2cQXmu6_4rJWVIww 5 1 1000 0 128.6kb 128.6kb
这就意味着刚才成功地将1000个文档编入bank索引(account 类型下面)。
搜索API
现在让我们开始使用一些简单的搜索,有两个基本的方法执行搜索:一个是通过 REST request URI 发送搜索参数,另一个是通过 REST request body 发送搜索参数;Request body方式更有表现力,允许你使用可读性比较好JSON格式定义你的搜索。我们尝试一个Request URI的方式,但是教程的其他部分我们将只使用Request Body方式。
搜索的REST API可以通_search
端点来访问,下面是获取bank索引下的所有文档的例子:
GET /bank/_search?q=*&sort=account_number:asc&pretty
让我们首先剖析一下这个搜索调用,我们正在搜索bank索引(_search
端点),参数q=*
命令Elasticsearch匹配这个索引下的所有文档,参数sort=account_number:asc
表明使用每个文档的account_number
字段进行升序排序,另外参数pretty
告诉Elasticsearch返回美化后的JSON结果。
然后,响应如下(展示部分):
{
"took" : 63,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"failed" : 0
},
"hits" : {
"total" : 1000,
"max_score" : null,
"hits" : [ {
"_index" : "bank",
"_type" : "account",
"_id" : "0",
"sort": [0],
"_score" : null,
"_source" : {"account_number":0,"balance":16623,"firstname":"Bradshaw","lastname":"Mckenzie","age":29,"gender":"F","address":"244 Columbus Place","employer":"Euron","email":"bradshawmckenzie@euron.com","city":"Hobucken","state":"CO"}
}, {
"_index" : "bank",
"_type" : "account",
"_id" : "1",
"sort": [1],
"_score" : null,
"_source" : {"account_number":1,"balance":39225,"firstname":"Amber","lastname":"Duke","age":32,"gender":"M","address":"880 Holmes Lane","employer":"Pyrami","email":"amberduke@pyrami.com","city":"Brogan","state":"IL"}
}, ...
]
}
}
对于响应,我们看一下下面几个部分:
took
Elasticsearch执行搜索的耗时,以毫秒为单位;timed_out
告诉我们搜索是否超时;_shards
告诉我们多少个分片被搜索到,并且显示搜索成功和失败的分片数;hits
搜索结果;hits.total
匹配搜索标准的文档总数;hits.hits
搜索结果的实际阵列(默认前10个文档);hits.sort
搜索结果中的排序key(如果按照分数排序,将会缺失);hits._score
和max_score
现在忽略这写字段;
下面是一个代替上面全部搜索方法的Request Body方法:
GET /bank/_search
{
"query": { "match_all": {} },
"sort": [
{ "account_number": "asc" }
]
}
与上面的URI中的q=*
相比的不同点是,我们提交了一个JSON格式的查询请求到_search
API。在下一个部分我们将讨论JSON的查询。
要明白一件重要的事,Elasticsearch一旦返回搜索结果,搜索请求就完全结束了,它不会保持任何服务端的资源,也不会维持搜索结果中的游标。这和其他平台是完全不同的,例如SQL平台,开始时可以获取查询结果前面的部分子集,如果你想获取(或者通过分页查询)结果的其余部分,可以通过某种服务端游标继续请求服务器。
介绍查询语言
Elasticsearch提供了JSON格式的领域专用的语言,你可以使用它来执行查询,具体参考 Query DSL。该查询语言是很全面的,乍看一下可能很吓人,实际上学习它的最好的方法是从一些基本的例子开始。
回到我们的最后一个例子,执行这个查询:
GET /bank/_search
{
"query": { "match_all": {} }
}
剖析一下上面的调用请求,query
部分告诉我们查询的定义是什么,简单的说,match_all
是我们想要执行的查询类型,match_all
查询是搜索指定索引的全部文档。
除了query
参数以外,我们还可以通过其它参数来干预搜索结果,在上一部分的例子中我们使用过sort
,这里我们使用一下size
:
GET /bank/_search
{
"query": { "match_all": {} },
"size": 1
}
注意,如果size
不指定的话,默认是10.
下面的例子是一个 match_all
的搜索,然后返回从11到20的文档:
GET /bank/_search
{
"query": { "match_all": {} },
"from": 10,
"size": 10
}
from
参数(0为基础)指定从索引的哪个文档开始,size
参数指定返回从from参数开始的多少个文档。这个特性在实现搜索结果的分页查询时是很有用的。注意,如果from
不指定,默认是0.
下面的例子是一个match_all
的搜索,然后按照账户余额进行降序排列,然后返回前10(默认)个文档。
GET /bank/_search
{
"query": { "match_all": {} },
"sort": { "balance": { "order": "desc" } }
}
搜索操作
现在我们已经看到了一些基本的搜索参数,让我们更深入地挖掘一下Query DSL. 让我们首先看一下返回的文档的字段,默认情况下,作为所有搜索的一部分返回完整的JSON文档,这个被称作资源(在搜索结果中的hits中的_source
字段)。如果我们不想要返回的整个资源文档,我们可以仅请求返回的资源中的部分字段。
下面的例子展示了如何搜索返回两个字段 account_number
和 balance
(_source
内部):
GET /bank/_search
{
"query": { "match_all": {} },
"_source": ["account_number", "balance"]
}
注意,上面的例子仅仅是减少了_source
的字段,它仍然仅返回一个名叫_source
的字段,但是在它只包含account_number
和 balance
两个字段。
如果你有SQL背景,这个在概念上有点类似于SQL SELECT FROM
的字段列表。
现在让我们把注意力转到查询部分,前面我们已经看到match_all
查询是如何匹配所有文档的,现在让我们介绍一个新的查询叫 match
query,可以认为它是基本字段的搜索查询(例如,指定一个字段或者多个字段来完成一次搜索)。
下面的例子是返回账户编号为20的文档:
GET /bank/_search
{
"query": { "match": { "account_number": 20 } }
}
下面的例子是返回地址包含“mill”的所有账户:
GET /bank/_search
{
"query": { "match": { "address": "mill" } }
}
下面的例子是返回地址包含“mill”或者“lane”的所有账户:
GET /bank/_search
{
"query": { "match": { "address": "mill lane" } }
}
下面的例子是match
的一个变形(match_phrase
),它返回地址中包含短语“mill lane”的所有账户:
GET /bank/_search
{
"query": { "match_phrase": { "address": "mill lane" } }
}
现在让我介绍bool(ean) query。bool
查询允许我们使用bool逻辑将小的查询组合成大查询。
下面的例子是组合两个match
查询,然后返回在地址中包含“mill” 和 “lane”的所有账户:
GET /bank/_search
{
"query": {
"bool": {
"must": [
{ "match": { "address": "mill" } },
{ "match": { "address": "lane" } }
]
}
}
}
在上面的例子中,bool must
分句指定所有查询条件必须是“true”才算匹配的文档。
与此相反,下面的例子组合两个match
查询,返回在地址中包含“mill” 或 “lane”的所有账户:
GET /bank/_search
{
"query": {
"bool": {
"should": [
{ "match": { "address": "mill" } },
{ "match": { "address": "lane" } }
]
}
}
}
在上面的例子中,bool should
分句指定一个查询列表,其中必须有一个是“true”的文档才算被匹配。
下面的例子组合两个match
查询,返回在地址中既不包含“mill” 也不包含 “lane”的所有账户:
GET /bank/_search
{
"query": {
"bool": {
"must_not": [
{ "match": { "address": "mill" } },
{ "match": { "address": "lane" } }
]
}
}
}
在上面的例子中,must_not
分句指定一个查询列表,其中必须都不是“true”的文档才算被匹配。
我们可以同时在一个bool
查询中组合must
, should
和 must_not
分句。此外,我们可以在任何bool
分句中组合bool
查询,以便模拟任何复杂的多级bool逻辑。
下面的例子返回年龄是40岁但不住在ID(aho)的所有人的账户:
GET /bank/_search
{
"query": {
"bool": {
"must": [
{ "match": { "age": "40" } }
],
"must_not": [
{ "match": { "state": "ID" } }
]
}
}
}
过滤操作
在前面的部分我们跳过了一个小细节所谓的文档得分(也就是搜索结果中的_score
字段)。得分是数值型的,它是文档和我们指定的搜索查询匹配程度的相对度量。得分越高,文档越相关;得分越低,文档越不相关。
但是查询并不总是需要得分,尤其是它们仅被用作文档集合的过滤器时。Elasticsearch探测到这种情况会自动地优化查询的执行,防止计算无用的得分。
在前面我们介绍的 bool query 也是支持filter
分句的,它允许使用查询来限制其它分句匹配的文档,而不改变得分的计算。作为一个例子,我们来介绍一下range
query ,它允许我们使用一个值域来过滤文档,这通常被用于数值型和日期型的过滤器。
下面的例子使用bool 查询获取余额在20000和30000之间的所有账户,换句话说,我们是想找到余额大于等于20000并且小于等于30000的账户。
GET /bank/_search
{
"query": {
"bool": {
"must": { "match_all": {} },
"filter": {
"range": {
"balance": {
"gte": 20000,
"lte": 30000
}
}
}
}
}
}
仔细分析一下上面的例子,bool查询包括一个match all
查询(查询的一部分)和一个range
查询(过滤的一部分)。我们可以将其它任何查询替换成查询和过滤组成。在上面的例子中,range查询是很有意义的,因为所有在范围内的文档都是相等的,没有哪个文档比另外的文档更相关。
除了 match_all
, match
, bool
, and range
查询之外,还有很多其它类型的查询,我们不会在这里讨论它们。因为我们对它们的工作原理有了基本的理解,将这些知识应用于其它类型的查询,学习和使用它们都不会太难。
聚合操作
聚合提供了对数据进行分组、提取统计结果的能力。想明白聚合的最简单的方法是将其大致等同于SQL分组和SQL的聚合函数。在Elasticsearch中,可以执行搜索获取hits,同时也能在同一个响应中返回区别于hits的聚合结果。从这一点来说,它是很强大和高效的,你可以执行查询和多聚合操作,并且这些操作结果一起返回,使用这样简单的API避免了网络的切换。
首先,下面的例子是将所有账户按照州来分组,然后返回按照数量的降序排列的前10个州(默认):
GET /bank/_search
{
"size": 0,
"aggs": {
"group_by_state": {
"terms": {
"field": "state.keyword"
}
}
}
}
在SQL中,上面的聚合在概念上类似于:
SELECT state, COUNT(*) FROM bank GROUP BY state ORDER BY COUNT(*) DESC
然后,响应如下(部分展示):
{
"took": 29,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"failed": 0
},
"hits" : {
"total" : 1000,
"max_score" : 0.0,
"hits" : [ ]
},
"aggregations" : {
"group_by_state" : {
"doc_count_error_upper_bound": 20,
"sum_other_doc_count": 770,
"buckets" : [ {
"key" : "ID",
"doc_count" : 27
}, {
"key" : "TX",
"doc_count" : 27
}, {
"key" : "AL",
"doc_count" : 25
}, {
"key" : "MD",
"doc_count" : 25
}, {
"key" : "TN",
"doc_count" : 23
}, {
"key" : "MA",
"doc_count" : 21
}, {
"key" : "NC",
"doc_count" : 21
}, {
"key" : "ND",
"doc_count" : 21
}, {
"key" : "ME",
"doc_count" : 20
}, {
"key" : "MO",
"doc_count" : 20
} ]
}
}
}
我们可以看出在ID
(Idaho)州由27个账户,在TX
(Texas)州有个27个账户,在AL
(Alabama)州有25个账户,等等。
注意上面我们设置size=0
是为了不展示搜索结果,因为我们仅想在响应中看到聚合结果。
在前面的聚合操作的基础上,下面的例子计算了每个州的账户余额平均值(同样仅展示账户总数降序排列的前10个州):
GET /bank/_search
{
"size": 0,
"aggs": {
"group_by_state": {
"terms": {
"field": "state.keyword"
},
"aggs": {
"average_balance": {
"avg": {
"field": "balance"
}
}
}
}
}
}
请注意我们是如何将average_balance
聚合嵌套在group_by_state
聚合中的,这是所有聚合中常见的模式,为了从数据中提取总结你需要的信息,可以嵌套聚合到任意其它的聚合之中。
在前面的聚合的基础上,现在让我们按照余额的平均值进行降序排列:
GET /bank/_search
{
"size": 0,
"aggs": {
"group_by_state": {
"terms": {
"field": "state.keyword",
"order": {
"average_balance": "desc"
}
},
"aggs": {
"average_balance": {
"avg": {
"field": "balance"
}
}
}
}
}
}
下面的例子演示了如何通过年龄组( 20-29, 30-39, 和 40-49)进行分组,然后按照性别分组,最后获得在每个年龄组每个性别中账户余额的平均值:
GET /bank/_search
{
"size": 0,
"aggs": {
"group_by_age": {
"range": {
"field": "age",
"ranges": [
{
"from": 20,
"to": 30
},
{
"from": 30,
"to": 40
},
{
"from": 40,
"to": 50
}
]
},
"aggs": {
"group_by_gender": {
"terms": {
"field": "gender.keyword"
},
"aggs": {
"average_balance": {
"avg": {
"field": "balance"
}
}
}
}
}
}
}
}
还有很多其它的聚合功能,这里我们就不详细一一介绍了,如果你想进一步学习聚合的操作, aggregations reference guide 是一个不错的参考文档。
6 结论
Elasticsearch是一个既简单又复杂的产品。到目前为止,我们已经了解了它的基本原理,如何查看它的内部,如何使用一些REST API操作它。我希望这个教程让你更好地理解了Elasticsearch是什么,更重要是,激发你去进一步探索Elasticsearch的其余特性。
Elasticsearch的起步教程终于翻译完了,这个翻译文档只是入门级的介绍Elasticsearch,希望能对你的学习有所帮助。