修改数据
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中包含 mill 或 lane 术语的所有帐户:
GET /bank/_search
{
"query": { "match": { "address": "mill lane" } }
}
此示例是 match 的变体, 它返回地址中包含 mill lane 短语的所有帐户:
GET /bank/_search
{
"query": { "match_phrase": { "address": "mill lane" } }
}
现在让我们介绍一下 bool查询。bool查询允许我们使用布尔逻辑将较小的查询组成更大的查询。
本示例构成两个查询, 并返回address中包含 mill 和 lane 的所有帐户:
GET /bank/_search
{
"query": {
"bool": {
"must": [
{ "match": { "address": "mill" } },
{ "match": { "address": "lane" } }
]
}
}
}
在上面的示例中, 子句指定bool must要视为匹配的文档必须为 true 的所有查询。
本示例构成两个查询, 并返回address中包含 mill 或 lane 的所有帐户:
GET /bank/_search
{
"query": {
"bool": {
"should": [
{ "match": { "address": "mill" } },
{ "match": { "address": "lane" } }
]
}
}
}
在上面的示例中, 子句bool should指定一个查询列表, 其中有一个都是 true, 才能将文档视为匹配项。
本示例构成两个查询, 并返回address中不包含 mill 和 lane 的所有帐户:
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 来处理它。