Elasticsearch 5.4.0 用户指南

博客原文一
博客原文二

翻译作品,水平有限,如有错误,烦请留言指正。原文请见 官网英文文档

Elasticsearch

起步

Elasticsearch 是一个大规模开源的全文搜索和分析引擎,你可以用它来快速(接近实时)存储、搜索和分析大量的数据。通常被用作基础的引擎,或者有复杂的搜索功能和要求的应用的技术支撑。

这儿是一些Elasticsearch简单的使用场景:

  • 在线网站的数据存储,允许你的客户对你售卖的产品进行搜索的场景。这时,你可以使用Elasticsearch存储你的全部产品信息分类和库存,然后提供搜索和自动填充建议。

  • 你想做日志和事务数据的收集,然后对它们进行分析和挖掘,期待找到某种趋势,或者进行统计、归纳和异常分析。在这种情况下,你可以使用logstash( Elasticsearch/Logstash/Kibana 栈的一部分)收集、汇总和解析数据,然后logstash将这些数据传送到Elasticsearch中,一旦数据进入Elasticsearch中,你就可以使用搜索和聚合来挖掘任何你想要的信息。

  • 价格预警平台,允许精打细算的客户指定特殊的规则,比如我对某个特定的电子产品比较感兴趣,在下个月任意一个供应商的价格下降到指定价格,然后提醒我。在这种情况下,你可以把供货商的价格推送到Elasticsearch中,利用它的反向搜索(过滤)能力匹配用户查询的价格走势,最后将已经发现的匹配项发送给客户。

  • 你有分析或者商业智能的需求,在大量数据(数百万或者数十亿的记录)中想做个快速的调查、分析、可视化和询问特殊问题。在这种情况下,你可以使用Elasticsearch存储数据,使用kibana构建自定义的仪表盘来展示你关心的数据。此外,你还可以使用Elasticsearch的聚合功能来做复杂的商业智能分析与数据查询。

接下来的教程,我将会引导你完成Elasticsearch的获取、安装和运行,然后稍微深入一点,介绍一些基本操作,比如建立索引、搜索和修改你的数据。在教程的最后你应该对Elasticsearch有一个很好的认识,它是什么,它是怎么工作的。无论是构建复杂的搜索程序还是从你的数据中挖掘情报,在如何使用Elasticsearch方面,希望你能受到启发。

1 基本概念

这里有一些Elasticsearch的主要核心概念,在开始的时候就理解了这些概念,对于你的学习过程有非常大的帮助。

准实时

Elasticsearch是一个准实时的搜索平台。意思是从你创建一个文档的索引到这个索引可以被搜索只有很小延迟(通常是1秒)。

集群

一个集群是一个或者多个节点(服务器)的集合,它们共同拥有所有的数据,并且提供跨节点的索引和搜索能力。一个集群由一个唯一的名字确定,默认情况下是“elasticsearch”,这个名字是很重要的,因为一个节点仅可能是一个集群的一部分,节点加入到哪个集群是根据这个名字来判断的。

一定不要在不同的环境使用相同的集群名称,否则你可能将节点加入到错误的集群中,例如你可以分别在开发环境、预备环境和生产环境使用logging-dev, logging-stagelogging-prod

注意,在一个集群中只有一个节点也是有效的和非常好的。此外,你可以有多个独立的集群,如果它们的有各自唯一的集群名字。

节点

一个节点是一台服务器,也是一个集群的一部分,存储你的数据,参与提供集群的索引和搜索能力。就像集群一样,节点也是由一个名字唯一确定的,这个名字默认是一个随机的UUID,它是在节点启动的时候分配的。如果你不想要默认的名字,你可以随意定义你的节点名称。这个名字在集群的管理上显得很重要,他可以让你确定地知道在elasticsearch集群中的节点对应网络中的哪一台服务器。

一个节点可以通过配置集群名称加入到任意的集群中,默认情况下,每个节点都被设置加入到一个名叫elasticsearch的集群中,这意味着,如果你在一个网络中(服务器之间能够彼此互相发现)启动大量这样的节点,它们将自动的形成一个集群,名叫elasticsearch

在一个集群中你可以有任意多的节点。此外,如果在当前节点运行的网络中没有其他的 Elasticsearch 的节点,只启动一个节点,它将会形成一个新的单节点集群,名叫elasticsearch

索引

索引是具有相似特性的文档的集合,例如,你可以为客户数据建立一个索引,你也可以为产品类数据建立索引,还有订单数据也可以建立索引。一个索引都有一个特定的名字(必需为小写)标识,在对索引的文档执行索引、搜索、更新和删除操作时,该名称用于指定索引。

在一个集群中,你可以定义任意多的索引。

类型

在一个索引中,你可以定义一个或者多个类型,类型是索引的逻辑类/分区,它的语义完全由你决定。一般来说,将一组具有公共字段的文档定义为一种类型。例如,假设你在运行一个博客平台,你的所有数据都存储在一个索引中,在这个索引中你可能为用户数据定义一个类型,博客数据定义另一个类型,注释数据为另一个类型。

文档

文档是可以被索引的信息的基本单位。例如,你可以为一个单独的客户数据建立一个文档,一个单独的产品也可以建立一个文档,还有一个订单文档。这些文档是用JSON格式的数据来表示的,这是一种在互联网上无处不在的数据交互格式。

在一个索引/类型中,你可以存储尽可能多的文档。注意,尽管一个文档实际是存储在索引中的,但是实际上你必须将文档分配到一个索引中的一个类型。

分片&备份

一个索引可以存储大量的数据,这些数据可能超过单个节点硬盘限制,例如,一个存储了十亿文档的索引占了1TB的磁盘空间,可能不太适合存储在一个节点的硬盘上,或者说单个节点服务于搜索请求太慢了。

为了解决这个问题,Elasticsearch提供了将索引细分为多块的能力,这些块被称为“分片”。在你创建索引的时候,可以简单地定义所需数目的分片,每一个分片本身是一个功能全面的、独立的“索引”,它可以被托管在集群中的任意节点。

索引分片有两个原因是非常重要的:

  • 你可以水平拆分/缩放存储数据卷。
  • 你可以分布式或者并行地执行跨分片(可能存储在多个节点上)的操作,因此可以提高性能和吞吐量。

分片的分布机制和分片的文档是如何聚集在一起实现一次用户的搜索请求,这些完全都是由Elasticsearch管理的,对用户来说是透明的。

在网络/云环境中随时都有可能失败,在一个分片/节点不知原因地掉线或者消失的情况下,故障转移机制还是非常有用的,强烈推荐。为此,Elasticsearch允许你为索引的分片创建一个或多个副本,这就是所谓的副本分片,或者简称为备份。

备份有两个原因是非常重要的:

  • 在分片或者节点故障的情况下,它提供了高可用性。正因为这一点,注意一个副本分片从来不会被分配和主分片(copy的来源节点)在同一个节点上,这一点是很重要的。
  • 它能提高你的搜索数据量和吞吐量,因为搜索可以并行地在多个副本上执行。

总结一下,一个索引能分成多个分片,一个索引能被复制零(无备份)或多次。一旦复制了之后,每个索引都有主分片(拷贝的来源分片)和副分片(主分片的拷贝)。一个索引的分片和副本的数量都可以在索引创建的时候定义,在索引创建之后,你可以随时动态的改变副本的数量,但是你不能事后修改分片的数量。

默认情况下,在Elasticsearch中的每个索引被分为5个主分片和1个副本,这就意味着在你的集群中至少有两个节点,你的索引将有5个主分片和另外5个副分片(一个完整的副本),每个索引总共10个分片。

注意,每一个Elasticsearch分片都是一个Lucene索引,在一个单独的Lucene索引中拥有的文档数量是有最大值的,根据LUCENE-5843中介绍,这个限制是2,147,483,519 (= Integer.MAX_VALUE - 128)个文档。你可以使用api _cat/shards 来监控分片的大小。

既然如此,我们就开始有趣的下一part吧。

2 安装

Elasticsearch要求java 8 以上,在写该文档时,建议你使用oracle的JDK版本1.8.0_131,关于java在各个平台的安装过程这里就不详细介绍了,在oracle的官网上能够找到oracle推荐的安装文档,我只想说,在你安装Elasticsearch之前,请检查你的当前运行的java版本(根据需要安装或升级java):

java -version
echo $JAVA_HOME

如果你已经安装了java,我们就可以开始下载和运行Elasticsearch了。在 www.elastic.co/downloads
这个网址上可以下载到可用的二进制包,还有过去发行的其它版本的包。对于每一个发行版本,你都可以选择zip或者tar压缩包,还有DEBRPM包。为了简单起见,我们以tar包为例。

让我首先使用下面的命令下载Elasticsearch5.4.1版本的tar文件(windows用户应该下载zip包):

curl -L -O https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-5.4.1.tar.gz

然后,使用下面的命令解压它(windows用户应该使用unzip来解压zip包):

tar -xvf elasticsearch-5.4.1.tar.gz

解压完成之后,将会在当前目录产生大量的文件和文件夹,然后我们进入到bin目录下,命令如下:

cd elasticsearch-5.4.1/bin

现在我们可以准备启动这个节点和单节点集群了(windows用户应该运行.bat文件):

./elasticsearch

如果一切顺利的话,你将看到很多信息输出,像下面这样:

[2016-09-16T14:17:51,251][INFO ][o.e.n.Node               ] [] initializing ...
[2016-09-16T14:17:51,329][INFO ][o.e.e.NodeEnvironment    ] [6-bjhwl] using [1] data paths, mounts [[/ (/dev/sda1)]], net usable_space [317.7gb], net total_space [453.6gb], spins? [no], types [ext4]
[2016-09-16T14:17:51,330][INFO ][o.e.e.NodeEnvironment    ] [6-bjhwl] heap size [1.9gb], compressed ordinary object pointers [true]
[2016-09-16T14:17:51,333][INFO ][o.e.n.Node               ] [6-bjhwl] node name [6-bjhwl] derived from node ID; set [node.name] to override
[2016-09-16T14:17:51,334][INFO ][o.e.n.Node               ] [6-bjhwl] version[5.4.1], pid[21261], build[f5daa16/2016-09-16T09:12:24.346Z], OS[Linux/4.4.0-36-generic/amd64], JVM[Oracle Corporation/Java HotSpot(TM) 64-Bit Server VM/1.8.0_60/25.60-b23]
[2016-09-16T14:17:51,967][INFO ][o.e.p.PluginsService     ] [6-bjhwl] loaded module [aggs-matrix-stats]
[2016-09-16T14:17:51,967][INFO ][o.e.p.PluginsService     ] [6-bjhwl] loaded module [ingest-common]
[2016-09-16T14:17:51,967][INFO ][o.e.p.PluginsService     ] [6-bjhwl] loaded module [lang-expression]
[2016-09-16T14:17:51,967][INFO ][o.e.p.PluginsService     ] [6-bjhwl] loaded module [lang-groovy]
[2016-09-16T14:17:51,967][INFO ][o.e.p.PluginsService     ] [6-bjhwl] loaded module [lang-mustache]
[2016-09-16T14:17:51,967][INFO ][o.e.p.PluginsService     ] [6-bjhwl] loaded module [lang-painless]
[2016-09-16T14:17:51,967][INFO ][o.e.p.PluginsService     ] [6-bjhwl] loaded module [percolator]
[2016-09-16T14:17:51,968][INFO ][o.e.p.PluginsService     ] [6-bjhwl] loaded module [reindex]
[2016-09-16T14:17:51,968][INFO ][o.e.p.PluginsService     ] [6-bjhwl] loaded module [transport-netty3]
[2016-09-16T14:17:51,968][INFO ][o.e.p.PluginsService     ] [6-bjhwl] loaded module [transport-netty4]
[2016-09-16T14:17:51,968][INFO ][o.e.p.PluginsService     ] [6-bjhwl] loaded plugin [mapper-murmur3]
[2016-09-16T14:17:53,521][INFO ][o.e.n.Node               ] [6-bjhwl] initialized
[2016-09-16T14:17:53,521][INFO ][o.e.n.Node               ] [6-bjhwl] starting ...
[2016-09-16T14:17:53,671][INFO ][o.e.t.TransportService   ] [6-bjhwl] publish_address {192.168.8.112:9300}, bound_addresses {{192.168.8.112:9300}
[2016-09-16T14:17:53,676][WARN ][o.e.b.BootstrapCheck     ] [6-bjhwl] max virtual memory areas vm.max_map_count [65530] likely too low, increase to at least [262144]
[2016-09-16T14:17:56,731][INFO ][o.e.h.HttpServer         ] [6-bjhwl] publish_address {192.168.8.112:9200}, bound_addresses {[::1]:9200}, {192.168.8.112:9200}
[2016-09-16T14:17:56,732][INFO ][o.e.g.GatewayService     ] [6-bjhwl] recovered [0] indices into cluster_state
[2016-09-16T14:17:56,748][INFO ][o.e.n.Node               ] [6-bjhwl] started

不过分深入细节的话,我们可以看到名为6-bjhwl(这是一组不同的字符,视你的情况而定)的节点已经启动了,并且它自己被选举为单节点的集群中的master节点。此刻你不用担心master意味着什么,这里重要的事情是,我们已经在一个集群中启动了一个节点。

正如前面所说的,我们可以覆盖集群和节点的名字,这个可以在启动Elasticsearch的命令中做到,命令如下:

./elasticsearch -Ecluster.name=my_cluster_name -Enode.name=my_node_name

同时还要注意信息中的http地址(192.168.8.112)和端口(9200),这是节点的访问入口。默认情况下,Elasticsearch使用端口9200提供REST API的访问,这个端口在必要的时候也是可以配置的。

3 探索集群

REST API

现在我们已经启动了我们的节点(和集群),它们正在运行中,下一步我们需要理解怎样和它交流。幸运的是,Elasticsearch提供了非常全面、强大的REST API,你可以使用这些API接口与你的集群交互。下面这些事情都可以通过API来完成:

  • 检查你的集群、节点和索引的健康情况、状态和一些统计
  • 管理你的集群、节点和索引数据、元数据
  • 执行CRUD(创建、读取、更新和删除)操作和针对索引的搜索操作
  • 执行高级的搜索操作,例如分页、排序、过滤、脚本、聚合,还有很多其他的操作

集群健康

让我们以基本的健康检查开始,健康检查可以让我们了解集群的运行情况。我们使用curl命令来调用这些API,你也可以使用任意其它的HTTP/REST调用工具。假设我们已经登录了运行着Elasticsearch的同一个节点,并且打开了一个shell命令行窗口。

为了检查集群的健康度,我们将使用_cat API.。你可以点击"VIEW IN CONSOLE"在Kibana’s Console上执行下面的命令,或者点击 "COPY AS CURL" 之后粘贴到你的终端上使用curl命令。

GET /_cat/health?v

然后,下面就是响应:

epoch      timestamp cluster       status node.total node.data shards pri relo init unassign pending_tasks max_task_wait_time active_shards_percent
1475247709 17:01:49  elasticsearch green           1         1      0   0    0    0        0             0                  -                100.0%

我们可以从中看到,集群的名字叫“elasticsearch”,是绿色的状态。

无论什么时候我们检查集群的健康状况,都是绿色、黄色、红色。绿色意味着一切都是好的(集群的所有功能都能正常使用);黄色意味着所有数据都是可用的,但是缺少一些分片(集群的所有功能都能正常使用);红色意味着不管什么原因一些数据是不可用的;注意,即使集群的状态是红色,还是有部分功能是可用的(例如,它将继续可以用来从可用的分片上搜索数据),但是你可能需要尽快修复它,因为你已经丢失了数据。

从上面的响应中我们还可以看出,总共就只有一个节点,零个分片是因为里面没有数据。注意我们使用的是默认的集群名字(Elasticsearch),Elasticsearch默认的使用单播网络来发现同一台机器上的其它节点,你也有可能意外地在一台机器上启动了多个节点,然后它们组成了一个集群。在这种情形下,你可能在上面的响应中看到多个节点。

我们也可以查看集群中的所有节点,像下面这样:

GET /_cat/nodes?v

然后,响应如下:

ip        heap.percent ram.percent cpu load_1m load_5m load_15m node.role master name
127.0.0.1           10           5   5    4.46                        mdi      *      PB2SGZY

这里我们可以看出,只有一个名叫“PB2SGZY”的节点,它是当前集群中的唯一的一个节点。

列出所有索引

现在让我们看一下我们的索引:

GET /_cat/indices?v

然后,响应如下:

health status index uuid pri rep docs.count docs.deleted store.size pri.store.size

这仅仅意味着在我们的集群中没有索引。

创建索引

现在让我们创建一个名叫“customer”索引,然后再次列出所有的索引:

PUT /customer?pretty
GET /_cat/indices?v

第一条命令使用动词PUT创建了一个名叫“customer”的索引,在命令的结尾仅仅跟了一个pretty,意思是任何响应都以JSON的形式打印出来。

然后,响应如下:

health status index    uuid                   pri rep docs.count docs.deleted store.size pri.store.size
yellow open   customer 95SQ4TSUT7mWBT7VNHH67A   5   1          0            0       260b           260b

第二条命令的结果告诉我们,现在我们由一个名叫“customer”的索引,它有5个主分片和1个副本(默认),其中没有任何文档。

你可能还注意到customer索引有一个黄色的健康标记,回顾我们之前的讨论,黄色意味着一些分片没有被分配。出现这种情况的原因是,Elasticsearch默认情况下会为这个索引创建一个副本,但是此刻集群中仅有一个节点,只有另一个节点加入到集群中时,这个副本才可能被分配(高可用)。一旦副本被分配到第二个节点中,这个索引的健康状态立马就会变成绿色。

索引和查询文档

现在让我们先在customer索引中放一些东西,还记得之前我们为文档创建索引,必须告诉Elasticsearch文档应该被放在索引中的什么类型下。

让我们将一个简单的客户文档放进customer索引中,external类型中,ID为1,就像下面这样:

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

然后,响应如下:

{
  "_index" : "customer",
  "_type" : "external",
  "_id" : "1",
  "_version" : 1,
  "result" : "created",
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "created" : true
}

从上面的响应中我们可以看出,一个新的客户文档被成功地创建在customer索引的external类型中,这个文档还有一个内部id为1,在索引文档时候它需要被指定。

特别需要注意的是,Elasticsearch没有明确要求你在索引一个文档之前首先创建一个索引,就前面的例子而言,如果事先customer索引不存在,Elasticsearch会自动的创建它。

现在让我们去检索一下刚刚编入索引的那个文档:

GET /customer/external/1?pretty

然后,响应如下:

{
  "_index" : "customer",
  "_type" : "external",
  "_id" : "1",
  "_version" : 1,
  "found" : true,
  "_source" : { "name": "John Doe" }
}

除了其中的found字段没有什么特别的地方,说明我们找到了请求的ID为1的那个文档,另一个字段是_source,它返回了我们上一步中编入索引的完全的JSON格式的文档。

删除索引

现在让我们删除刚刚创建的索引,然后再次列出所有的索引:

DELETE /customer?pretty
GET /_cat/indices?v

然后,响应如下:

health status index uuid pri rep docs.count docs.deleted store.size pri.store.size

这就是说索引已经被成功删除,现在我们又回到了开始时集群中什么都没有的状态。

在继续学习之前,让我们再仔细看看我们学到的一些API命令:

PUT /customer
PUT /customer/external/1
{
  "name": "John Doe"
}
GET /customer/external/1
DELETE /customer

如果我们仔细研究上面的命令,实际上我们可以看到一种如何从Elasticsearch中访问数据的模式,这种模式可以概括如下:

<REST Verb> /<Index>/<Type>/<ID>

这种REST访问模式在所有的API命令中是普遍存在的,如果你能简单地记住它,在掌握Elasticsearch上,你有了一个良好的开端。

4 修改数据

Elasticsearch提供准实时的数据操作和搜索功能。默认情况下,从你创建索引/更新/删除你的数据到展现出搜索结果应该是只有1秒的延迟(刷新间隔),这是与像SQL等其它平台的重要区别,它们中的事务操作完成之后数据立即可用。

索引和替换文档

我们前面已经知道了如何将一个单独的文档编入索引,让我们回顾一下命令:

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

同样,上面的命令将一个指定的文档编入到customer索引中,类型为external,ID为1. 如果我们使用不同(或者相同)的文档再次执行上面的命令,Elasticsearch将在现存的ID为1的上面替换(或者重建)一个新文档:

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

上面的命令改变了ID为1的文档的名字,由 "John Doe" 改为 "Jane Doe"。另一方面,如果我们使用不同的ID,一个新的文档将会被编入索引,而索引中已经存在的文档保持不变。

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

上面的命令将一个ID为2的新文档编入了索引。

当我们索引文档的时候,ID部分是可选的,如果你不指定的话,Elasticsearch将随机生成一个ID,然后使用它去索引文档。Elasticsearch实际生成的ID(或者是前面例子中我们明确指定的ID)将作为索引API调用的一部分。

下面的例子展示了在不明确指定ID的情况下如何索引一个文档:

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

注意,上面的例子中我们使用的是动词POST,而不是PUT,因为我们没有指定ID。

更新文档

另外,除了可以索引和替换文档之外,我们还可以更新文档。需要注意的是,Elasticsearch背后并没有真正的做更新操作,无论我们什么时候做更新操作,Elasticsearch都是删除旧的文档,然后将更新后的文档一次性地作为一个新文档编入索引。

下面的例子展示了,如何更新前面的文档(ID为1),将它的name字段改为"Jane Doe":

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

下面的例子展示了,如何更新前面的文档(ID为1),将它的name字段改为"Jane Doe",同时增加一个新的字段age:

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

更新操作也可以使用简单的脚本,下面的例子使用脚本将age字段的值增加5:

POST /customer/external/1/_update?pretty
{
  "script" : "ctx._source.age += 5"
}

在上面的例子中,ctx._source 是指即将更新的当前文档。

注意,在写这篇文档的时候,更新操作还只能一次更新一个文档。未来,elasticsearch可能提供根据查询条件更新多个文档的能力(就像SQL UPDATE-WHERE语句一样)。

删除文档

删除文档相对来说是比较简单的,下面的例子展示了如何删除ID为2的客户文档:

DELETE /customer/external/2?pretty

参见 Delete By Query API 删除指定查询条件的所有文档。删除整个索引比根据查询API删除该索引下的所有文档更高效。

批处理

我们除了能索引、更新和删除单个文档,Elasticsearch还提供了使用 _bulk
API
批量执行上面的任何操作。这个功能是很重要的,它提供了一种尽可能快并且尽量少的网络切换条件下执行多个操作的高效机制。

做一个快速示范,下面是使用一个bulk操作将两个文档(ID 1 - John Doe and ID 2 - Jane Doe)编入索引的例子:

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

下面的例子使用一个bulk操作更新第一个文档(ID为1),然后删除第二个文档(ID为2):

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

注意上面的删除操作,在它的后面没有指定对应的源文档,因为删除操作仅需要被删除文档的ID。

多个操作中的一个失败不会导致整个bulk操作的失败,如果其中一个操作不管什么原因失败了,将会继续执行它后面剩下的操作。在bulk操作的返回信息中将提供每个操作的执行状态,因此你可以检查某个指定的操作是否失败了。

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,希望能对你的学习有所帮助。

推荐阅读更多精彩内容