5、Exploring Your Data(探索数据)
Sample Dataset(样本数据集)
现在我们已经学会了基础知识,让我们尝试在更真实的数据集上操作。我准备了一份顾客银行账户信息的虚构的 JSON 文档样本。每个文档都有以下的schema(模式):
{
"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/ 生成的这份数据,因为这些都是随机生成的,所以请忽略实际的值和数据的语义。
Loading the Sample Dataset(下载样本数据集)
您可以从这里下载这份样本数据集(accouts.json)。提取它到当前的目录,然后加载到集群中,如下所示 :
curl -H "Content-Type: application/json" -XPOST "localhost:9200/bank/_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 l7sSYV2cQX 4 5 1 1000 0 128.6kb 128.6kb
这意味着我们刚才成功的批量索引了 1000 份文档到 bank
索引(_doc类型下)。
5.1 The Search API(搜索 API)
现在让我们从一些简单的搜索开始。这里两个运行搜索的基本方法 : 一个是通过使用 REST请求URI 发送搜索参数,另一个是通过使用 REST请求体 来发送搜索参数。请求体的方法可以让您更具有表现力,并且可以在一个更可读的 JSON 格式中定义您的搜索。我们会尝试使用一个请求URI 的示例,但是在本教程的其它部分,我们将只使用请求体的方式。
搜索的 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,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 1000,
"max_score" : null,
"hits" : [ {
"_index" : "bank",
"_type" : "_doc",
"_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" : "_doc",
"_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 执行查询的时间,单位是毫秒 -
time_out
: 查询是否超时 -
_shards
:查询了多少个分片,其中有几个成功的,几个失败的 -
hits
: 查询结果 -
hits.total
: 总共有多少个文档满足查询条件-
hits.total.value
:总命中数的值(必须在hits.total.relation
的上下文中解释) -
hits.total.relation
:hits.total.value
是否是确切的命中数,在这种情况下它等于“eq”
。或总命中数的下限(大于或等于),在这种情况下它等于“gte
。
-
-
hits.hits
: 实际的查询结果(数组形式表示,默认为前 10 个文档) -
sort
: 查询结果的排序字段(没有则按 score 排序) -
score
和max_score
:查询的得分,最高得分(现在暂时忽略这些字段)
hits.total
的准确性由请求参数track_total_hits
控制,当设置为true时,请求将准确跟踪总命中(“关系”:“eq”
)。它默认为10,000,这意味着总命中数可以精确跟踪多达10,000个文档。您可以通过将track_total_hits
显式设置为true来强制进行准确计数。有关详细信息,请参阅[request body]文档。
下例是对上面的搜索使用request 请求体方式进行的相同搜索: :
GET /bank/_search
{
"query":{"match_all":{}},
"sort":[
{"account_number":"asc"}
]
}
这里的不同点就是用一个JSON风格的request请求体代替了q=*
。我们将在下一部分讨论这个 JSON 查询。
一旦您搜索的结果被返回,Elasticsearch 便完成了这次请求,并且不会维护任何服务端的资源或者 cursor(游标)结果。这与其它的平台形成了鲜明的对比。例如在SQL中,您可以首先获得查询结果的子集,如果您想要使用一些服务端有状态的 cursor(光标)来抓取(或者通过分页)其它的结果,您必须再次回到服务器。
5.2 Introducing the Query Language(介绍查询语言)
Elasticsearch 提供了一个可以执行查询的 Json 风格的语句。这个被称为 Query DSL(domain-specific language 领域特定语言)。该查询语言非常全面,可能刚开始学的时候会感觉有点复杂,学习它的最好方式是从一些基础的示例开始。
回到上个例子,我们执行了这个查询 :
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
并且返回第10~19 的文档。
GET /bank/_search
{
"query":{"match":{}},
"from": 10,
"size": 10
}
from 参数指定的是从第几条记录开始返回,其默认值为0,size参数指定的是返回结果的条数。在实现分页搜索时这个功能是很有用的。
下面的例子做了一个 match_all
,结果按balance
降序排序且返回了前10(默认大小)个文档。
GET /bank/_search
{
"query":{"match_all":{}},
"sort":{"balance":{"order":"desc"}}
}
5.3 Executing Searches(执行查询)
现在我们已经了解了基本的搜索参数,让我们深入探讨更多的 DSL。我们首先看一下返回的文档字段。默认情况下,完整的 JSON 文档会被作为所有搜索的一部分返回。这被称为“source”(hits
中的_source
字段)。如果我们不希望返回整个 source 文档,我们可以只请求返回 source 中的一些字段。
下例演示了如何从搜索中返回account_number
和 balance
(在 _source 之内)字段,如下所示 :
GET /bank/_search
{
"query":{"match_all":{}},
"_source":["account_number","balance"]
}
注意,上面的的响应中将只有一个_source
字段,并且里面只有account_number
和balance
两个字段。如果你会SQL语句,那么这个请求就类似于SQL中的SELECT FROM
字段列表
现在让我们继续关注query部分。此前,我们了解了 match_all
是如何查询所有文档的。我们现在介绍一个名为 match query 的新查询,它可以被视为基本的字段化搜索查询(例如,针对一个指定字段或一组字段来搜索)。
下面例子返回了account_number
为 20 的文档 :
GET /bank/_search
{
"query":{"match":{"account_number":20}}
}
下面例子返回了在address
中包含mill
的账户 :
GET /bank/_search
{
"quer":{"match":{"address":"mill"}}
}
下面例子返回了在address
中包含了 mill
或 lane
的账户 :
GET /bank/_search
{
"query":{"match":{"address":"mill lane"}}
}
下面例子是math(match_phrase
)的另一种方式,它返回了在 address
中所有包含mill lane
的账户 :
GET /bank/_search
{
"query":{"match_phrase":{"address":"mill lane"}}
}
现在我们介绍 bool query。bool查询允许我们组合使用布尔逻辑。
下面例子构建两个 match查询,并且返回了在 address
中包含了mill
和 lane
的账户 :
GET /bank/_search
{
"query": {
"bool": {
"must": [
{ "match": { "address": "mill" } },
{ "match": { "address": "lane" } }
]
}
}
}
这个例子中要求must大括号里面的查询条件必须都为真,才认为满足查询条件。
相反,下面这个例子用should组装两个match条件,返回的结果将是address
字段里面包含mill
或是包含lane
的记录:
GET /bank/_search
{
"query":{
"bool":{
"should":{
{"match":{"address":"mill"}},
{"match":{"address":"lane"}}
}
}
}
}
下面例子构建两个match
查询,并且回了在 address 中既不包含 “mill” 也不包含 “lane” 的账户 :
GET /bank/_search
{
"query":{
"bool":{
"must_not":[
{"match":{"address","mill"}},
{"match":{"address","lane"}}
]
}
}
}
这个例子中要求must_not大括号里面的查询条件都为false。
我们可以在 bool 查询中同时联合 must
,should
和 must_not
语句。此外,我们可以在任何一个bool
语句内部构建bool
查询,从而模拟出任何复杂的多级别的 boolean 逻辑。
下面的例子返回了age
为40
但是 state
不为ID
的账户 :
GET /bank/_search
{
"query":{
"bool":{
"must":[
{"match":{"age":40}}
],
"must_not":[
{"match":{"state":"ID"}}
]
}
}
}
5.4 Executing Filters(执行过滤)
在上一节中,我们跳过了一些名为score(在搜索结果中的 _score
字段)的细节。score是一个数值,它是文档与我们指定的搜索查询匹配程度的相对度量。分数越高,文档的相关度更高,分数越低,文档的相关度越低。
并不是所有的查询都需要产生分数,特别是当它们只用于filtering文档集时。ElasticSearch检测到这些情况,并自动优化查询执行,以避免计算无用的分数。
在上一节中我们介绍的 bool
查询也支持 filter
子句,这些子句允许我们使用查询来限制将与其他子句匹配的文档,而不会更改计算得分的方式。举个例子,让我们介绍下range
查询,它可以让我们通过一系列的值过滤文档。这通常用于数字或者日期过滤。
下面的例子使用了一个 bool 查询来返回balances
在20000 ~ 30000
之间的账户(包含 20000 和 30000)。换言之,我们想要去找出balances
大于或等于 20000 且小于或等于 30000 的账户。
GET /bank/_search
{
"query":{
"bool":{
"must":{"match_all":{}},
"filter:{
"range":{
"balance":{
"gte":20000,
"lte":30000
}
}
}
}
}
}
分析上面的结构,bool 查询包含了一个 match_all 查询
(query部分),和一个 range
(范围)查询(filter部分)。我们可以将任何其他查询替换为query和filter部分。在上述情况下,range范围内的文档相当于匹配,即没有文档比范围内的文档更相关。
除了 match_all
,match
,bool
和range
查询之外 ,还有很多在这里我们我没有用到的其它有用的查询类型。既然我们对于它们是如何工作的方式有了一个基本的了解,在学习和尝试其它的查询类型中应用这些知识应该不会太困难。
5.5 Executing Aggregations(执行聚合)
aggregations提供了从数据中分组和统计数据的能力。最简单的聚合方法大致等同于SQL的group by 和SQL的聚合函数。在elasticsearch中,您可以同时执行查询和聚合的结果。这是非常强大且有效的,您可以执行查询和多个聚合,并且在一次请求中得到各自的(任何一个的)返回结果,避免频繁的网络请求。
首先,下面的例子是对所有account
按照state
进行分组,然后返回按计数降序排序的前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 LIMIT 10;
响应如下(部分显示):
{
"took": 29,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped" : 0,
"failed": 0
},
"hits" : {
"total" : {
"value": 1000,
"relation": "eq"
},
"max_score" : null,
"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 个 account(账户),在 TX(Texas)有 27 个 account(账户),在 AL(Alabama)有25 个账户等等。
注意我们设置了size=0
以不显示搜索结果,因为我们只希望在响应中看聚合结果。
基于前面的聚合,下面的例子按state
计算了平均的账户balance
(同样,仅针对按计数降序排序的前10个状态 ):
GET /bank/_search
{
"size":0,
"aggs":{
"group_by_state":{
"terms":{
"fields":"state.keyword"
},
"aggs":{
"average_balanece":{
"avg":{
"field":"balance"
}
}
}
}
}
}
请注意,我们如何将average_balance
聚合嵌套在group_by_state
内。这是所有聚合的通用模式。可以在聚合中任意嵌套聚合,以从数据中提取所需的统计信息。
基于前面的聚合,现在我们按average_balance
降序排序 :
GET /bank/_search
{
"size":0,
"aggs":{
"group_by_state":{
"terms":{
"field":"state.keyword",
"order":{
"average_balance":"desc"
}
},
"aggs":{
"average_balance":{
"avg":{
"field":"balance"
}
}
}
}
}
}
下面的例子演示了我们如何按age
(20-29岁,30-39岁,和 40-49岁)分组,然后按gender
分组,最后获得每个年龄段每个性别的平均账户余额 :
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"
}
}
}
}
}
}
}
}
还有一些我们这里没有细讲的其它的聚合功能。如果您想要做进一步的实验,聚合参考指南是一个比较好的起点。
6、Conclusion(总结)
Elasticsearch 既是一个简单,又是一个复杂的产品。我们现在学会了它的基础部分,如何去看它内部,以及如何使用一些 REST API 来操作它。我希望本教程可以让您更好的了解 Elasticsearch 是什么,更重要的是,可以促使你进一步尝试它更强大的功能。