logo头像
Snippet 博客主题

Elasticsearch用户指南(二)

本文于338天之前发表,文中内容可能已经过时。

书接上文《Elasticsearch用户指南(一)》,翻译作品,水平有限,如有错误,烦请留言指正。原文请见 官网英文文档

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._scoremax_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_numberbalance_source内部):

GET /bank/_search
{
  "query": { "match_all": {} },
  "_source": ["account_number", "balance"]
}

注意,上面的例子仅仅是减少了_source的字段,它仍然仅返回一个名叫_source的字段,但是在它只包含account_numberbalance 两个字段。

如果你有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) querybool查询允许我们使用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, shouldmust_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,希望能对你的学习有所帮助。

上一篇