elasticsearch6.7 01.入门指南(4)

时间:2024-01-04 14:26:20

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.relationhits.total.value是否是确切的命中数,在这种情况下它等于“eq”。或总命中数的下限(大于或等于),在这种情况下它等于“gte
  • hits.hits: 实际的查询结果(数组形式表示,默认为前 10 个文档)
  • sort: 查询结果的排序字段(没有则按 score 排序)
  • scoremax_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_numberbalance(在 _source 之内)字段,如下所示 :

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

注意,上面的的响应中将只有一个_source字段,并且里面只有account_numberbalance两个字段。如果你会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中包含了 milllane的账户 :

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中包含了milllane的账户 :

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 查询中同时联合 mustshouldmust_not语句。此外,我们可以在任何一个bool语句内部构建bool查询,从而模拟出任何复杂的多级别的 boolean 逻辑。

下面的例子返回了age40 但是 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 查询来返回balances20000 ~ 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_allmatchboolrange查询之外 ,还有很多在这里我们我没有用到的其它有用的查询类型。既然我们对于它们是如何工作的方式有了一个基本的了解,在学习和尝试其它的查询类型中应用这些知识应该不会太困难。

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 是什么,更重要的是,可以促使你进一步尝试它更强大的功能。