ElasticSearch入门 官方文档翻译 - 4.Exploring Your Data

探索你的数据

样本数据集

现在我们对于基本的东西已经有了一些感觉,现在让我们尝试使用一些更加贴近现实的数据集。我已经准备了一些假想的客户的银行账户信息的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"
}

你可以从这里下载这个样本数据集。将其解压到当前目录下,如下,将其加载到我们的集群里:

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个文档到银行索引中(account类型)。

搜索API

现在,让我们以一些简单的搜索来开始。有两种基本的方式来运行搜索:一种是在REST请求的URI中发送搜索参数,另一种是将搜索参数发送到REST请求体中。请求体方法的表达能力更好,并且你可以使用更加可读的JSON格式来定义搜索。我们将尝试使用一次请求URI作为例子,但是教程的后面部分,我们将仅仅使用请求体方法。

搜索的REST API可以通过_search端点来访问。下面这个例子返回bank索引中的所有的文档:

curl -XGET 'localhost:9200/bank/_search?q=*&sort=account_number:asc&pretty&pretty'

我们仔细研究一下这个查询调用。我们在bank索引中搜索(_search端点),并且q=*参数指示ES去匹配这个索引中所有的文档。sort=account_number:asc参数指示使用account_number升序来排序每个文档。pretty参数,和以前一样,仅仅是告诉ES返回美观的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" : "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 —— ES执行这个搜索的耗时,以毫秒为单位
  • timed_out —— 指明这个搜索是否超时
  • _shards —— 指出多少个分片被搜索了,同时也指出了成功/失败的被搜索的shards的数量
  • hits —— 搜索结果
  • hits.total —— 能够匹配我们查询标准的文档的总数目
  • hits.hits —— 真正的搜索结果数据(默认只显示前10个文档)
  • hits.sort —— 结果的排序键(如果根据score排序就没有)
  • _scoremax_score —— 现在先忽略这些字段

使用请求体方法的等价搜索是:

curl -XGET 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d'
{
  "query": { "match_all": {} },
  "sort": [
    { "account_number": "asc" }
  ]
}
'

这里的不同之处在于,并不是向URI中传递q=*,取而代之的是,我们在_search API的请求体中POST了一个JSON格式请求体。我们将在下一部分中讨论这个JSON查询。

有一点需要重点理解一下,一旦你取回了你的搜索结果,ES就完成了使命,它不会维护任何服务器端的资源或者在你的结果中打开游标。这是和其它类似SQL的平台的一个鲜明的对比, 在那些平台上,你可以在前面先获取你查询结果的一部分,然后如果你想获取结果的剩余部分,你必须继续返回服务端去取,这个过程使用一种有状态的服务器端游标技术。

介绍查询语言

Elasticsearch提供一种JSON风格的特定领域语言,利用它你可以执行查询。这被称为Query DSL。这个查询语言相当全面,第一眼看上去可能有些咄咄逼人,但是最好的学习方法就是以几个基础的例子来开始。

回到我们上一个例子,我们执行了这个查询:

curl -XGET 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d'
{
  "query": { "match_all": {} }
}
'

分解以上的这个查询,其中的query部分告诉我查询的定义,match_all部分就是我们想要运行的查询的类型。match_all查询,就是简单地查询一个指定索引下的所有的文档。

不只是query参数,我们也传递了另一个影响查询结果的参数。在上面的例子中,我们传递了sort,这里我们传递size

curl -XGET 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d'
{
  "query": { "match_all": {} },
  "size": 1
}
'

注意,如果没有指定size的值,那么它默认就是10。

下面的例子,做了一次match_all并且返回第11到第20个文档:

curl -XGET 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d'
{
  "query": { "match_all": {} },
  "from": 10,
  "size": 10
}
'

其中的from参数(0-based)从哪个文档开始,size参数指明从from参数开始,要返回多少个文档。这个特性对于搜索结果分页来说非常有帮助。注意,如果不指定from的值,它默认就是0。

下面这个例子做了一次match_all并且以账户余额降序排序,最后返前十个文档:

curl -XGET 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d'
{
  "query": { "match_all": {} },
  "sort": { "balance": { "order": "desc" } }
}
'

执行搜索

现在我们已经知道了几个基本的参数,让我们进一步发掘查询语言吧。首先我们看一下返回文档的字段。默认情况下,是返回完整的JSON文档的。这可以通过source来引用(搜索hits中的_sourcei字段)。如果我们不想返回完整的源文档,我们可以指定返回的几个字段。

下面这个例子说明了怎样返回两个字段account_numberbalance(当然,这两个字段都是指_source中的字段),以下是具体的搜索:

curl -XGET 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d'
{
  "query": { "match_all": {} },
  "_source": ["account_number", "balance"]
}
'

注意到上面的例子仅仅是简化了_source字段。它仍将会返回一个叫做_source的字段,但是仅仅包含account_numberbalance两个字段。

如果你有SQL背景,上述查询在概念上有些像SQL的SELECT FROM。

现在让我们进入到查询部分。之前,我们看到了match_all查询是怎样匹配到所有的文档的。现在我们介绍一种新的查询,叫做match query,这可以看成是一个简单的字段搜索查询(比如对应于某个或某些特定字段的搜索)。

下面这个例子返回账户编号为20的文档:

curl -XGET 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d'
{
  "query": { "match": { "account_number": 20 } }
}
'

下面这个例子返回地址中包含mill的所有账户:

curl -XGET 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d'
{
  "query": { "match": { "address": "mill" } }
}
'

下面这个例子返回地址中包含mill或者包含lane的账户:

curl -XGET 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d'
{
  "query": { "match": { "address": "mill lane" } }
}
'

下面这个例子是match的变体(match_phrase),它会去匹配短语mill lane

curl -XGET 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d'
{
  "query": { "match_phrase": { "address": "mill lane" } }
}
'

现在,让我们介绍一下bool query。布尔查询允许我们利用布尔逻辑将较小的查询组合成较大的查询。

现在这个例子组合了两个match查询,这个组合查询返回包含milllane的所有的账户:

 curl -XGET 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d'
{
  "query": {
    "bool": {
      "must": [
        { "match": { "address": "mill" } },
        { "match": { "address": "lane" } }
      ]
    }
  }
}
'

在上面的例子中,bool must语句指明了,对于一个文档,所有的查询都必须为真,这个文档才能够匹配成功。

相反的,下面的例子组合了两个match查询,它返回的是地址中包含mill或者lane的所有的账户:

 curl -XGET 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d'
{
  "query": {
    "bool": {
      "should": [
        { "match": { "address": "mill" } },
        { "match": { "address": "lane" } }
      ]
    }
  }
}
'

在上面的例子中,bool should语句指明,对于一个文档,查询列表中,只要有一个查询匹配,那么这个文档就被看成是匹配的。

现在这个例子组合了两个查询,它返回地址中既不包含mill,同时也不包含lane的所有的账户信息:

curl -XGET 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d'
{
  "query": {
    "bool": {
      "must_not": [
        { "match": { "address": "mill" } },
        { "match": { "address": "lane" } }
      ]
    }
  }
}
'

在上面的例子中, bool must_not语句指明,对于一个文档,查询列表中的的所有查询都必须都不为真,这个文档才被认为是匹配的。

我们可以在一个bool查询里一起使用mustshouldmust_not。此外,我们可以将bool查询放到这样的bool语句中来模拟复杂的、多等级的布尔逻辑。

下面这个例子返回40岁以上并且不生活在ID(daho)的人的账户:

curl -XGET 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d'
{
  "query": {
    "bool": {
      "must": [
        { "match": { "age": "40" } }
      ],
      "must_not": [
        { "match": { "state": "ID" } }
      ]
    }
  }
}
'

执行过滤器

在先前的章节中,我们跳过了文档得分的细节(搜索结果中的_score字段)。这个得分是与我们指定的搜索查询匹配程度的一个相对度量。得分越高,文档越相关,得分越低文档的相关度越低。

但不是所有的查询都需要产生分数,特别是查询条件仅仅用于“过滤”文档集时。ES检测到这些情况时会自动优化查询执行,避免计算多余的分数。

我们前面介绍的bool query也支持允许使用查询条件去约束被其他语句匹配的文档的filter语句,而filter语句不会改变分数的计算方式。举个栗子,让我们来介绍下允许使用范围值来过滤的range query。它通常用来过滤数值和日期。

这个例子用一个布尔查询来返回所有余额在20000和30000之间的账户。换句话说,我们要找到所有余额大于等于20000且小于等于30000的账户。

curl -XGET 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d'
{
  "query": {
    "bool": {
      "must": { "match_all": {} },
      "filter": {
        "range": {
          "balance": {
            "gte": 20000,
            "lte": 30000
          }
        }
      }
    }
  }
}
'

分解上面的例子,被过滤的查询包含一个match_all查询(查询部分)和一个过滤器(filter部分)。我们可以在查询部分中放入其他查询,在filter部分放入其它过滤器。在上面的应用场景中,由于所有的在这个范围之内的文档都是平等的(或者说相关度都是一样的),没有一个文档比另一个文档更相关,所以这个时候使用范围过滤器就非常合适了。

除了match_all, match, bool, filteredrange查询,还有很多可用的查询类型,我们这里不会涉及。由于我们已经对它们的工作原理有了基本的理解,将其应用到其它类型的查询、过滤器上也不是件难事。

执行聚合

聚合提供了分组并统计数据的能力。理解聚合的最简单的方式是将其粗略地等同为SQL的GROUP BY和SQL聚合函数。在ES中,你可以在一个响应中同时返回命中的数据和聚合结果。你可以使用简单的API同时运行查询和多个聚合,并以一次返回,这避免了来回的网络通信,这是非常强大和高效的。

作为开始的一个例子,我们按照state分组,按照州名的计数倒序排序:

curl -XGET 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d'
{
  "size": 0,
  "aggs": {
    "group_by_state": {
      "terms": {
        "field": "state.keyword"
      }
    }
  }
}
'

在SQL中,上面的聚合在概念上类似于:

SELECT COUNT(*) from bank GROUP BY state ORDER BY COUNT(*) DESC

响应(其中一部分)是:

{
  "took": 29,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped" : 0,
    "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
      } ]
    }
  }
}

我们可以看到AL(abama)有21个账户,TX有17个账户,ID(daho)有15个账户,依此类推。

注意我们将size设置成0,这样我们就可以只看到聚合结果了,而不会显示命中的结果。

在先前聚合的基础上,现在这个例子计算了每个州的账户的平均余额(还是按照账户数量倒序排序的前10个州):

curl -XGET 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d'
{
  "size": 0,
  "aggs": {
    "group_by_state": {
      "terms": {
        "field": "state.keyword"
      },
      "aggs": {
        "average_balance": {
          "avg": {
            "field": "balance"
          }
        }
      }
    }
  }
}
'

注意,我们把average_balance聚合嵌套在了group_by_state聚合之中。这是所有聚合的一个常用模式。你可以任意的聚合之中嵌套聚合,这样你就可以从你的数据中抽取出想要的概述。

基于前面的聚合,现在让我们按照平均余额进行排序:

curl -XGET 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d'
{
  "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)分组,然后在用性别分组,然后为每一个年龄段的每一个性别计算平均账户余额:

curl -XGET 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d'
{
  "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是一个非常好的起点。

总结

ES既是一个简单的产品,也是一个复杂的产品。我们现在已经学习到了基础部分,它的一些原理,以及怎样用REST API来做一些工作。我希望这个教程已经使你对ES是什么有了一个更好的理解,跟重要的是,能够激发你继续实验ES的其它特性。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容