ElasticSearch入门教程(3)—探索ES数据

数据集样本

现在,我们已经了解了ElasticSearch的基础知识,接下来我们将会研究一个更加具有现实意义的数据集。我已经准备了一个虚拟的客户银行账户信息的JSON文档示例。每个文档都具有以下模式:

  1. {
  2. "account_number": 0,
  3. "balance": 16623,
  4. "firstname": "Bradshaw",
  5. "lastname": "Mckenzie",
  6. "age": 29,
  7. "gender": "F",
  8. "address": "244 Columbus Place",
  9. "employer": "Euron",
  10. "email": "bradshawmckenzie@euron.com",
  11. "city": "Hobucken",
  12. "state": "CO"
  13. }

为了方便起见,这些数据是通过www.json-generator.com/生成的。因此,请忽略这些数据的实际值和语义,它们全都是随机生成的。

加载数据集样本

你可以从此处下载数据集样本。将其解压至我们的当前目录,然后将这些数据加载至我们的集群之中,如下所示:

  1. curl -H "Content-Type: application/json" -XPOST 'localhost:9200/bank/account/_bulk?pretty&refresh' --data-binary "@accounts.json"
  2. curl 'localhost:9200/_cat/indices?v'

集群的响应如下图所示:

bank索引的文档列表

上述的响应信息表示我们刚刚成功地将1000个文档批量索引至bank索引之中(归属于account类型)。

搜索API

现在,我们可以从一些简单的搜索示例开始。有两种运行搜索示例的基本方法:一种是通过REST请求的URI来发送搜索参数,另一种是通过REST请求体来发送搜索参数。使用请求体的方法使得你能够更加清晰地表达搜索请求,你还能够以一种更具可读性的JSON格式来定义搜索请求。我们将通过以下示例来尝试使用请求URI的方法,但是在本文的其余部分中,我们将专门使用请求体的方法。

你可以通过_search端点来访问用于搜索的REST API命令。以下示例会返回bank索引中的所有文档:

  1. GET /bank/_search?q=*&sort=account_number:asc&pretty

首先,我们需要仔细分析搜索请求的调用。我们在bank索引中进行搜索(通过_search端点),q=*参数会告诉ElasticSearch匹配索引中的所有文档。sort=account_number:asc参数会使用每个文档的account_number字段,对搜索结果进行升序排序。再一次地,pretty参数会告诉ElasticSearch以一种良好的JSON格式输出搜索结果。

集群的响应如下图所示(只显示一部分结果):

搜索bank索引的所有文档

对于上述的响应信息,我们可以看到以下几个部分:

  • took — ElasticSearch执行搜索操作耗费的时间,以毫秒为单位。
  • timed_out — 表示搜索操作是否超时
  • _shards — 表示搜索的分片数量,以及搜索成功/失败的分片数量。
  • hits — 搜索结果
  • hits.total — 匹配搜索条件的文档总数
  • hits.hits — 搜索结果的实际数组(默认为前10个文档)
  • hits.sort — 用于结果排序的关键字(如果通过得分来排序,则不会有这项信息)
  • hits._scoremax_score — 暂时忽略这些字段

以下示例会使用请求体方法进行搜索,运行结果和上述示例完全相同:

  1. GET /bank/_search
  2. {
  3. "query": { "match_all": {} },
  4. "sort": [
  5. { "account_number": "asc" }
  6. ]
  7. }

此处的区别是,我们向名为_search的API提交了一个JSON风格的查询请求体,而不是在URI中传递q=*参数。我们将在下一节中讨论这个JSON查询。

一旦ElasticSearch向你返回了搜索结果,这就表示搜索请求已经处理完成了,ElasticSearch不会维护任何类型的服务端资源,也不会在搜索结果中打开游标,理解这一点非常重要。这种行为和许多其他的平台(如SQL)形成了鲜明的对比,在这些平台中,你可能会首先获得查询结果的一部分子集,然后如果你想要使用某种有状态的服务端游标来获取(或翻页浏览)剩余的结果,那么你必须不断地返回服务端。

查询语言简介

ElasticSearch提供了一种JSON风格的领域特定语言(DSL),你可以使用它执行查询操作。这种语言称为查询DSL。查询语言是非常全面的,咋看起来可能令人望而生畏,但是学习它的最好的方法实际上需要从几个基本示例开始。

以下示例会搜索bank索引中的所有文档:

  1. GET /bank/_search
  2. {
  3. "query": { "match_all": {} }
  4. }

仔细分析上述示例,query部分表示查询的具体定义,match_all部分表示我们想要运行的查询类型。match_all查询会在指定的索引中简单地搜索所有的文档。

除了query参数之外,我们还可以传递其他参数,这些参数可能会影响搜索结果。在上一节的示例中,我们传递sort参数,此处我们传递size参数:

  1. GET /bank/_search
  2. {
  3. "query": { "match_all": {} },
  4. "size": 1
  5. }

注意,如果没有指定size参数,那么默认值是10。

以下示例会执行一次match_all查询,然后返回第11个至第20个文档。

  1. GET /bank/_search
  2. {
  3. "query": { "match_all": {} },
  4. "from": 10,
  5. "size": 10
  6. }

from参数指定从哪个文档下标(首个下标为0)开始返回结果;size参数指定从from参数指定的位置,返回多少个结果。此功能在实现搜索结果分页查询时非常有用。注意,如果没有指定from参数,那么默认值是0。

以下示例会执行一次match_all查询,然后根据账号的balance字段进行降序排序,最后返回前10个(默认大小)结果文档。

  1. GET /bank/_search
  2. {
  3. "query": { "match_all": {} },
  4. "sort": { "balance": { "order": "desc" } }
  5. }

执行搜索命令

现在,我们已经了解了一些基本的搜索参数,我们接下来将会进一步深入学习查询DSL。首先,我们要看看返回的文档字段。在默认情况下,完整的JSON文档会作为所有搜索的一部分结果返回。这些结果被称为源文档(也就是搜索结果中的_source字段)。如果我们不希望返回整个文档,那么我们可以只请求返回的源文档内的一些指定的字段。

以下示例演示如何返回两个字段(_source中的account_numberbalance字段)作为搜索结果:

  1. GET /bank/_search
  2. {
  3. "query": { "match_all": {} },
  4. "_source": ["account_number", "balance"]
  5. }

注意,上述示例只是简单地减少了_source字段的内容。它仍然会返回一个名为_source的字段,但是这个字段只包含account_numberbalance字段。

如果你具有SQL的背景知识,那么上述示例在概念上有点类似于SQL的SELECT FROM字段列表。

现在,我们应该关注查询部分了。此前,我们已经知道如何使用match_all查询来匹配所有的文档。现在,我们将要介绍一种新的查询方式,叫做<a href="https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-match-query.html" title="match“>match查询,它是一种基本的按字节搜索和查询的方法(例如,对指定的一个字段或字段集合进行搜索)。

以下示例会返回账号等于20的文档:

  1. GET /bank/_search
  2. {
  3. "query": { "match": { "account_number": 20 } }
  4. }

以下示例会返回在地址中包含词语“mill”的所有文档:

  1. GET /bank/_search
  2. {
  3. "query": { "match": { "address": "mill" } }
  4. }

以下示例会返回在地址中包含词语“mill”或“lane”的所有文档:

  1. GET /bank/_search
  2. {
  3. "query": { "match": { "address": "mill lane" } }
  4. }

以下示例是matchmatch_phrase)查询的一个变体,它会返回在地址中包含短语“mill lane”的所有账号:

  1. GET /bank/_search
  2. {
  3. "query": { "match_phrase": { "address": "mill lane" } }
  4. }

现在,我们将介绍<a href="https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-bool-query.html" title="bool“>bool查询。bool查询使得我们能够使用布尔逻辑,将较小的查询组合为较大的查询。

以下示例会组合两个match查询,然后返回在地址中包含“mill”和“lane”的所有账号:

  1. GET /bank/_search
  2. {
  3. "query": {
  4. "bool": {
  5. "must": [
  6. { "match": { "address": "mill" } },
  7. { "match": { "address": "lane" } }
  8. ]
  9. }
  10. }
  11. }

在上述示例中,bool must子句指定了所有结果必须为真的查询,文档必须同时匹配这两个条件才能作为查询结果。

相反,以下示例也会组合两个match查询,但是返回在地址中包含“mill”或“lane”的所有账号:

  1. GET /bank/_search
  2. {
  3. "query": {
  4. "bool": {
  5. "should": [
  6. { "match": { "address": "mill" } },
  7. { "match": { "address": "lane" } }
  8. ]
  9. }
  10. }
  11. }

在上述示例中,bool should子句指定了结果为真的查询列表,只要文档匹配其中任意一个条件就可以作为查询结果。

以下示例会组合两个match查询,然后返回在地址中既不包含“mill”,也不包含“lane”的所有账号:

  1. GET /bank/_search
  2. {
  3. "query": {
  4. "bool": {
  5. "must_not": [
  6. { "match": { "address": "mill" } },
  7. { "match": { "address": "lane" } }
  8. ]
  9. }
  10. }
  11. }

在上述示例中,bool must_not子句指定了结果为假的查询列表,文档必须同时不匹配这两个条件才能作为查询结果。

我们可以在一个bool查询中同时组合使用mustshouldmust_not子句。此外,我们还可以在bool查询中组合任意多个bool子句,这样便能模拟任何复杂的多层级布尔逻辑。

以下示例会返回年龄为40岁,并且不居住在爱达荷州(ID)的储户的账号:

  1. GET /bank/_search
  2. {
  3. "query": {
  4. "bool": {
  5. "must": [
  6. { "match": { "age": "40" } }
  7. ],
  8. "must_not": [
  9. { "match": { "state": "ID" } }
  10. ]
  11. }
  12. }
  13. }

执行筛选命令

在上一节中,我们跳过了一个叫做文档评分的小细节(也就是搜索结果中的_score字段)。评分是一个数值,它是一种用于判断文档和我们指定的搜索查询之间的匹配程度的相对度量。若评分越高,则文档的相关度就越高;若评分越低,则文档的相关度就越低。

但是,查询并不总是需要生成评分,特别是当查询仅仅用于“筛选”文档集合时。为了避免计算无用的评分,ElasticSearch会发现这些情况,然后自动优化查询的执行过程。

我们在上一节中介绍的<a href="https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-bool-query.html" title="bool“>bool查询也支持filter子句,这个子句使得我们能够在不改变评分计算方法的前提下,只使用一次查询就可以限制和其他子句相匹配的结果文档。在以下示例中,我们会引入<a href="https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-range-query.html" title="range“>range查询,它使得我们能够通过某个范围的值来筛选结果文档。这种查询通常用于数字或日期的筛选。

以下示例会执行一次布尔查询,返回账户余额在20000至30000之间(闭区间)的所有账号。换句话说,我们希望找到账户余额大于等于20000,并且小于等于30000的账号。

  1. GET /bank/_search
  2. {
  3. "query": {
  4. "bool": {
  5. "must": { "match_all": {} },
  6. "filter": {
  7. "range": {
  8. "balance": {
  9. "gte": 20000,
  10. "lte": 30000
  11. }
  12. }
  13. }
  14. }
  15. }
  16. }

仔细分析上述的示例,我们发现这个布尔查询包含一个match_all查询(查询部分)和一个range查询(筛选部分)。我们可以将任何其他的查询替换至上述的查询部分和筛选部分。在上述的示例中,范围查询的执行结果是完全正确的,因为落在范围内的文档全部都是“相等”匹配的,也就是说,没有一个文档的相关度比另一个文档更高。

除了match_allmatchboolrange查询之外,ElasticSearch还有很多其他的查询类型,我们不会在本文中深入学习这些查询类型。由于我们已经基本了解ElasticSearch的查询操作是如何工作的,所以要将这些知识应用到其他查询类型的学习和实验中并不会太难。

执行聚合命令

聚合操作可以将你的数据进行分组,还可以从你的数据中提取统计数据。你可以将聚合操作大致想象为SQL的GROUP BY操作,以及SQL的聚合函数。在ElasticSearch中,你能够在执行搜索操作之后获得返回的命中结果,同时还能够获得返回的聚合结果,这两种结果都在同一个响应消息之中。这是一项非常强大和高效的功能,因为只要通过一次请求,你就可以运行多个查询和聚合操作,然后一次性获得这些操作返回的所有结果。通过这个简单明了的API命令,便可以避免不必要的网络往返开销。

首先,以下示例会根据账号所属的州,对所有账号进行分组,然后返回前10个(默认)州的数据,按照账号数量降序排列(也是默认):

  1. GET /bank/_search
  2. {
  3. "size": 0,
  4. "aggs": {
  5. "group_by_state": {
  6. "terms": {
  7. "field": "state.keyword"
  8. }
  9. }
  10. }
  11. }

如果使用SQL语言,那么上述的聚合操作可以类似地写为:

  1. SELECT state, COUNT(*) FROM bank GROUP BY state ORDER BY COUNT(*) DESC

集群的响应如下图所示(只显示一部分):

根据账号所属的州进行分组

我们可以看到,在ID(爱达荷州)中有27个账号,随后的TX(德克萨斯州)中有27个账号,再随后的AL(阿拉巴马州)中有25个账号,以此类推。

注意,我们设置了size=0,这样便不会显示搜索命中的结果,因为我们只想在响应消息中看到聚合结果。

在前面的聚合示例的基础上,以下示例会计算每个州的平均账户余额(再一次地,只会显示前10个州,按照账号数量降序排列):

  1. GET /bank/_search
  2. {
  3. "size": 0,
  4. "aggs": {
  5. "group_by_state": {
  6. "terms": {
  7. "field": "state.keyword"
  8. },
  9. "aggs": {
  10. "average_balance": {
  11. "avg": {
  12. "field": "balance"
  13. }
  14. }
  15. }
  16. }
  17. }
  18. }

注意,我们是如何将average_balance聚合操作内嵌至group_by_state聚合操作中的。这是所有聚合操作的常见模式。你可以将聚合操作内嵌至其他聚合操作,这样便能从你的数据中任意地提取关键的概要数据。

在前面的聚合示例的基础上,以下示例会降序排列每个州的平均账户余额:

  1. GET /bank/_search
  2. {
  3. "size": 0,
  4. "aggs": {
  5. "group_by_state": {
  6. "terms": {
  7. "field": "state.keyword",
  8. "order": {
  9. "average_balance": "desc"
  10. }
  11. },
  12. "aggs": {
  13. "average_balance": {
  14. "avg": {
  15. "field": "balance"
  16. }
  17. }
  18. }
  19. }
  20. }
  21. }

以下示例说明了我们如何针对年龄组(20-29、30-39和40-49)进行分组,然后再针对性别进行分组,最后获取每个年龄组、每种性别的平均账户余额:

  1. GET /bank/_search
  2. {
  3. "size": 0,
  4. "aggs": {
  5. "group_by_age": {
  6. "range": {
  7. "field": "age",
  8. "ranges": [
  9. {
  10. "from": 20,
  11. "to": 30
  12. },
  13. {
  14. "from": 30,
  15. "to": 40
  16. },
  17. {
  18. "from": 40,
  19. "to": 50
  20. }
  21. ]
  22. },
  23. "aggs": {
  24. "group_by_gender": {
  25. "terms": {
  26. "field": "gender.keyword"
  27. },
  28. "aggs": {
  29. "average_balance": {
  30. "avg": {
  31. "field": "balance"
  32. }
  33. }
  34. }
  35. }
  36. }
  37. }
  38. }
  39. }

ElasticSearch还有很多其他的聚合操作,我们不会在本文中深入学习这些聚合操作。如果你想要深入体验聚合操作,那么聚合操作参考指南将会是一个很好的切入点。