Getting Started
基本概念
Near Realtime(近实时)
Elasticsearch 是一个near-realtime(接近于实时)的搜索平台。从索引文档到它被搜索到只用轻微的延迟(通常是1秒)
Cluster(集群)
集群是一个或多个节点的集合,这些节点被用来保存数据或提供索引和跨节点搜索服务。集群有唯一的名称标识(cluster name),通常是elasticsearch。一个node只有通过这个唯一的名称才可以加入这个集群。
不同的环境应该使用不同的cluster name。以免节点加错集群。
单节点集群是有效的,你也可以建多个有不同cluster name的集群。
Node(节点)
节点是集群的一个服务,被用来存储数据,并参与集群的索引和提供搜索功能。和集群一样,节点通过名字区分,默认情况下这个名字是一个随机的UUID。如果你不希望使用默认的节点名,可以重新定义。如果你希望识别出哪些服务与那些节点有关系,这个名称很关键。
每个节点可以通过cluster name加入到集群。每个节点都会默认的加入到名为elasticsearch的集群。这意味着,如果你启动多个node,并且他们在网络上互相可见,他们将会自动组成一个名为elasticsearch的集群。
一个集群中可以有多个节点。如果网络上没有其他节点,启动单个节点将默认形成一个名为Elasticsearch的单节点集群。
Index(索引)
索引是一个有相似特征的文档的集合,例如,你可以有一个用户数据索引,一个产品目录索引,和其他数据索引。通过一个全小写的名称定义一个索引,此名称用于在对其中的文档执行索引(动词)、搜索、更新和删除操作时用到。
在一个集群中可以定义多个索引。
Type(类型)
类型是索引的逻辑分区,你可以在同一个索引下为不同的文档建立类型(type)。比如一个用户类型,一个文章类型。现在已经不可能在索引中创建多个类型,而且类型的整个概念将在以后的版本中被删除。
在Elasticsearch 6.0.0或更高版本中创建的索引可能只包含一个映射类型。5.x中创建的具有多种映射类型的索引将继续在Elasticsearch中发挥作用。映射类型将在Elasticsearch 7.0.0中完全删除。
Document(文档)
文档是可以被索引的基础单元。比如单个用户作为一个文档、单个产品作为一个文档。文档用json表示。
在同一个索引或类型中,可以保存多个文档。虽然文档物理上保存在索引中,但是一个文档必须被分配给索引中的类型。
Shards&Replicas(分片和副本)
索引可能存储超过单个节点硬件资源限制的大量数据。例如,一个包含10亿个文档、占用1TB磁盘空间的索引可能不适用于单个节点的磁盘,或者只有一个几点可能处理搜索请求的速度太慢。
为了解决这个问题,elasticsearch提供了可以将一个索引分为多个部分的能力。每个部分称为一个分片。当你创建索引的时候,你可以简单的定义你想要的分片的数量。每个分片本身就是一个功能完整且独立的”索引”,可以驻留在集群中的任何节点上。
分片重要的两个原因:
它允许你水平扩展和缩放你的数据卷。
它允许你跨分片(可能在多个节点上)分布和并行化操作,从而提高性能/吞吐量。
分片如何分布以及如何将其文档聚合回搜索请求的机制完全由Elasticsearch管理,对于用户来说是透明的。
在随时可能出现故障的云环境中,当节点或分片由于某种原因脱机或消失时,故障转移机制是非常有用的。所以强烈建议开启故障转移机制。为此,Elasticsearch允许您将索引分片的一个或多个副本复制为所谓的复制分片,或简称为副本。
副本重要的两个原因
当分片或节点出现问题的时候,它提供了高可用性。因此,一定要注意的是副本不能与它被复制的原始/主分片分配在同一个节点上。
因为搜索可以并行地在所有副本上执行,所以它允许您扩展搜索量/吞吐量。
总而言之,每个索引可以被分成多个分片。索引还可以复制0次(即没有副本)或更多次。复制之后,每个索引都将拥有主分片(从中复制的原始碎片)和副本分片(主碎片的副本)。
可以在创建索引时为每个索引定义分片和副本的数量。创建索引之后,还可以随时动态更改副本的数量。您可以使用_shrink和_split api更改现有索引的分片数量,但是这不是一项简单的任务,预先计划正确的分片数量是最优的方法。
默认情况下,Elasticsearch中的每个索引分配5个主分片和1个副本,这意味着如果您的集群中至少有两个节点,那么您的索引将拥有5个主分片和5个副本分片(1个完整副本),每个索引总共有10个分片。
每个Elasticsearch分片都是Lucene索引,在一个Lucene索引中可以拥有的文档数量
有一个最大值。截至LUCENE-5843,该限制为2,147,483,519个(=Integer.MAX_VALUE – 128)文档。可以使用_cat/shards API监视分片规模和大小。
安装
Helm安装:
Es: http://songjxin.cn/?p=246
Kibana: http://songjxin.cn/?p=267
其他安装:
https://www.elastic.co/guide/en/elasticsearch/reference/6.5/_installation.html
探索集群
现在我们已经启动并运行了节点(和集群),下一步是了解如何与它通信。Elasticsearch提供了一个非常全面和强大的REST API,您可以使用它与集群交互。使用API可以做的事情如下:
检查集群、节点和索引健康状态、状态和统计信息
管理集群、节点和索引数据和元数据
对索引执行CRUD(创建、读取、更新和删除)和搜索操作
执行高级搜索操作,如分页、排序、筛选
集群健康
GET /_cat/health?v |
Status:
Green 集群正常
Yellow 所有主分片正常,副分片存在异常
Red 部分主分片异常
当集群为红色时,它将继续为来自可用碎片的搜索请求提供服务,但是您可能需要尽快修复它,因为存在未分配的碎片。
GET /_cat/nodes?v |
列出所有目录
GET /_cat/indices?v |
创建一个索引
PUT /customer?pretty GET /_cat/indices?v |
第一条命令是用PUT命令创建一个名为customerd的索引。我们只是在调用的末尾追加pretty,告诉它打印JSON响应(如果有的话)。
响应中红色的提示es版本7.0之后分片的默认数是从5变到1 可以忽略。
第二个命令的结果告诉我们,现在有一个名为customer的索引,它有5个主碎片和1个副本(默认值),其中包含0个文档。
索引并查询文档
现在让我们把一些东西放入我们的客户索引中。我们将一个简单的客户文档编入客户索引,ID为1,如下所示:
PUT /customer/_doc/1?pretty { “name”: “John Doe” } |
从上面,我们可以看到在customer索引中成功地创建了一个新的客户文档。文档的内部id也为1,这是我们在索引时指定的。
需要注意的是,Elasticsearch不要求您先显式地创建索引,然后才能将文档索引到其中。在前面的例子中,Elasticsearch将自动创建客户索引,如果它之前不存在的话。
GET /customer/_doc/1?pretty |
found字段 表明我们找到了一个带有请求ID 1的文档
_source字段 返回我们从上一步索引的完整JSON文档
删除一个索引
DELETE /customer?pretty GET /_cat/indices?v |
我们可以看到在Elasticsearch中访问数据的模式:
<HTTP Verb> /<Index>/<Type>/<ID> |
修改数据
Elasticsearch提供近实时的数据处理和搜索功能。默认情况下,从索引/更新/删除数据到出现在搜索结果中,可能会有一秒的延迟(刷新间隔)。这是与其他平台(如SQL)的一个重要区别,在SQL中,事务完成后数据立即可用。
索引和替换文档
我们添加一条数据
PUT /customer/_doc/1?pretty { “name”: “John Doe” } |
上面将指定的文档索引到customer索引中,ID为1。如果我们用不同(或相同)的文档再次执行上述命令,Elasticsearch将用ID为1替换(即重新索引)现有文档之上的新文档:
PUT /customer/_doc/1?pretty { “name”: “Jane Doe” } |
上面将ID为1的文档名称从”John Doe”更改为”Jane Doe”。另一方面,如果我们使用不同的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” } |
更新文档
除了能够索引和替换文档之外,我们还可以更新文档。请注意,Elasticsearch实际上并没有在底层进行就地更新。每当我们进行更新时,Elasticsearch都会删除旧文档,然后将更新应用到的新文档一次性索引到新文档。
这个例子展示了如何通过将name字段更改为”Jane Doe”来更新我们之前的文档(ID为1):
POST /customer/_doc/1/_update?pretty { “doc”: { “name”: “Jane Doe” } } |
这个和之前的替换文档没有区别。
这个例子展示了如何更新我们之前的文档(ID为1),方法是将name字段更改为”Jane Doe”,同时添加一个age字段:
POST /customer/_doc/1/_update?pretty { “doc”: { “name”: “Jane Doe”, “age”: 20 } } |
还可以使用简单的脚本执行更新。本例使用脚本将年龄增加5岁:
POST /customer/_doc/1/_update?pretty { “script” : “ctx._source.age += 5” } |
上面例子中,ctx._source指的是将要升级的文档的source
Elasticsearch提供了在给定查询条件(如SQL update – where语句)下更新多个文档的能力。详情查看docs-update-by-query API。
删除文档
删除文档相当简单。这个例子展示了如何删除我们之前ID为2的客户:
DELETE /customer/_doc/2?pretty |
查看_delete_by_query API来删除与特定查询匹配的所有文档。删除整个索引要比使用delete By Query API删除所有文档有效得多。
批量处理
除了能够索引、更新和删除单个文档外,Elasticsearch还提供了使用_bulk API批量执行上述任何操作的能力。这个功能非常重要,因为它提供了一种非常有效的机制,可以以尽可能少的网络往返尽可能快地执行多个操作。
作为一个快速示例,以下命令在一个批量操作中索引两个文档(ID 1 – John Doe和ID 2 – Jane Doe):
POST /customer/_doc/_bulk?pretty {“index”:{“_id”:”1″}} {“name”: “John Doe” } {“index”:{“_id”:”2″}} {“name”: “Jane Doe” } |
探索数据
准备数据
下载随机生成的数据,并上传到es
wget “https://github.com/elastic/elasticsearch/blob/master/docs/src/test/resources/accounts.json” curl -H “Content-Type: application/json” -XPOST “10.110.25.74:54206/bank/_doc/_bulk?pretty&refresh” –data-binary “@accounts.json” curl “10.110.25.74:54206/_cat/indices?v” |
搜索API(search API)
运行搜索有两种基本方法:一种是通过REST request URI发送搜索参数,另一种是通过REST request body。Request body的方法更直观,并且可以使用可读性更强的json来格式化。下面是一个request URI方法的示例,但是在之后的部分所有的实例将只使用request body的方法:
返回银行索引中所有文档:
GET /bank/_search?q=*&sort=account_number:asc&pretty |
我们在 blank (/bank) 索引中执行 search (_search)操作。参数q=*
指示Elasticsearch匹配索引中的所有文档。sort=account_number:asc参数指示使用每个文档的account_number字段按升序对结果进行排序。Pretty参数只是告诉Elasticsearch返回json格式的结果。
在本次返回结果中我们可以看到以下部分:
took – Elasticsearch执行搜索的毫秒时间
timed_out – 告诉我们搜索是否超时
_shards – 告诉我们搜索了多少分片,以及成功/失败搜索分片的个数。
Hits – 搜索结果。
hits.total – 符合我们搜索条件的文档总数
hits.hits – 实际搜索结果数组(默认为前10个文档)
下面是上面使用request body方法进行的相同搜索:
GET /bank/_search { “query”: { “match_all”: {} }, “sort”: [ { “account_number”: “asc” } ] } |
这里的不同之处在于,我们没有在URI中传递q=*,而是向_search API提供了json风格的查询请求体。
一旦您获得了搜索结果,Elasticsearch就完全完成了请求,不会维护任何类型的服务器端资源,也不会在结果中打开游标。和sql等其他平台不同的是,sql可以动态的获取结果,比如预先获取其中一部分,然后通过不停的请求服务器获取其他数据。
查询语言
Elasticsearch提供了一种json风格的特定于领域的语言——Query DSL,您可以使用它来执行查询。
继续查看上一个例子
GET /bank/_search { “query”: { “match_all”: {} } } SQL:select * from bank limit 0 10; |
query部分告诉我们我们查询定义是什么,match_all部分只是我们想要查询的类型,match_all
查询只是搜索指定索引中的所有文除了查询参数,我们还可以传递其他参数来影响搜索结果。在上面我们以sort传递的例子中,这里我们以size传递:
GET /bank/_search { “query”: { “match_all”: {} }, “size”: 1 } SQL: select * from bank limit 0,1; |
如果size没有定义,它的默认值是0。
这个例子执行match_all并返回文档10到19:
GET /bank/_search { “query”: { “match_all”: {} }, “from”: 10, “size”: 10 } SQL:select * from bank limit 10,10; |
from参数(从0开始)指定从哪个文档索引开始,size参数指定从from参数开始返回多少文档。该特性在实现搜索结果分页时非常有用。注意,如果没有指定from,则默认为0。
这个示例执行match_all并按帐户余额降序排序结果,并返回前10个(默认大小)文档:
GET /bank/_search { “query”: { “match_all”: {} }, “sort”: { “balance”: { “order”: “desc” } } } SQL:select * from bank order by balance desc limit 0,10; |
搜索
让我们首先看看返回的文档字段。默认情况下,完整的JSON文档作为所有搜索的一部分返回。这称为源(搜索命中的_source字段)。如果不希望返回整个源文档,则可以只从源中请求返回几个字段。
这个例子展示了如何从搜索中返回两个字段account_number和balance(在_source内部):
GET /bank/_search { “query”: { “match_all”: {} }, “_source”: [“account_number”, “balance”] } SQL:select account_number,balance from bank; |
注意,上面的示例只是简化了_source字段。它仍然只返回一个名为_source的字段,但是其中只包含account_number和balance字段。
现在让我们转到查询部分。在前面,我们已经看到了如何使用match_all查询来匹配所有文档。现在我们引入一个新的查询,称为match查询,它可以看作是一个基本的字段搜索查询(即针对特定字段或字段集进行的搜索)。
这个例子返回编号为20的帐户:
GET /bank/_search { “query”: { “match”: { “account_number”: 20 } } } SQL: select * from bank where account_number=20; |
本例返回地址中包含术语”mill”的所有帐户:
GET /bank/_search { “query”: { “match”: { “address”: “mill” } } } SQL: select * from bank where address REGEXP ‘\\bmill\\b’ limit 0,10; |
本例返回地址中包含术语”mill”或”lane”的所有帐户:
GET /bank/_search { “query”: { “match”: { “address”: “mill lane” } } } select * from bank where address REGEXP ‘((\\bmill\\b)|(\\blane\\b))’ limit 0,10; |
这个例子是match (match_phrase)的变体,它返回地址中包含短语”mill lane”的所有帐户:
GET /bank/_search { “query”: { “match_phrase”: { “address”: “mill lane” } } } SQL:select * from bank where address REGEXP ‘\\bmill lane\\b’; |
现在让我们介绍bool查询。bool查询允许我们使用布尔逻辑将较小的查询组合成较大的查询。
本例由两个匹配查询组成,返回地址中包含”mill”和”lane”的所有帐户:
GET /bank/_search { “query”: { “bool”: { “must”: [ { “match”: { “address”: “mill” } }, { “match”: { “address”: “lane” } } ] } } } SQL: select * from bank where address REGEXP ‘\\bmill\\b’ and address regexp ‘\\blane\\b’ ; |
在上面的例子中,bool must子句指定了所有必须为true的查询,以便将文档视为匹配。
与此相反,本例由两个匹配查询组成,返回地址中包含”mill”或”lane”的所有帐户:
GET /bank/_search { “query”: { “bool”: { “should”: [ { “match”: { “address”: “mill” } }, { “match”: { “address”: “lane” } } ] } } } SQL:select * from bank where address REGEXP ‘\\bmill\\b’ or address regexp ‘\\blane\\b’ limit 0,10 ; |
在上面的例子中,bool should子句指定了一个查询列表,如果文档被认为是匹配的,那么其中任何一个查询都必须为true。
本例由两个匹配查询组成,返回地址中既不包含”mill”也不包含”lane”的所有帐户:
GET /bank/_search{ “query”: { “bool”: { “must_not”: [ { “match”: { “address”: “mill” } }, { “match”: { “address”: “lane” } } ] } } } SQL:select * from bank where address not REGEXP ‘\\bmill\\b’ and address not regexp ‘\\blane\\b’ ; |
在上面的例子中,bool must_not子句指定了一个查询列表,其中没有一个查询必须为true,文档才能被认为是匹配的。
我们可以在bool查询中同时组合must、should和must_not子句。此外,我们可以在这些bool子句中组合bool查询来模拟任何复杂的多层布尔逻辑。
这个例子返回所有40岁但不居住在ID中的人的帐户:
GET /bank/_search { “query”: { “bool”: { “must”: [ { “match”: { “age”: “40” } } ], “must_not”: [ { “match”: { “state”: “ID” } } ] } } } SQL: select * from bank where age = 40 and state != ‘ID’ limit 0,10; |
过滤
在上一节中,我们跳过了一个称为文档得分(搜索结果中的_score字段)的小细节。分数是一个数值,它是文档与我们指定的搜索查询匹配程度的一个相对度量。分数越高,文档越相关,分数越低,文档越不相关。
但是查询并不总是需要生成分数,特别是当它们只用于”过滤”时。Elasticsearch能够发现这种情况并自动优化查询执行,以避免计算无用的分数。
我们在上一节中介绍的bool查询还支持筛选子句,这些子句允许我们不更改计算分数方式的情况下过滤文档。 例如,让我们引入范围查询,它允许我们通过一系列值过滤文档。这通常用于数字或日期筛选。
本例使用bool查询返回所有余额在20000到30000之间的帐户(包括在内)。换句话说,我们希望找到余额大于等于20000和小于等于30000的账户。
GET /bank/_search { “query”: { “bool”: { “must”: { “match_all”: {} }, “filter”: { “range”: { “balance”: { “gte”: 20000, “lte”: 30000 } } } } } } SQL: select * from bank where balance >= 20000 and balance <= 30000 ; |
仔细分析上述内容,bool查询包含一个match_all查询(查询部分)和一个range查询(筛选部分)。我们可以将任何其他查询替换为查询和筛选器部分。在上述情况下,范围查询非常有意义,因为属于范围的文档都”相等”匹配。
聚合
聚合提供了对数据进行分组和提取统计信息的能力。考虑聚合最简单的方法是将其大致等同于SQL GROUP by和SQL聚合函数。在Elasticsearch中,您可以执行返回命中的搜索,同时在一个响应中返回与所有命中分离的聚合结果。这是非常强大和有效的,因为您可以运行查询和多个聚合,并一次性获得两个(或两个)操作的结果,从而避免使用简洁和简化的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 LIMIT 10; |
存在误差。
我们可以看到ID 有27个账号,TX 有27个账号,AL有25个账号,依此类推。
注意,我们将size=0设置为不显示搜索结果(hits),因为我们只想在响应中看到聚合结果。
在上一个聚合的基础上,这个例子按状态计算平均账户余额(同样只针对按计数降序排序的前10个状态):
GET /bank/_search { “size”: 0, “aggs”: { “group_by_state”: { “terms”: { “field”: “state.keyword” }, “aggs”: { “average_balance”: { “avg”: { “field”: “balance” } } } } } } SQL:SELECT state, COUNT(*),AVG(balance) FROM bank GROUP BY state ORDER BY COUNT(*) DESC limit 10; |
注意我们如何将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” } } } } } } SQL:SELECT state, COUNT(*),AVG(balance) FROM bank GROUP BY state ORDER BY AVG(balance) DESC limit 10; |
这个例子演示了我们如何根据年龄(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” } } } } } } } } |
还有许多其他聚合功能,我们在这里不详细介绍。如果您想做进一步的实验,聚合参考指南是一个很好的起点。
结语
弹性搜索是一个简单而复杂的乘积。到目前为止,我们已经了解了它是什么,如何查看它的内部,以及如何使用一些REST api使用它。希望本教程能让您更好地理解什么是Elasticsearch,更重要的是,它还能激发您进一步试验它的其他伟大特性!