Elasticsearch (简称ES) 是一个高度可伸缩全文搜索和分析引擎。 你可以用它快速近实时存储、搜索、分析大量数据。 它通常作为应用内一个提供复杂查询的引擎组件。

下面是ES的几个使用场景:

  1. 在电商场景下, 用户需要搜索商店种的商品。 这个时候你可以使用ES存储商品目录和清单,给用户提供搜索功能和自动完成功能。

  2. 如果你想收集日志或者事物数据然后进行分析挖掘,发现其中的趋势、统计、总结或者异常。 这种情况下,你可以使用Logstash(ELK技术栈的一个)来收集、聚合分析数据,
    然后Logstash将这些数据输出到ES, 一旦这些数据进入倒了ES, 你就可以运行搜索和聚合来挖掘任何感兴趣的信息。

  3. 你运行着一个价格提醒平台, 允许那些对价格感兴趣的用户指定规则, 比如 “我想买一个电子产品, 如果下个月内这个商品价格降到低于$x的时候通知我”。 这种情况下你可以抓取
    各供应商的此商品价格, 然后推入ES,利用ES的反向搜索特性去查询符合用户需求的价格,将匹配的结果推送给用户。

  4. 你有一个分析/BI需求, 需要在大量的数据上(百万或者数十亿记录)进行调查、分析、可视化并且提一些特定的问题。 这种情况下,你可以使用ES存储数据然后使用
    Kibana(ELK技术栈的一部分)定制专属的仪表盘,从各个方面展示数据。 此外你还可以使用ES的聚合功能来进行一些复杂的查询。

在下面的部分, 本文将指导你安装并启动一个ES实例, 简单了解一下它的用法, 例如索引、搜索、修改数据。 在阅读本文以后, 你应该知道了什么是ES, 它是怎么工作的,
希望你能受到启发怎么使用ES构建你的搜索应用或者挖掘你的数据。

1. 基本概念

ES有几个核心概念, 了解这些概念将极大的促进你的学习进程。

近实时(NRT)

ES是一个近实时搜索平台。 这意味着从你索引一个文档到这个文档可以备搜索有很小的延迟(大约1秒钟)。

集群

集群是一个或多个机器/节点管理你的所有数据, 在这些所有节点上提供联和索引和搜索功能。 一个集群通过一个唯一的集群名标识(默认是 elasticsearch)。 集群名非常重要,
只有当一个节点的集群名与集群一致的时候, 此节点才能加入到集群内。

节点

节点是集群中的一个单服务,有存储数据、参与集群索引和搜索的功能。 类似于集群, 一个节点也是通过一个唯一的节点名来标识, 默认的节点名是启动的时候生成的一个UUID,
你可以定义而任意节点名代替默认的。 这个名字很重要。当你进行集群管理的时候, 查看哪些节点在当前的网络中以及哪些节点在集群中, 都需要这个节点名来辨认。

一个节点可以配置集群名加入某个集群。默认情况下, 每个节点都加入名字为 elasticsearch 的集群, 这意味着, 如果你在一个网络内启动了多个节点, 并且假如他们能够发现彼此
,他们将自动组成一个集群, 叫 elasticsearch

索引

索引是一系列有共同特征的文档的集合。 例如, 你可以为顾客数据创建一个索引, 为产品的目录创建一个索引,还有另外一个订单索引。 一个索引通过索引名(必须是全小写)来标识。
当进行索引、搜索、更新、删除文档的时候需要此索引名来指明索引。

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

Type 类型/映射

在索引内, 可以定义一个或多个映射。 映射可以理解为逻辑上的索引分区, 一个映射的用途完全取决于用户。 一般来说, 映射用来定义有一系列相同属性的文档。 例如,
假设你有一个博客平台,并且将数据存入了一个索引。 在这个索引内部, 你可以为User定义一个映射, 为Blog定义一个映射, 为评论定义一个映射。

文档

文档是可索引信息的基本单元。 例如, 你有一个文档对应于一个用户, 另一个文档对应于一个商品, 另一个文档对应于订单。 文档以JSON表示。

在索引的映射里面, 你可以存储任意多的文档。 虽然文档在物理上属于索引, 但是文档必须指定索引内的映射类型。 也就是说, 我们先定义索引,
然后创建映射, 然后将文档多索引到映射里面。

分片 & 复制

一个索引可能存储非常多的数据以至于超过单个节点的硬件限制。 例如一个有数十亿文档的索引, 大概占据1TB的磁盘空间, 这样可能就不适合存在一个节点(尽管可以这么做),
因为可能会导致查询搜索非常缓慢。

为了解决这个问题, ES提供了将索引细分到多个机器上面的能力, 称为分片。 在创建索引的时候, 可以指定此索引有几个分片。 每个分片都是独立、完整的,
可以存于集群中的任意一个节点。

分片特性非常重要, 有以下原因:

  1. 可以水平伸缩、扩容
  2. 可以在多个分片之间分布式、并行的操作查询(可能是在多个节点上), 这样可以增加性能/吞吐量.

至于分片如何分布、索引的文档如何聚合回到查询请求里面, 这些都是ES本身就提供的, 对用户是透明的。

基于以下两点原因,复制也很重要:

  1. 它提供了高可用防止节点或者分片不可用。 从这点来说, 从分片不会跟主分片分配到同一个节点上面。
  2. 可以扩展查询吞吐量, 因为查询可以并行的在从分片执行。 这一点类似于上面分片的功能。

总结一下, 每个索引可以切分到多个分片上。 一个索引也可以有0个或多个复制副本。 一旦配置了复制, 每个索引将会有一个主分片和从分片。分片的数量和复制的
数量可以在创建索引的时候指定。 索引创建之后, 可以动态修改复制的数量,但是不可修改分片的数量。

默认情况下, 每个索引分配5个主分片和1个从分片. 这意味着如果集群中有两个节点, 你的索引将包括5个主分片和另外5个从分片, 这样每个索引就是10个分片。

每个ES的索引都是一个Lucene的索引。 Lucene的索引中的文档有最大个数限制: 根据LUCENE-5843描述,
限制是147,483,519 (= Integer.MAX_VALUE - 128)。 你可以使用
_cat/shards api监控分片大小。

2. 安装

ES需要java8. 根据这篇文章, 推荐使用Oracle JDK version 1.8.0_73. 在安装ES之前, 先检查java的版本号, 通过命令

java -version
echo $JAVA_HOME

一旦Java安装完毕, 就可以下载安装ES了。 二进制的包可以从http://www.elastic.co/downloads下载, 对于每个发布版本,
你可以选择下载zip包或者tar包, 或者DEB 以及 RPM包。 为了简单这里我们使用tar包。

按照下面方法下载ES5.1.1(windows应该下载zip包):

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

解压:

tar -xvf elasticsearch-5.1.1.tar.gz

解压完成后, 在当前目录会生成一堆文件和文件夹。 进入到 bin 目录:

cd elasticsearch-5.1.1/bin

现在就可以运行这个节点:

./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.1.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”(在你得试验中可能不一样),此节点已经启动并且选举为了单节点集中的主节点。 现在先不用关心主节点的意思。
重要的是现在我们已经启动了一个单节点的集群。

我们之前提到, 我们可以覆盖默认的集群名和节点名, 这个操作也可以在命令行下完成:

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

另外主意http的那一段输出, 我们可以使用192.168.8.112:9200访问此节点, ES默认使用9200端口提供REST API。 此端口也可以更改。

3. 管理集群

REST API

现在我们启动并运行了一个节点, 下一步是弄明白怎么简单的使用ES。 幸运的是ES提供了非常强大且易于理解REST API, 使用这些API与集群交互。 比如我们可以用API做一下事情:

  1. 检查集群、节点、索引的健康、状态、统计信息等
  2. 管理集群、节点、索引数据和元数据
  3. 在索引上执行CRUD操作以及搜索操作
  4. 执行高级搜索, 例如分页,排序,过滤,脚本查询,聚合以及其他高级特性

集群健康状态

我们从基本的健康检查开始, 通过健康检查我们可以知道集群的运行状态。 我们用curl命令来发送HTTP/HTTPS请求, 不过你也可以使用任何其他工具。
假设我们仍然使用之前启动的节点。

我们使用 _cat API 来检查健康状态。 在命令行输入

curl -XGET 'localhost:9200/_cat/health?v&pretty'

pretty参数用于格式化json输出。 能看到输出:

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的集群已经启动,是"green"状态。

ES有三种状态, green, yellow, red。
green是正常状态, 表示一切都很好。
yellow表示数据分片正常但是有复制分片不可用(集群依然正常可用)。
red表示有一些数据分片不可用。 主意集群即使是red状态, 集群的功能仍然是部分可用(集群可以继续在剩余的分片上执行查询), 但是你应该尽快修复此问题,因为已经有数据丢失了。

在上面的输出, 可以看到总共1个节点0个分片, 因为里面还没有数据。 注意我们用的是默认的集群名elasticsearch, ES使用网络广播来发现网络内的其他节点,所以你可能碰巧启动了
多个节点并且他们都加入了一个集群。 这种情况下你可能看到多个节点。

也可以获取集群中的所有节点信息:

curl -XGET 'localhost:9200/_cat/nodes?v&pretty'

输出类似:

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

查看所有索引

现在看一下索引:

curl -XGET 'localhost:9200/_cat/indices?v&pretty'

输出:

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

意味着集群中尚未有索引。

创建索引

现在创建一个叫做 customer的索引, 再来看一下索引

curl -XPUT 'localhost:9200/customer?pretty&pretty'
curl -XGET 'localhost:9200/_cat/indices?v&pretty'

第一个命令创建一个customer的索引, 使用PUT方法。 查看输出:

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个复制分片0个文档。

你可能注意到customer索引的健康状态是yellow, 我们之前说过yellow表示没有复制分片, 造成这种情况的原因是ES默认为索引创建一个复制分片, 但是我们只有一个node,
而复制分片不能与主分片在同一个node上面, 所以无法分配复制分片。 如果有另一个node加入进来, 并且复制分片分配到另一个node上面, 健康状态就变成green了。

索引和查询一个文档

现在我们向customer索引中添加文档。 前面说过, 向索引添加文档之前,需要先创建映射。 我们来向customer索引中的external类型添加一个文档, 文档id设置为1:

curl -XPUT 'localhost:9200/customer/external/1?pretty&pretty' -d'
{
  "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.

向索引中添加文档的时候, ES并不强制要先创建索引。 在上面的例子中, 如果索引不存在, ES会自动创建一个。

现在我们看一下刚刚添加的文档:

curl -XGET 'localhost:9200/customer/external/1?pretty&pretty'

输出:

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

输出都很正常, 除了 found_source, 表示我们找到了id为1的文档。_source 表示我们索引的时候填充的json。

删除索引

我们删除刚才创建的索引, 然后再列出索引看一下;

curl -XDELETE 'localhost:9200/customer?pretty&pretty'
curl -XGET 'localhost:9200/_cat/indices?v&pretty'

输出:

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

表示索引已经成功删除了。 现在我们回到了集群中没有任何东西的状态。

在继续向下学习之前, 我们先看一下目前学习到的REST API:

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

curl命令:

curl -XPUT 'localhost:9200/customer?pretty'
curl -XPUT 'localhost:9200/customer/external/1?pretty' -d'
{
  "name": "John Doe"
}'
curl -XGET 'localhost:9200/customer/external/1?pretty'
curl -XDELETE 'localhost:9200/customer?pretty'

如果学习的足够仔细, 应该可以发现我们访问ES数据的模式:

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

这个模式在ES的API中很常见, 如果你记住了这个模式, 那么管理ES就有了一个好的开始。

4. 修改数据

ES提供近实时的操作数据以及搜索的功能。 一般情况下, 从你增加/修改/删除数据到影响搜索结果, 大概需要1秒钟时间。 这一点与其他数据库比如SQL不同,
数据库在事物一提交就立即生效了。

索引/替换文档

我们前面说了如何索引文档:

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

curl:

curl -XPUT 'localhost:9200/customer/external/1?pretty&pretty' -d'
{
  "name": "John Doe"
}'

现在我们使用另一个不同的文档或者相同的文档重复执行上面的命令,ES将替换存在的文档:

curl -XPUT 'localhost:9200/customer/external/1?pretty&pretty' -d'
{
  "name": "Jane Doe"
}'

上面的操作把id是1的文档的名字, 从 John Doe 改为 Jane Doe。 如果我们使用不同的id, 那么ES将新增一个索引文档, 已有的文档不受影响。

curl -XPUT 'localhost:9200/customer/external/2?pretty&pretty' -d'
{
  "name": "Jane Doe"
}'

上面操作新索引了一个id为2的文档。

索引文档的时候, id是可选的。 如果没有指定id, ES将生成随机id。 生成的id或者我们显式指定的id会在返回结果中体现。

下面的例子显示了不指定id来索引文档:

curl -XPOST 'localhost:9200/customer/external?pretty&pretty' -d'
{
  "name": "Jane Doe"
}'

注意上面的操作我们使用了POST来代替PUT, 因为我们没有指定id。

更新文档

除了可以替换和新增文档, 我们还能更新文档。 注意ES内部并不进行更新, 当我们指定update操作的时候,ES在一个操作中删除旧的文档然后索引新的文档。
下面的例子将之前id为1的文档的name字段, 更新为 Jane Doe:

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

curl:

curl -XPOST 'localhost:9200/customer/external/1/_update?pretty&pretty' -d'
{
  "doc": { "name": "Jane Doe" }
}'

更新name字段同时增加age字段:

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

更新的时候也可以使用script脚本更新, 下面的例子使用脚本将age字段增加5:

curl -XPOST 'localhost:9200/customer/external/1/_update?pretty&pretty' -d'
{
  "script" : "ctx._source.age += 5"
}'

上面的例子中,ctx._source 指将要更新的文档。

注意目前更新只能每次更新一个文档。 将来ES也许会支持更新多个文档。

删除文档

删除一个文档非常直观简单, 下面删除id为2的文档:

curl -XDELETE 'localhost:9200/customer/external/2?pretty&pretty'

查阅条件删除API来删除指定条件的文档,
如果要删除所有文档, 可以直接删除索引, 这样比按照条件删除所有文档更有效率。

批量处理

除了可以对单个文档进行增删改查, ES还提供了批量执行增删改查的能力。
批量操作使用批量API
这个功能提供了高效的操作多个数据的方法, 并且只占用极少量的网络流量。

一个简单的例子, 下面操作批量更新两个文档(id为1的和id为2的):

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

curl:

curl -XPOST 'localhost:9200/customer/external/_bulk?pretty&pretty' -d'
{"index":{"_id":"1"}}
{"name": "John Doe" }
{"index":{"_id":"2"}}
{"name": "Jane Doe" }'

下面的例子更新id为1的文档删除id为2的文档:

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

curl:

curl -XPOST 'localhost:9200/customer/external/_bulk?pretty&pretty' -d'
{"update":{"_id":"1"}}
{"doc": { "name": "John Doe becomes Jane Doe" } }
{"delete":{"_id":"2"}}'

注意上面的删除动作, 没用对应的文档信息, 因为删除只需要id就可以了。

批量API依次顺序执行所有的操作, 如果有一个动作失败了, ES会跳过这个操作继续执行后续的操作。 当批量API返回的时候, 会提供每个操作的执行结果状态,
可以用来检查每个操作是不是成功了。

5. 浏览数据

现在我们基本了解了ES, 下面我们试试在更真实的数据集上面工作。 我准备了一些虚构的银行客户的账号文档。 文档的结构如下:

{
    "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来生成的, 所以不要考虑文档的属性不合理的值,因为他们都是随机生成的。

加载数据集

curl -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

显示我们向bank索引的account类型中添加了1000个文档。

搜索API

我们从一些简单的搜索开始。 有两种基本的执行查询的方法: 一种是通过将查询参数通过url参数传递, 另一种是通过request body 传递。 第二种方法查询更清晰, 并且可以
传递格式化好的json。 本文对两种方式都会有介绍。

搜索的api以_search结尾, 下面的查询返回bank索引的所有的文档:

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

curl -XGET 'localhost:9200/bank/_search?q=*&sort=account_number:asc&pretty&pretty'

我们来分析一下上面的查询请求, 我们在bank索引下搜索, q=* 参数表示查询索引内的所有文档, sort=account_number:asc参数表示按照account_number正序排序,
pretty参数用于格式化输出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"}
    }, ...
    ]
  }
}

从输出结果,我们可以看到如下信息:

  1. took - 执行搜索的时间, 毫秒数
  2. timed_out - 告诉我们查询是否超时了
  3. _shards - 告诉我们搜索了多少各分片, 以及成功查询了几个分片,失败了几个分片
  4. hits - 查询结果
  5. hits.total - 查询结果的总数量
  6. hits.hits - 真正的查询结果数组
  7. sort - 结果集排序的序号, 如果按照得分来排序, 此字段不显示
  8. _scoremax_score - 现在先不用管这两个字段的意思, 后续会有说明。

同样的查询, 使用request body 传递参数:

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

curl:

curl -XGET 'localhost:9200/bank/_search?pretty' -d'
{
  "query": { "match_all": {} },
  "sort": [
    { "account_number": "asc" }
  ]
}'

不同之处就是这个省略了q=* 参数, 而是将json格式的查询放到了request body内。 下一节我们将讨论json的结构。

需要注意的一点是, 一旦查询结果返回了, ES就执行完了这个查询, ES不会维护任何服务端资源, 也不会打开游标。 这点与其他的不同, 比如在SQL中,
你可能得到查询的一部分数据, 当你请求下一页的时候, 程序需要使用服务端状态标识比如cursor不断的去服务端获取下一页数据。

查询语言简介

ES提供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
}

curl

curl -XGET 'localhost:9200/bank/_search?pretty' -d'
{
  "query": { "match_all": {} },
  "size": 1
}'

size如果不传,默认为10.

这个例子查询所有文档, 返回第11到20个文档:

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

from 参数(从0开始) 指定开始返回文档的下标, 默认是0。 size 指定共返回多少各文档, 默认是10。 fromsize 类似于 mysql 的limit
这个特性在分页的时候非常有用。

下面的例子查询所有的文档, 按照balance倒序排序,并返回前10个文档:

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

执行查询

通过前面的介绍我们知道了一些基本的查询参数。 现在我们深入了解一下查询语言。 先看一下返回的文档字段。 默认情况下, es在返回结果的_source字段中返回文档的所有字段。
如果不想返回所有字段, 也可以仅仅请求部分字段。

下面的例子在请求参数中使用 _source 参数, 指定返回的字段:

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

curl:

curl -XGET 'localhost:9200/bank/_search?pretty' -d'
{
  "query": { "match_all": {} },
  "_source": ["account_number", "balance"]
}'

上面的查询就会在_source 字段中只返回 "account_number" 和 "balance" 两个字段。

如果你用过SQL, 这个性质类似于SQL的 select column_list from 这个概念。

现在看一下查询部分。 前面我们学习了怎么使用match_all来查询所有文档。 现在引入match查询(可以理解为按照文档的基本属性查询)。

下面的例子返回account_number=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_phrase, 它是match的一个变种。下面的例子返回address中包含"mill lane"的文档:

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

下面看一下bool query。 使用bool可以将多个小查询组成一个大的复杂的查询。

下面的例子将两个match查询组合起来, 查询address中包含mill和lane的文档:

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

curl:

curl -XGET 'localhost:9200/bank/_search?pretty' -d'
{
  "query": {
    "bool": {
      "must": [
        { "match": { "address": "mill" } },
        { "match": { "address": "lane" } }
      ]
    }
  }
}'

上面的例子中, bool查询的must指定文档必须同时符合两个条件。

相反的, 下面的例子将两个match查询组合起来, 查询address中包含mill或者lane的文档:

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

上面的例子, bool查询的should 指定文档至少要满足多个条件中的一个。

下面的例子将两个match查询组合起来, 查询address中即不包含mill又不包含lane的文档:

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

must_not 指定文档不满足所有条件。

我们可以将mustshouldmust_not在一个bool中组合起来使用,甚至还可以将bool查询嵌套使用。

下面的例子, 查询所有age为40但是state不包含ID:

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

执行Filter

前面的部分, 我们省略了文档的评分score(返回结果中的_score字段), score是一个数值型的值, 用于表示这个文档跟我们查询的条件的匹配程度。 score越高,
表示文档与查询条件越匹配, 反之则表示越不匹配。

但是并不是所有的查询都需要生成score,尤其是使用filter查询。 ES会自动检测这种情况, 然后优化查询的执行过程, 不计算score。

我们前面介绍的bool查询同样支持 filter子句,用于限制查询的文档符合某些条件, 但是不用计算score。 举例来说, 我们看一下可以查询范围的range查询,range通常
用户过滤日期或者数值。

这个例子用bool 查询来查询balance在2000(包含)到3000(包含)之间的文档:

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

分析上面的查询, bool查询有一个must子句和一个filter子句,我们可以将任意其他的查询都改为这种query和filter模式。 在上面的例子中, 上面的查询在所有的
文档中过滤balance在2000-3000之间的, 所以score没有任何意义。 不存在一个文档比另一个文档更匹配的情况。

除了match_all,match, bool,range 查询, 还有其他类型的查询, 这里先不深入去讨论了。 我们已经了解了这些查询的工作方式,
那么去学习和使用其他类型的查询应该也不难。

聚合

聚合提供了对数据的分组和统计的功能。 学习ES聚合的简单方法就是对比着SQL的group和其他聚合方法。 在ES中可以返回查询的结果和聚合的结果一起返回回来。
这个功能可以让你用简明的api在同一个请求中执行查询和聚合并将结果一起返回, 避免了多次网络请求。

从下面的这个例子开始, 按照文档的state字段分组, 按照分组后states的数量倒叙排序(默认)然后返回前10个(默认):

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

curl:

curl -XGET 'localhost:9200/bank/_search?pretty' -d'
{
  "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
      } ]
    }
  }
}

注意我们设置size=0禁止显示搜索结果, 因为我们仅仅想要聚合的结果。

在上面例子的基础上, 这个例子计算每个state的balance的平均值, 按照分组后states的数量倒叙排序(默认)然后返回前10个(默认):

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

curl:

curl -XGET 'localhost:9200/bank/_search?pretty' -d'
{
  "size": 0,
  "aggs": {
    "group_by_state": {
      "terms": {
        "field": "state.keyword"
      },
      "aggs": {
        "average_balance": {
          "avg": {
            "field": "balance"
          }
        }
      }
    }
  }
}'

注意average_balancegroup_by_state内部。

这个是聚合查询的通用模式, 你可以在聚合操作里面嵌套聚合, 用于抽取想要的任何数据。

再上面的例子基础上, 增加按照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), 然后按照性别gender, 最后获取每个年龄段每个性别的平均balance:

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. 总结

ES是一个即简单又复杂的产品, 目前为止我们学习了ES的基础操作以及一些简单的REST APIs。 希望本文能让你理解什么是elasticsearch, 更重要的是,
希望你能继续探索ES的其他伟大的特性。

本文翻译自ES官网文档。 转载请注明出处。
如翻译有误, 请联系作者: wp_mailbox@163.com

Q.E.D.