elasticsearch(二)---基本数据操作

1,263 阅读11分钟

修改数据

Elasticsearch 提供了近实时的数据操作和搜索功能。默认情况下, 您可以期望从索引/更新/删除数据的时间到一个秒延迟 (刷新间隔), 直到显示在搜索结果中的时间。这是与 SQL 类似的其他平台的重要区别, 其中数据在事务完成后立即可用。

索引、替换文档

索引单个文档。让我们再次记住该命令:

PUT /customer/_doc/1?pretty
{
  "name": "John Doe"
}

同样, 上述将将指定的文档索引为客户索引, ID 为1。如果我们再用不同 (或相同) 的文档再次执行上述命令, Elasticsearch 将替换 (即 reindex) 一个新文档, 上面有一个 ID 为1的文件:

PUT /customer/_doc/1?pretty
{
  "name": "Jane Doe"
}

上述更改的文件名称, 其 ID 为1从 "无名氏" 到 "无名氏"。另一方面, 如果我们使用不同的 ID, 则会对新文档进行索引, 并且索引中已经存在的现有文档将保持不变。

PUT /customer/_doc/2?pretty
{
  "name": "Jane Doe"
}

上面的索引是一个 ID 为2的新文档。

索引时, ID 部分是可选的。如果未指定, Elasticsearch 将生成随机 ID, 然后使用它对文档进行索引。

实际 ID Elasticsearch 生成 (或在前面的示例中显式指定的任何内容) 作为索引 API 调用的一部分返回。

此示例演示如何索引没有显式 ID 的文档:

POST /customer/_doc?pretty
{
  "name": "Jane Doe"
}

请注意, 在上述情况下, 我们使用的是POST而不是PUT, 因为我们没有指定 ID。

更新文档

除了能够索引和替换文档之外, 我们还可以更新文档。注意,每次进行更新时, Elasticsearch 都会删除旧文档, 然后用一次快照将更新应用于该文档, 对其进行索引

本示例演示如何通过将名称字段更改为 "无名氏" 来更新以前的文档 (ID 为 1):

POST /customer/_doc/1/_update?pretty
{
  "doc": { "name": "Jane Doe" }
}

本示例演示如何通过将 "名称" 字段更改为 "无名氏", 并同时向其添加 "年龄" 字段来更新我们以前的文档 (ID 1):

POST /customer/_doc/1/_update?pretty
{
  "doc": { "name": "Jane Doe", "age": 20 }
}

删除文档

删除文档相当简单。此示例演示如何删除 ID 为2的客户:

DELETE /customer/_doc/2?pretty

批量处理

除了能够索引、更新和删除单个文档之外, Elasticsearch 还提供了使用bulk API在批处理中执行上述任何操作的能力。此功能很重要, 因为它提供了一个非常有效的机制, 尽可能快地完成多个操作, 尽可能少使用网络往返。

例如, 以下调用在一个批量操作中索引两个文档 (id 1-无名氏和 id 2-无名氏):

POST /customer/_doc/_bulk?pretty
{"index":{"_id":"1"}}
{"name": "John Doe" }
{"index":{"_id":"2"}}
{"name": "Jane Doe" }

本示例更新第一个文档 (id 为 1), 然后在一个批量操作中删除第二个文档 (id 为 2):

POST /customer/_doc/_bulk?pretty
{"update":{"_id":"1"}}
{"doc": { "name": "John Doe becomes Jane Doe" } }
{"delete":{"_id":"2"}}

注意, 对于删除操作, 在它之后没有相应的源文档, 因为删除只要求删除文档的 ID。

由于其中一个操作失败, 批量 API 不会失败。如果单个操作因任何原因而失败, 它将继续处理其后面的其余操作。当批量 API 返回时, 它将为每个操作提供一个状态 (与发送的顺序相同), 以便您可以检查特定操作是否失败。

浏览数据

现在, 我们已经看到了基本知识, 让我们尝试一个更现实的数据集。我已经准备了一个关于客户银行帐户信息的虚拟 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"
}

搜索API

现在让我们从一些简单的搜索开始。运行搜索有两种基本方法: 一种是通过rest 请求 URI发送搜索参数, 另一种是通过rest 请求正文发送。请求正文方法允许您更具表现力, 也可以用更可读的 JSON 格式定义搜索。

我们将尝试一个请求 URI 方法的示例, 但对于本教程的其余部分, 我们将专门使用请求正文方法。

用于搜索的 REST API 可从端点访问。本示例返回银行索引中的所有文档:_search

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

让我们先解剖一下上面的查询语句。我们在bank索引中搜索 (端点), q=*参数指示 Elasticsearch 匹配索引中的所有文档。sort=account_number:asc该参数指示使用每个文档的字段升序对结果进行排序。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 执行搜索的时间 (以毫秒为单位)

timed_out–告诉我们搜索超时与否

_shards–告诉我们搜索了多少碎片, 以及成功/失败的搜索碎片的计数

hits–搜索结果

hits.total–与搜索条件匹配的文档总数

hits.hits–实际的搜索结果数组 (默认为前10个文档)

hits.sort-为结果排序键 (如果按分数排序则丢失)

下面是使用备选请求正文方法的相同的精确搜索:

GET /bank/_search
{
  "query": { "match_all": {} },
  "sort": [
    { "account_number": "asc" }
  ]
}

这里的区别在于, 我们不是在 URI 中传递, 而是向 API 提供 JSON 样式的查询请求正文。

注意, 当你得到搜索结果, Elasticsearch 已经请求结束。 与许多其他平台 (如 SQL) 不同, SQL最初可能会获得查询结果的部分子集, 然后要使用某种方法(如翻页) 继续返回到服务器再次查询获取其余的结果。

引入查询语言

Elasticsearch 提供了一个 JSON 样式的域特定语言, 您可以使用它来执行查询。这称为查询 DSL。查询语言非常全面, 乍一看可能很吓人, 但真正了解它的最好方法是从几个基本示例开始。

回到最后一个示例, 我们执行了以下查询:

GET /bank/_search
{
  "query": { "match_all": {} }
}

我们还可以通过其他参数来影响搜索结果。在上面的部分中, 我们通过size设置查询结果数量:

GET /bank/_search
{
  "query": { "match_all": {} },
  "size": 1
}

请注意, 如果size未指定, 则默认为10

本示例执行并返回文档10到 19:

GET /bank/_search
{
  "query": { "match_all": {} },
  "from": 10,
  "size": 10
}

本示例按帐户余额降序排列结果:

GET /bank/_search
{
  "query": { "match_all": {} },
  "sort": { "balance": { "order": "desc" } }
}

执行搜索

既然我们已经看到了一些基本的搜索参数, 让我们在查询 DSL 中多挖掘一些。

让我们先来看看返回的文档字段。默认情况下, 完整的 JSON 文档作为所有搜索的一部分返回。这称为_source源 (搜索命中中的字段)。如果我们不希望返回整个源文档, 我们就有能力请求返回源中的几个字段。

此示例演示如何从搜索中返回两个字段:

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

类似 select account_number,balance from table;

以前, 我们已经看到了如何使用查询来匹配所有文档。现在让我们介绍一个名为 " 匹配查询" 的新查询, 它可以被看作是一个基本的搜索查询 (即针对特定字段或字段集进行的搜索)。

本示例返回编号为20的帐户:

GET /bank/_search
{
  "query": { "match": { "account_number": 20 } }
}

本示例返回address中包含 mill 一词的所有帐户:

GET /bank/_search
{
  "query": { "match": { "address": "mill" } }
}

本示例返回address中包含 milllane 术语的所有帐户:

GET /bank/_search
{
  "query": { "match": { "address": "mill lane" } }
}

此示例是 match 的变体, 它返回地址中包含 mill lane 短语的所有帐户:

GET /bank/_search
{
  "query": { "match_phrase": { "address": "mill lane" } }
}

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

本示例构成两个查询, 并返回address中包含 milllane 的所有帐户:

GET /bank/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "address": "mill" } },
        { "match": { "address": "lane" } }
      ]
    }
  }
}

在上面的示例中, 子句指定bool must要视为匹配的文档必须为 true 的所有查询。

本示例构成两个查询, 并返回address中包含 milllane 的所有帐户:

GET /bank/_search
{
  "query": {
    "bool": {
      "should": [
        { "match": { "address": "mill" } },
        { "match": { "address": "lane" } }
      ]
    }
  }
}

在上面的示例中, 子句bool should指定一个查询列表, 其中有一个都是 true, 才能将文档视为匹配项。

本示例构成两个查询, 并返回address中不包含 milllane 的所有帐户:

GET /bank/_search
{
  "query": {
    "bool": {
      "must_not": [
        { "match": { "address": "mill" } },
        { "match": { "address": "lane" } }
      ]
    }
  }
}

本示例返回40岁的人的所有帐户, 但不居住在 ID (阿霍) 中:

GET /bank/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "age": "40" } }
      ],
      "must_not": [
        { "match": { "state": "ID" } }
      ]
    }
  }
}

执行筛选

在上一节中, 我们跳过一个名为_score "文档分数" (搜索结果中的字段) 的小细节。

分数是一个数值, 它是文档与我们指定的搜索查询匹配程度的相对度量值。

分数越高, 文档越相关, 分数越低, 文档越不相关。

但是查询并不总是需要产生分数, 特别是当它们仅用于 "筛选" 文档集时。

Elasticsearch 检测这些情况, 并自动优化查询执行, 以不计算无用的分数。

例如, 让我们介绍范围查询, 它允许我们按一系列值筛选文档。这通常用于数字或日期筛选。

本示例使用 bool 查询返回包含20000和30000之间的余额的所有帐户。换言之, 我们希望找到一个余额大于或等于20000且小于或等于30000的帐户。

GET /bank/_search
{
  "query": {
    "bool": {
      "must": { "match_all": {} },
      "filter": {
        "range": {
          "balance": {
            "gte": 20000,
            "lte": 30000
          }
        }
      }
    }
  }
}

解剖上面, 布尔查询包含查询 (查询部分) 和查询 (筛选器部分)。在上述情况下, 范围查询是完全无意义的, 因为只要在这个范围内的文件,他们的分数应该相同,没有谁比谁更相关。所以,我们可以不用关心他们的文档分数,从而使用filter筛选文档。

执行聚合

聚合提供了从数据中分组和提取统计信息的能力。考虑聚合的最简单方法是大致将它等同于 sql group和 sql 聚合函数。在 Elasticsearch 中, 您有能力执行返回命中的搜索, 同时返回聚合结果, 并在一个响应中与命中全部分开。这是非常强大和高效的, 在这个意义上, 您可以运行查询和多个聚合, 并获得两个 (或两个以上) 操作的结果, 以避免网络往返。

首先, 本示例按状态对所有帐户进行分组, 然后返回按降序 (也为默认值) 排序的前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" : 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爱达荷州) 有27个帐户, 其次是27帐户 (TX得克萨斯州), 其次是25帐户 (AL阿拉巴马州), 等等。

请注意, 我们设置为不显示搜索命中size=0, 因为我们只希望看到聚合结果在响应中。

本示例计算平均帐户余额 (仅针对按降序排序的前10个状态):

GET /bank/_search
{
  "size": 0,
  "aggs": {
    "group_by_state": {
      "terms": {
        "field": "state.keyword"
      },
      "aggs": {
        "average_balance": {
          "avg": {
            "field": "balance"
          }
        }
      }
    }
  }
}

在上一个聚合的基础上, 现在让我们按降序排序平均余额:

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"
              }
            }
          }
        }
      }
    }
  }
}

结论

Elasticsearch 是一个简单而复杂的产品。迄今为止, 我们已经了解了它的基本知识、如何查看它的内部以及如何使用一些 REST api 来处理它。