Elasticesearch是一个实时分布式搜索分析引擎,它能让你以前所未有的速度和规模,去探索你的数据。它被用作全文检索结构化搜索分析,以及这三个功能的组合。
Elasticsearch 能运行在你的笔记本电脑上,或者扩展到数百台服务器上来处理PB级数据。
Elasticsearch 中没有一个单独的组件是全新的或者是革命性的。全文搜索很久之前就已经可以做到了, 就像很早之前出现的分析系统和分布式数据库。 革命性的成果在于将这些单独的,有用的组件融合到一个单一的、一致的、实时的应用中。对于初学者而言它的门槛相对较低, 而当你的技能提升或需求增加时,它也始终能满足你的需求。
如果你现在打开这本书,是因为你拥有数据。除非你准备使用它 做些什么 ,否则拥有这些数据将没有意义。
不幸的是,大部分数据库在从你的数据中提取可用知识时出乎意料的低效。 当然,你可以通过时间戳或精确值进行过滤,但是它们能够全文检索处理同义词通过相关性给文档评分么? 它们能从同样的数据中生成分析与聚合数据吗?最重要的是,它们能实时地做到上述操作,而不经过大型批处理的任务么?
这就是 Elasticsearch 脱颖而出的地方:Elasticsearch 鼓励你去探索与利用数据,而不是因为查询数据太困难,就让它们烂在数据仓库里面。


1. 你知道的,为了搜索……

Elasticsearch是一个开源的搜索引擎,建立在一个全文搜索引擎库 Apache Lucene™ 基础之上。 Lucene 可以说是当下最先进、高性能、全功能的搜索引擎库—无论是开源还是私有。
但是 Lucene 仅仅只是一个库。为了充分发挥其功能,你需要使用 Java 并将 Lucene 直接集成到应用程序中。 更糟糕的是,您可能需要获得信息检索学位才能了解其工作原理。Lucene 非常 复杂。
Elasticsearch也是使用Java编写的,它的内部使用Lucene做索引与搜索,但是它的目的是使全文检索变得简单, 通过隐藏Lucene的复杂性,取而代之的提供一套简单一致的RESTful API
然而,Elasticsearch 不仅仅是 Lucene,并且也不仅仅只是一个全文搜索引擎。 它可以被下面这样准确的形容:

  • 一个分布式的实时文档存储,每个字段 可以被索引与搜索
  • 一个分布式实时分析搜索引擎
  • 能胜任上百个服务节点的扩展,并支持 PB 级别的结构化或者非结构化数据

    1.1 安装并允许Elasticsearch

1.2 和Elasticsearch交互

和 Elasticsearch 的交互方式取决于你是否使用 Java

Java API

如果你正在使用 Java,在代码中你可以使用 Elasticsearch 内置的两个客户端:

(1)节点客户端(Node client)

节点客户端作为一个非数据节点加入到本地集群中。换句话说,它本身不保存任何数据,但是它知道数据在集群中的哪个节点中,并且可以把请求转发到正确的节点。

(2)传输客户端(Transport client)

轻量级的传输客户端可以将请求发送到远程集群。它本身不加入集群,但是它可以将请求转发到集群中的一个节点上。
两个 Java 客户端都是通过 9300 端口并使用 Elasticsearch 的原生 传输 协议和集群交互。集群中的节点通过端口 9300 彼此通信。如果这个端口没有打开,节点将无法形成一个集群。

RESTful API with JSON over HTTP

所有其他语言可以使用 RESTful API 通过端口 9200 和 Elasticsearch 进行通信,你可以用你最喜爱的 web 客户端访问 Elasticsearch 。事实上,正如你所看到的,你甚至可以使用 curl 命令来和 Elasticsearch 交互。

1.3 面向文档

在应用程序中对象很少只是一个简单的键和值的列表。通常,它们拥有更复杂的数据结构,可能包括日期、地理信息、其他对象或者数组等。
也许有一天你想把这些对象存储在数据库中。使用关系型数据库的行和列存储,这相当于是把一个表现力丰富的对象塞到一个非常大的电子表格中:为了适应表结构,你必须设法将这个对象扁平化—通常一个字段对应一列—而且每次查询时又需要将其重新构造为对象。
Elasticsearch是面向文档 的,意味着它存储整个对象或文档。Elasticsearch不仅存储文档,而且索引每个文档的内容,使之可以被检索。在 Elasticsearch 中,我们对文档进行索引、检索、排序和过滤—而不是对行列数据。

JSON

ES使用JSON作为文档的序列化格式。
下面这个 JSON 文档代表了一个 user 对象:

  1. {
  2. "email": "john@smith.com",
  3. "first_name": "John",
  4. "last_name": "Smith",
  5. "info": {
  6. "bio": "Eco-warrior and defender of the weak",
  7. "age": 25,
  8. "interests": [ "dolphins", "whales" ]
  9. },
  10. "join_date": "2014/05/01"
  11. }

1.4 基本概念

  • 一个 Elasticsearch 集群可以包含多个索引
  • 每个索引可以包含多个类型(类型已废除,ES已不存在类型的概念)
  • 这些不同的类型存储着多个文档
  • 每个文档又有多个属性

    1.5 分布式特性

    Elasticsearch 天生就是分布式的,并且在设计时屏蔽了分布式的复杂性。
    Elasticsearch 在分布式方面几乎是透明的。教程中并不要求了解分布式系统、分片、集群发现或其他的各种分布式概念。可以使用笔记本上的单节点轻松地运行教程里的程序,但如果你想要在 100 个节点的集群上运行程序,一切依然顺畅。
    Elasticsearch 尽可能地屏蔽了分布式系统的复杂性。这里列举了一些在后台自动执行的操作:

  • 分配文档到不同的容器或分片中,文档可以储存在一个或多个节点中

  • 按集群节点来均衡分配这些分片,从而对索引和搜索过程进行负载均衡
  • 复制每个分片以支持数据冗余,从而防止硬件故障导致的数据丢失
  • 将集群中任一节点的请求路由到存有相关数据的节点
  • 集群扩容时无缝整合新节点,重新分配分片以便从离群节点恢复

当阅读本书时,将会遇到有关 Elasticsearch 分布式特性的补充章节。这些章节将介绍有关集群扩容故障转移(集群内的原理) 、应对文档存储(分布式文档存储) 、执行分布式搜索(执行分布式检索) ,以及分区(shard)及其工作原理(分片内部原理) 。


2. 集群内的原理

2.1 空集群

一个运行中的 Elasticsearch 实例称为一个节点,而集群是由一个或者多个拥有相同 cluster.name 配置的节点组成, 它们共同承担数据和负载的压力。当有节点加入集群中或者从集群中移除节点时,集群将会重新平均分布所有的数据。
当一个节点被选举成为主节点时, 它将负责管理集群范围内的所有变更,例如增加、删除索引,或者增加、删除节点等。 而主节点并不需要涉及到文档级别的变更和搜索等操作,所以当集群只拥有一个主节点的情况下,即使流量的增加它也不会成为瓶颈。 任何节点都可以成为主节点。我们的示例集群就只有一个节点,所以它同时也成为了主节点。
作为用户,我们可以将请求发送到 集群中的任何节点 ,包括主节点。 每个节点都知道任意文档所处的位置,并且能够将我们的请求直接转发到存储我们所需文档的节点。 无论我们将请求发送到哪个节点,它都能负责从各个包含我们所需文档的节点收集回数据,并将最终结果返回給客户端。 Elasticsearch 对这一切的管理都是透明的。

2.2 集群健康

2.3 添加索引

我们往Elasticsearch添加数据时需要用到索引——保存相关数据的地方。索引实际上是指向一个或者多个物理 分片的逻辑命名空间
一个 分片 是一个底层的 工作单元 ,它仅保存了全部数据中的一部分。 在分片内部机制中,我们将详细介绍分片是如何工作的,而现在我们只需知道一个分片是一个 Lucene 的实例,以及它本身就是一个完整的搜索引擎。 我们的文档被存储和索引到分片内,但是应用程序是直接与索引而不是与分片进行交互。
Elasticsearch 是利用分片将数据分发到集群内各处的。分片是数据的容器,文档保存在分片内,分片又被分配到集群内的各个节点里。 当你的集群规模扩大或者缩小时, Elasticsearch 会自动的在各节点中迁移分片,使得数据仍然均匀分布在集群里。
一个分片可以是 主 分片或者 副本 分片。 索引内任意一个文档都归属于一个主分片,所以主分片的数目决定着索引能够保存的最大数据量。
一个副本分片只是一个主分片的拷贝。副本分片作为硬件故障时保护数据不丢失的冗余备份,并为搜索和返回文档等读操作提供服务。
在索引建立的时候就已经确定了主分片数(默认5个,可设置),但是副本分片数可以随时修改。

2.4 添加故障转移

2.5 水平扩容

2.6 应对故障


3. 数据输入和输出

面向对象编程语言如此流行的原因之一是对象帮我们表示和处理现实世界具有潜在的复杂的数据结构的实体。但是当我们需要存储这些实体时问题来了,传统上,我们以行和列的形式存储数据到关系型数据库中,相当于使用电子表格。 正因为我们使用了这种不灵活的存储媒介导致所有我们使用对象的灵活性都丢失了。
但是否我们可以将我们的对象按对象的方式来存储?这样我们就能更加专注于 使用 数据,而不是在电子表格的局限性下对我们的应用建模。 我们可以重新利用对象的灵活性。
一个 对象 是基于特定语言的内存的数据结构。为了通过网络发送或者存储它,我们需要将它表示成某种标准的格式。 JSON 是一种以人可读的文本表示对象的方法。 它已经变成 NoSQL 世界交换数据的事实标准。当一个对象被序列化成为 JSON,它被称为一个 JSON 文档 。
Elastcisearch 是分布式的 文档 存储。它能存储和检索复杂的数据结构—序列化成为JSON文档—以 实时 的方式。 换句话说,一旦一个文档被存储在 Elasticsearch 中,它就是可以被集群中的任意节点检索到。
在Elasticsearch中, 每个字段的所有数据都是默认被索引的。 即每个字段都有为了快速检索设置的专用倒排索引。而且,不像其他多数的数据库,它能在同一个查询中使用所有这些倒排索引,并以惊人的速度返回结果。

3.1 什么是文档?

在大多数应用中,多数实体或对象可以被序列化为包含键值对的 JSON 对象。 一个 键 可以是一个字段或字段的名称,一个 值 可以是一个字符串,一个数字,一个布尔值, 另一个对象,一些数组值,或一些其它特殊类型诸如表示日期的字符串,或代表一个地理位置的对象。

  1. {
  2. "name": "John Smith",
  3. "age": 42,
  4. "confirmed": true,
  5. "join_date": "2014-06-01",
  6. "home": {
  7. "lat": 51.5,
  8. "lon": 0.1
  9. },
  10. "accounts": [
  11. {
  12. "type": "facebook",
  13. "id": "johnsmith"
  14. },
  15. {
  16. "type": "twitter",
  17. "id": "johnsmith"
  18. }
  19. ]
  20. }

通常情况下,我们使用的术语 对象 和 文档 是可以互相替换的。不过,有一个区别: 一个对象仅仅是类似于 hash 、 hashmap 、字典或者关联数组的 JSON 对象,对象中也可以嵌套其他的对象。 对象可能包含了另外一些对象。在 Elasticsearch 中,术语 文档 有着特定的含义。它是指最顶层或者根对象, 这个根对象被序列化成 JSON 并存储到 Elasticsearch 中,指定了唯一 ID。

3.2 文档元数据

一个文档不仅仅包含它的数据 ,也包含元数据 —— 有关文档的信息。 三个必须的元数据元素如下:

  • _index :文档所属索引。
  • _type :文档所属类型。
  • _id :文档的唯一标识。

    _index

    一个索引应该是因共同的特性而被分组到一起的文档集合。例如,存储所有的产品在所有 products 中,而存储所有的销售的交易到索引 sales 中。虽然也允许存储不相关的数据到一个索引中,但这通常看作是一个反模式的做法。

    实际上,在ES中,我们的数据(文档)是被存储和索引在分片中,而一个索引仅仅是逻辑上的命名空间,这个命名空间由一个或多个分片组合在一起。不过这是是内部实现的细节,应用程序不用关心。

索引名必须小写,不能以下划线开头,不能包含逗号。

_type

数据可能在索引中只是松散的组合在一起,但是通常明确定义一些数据中的子分区是很有用的。 例如,所有的产品都放在一个索引中,但是你有许多不同的产品类别,比如 “electronics” 、 “kitchen” 和 “lawn-care”。这些文档共享一种相同的(或非常相似)的模式:他们有一个标题、描述、产品代码和价格。他们只是正好属于“产品”下的一些子类。
Elasticsearch 公开了一个称为 types (类型)的特性,它允许您在索引中对数据进行逻辑分区。不同 types 的文档可能有不同的字段,但最好能够非常相似。 我们将在 类型和映射 中更多的讨论关于 types 的一些应用和限制。

特别特别注意: 从ES 5.0版本开始,ES就已经开始逐渐移除类型,到最新的ES 7.x,基本已经移除了所有类型API,因此ES中已经不存在“类型”这个概念了。 参考:

_id

ID 是一个字符串,当它和 _index 以及 _type 组合就可以唯一确定 Elasticsearch 中的一个文档。 当你创建一个新的文档,要么提供自己的 _id ,要么让 Elasticsearch 帮你生成。

其他元数据

还有一些其他的元数据元素,他们将在“类型和映射”一节中进行介绍。通过前面已经列出的元数据元素, 我们已经能存储文档到 Elasticsearch 中并通过 ID 检索它。换句话说,使用 Elasticsearch 作为文档的存储介质。

3.3 索引文档

使用自定义的ID

如果你的文档有一个自然的标识符 (例如,一个 user_account 字段或其他标识文档的值),你应该使用如下方式的 index API 并提供你自己 _id :

  1. PUT /{index}/{type}/{自定义id}
  2. {
  3. "field": "value",
  4. ...
  5. }

在 Elasticsearch 中每个文档都有一个版本号。当每次对文档进行修改时(包括删除), _version 的值会递增。 在“处理冲突”中,我们将讨论怎样使用 _version 号码确保你的应用程序中的一部分修改不会覆盖另一部分所做的修改。

自动生成ID

如果你的数据没有自然的 ID, Elasticsearch 可以帮我们自动生成 ID 。 请求的结构调整为: 不再使用 PUT 谓词(“使用这个 URL 存储这个文档”), 而是使用 POST 谓词(“存储文档在这个 URL 命名空间下”)。

3.4 取回一个文档

为了从 Elasticsearch 中检索出文档,我们仍然使用相同的 _index , _type , 和 _id ,但是 HTTP 谓词更改为 GET :

  1. GET /website/blog/123?pretty

响应体包括目前已经熟悉了的元数据元素,再加上 _source 字段,这个字段包含我们索引数据时发送给 Elasticsearch 的原始 JSON 文档:

  1. {
  2. "_index" : "website",
  3. "_type" : "blog",
  4. "_id" : "123",
  5. "_version" : 1,
  6. "found" : true,
  7. "_source" : {
  8. "title": "My first blog entry",
  9. "text": "Just trying this out...",
  10. "date": "2014/01/01"
  11. }
  12. }

返回文档的一部分

默认情况下, GET 请求会返回整个文档,这个文档正如存储在 _source 字段中的一样。但是也许你只对其中的 title 字段感兴趣。单个字段能用 _source 参数请求得到,多个字段也能使用逗号分隔的列表来指定。

  1. GET /website/blog/123?_source=title,text

或者,如果你只想得到 _source 字段,不需要任何元数据,你能使用 _source 端点:

  1. GET /website/blog/123/_source

3.5 检查文档是否存在

如果只想检查一个文档是否存在—根本不想关心内容—那么用 HEAD 方法来代替 GET 方法。如果文档存在, Elasticsearch 将返回一个 200 ok 的状态码;若文档不存在, Elasticsearch 将返回一个 404 Not Found 的状态码。

3.6 更新整个文档

在 Elasticsearch 中文档是 不可改变 的,不能修改它们。相反,如果想要更新现有的文档,需要 重建索引 或者进行替换, 我们可以使用相同的 index API 进行实现,在 索引文档 中已经进行了讨论。

  1. PUT /website/blog/123
  2. {
  3. "title": "My first blog entry",
  4. "text": "I am starting to get the hang of this...",
  5. "date": "2014/01/02"
  6. }

在响应体中,我们能看到 Elasticsearch 已经增加了 _version 字段值:

  1. {
  2. "_index" : "website",
  3. "_type" : "blog",
  4. "_id" : "123",
  5. "_version" : 2,
  6. "created": false
  7. }

在内部,Elasticsearch 已将旧文档标记为已删除,并增加一个全新的文档。 尽管你不能再对旧版本的文档进行访问,但它并不会立即消失。当继续索引更多的数据,Elasticsearch 会在后台清理这些已删除文档。
在本章的后面部分,我们会介绍 update API, 这个 API 可以用于 partial updates to a document 。 虽然它似乎对文档直接进行了修改,但实际上 Elasticsearch 按前述完全相同方式执行以下过程:

  1. 从旧文档构建 JSON
  2. 更改该 JSON
  3. 删除旧文档
  4. 索引一个新文档

唯一的区别在于, update API 仅仅通过一个客户端请求来实现这些步骤,而不需要单独的 get 和 index 请求。

3.7 创建新文档

当我们索引一个文档,怎么确认我们正在创建一个完全新的文档,而不是覆盖现有的呢?
请记住, _index 、 _type 和 _id 的组合可以唯一标识一个文档。所以,确保创建一个新文档的最简单办法是,使用索引请求的 POST 形式让 Elasticsearch 自动生成唯一 _id :

  1. POST /website/blog/
  2. { ... }

然而,如果已经有自己的 _id ,那么我们必须告诉 Elasticsearch ,只有在相同的 _index 、 _type 和 _id 不存在时才接受我们的索引请求。这里有两种方式,他们做的实际是相同的事情。使用哪种,取决于哪种使用起来更方便。
第一种方法使用 op_type 查询-字符串参数:

  1. PUT /website/blog/123?op_type=create
  2. { ... }

第二种方法是在 URL 末端使用 /_create :

  1. PUT /website/blog/123/_create
  2. { ... }

如果创建新文档的请求成功执行,Elasticsearch 会返回元数据和一个 201 Created 的 HTTP 响应码。
另一方面,如果具有相同的 _index 、 _type 和 _id 的文档已经存在,Elasticsearch 将会返回 409 Conflict 响应码,以及如下的错误信息:

  1. {
  2. "error": {
  3. "root_cause": [
  4. {
  5. "type": "document_already_exists_exception",
  6. "reason": "[blog][123]: document already exists",
  7. "shard": "0",
  8. "index": "website"
  9. }
  10. ],
  11. "type": "document_already_exists_exception",
  12. "reason": "[blog][123]: document already exists",
  13. "shard": "0",
  14. "index": "website"
  15. },
  16. "status": 409
  17. }

3.8 删除文档

删除文档的语法和我们所知道的规则相同,只是使用 DELETE 方法:

  1. DELETE /website/blog/123

如果找到该文档,Elasticsearch 将要返回一个 200 ok 的 HTTP 响应码,和一个类似以下结构的响应体。注意,字段 _version 值已经增加:

  1. {
  2. "found" : true,
  3. "_index" : "website",
  4. "_type" : "blog",
  5. "_id" : "123",
  6. "_version" : 3
  7. }

如果文档没有找到,我们将得到 404 Not Found 的响应码和类似这样的响应体:

3.9 处理冲突

在数据库领域中,有两种方法通常被用来确保并发更新时变更不会丢失:
(1)悲观并发控制
这种方法被关系型数据库广泛使用,它假定有变更冲突可能发生,因此阻塞访问资源以防止冲突。 一个典型的例子是读取一行数据之前先将其锁住,确保只有放置锁的线程能够对这行数据进行修改。
(2)乐观并发控制
Elasticsearch 中使用的这种方法假定冲突是不可能发生的,并且不会阻塞正在尝试的操作。 然而,如果源数据在读写当中被修改,更新将会失败。应用程序接下来将决定该如何解决冲突。 例如,可以重试更新、使用新的数据、或者将相关情况报告给用户。

3.10 乐观并发控制

Elasticsearch 是分布式的。当文档创建、更新或删除时, 新版本的文档必须复制到集群中的其他节点。Elasticsearch 也是异步和并发的,这意味着这些复制请求被并行发送,并且到达目的地时也许顺序是乱的。 Elasticsearch 需要一种方法确保文档的旧版本不会覆盖新的版本。
当我们之前讨论 index , GET 和 delete 请求时,我们指出每个文档都有一个 _version (版本)号,当文档被修改时版本号递增。 Elasticsearch 使用这个 _version 号来确保变更以正确顺序得到执行。如果旧版本的文档在新版本之后到达,它可以被简单的忽略。
我们可以利用 _version 号来确保 应用中相互冲突的变更不会导致数据丢失。我们通过指定想要修改文档的 version 号来达到这个目的。 如果该版本不是当前版本号,我们的请求将会失败。

  1. PUT /website/blog/1?version=1
  2. {
  3. "title": "My first blog entry",
  4. "text": "Starting to get the hang of this..."
  5. }

如果目标版本号已经发生变更,那么将返回409状态码以及错误响应体。

通过外部系统使用版本控制

3.11 文档的部分更新

我们也介绍过文档是不可变的:他们不能被修改,只能被替换。 update API 必须遵循同样的规则。 从外部来看,我们在一个文档的某个位置进行部分更新。然而在内部, update API 简单使用与之前描述相同的 检索-修改-重建索引 的处理过程。 区别在于这个过程发生在分片内部,这样就避免了多次请求的网络开销。通过减少检索和重建索引步骤之间的时间,我们也减少了其他进程的变更带来冲突的可能性。

3.12 取回多个文档

Elasticsearch 的速度已经很快了,但甚至能更快。 将多个请求合并成一个,避免单独处理每个请求花费的网络延时和开销。 如果你需要从 Elasticsearch 检索很多文档,那么使用 multi-get 或者 mget API 来将这些检索请求放在一个请求中,将比逐个文档请求更快地检索到全部文档。

3.13 代价较小的批量操作

与 mget 可以使我们一次取回多个文档同样的方式, bulk API 允许在单个步骤中进行多次 create 、 index 、 update 或 delete 请求。 如果你需要索引一个数据流比如日志事件,它可以排队和索引数百或数千批次。


4. 分布式文档存储

4.1 路由一个文档到一个分片中

当索引一个文档的时候,文档会被存储到一个主分片中。 Elasticsearch 如何知道一个文档应该存放到哪个分片中呢?当我们创建文档时,它如何决定这个文档应当被存储在分片 1 还是分片 2 中呢?
首先这肯定不会是随机的,否则将来要获取文档的时候我们就不知道从何处寻找了。实际上,这个过程是根据下面这个公式决定的:

shard = hash(routing) % number_of_primary_shards

routing 是一个可变值,默认是文档的 _id ,也可以设置成一个自定义的值。 routing 通过 hash 函数生成一个数字,然后这个数字再除以 number_of_primary_shards (主分片的数量)后得到 余数 。这个分布在 0 到 number_of_primary_shards-1 之间的余数,就是我们所寻求的文档所在分片的位置。
这就解释了为什么我们要在创建索引的时候就确定好主分片的数量,并且永远不会改变这个数量:因为如果数量变化了,那么所有之前路由的值都会无效,文档也再也找不到了。
所有的文档 API( get 、 index 、 delete 、 bulk 、 update 以及 mget )都接受一个叫做 routing 的路由参数 ,通过这个参数我们可以自定义文档到分片的映射。一个自定义的路由参数可以用来确保所有相关的文档——例如所有属于同一个用户的文档——都被存储到同一个分片中。我们也会在扩容设计这一章中详细讨论为什么会有这样一种需求。

4.2 主分片和副本分片如何交互

我们可以发送请求到集群中的任一节点。 每个节点都有能力处理任意请求。 每个节点都知道集群中任一文档位置,所以可以直接将请求转发到需要的节点上。 在下面的例子中,将所有的请求发送到 Node 1 ,我们将其称为 协调节点(coordinating node) 。

4.3 新建、索引和删除文档

新建、索引和删除 请求都是 写 操作, 必须在主分片上面完成之后才能被复制到相关的副本分片。
以下是在主副分片和任何副本分片上面 成功新建,索引和删除文档所需要的步骤顺序:

  1. 客户端向 Node 1 发送新建、索引或者删除请求。
  2. 节点使用文档的 _id 确定文档属于分片 0 。请求会被转发到 Node 3,因为分片 0 的主分片目前被分配在 Node 3 上。
  3. Node 3 在主分片上面执行请求。如果成功了,它将请求并行转发到 Node 1Node 2 的副本分片上。一旦所有的副本分片都报告成功, Node 3 将向协调节点报告成功,协调节点向客户端报告成功。

在客户端收到成功响应时,文档变更已经在主分片和所有副本分片执行完成,变更是安全的。
有一些可选的请求参数允许您影响这个过程,可能以数据安全为代价提升性能。这些选项很少使用,因为Elasticsearch已经很快,但是为了完整起见,在这里阐述如下:

  • consistency
  • timeout

    4.4 取回一个文档

    以下是从主分片或者副本分片检索文档的步骤顺序:
  1. 客户端向 Node 1 发送获取请求。
  2. 节点使用文档的 _id 来确定文档属于分片 0 。分片 0 的副本分片存在于所有的三个节点上。 在这种情况下,它将请求转发到 Node 2
  3. Node 2 将文档返回给 Node 1 ,然后将文档返回给客户端。

在处理读取请求时,协调结点在每次请求的时候都会通过轮询所有的副本分片来达到负载均衡。
在文档被检索时,已经被索引的文档可能已经存在于主分片上但是还没有复制到副本分片。 在这种情况下,副本分片可能会报告文档不存在,但是主分片可能成功返回文档。 一旦索引请求成功返回给用户,文档在主分片和副本分片都是可用的。

4.5 局部更新文档

以下是部分更新一个文档的步骤:

  1. 客户端向 Node 1 发送更新请求。
  2. 它将请求转发到主分片所在的 Node 3
  3. Node 3 从主分片检索文档,修改 _source 字段中的 JSON ,并且尝试重新索引主分片的文档。 如果文档已经被另一个进程修改,它会重试步骤 3 ,超过 retry_on_conflict 次后放弃。
  4. 如果 Node 3 成功地更新文档,它将新版本的文档并行转发到 Node 1Node 2 上的副本分片,重新建立索引。 一旦所有副本分片都返回成功, Node 3 向协调节点也返回成功,协调节点向客户端返回成功。

update API 还接受在 新建、索引和删除文档 章节中介绍的 routing 、 replication 、 consistency 和 timeout 参数。

4.6 多文档模式

mget 和 bulk API 的模式类似于单文档模式。区别在于协调节点知道每个文档存在于哪个分片中。 它将整个多文档请求分解成 每个分片 的多文档请求,并且将这些请求并行转发到每个参与节点。
协调节点一旦收到来自每个节点的应答,就将每个节点的响应收集整理成单个响应,返回给客户端。


5. 搜索——最基本的工具

现在,我们已经学会了如何使用 Elasticsearch 作为一个简单的 NoSQL 风格的分布式文档存储系统。我们可以将一个 JSON 文档扔到 Elasticsearch 里,然后根据 ID 检索。但 Elasticsearch 真正强大之处在于可以从无规律的数据中找出有意义的信息——从“大数据”到“大信息”。
Elasticsearch 不只会存储(stores)文档,为了能被搜索到也会为文档添加索引(indexes),这也是为什么我们使用结构化的 JSON 文档,而不是无结构的二进制数据。
文档中的每个字段都将被索引并且可以被查询 。不仅如此,在简单查询时,Elasticsearch 可以使用 所有(all) 这些索引字段,以惊人的速度返回结果。这是你永远不会考虑用传统数据库去做的一些事情。
搜索(search) 可以做到:

  • 在类似于 gender 或者 age 这样的字段上使用结构化查询,join_date 这样的字段上使用排序,就像SQL的结构化查询一样。
  • 全文检索,找出所有匹配关键字的文档并按照相关性(relevance)排序后返回结果。
  • 以上二者兼而有之。

很多搜索都是开箱即用的,为了充分挖掘 Elasticsearch 的潜力,你需要理解以下三个概念:

  • 映射(Mapping):描述数据在每个字段内如何存储。
  • 分析(Analysis):全文是如何处理使之可以被搜索的。
  • 领域特定查询语言(Query DSL):Elasticsearch 中强大灵活的查询语言。

    5.1 空搜索

    搜索API的最基础的形式是没有指定任何查询的空搜索,它简单地返回集群中所有索引下的所有文档:

    1. GET /_search

    返回的结果(为了界面简洁编辑过的)像这样:

    1. {
    2. "hits" : {
    3. "total" : 14,
    4. "hits" : [
    5. {
    6. "_index": "us",
    7. "_type": "tweet",
    8. "_id": "7",
    9. "_score": 1,
    10. "_source": {
    11. "date": "2014-09-17",
    12. "name": "John Smith",
    13. "tweet": "The Query DSL is really powerful and flexible",
    14. "user_id": 2
    15. }
    16. },
    17. ... 9 RESULTS REMOVED ...
    18. ],
    19. "max_score" : 1
    20. },
    21. "took" : 4,
    22. "_shards" : {
    23. "failed" : 0,
    24. "successful" : 10,
    25. "total" : 10
    26. },
    27. "timed_out" : false
    28. }

    hits

    返回结果中最重要的部分是 hits ,它包含 total 字段来表示匹配到的文档总数,并且一个 hits 数组包含所查询结果的前十个文档。
    在 hits 数组中每个结果包含文档的 _index 、 _type 、 _id ,加上 _source 字段。这意味着我们可以直接从返回的搜索结果中使用整个文档。这不像其他的搜索引擎,仅仅返回文档的ID,需要你单独去获取文档。
    每个结果还有一个 _score ,它衡量了文档与查询的匹配程度。默认情况下,首先返回最相关的文档结果,就是说,返回的文档是按照 _score 降序排列的。在这个例子中,我们没有指定任何查询,故所有的文档具有相同的相关性,因此对所有的结果而言 1 是中性的 _score 。
    max_score 值是与查询所匹配文档的 _score 的最大值。

    took

    took值告诉我们执行整个搜索请求耗费了多少毫秒。

    shards

    _shards 部分告诉我们在查询中参与分片的总数,以及这些分片成功了多少个失败了多少个。正常情况下我们不希望分片失败,但是分片失败是可能发生的。如果我们遭遇到一种灾难级别的故障,在这个故障中丢失了相同分片的原始数据和副本,那么对这个分片将没有可用副本来对搜索请求作出响应。假若这样,Elasticsearch 将报告这个分片是失败的,但是会继续返回剩余分片的结果。

    timeout

    timed_out 值告诉我们查询是否超时。默认情况下,搜索请求不会超时。如果低响应时间比完成结果更重要,你可以指定 timeout 为 10 或者 10ms(10毫秒),或者 1s(1秒):

    1. GET /_search?timeout=10ms

    在请求超时之前,Elasticsearch 将会返回已经成功从每个分片获取的结果。
    应当注意的是 timeout 不是停止执行查询,它仅仅是告知正在协调的节点返回到目前为止收集的结果并且关闭连接。在后台,其他的分片可能仍在执行查询即使是结果已经被发送了。使用超时是因为 SLA(服务等级协议)对你是很重要的,而不是因为想去中止长时间运行的查询。

    5.2 多索引,多类型

    如果不对某一特殊的索引或者类型做限制,就会搜索集群中的所有文档。Elasticsearch 转发搜索请求到每一个主分片或者副本分片,汇集查询出的前10个结果,并且返回给我们。
    然而,经常的情况下,你想在一个或多个特殊的索引并且在一个或者多个特殊的类型中进行搜索。我们可以通过在URL中指定特殊的索引和类型达到这种效果。

    5.3 分页

    和 SQL 使用 LIMIT 关键字返回单个 page 结果的方法相同,Elasticsearch 接受 from 和 size 参数:

  • size显示应该返回的结果数量,默认是 10

  • from显示应该跳过的初始结果数量,默认是 0

如果每页展示 5 条结果,可以用下面方式请求得到 1 到 3 页的结果:

  1. GET /_search?size=5
  2. GET /_search?size=5&from=5
  3. GET /_search?size=5&from=10

请记住一个请求经常跨越多个分片,每个分片都产生自己的排序结果,这些结果需要进行集中排序以保证整体顺序是正确的。

_在分布式系统中深度分页 理解为什么深度分页是有问题的,我们可以假设在一个有 5 个主分片的索引中搜索。 当我们请求结果的第一页(结果从 1 到 10 ),每一个分片产生前 10 的结果,并且返回给 协调节点 ,协调节点对 50 个结果排序得到全部结果的前 10 个。 现在假设我们请求第 1000 页,结果从 10001 到 10010 。所有都以相同的方式工作除了每个分片不得不产生前10010个结果以外。 然后协调节点对全部 50050 个结果排序最后丢弃掉这些结果中的 50040 个结果。 可以看到,在分布式系统中,对结果排序的成本随分页的深度成指数上升。这就是 web 搜索引擎对任何查询都不要返回超过 1000 个结果的原因。

5.4 轻量搜索

有两种形式的搜索API:一种是 “轻量的” 查询字符串版本,要求在查询字符串中传递所有的参数,另一种是更完整的请求体版本,要求使用 JSON 格式和更丰富的查询表达式作为搜索语言。


6. 映射和分析

通过请求 gb 索引中 tweet 类型的映射(或模式定义),让我们看一看 Elasticsearch 是如何解释我们文档结构的:

  1. GET /gb/_mapping/tweet

结果:

  1. {
  2. "gb": {
  3. "mappings": {
  4. "tweet": {
  5. "properties": {
  6. "date": {
  7. "type": "date",
  8. "format": "strict_date_optional_time||epoch_millis"
  9. },
  10. "name": {
  11. "type": "string"
  12. },
  13. "tweet": {
  14. "type": "string"
  15. },
  16. "user_id": {
  17. "type": "long"
  18. }
  19. }
  20. }
  21. }
  22. }
  23. }

基于对字段类型的猜测, Elasticsearch 动态为我们产生了一个映射。这个响应告诉我们 date 字段被认为是 date 类型的。由于 _all 是默认字段,所以没有提及它。但是我们知道 _all 字段是 string 类型的。

6.1 精确值 VS 全文

Elasticsearch 中的数据可以概括的分为两类:精确值全文
精确值如它们听起来那样精确。例如日期或者用户 ID,但字符串也可以表示精确值,例如用户名或邮箱地址。对于精确值来讲,Foo 和 foo 是不同的,2014 和 2014-09-15 也是不同的。
另一方面,全文 是指文本数据(通常以人类容易识别的语言书写),例如一个推文的内容或一封邮件的内容。
精确值很容易查询。结果是二元的:要么匹配查询,要么不匹配。这种查询很容易用 SQL 表示:

  1. WHERE name = "John Smith"
  2. AND user_id = 2
  3. AND date > "2014-09-15"

查询全文数据要微妙的多。我们问的不只是“这个文档匹配查询吗”,而是“该文档匹配查询的程度有多大?”换句话说,该文档与给定查询的相关性如何?
我们很少对全文类型的域做精确匹配。相反,我们希望在文本类型的域中搜索。不仅如此,我们还希望搜索能够理解我们的意图 :

  • 搜索 UK ,会返回包含 United Kindom 的文档。
  • 搜索 jump ,会匹配 jumpedjumpsjumping ,甚至是 leap
  • 搜索 johnny walker 会匹配 Johnnie Walkerjohnnie depp 应该匹配 Johnny Depp
  • fox news hunting 应该返回福克斯新闻( Foxs News )中关于狩猎的故事,同时, fox hunting news 应该返回关于猎狐的故事。

为了促进这类在全文域中的查询,Elasticsearch 首先 分析 文档,之后根据结果创建 倒排索引 。在接下来的两节,我们会讨论倒排索引和分析过程。

6.2 倒排索引

Elasticsearch使用一种称为倒排索引的结构,它适用于快速的全文搜索。一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表。
例如,假设我们有两个文档,每个文档的 content 字段包含如下内容:

  1. The quick brown fox jumped over the lazy dog
  2. Quick brown foxes leap over lazy dogs in summer

为了创建倒排索引,我们首先将每个文档的content字段拆分成单独的词(我们称它为 词条 或 tokens ),创建一个包含所有不重复词条的排序列表,然后列出每个词条出现在哪个文档。结果如下所示:
image.png
现在,如果我们想搜索 quick brown ,我们只需要查找包含每个词条的文档:
image.png
两个文档都匹配,但是第一个文档比第二个匹配度更高。如果我们使用仅计算匹配词条数量的简单 相似性算法 ,那么,我们可以说,对于我们查询的相关性来讲,第一个文档比第二个文档更佳。
但是,我们目前的倒排索引有一些问题:

  • Quickquick 以独立的词条出现,然而用户可能认为它们是相同的词。
  • foxfoxes 非常相似, 就像 dogdogs ;他们有相同的词根。
  • jumpedleap, 尽管没有相同的词根,但他们的意思很相近。他们是同义词。

如果我们将词条规范为标准模式,那么我们可以找到与用户搜索的词条不完全一致,但具有足够相关性的文档。例如:

  • Quick 可以小写化为 quick
  • foxes 可以 词干提取 —变为词根的格式— 为 fox 。类似的, dogs 可以为提取为 dog
  • jumpedleap 是同义词,可以索引为相同的单词 jump

现在索引看上去像这样:
image.png
这还远远不够。我们搜索 +Quick +fox 仍然 会失败,因为在我们的索引中,已经没有 Quick 了。但是,如果我们对搜索的字符串使用与 content 域相同的标准化规则,会变成查询 +quick +fox ,这样两个文档都会匹配!

总结:你只能搜索在索引中出现的词条,所以索引文本和查询字符串必须标准化为相同的格式

分词和标准化的过程称为 分析 , 我们会在下个章节讨论。

6.3 分析与分析器

分析包含下面的过程:

  • 分词——首先,将一块文本分成适合于倒排索引的独立的 词条
  • 标准化——之后,将这些词条统一化为标准格式以提高它们的“可搜索性”,或者 recall

分析器执行上面的工作。 分析器 实际上是将三个功能封装到了一个包里:
字符过滤器
首先,字符串按顺序通过每个字符过滤器 。他们的任务是在分词前整理字符串。一个字符过滤器可以用来去掉HTML,或者将 & 转化成 and。
分词器
其次,字符串被 分词器 分为单个的词条。一个简单的分词器遇到空格和标点的时候,可能会将文本拆分成词条。
Token 过滤器
最后,词条按顺序通过每个 token 过滤器 。这个过程可能会改变词条(例如,小写化 Quick ),删除词条(例如, 像 a, and, the 等无用词),或者增加词条(例如,像 jump 和 leap 这种同义词)。
Elasticsearch提供了开箱即用的字符过滤器、分词器和token 过滤器。 这些可以组合起来形成自定义的分析器以用于不同的目的。我们会在 自定义分析器 章节详细讨论。

内置分析器

但是, Elasticsearch还附带了可以直接使用的预包装的分析器。接下来我们会列出最重要的分析器。

  • 标准分析器:标准分析器是Elasticsearch默认使用的分析器。它是分析各种语言文本最常用的选择。它根据 Unicode 联盟 定义的 单词边界 划分文本。删除绝大部分标点。
  • 简单分析器:简单分析器在任何不是字母的地方分隔文本,将词条小写。
  • 空格分析器:空格分析器在空格的地方划分文本。
  • 语言分析器:特定语言分析器可用于 很多语言。它们可以考虑指定语言的特点。例如, 英语 分析器附带了一组英语无用词(常用单词,例如 and 或者 the ,它们对相关性没有多少影响),它们会被删除。 由于理解英语语法的规则,这个分词器可以提取英语单词的 词干 。

    什么时候使用分析器

    当我们 索引 一个文档,它的全文类型的字段被分析成词条以用来创建倒排索引。 但是,当我们在全文类型字段 搜索 的时候,我们需要将查询字符串通过 相同的分析过程 ,以保证我们搜索的词条格式与索引中的词条格式一致。
    全文查询,理解每个字段是如何定义的,因此它们可以做正确的事:

  • 当你查询一个 全文 字段时, 会对查询字符串应用相同的分析器,以产生正确的搜索词条列表。

  • 当你查询一个 精确值 字段时,不会分析查询字符串,而是搜索你指定的精确值。

    测试分析器

    有些时候很难理解分词的过程和实际被存储到索引中的词条,特别是你刚接触Elasticsearch。为了理解发生了什么,你可以使用 analyze API 来看文本是如何被分析的。在消息体里,指定分析器和要分析的文本:

    指定分析器

    当Elasticsearch在你的文档中检测到一个新的字符串字段,它会自动设置其为一个全文字符串字段,使用标准分析器对它进行分析。
    你不希望总是这样。可能你想使用一个不同的分析器,适用于你的数据使用的语言。有时候你想要一个字符串字段就是一个字符串字段,不使用分析,直接索引你传入的精确值,例如用户ID或者一个内部的状态域或标签。
    要做到这一点,我们必须手动指定这些字段的映射

    6.4 映射

    为了能够将时间域视为时间,数字域视为数字,字符串域视为全文或精确值字符串, Elasticsearch 需要知道每个域中数据的类型。这个信息包含在映射中。
    如 数据输入和输出 中解释的,索引中每个文档都有 类型 。每种类型都有它自己的 映射 ,或者 模式定义 。映射定义了类型中的域,每个域的数据类型,以及Elasticsearch如何处理这些域。映射也用于配置与类型有关的元数据。
    我们会在 类型和映射 详细讨论映射。本节,我们只讨论足够让你入门的内容。

    核心简单域类型

    Elasticsearch 支持如下简单域类型:

  • 字符串: string

  • 整数 : byte, short, integer, long
  • 浮点数: float, double
  • 布尔型: boolean
  • 日期: date

当你索引一个包含新域的文档—之前未曾出现— Elasticsearch 会使用 动态映射 ,通过JSON中基本数据类型,尝试猜测域类型,使用如下规则:
image.png

查看映射

通过 /_mapping ,我们可以查看 Elasticsearch 在一个或多个索引中的一个或多个类型的映射。

自定义域映射

尽管在很多情况下基本域数据类型已经够用,但你经常需要为单独域自定义映射,特别是字符串域。自定义映射允许你执行下面的操作:

  • 全文字符串域和精确值字符串域的区别
  • 使用特定语言分析器
  • 优化域以适应部分匹配
  • 指定自定义数据格式
  • 还有更多

域最重要的属性是 type 。对于不是 string 的域,你一般只需要设置 type :

{
    "number_of_clicks": {
        "type": "integer"
    }
}

string 域映射的两个最重要属性是 index 和 analyzer 。

index

index 属性控制怎样索引字符串。它可以是下面三个值:

  • analyzed :首先分析字符串,然后索引它。换句话说,以全文索引这个域。
  • not_analyzed :索引这个域,所以它能够被搜索,但索引的是精确值。不会对它进行分析。
  • no :不索引这个域。这个域不会被搜索到。

string 域 index 属性默认是 analyzed 。如果我们想映射这个字段为一个精确值,我们需要设置它为 not_analyzed :

{
    "tag": {
        "type":     "string",
        "index":    "not_analyzed"
    }
}

其他简单类型(例如 long , double , date 等)也接受 index 参数,但有意义的值只有 no 和 not_analyzed , 因为它们永远不会被分析。

analyzer

对于 analyzed 字符串域,用 analyzer 属性指定在搜索和索引时使用的分析器。默认, Elasticsearch 使用 standard 分析器, 但你可以指定一个内置的分析器替代它,例如 whitespace 、 simple 和 english:

{
    "tweet": {
        "type":     "string",
        "analyzer": "english"
    }
}

在 自定义分析器 ,我们会展示怎样定义和使用自定义分析器。

更新映射

当你首次创建一个索引的时候,可以指定类型的映射。你也可以使用 /_mapping 为新类型(或者为存在的类型更新映射)增加映射。

尽管你可以增加一个存在的映射,你不能修改存在的域映射。如果一个域的映射已经存在,那么该域的数据可能已经被索引。如果你意图修改这个域的映射,索引的数据可能会出错,不能被正常的搜索。

我们可以更新一个映射来添加一个新域,但不能将一个存在的域从 analyzed 改为 not_analyzed 。

测试映射

你可以使用 analyze API 测试字符串域的映射。

6.5 复杂核心字段类型

除了我们提到的简单标量数据类型, JSON 还有 null 值,数组,和对象,这些 Elasticsearch 都是支持的。

多值域

对于数组,没有特殊的映射需求。任何域都可以包含0、1或者多个值,就像全文域分析得到多个词条。
这暗示,数组中所有的值必须是相同数据类型的 。你不能将日期和字符串混在一起。如果你通过索引数组来创建新的域,Elasticsearch 会用数组中第一个值的数据类型作为这个域的 类型 。

空域

当然,数组可以为空。这相当于存在零值。 事实上,在 Lucene 中是不能存储 null 值的,所以我们认为存在 null 值的域为空域。
下面三种域被认为是空的,它们将不会被索引:

"null_value":               null,
"empty_array":              [],
"array_with_null_value":    [ null ]

多层级对象

内部对象 经常用于嵌入一个实体或对象到其它对象中。

内部对象的映射

Elasticsearch 会动态监测新的对象域并映射它们为 对象 ,在 properties 属性下列出内部域:

{
  "gb": {
    "tweet": { 
      "properties": {
        "tweet":            { "type": "string" },
        "user": { 
          "type":             "object",
          "properties": {
            "id":           { "type": "string" },
            "gender":       { "type": "string" },
            "age":          { "type": "long"   },
            "name":   { 
              "type":         "object",
              "properties": {
                "full":     { "type": "string" },
                "first":    { "type": "string" },
                "last":     { "type": "string" }
              }
            }
          }
        }
      }
    }
  }
}

内部对象是如何索引的

Lucene 不理解内部对象。 Lucene 文档是由一组键值对列表组成的。为了能让 Elasticsearch 有效地索引内部类,它把我们的文档转化成这样:

{
    "tweet":            [elasticsearch, flexible, very],
    "user.id":          [@johnsmith],
    "user.gender":      [male],
    "user.age":         [26],
    "user.name.full":   [john, smith],
    "user.name.first":  [john],
    "user.name.last":   [smith]
}

内部对象数组

假设我们有个 followers 数组:

{
    "followers": [
        { "age": 35, "name": "Mary White"},
        { "age": 26, "name": "Alex Jones"},
        { "age": 19, "name": "Lisa Smith"}
    ]
}

这个文档会像我们之前描述的那样被扁平化处理,结果如下所示:

{
    "followers.age":    [19, 26, 35],
    "followers.name":   [alex, jones, lisa, smith, mary, white]
}

{age: 35} 和 {name: Mary White} 之间的相关性已经丢失了,因为每个多值域只是一包无序的值,而不是有序数组。因此我们不能精确查询“是否有一个26岁且名字叫 Alex Jones 的追随者?”。
嵌套对象可以解决这个问题的。

6.6 请求体查询

空查询

查询表达式

查询与过滤

过滤查询(Filtering queries)只是简单的检查包含或者排除,这就使得计算起来非常快。
相反,评分查询(scoring queries)不仅仅要找出匹配的文档,还要计算每个匹配文档的相关性,计算相关性使得它们比不评分查询费力的多。同时,查询结果并不缓存。
通常的规则是,使用查询(query)语句来进行 全文 搜索或者其它任何需要影响 相关性得分 的搜索。除此以外的情况都使用过滤(filters)。

最重要的查询

组合多查询

验证查询

6.7 排序与相关性

默认情况下,返回的结果是按照相关性进行排序的——最相关的文档排在最前。 在本章的后面部分,我们会解释相关性意味着什么以及它是如何计算的, 不过让我们首先看看sort参数以及如何使用它。

排序

在 Elasticsearch 中, 相关性得分由一个浮点数进行表示,并在搜索结果中通过 _score 参数返回, 默认排序是 _score 降序。

按照字段的值排序

我们可以使用 sort 参数进行实现。

多级排序

多值字段的排序

字符串排序与多字段

什么是相关性?

Doc Values 介绍

6.8 执行分布式检索

一个 CRUD 操作只对单个文档进行处理,文档的唯一性由 _index, _type, 和 routing values (通常默认是该文档的 _id )的组合来确定。 这表示我们确切的知道集群中哪个分片含有此文档。
搜索需要一种更加复杂的执行模型因为我们不知道查询会命中哪些文档: 这些文档有可能在集群的任何分片上。 一个搜索请求必须询问我们关注的索引(index or indices)的所有分片的某个副本来确定它们是否含有任何匹配的文档。
但是找到所有的匹配文档仅仅完成事情的一半。 在 search 接口返回一个 page 结果之前,多分片中的结果必须组合成单个排序列表。 为此,搜索被执行成一个两阶段过程,我们称之为 query then fetch

查询阶段

在初始 查询阶段 时, 查询会广播到索引中每一个分片拷贝(主分片或者副本分片)。 每个分片在本地执行搜索并构建一个匹配文档的优先队列(堆)。
当一个搜索请求被发送到某个节点时,这个节点就变成了协调节点。 这个节点的任务是广播查询请求到所有相关分片并将它们的响应整合成全局排序后的结果集合,这个结果集合会返回给客户端。
第一步是广播请求到索引中每一个节点的分片拷贝。就像 document GET requests 所描述的, 查询请求可以被某个主分片或某个副本分片处理, 这就是为什么更多的副本(当结合更多的硬件)能够增加搜索吞吐率。 协调节点将在之后的请求中轮询所有的分片拷贝来分摊负载。
每个分片在本地执行查询请求并且创建一个长度为 from + size 的优先队列。也就是说,每个分片创建的结果集足够大,均可以满足全局的搜索请求。 分片返回一个轻量级的结果列表到协调节点,它仅包含文档 ID 集合以及任何排序需要用到的值,例如 _score 。
协调节点将这些分片级的结果合并到自己的有序优先队列里,它代表了全局排序结果集合。至此查询过程结束。

取回阶段

查询阶段标识哪些文档满足搜索请求(此时只有文档ID和需要排序的信息),但是我们仍然需要取回这些文档。这是取回阶段的任务。
分布式取回阶段由以下步骤构成:

  1. 协调节点辨别出哪些文档需要被取回并向相关的分片提交 multi-get 请求。
  2. 每个分片加载并 丰富 文档,如果有需要的话,接着返回文档给协调节点。
  3. 一旦所有的文档都被取回了,协调节点返回结果给客户端。

    深分页(Deep Pagination) 先查后取的过程支持用 from 和 size 参数分页,但是这是有限制的。 要记住需要传递信息给协调节点的每个分片必须先创建一个 from + size 长度的队列,协调节点需要根据 number_of_shards * (from + size) 排序文档,来找到被包含在 size 里的文档。 取决于你的文档的大小,分片的数量和你使用的硬件,给 10,000 到 50,000 的结果文档深分页( 1,000 到 5,000 页)是完全可行的。但是使用足够大的 from 值,排序过程可能会变得非常沉重,使用大量的CPU、内存和带宽。因为这个原因,我们强烈建议你不要使用深分页。 如果你 确实 需要从你的集群取回大量的文档,你可以通过用 scroll 查询禁用排序使这个取回行为更有效率,我们会在 later in this chapter 进行讨论。

搜索选项

有几个查询参数可以影响搜索过程。

preference

偏好这个参数 preference 允许 用来控制由哪些分片或节点来处理搜索请求。

timed_out

参数 timeout 告诉 分片允许处理数据的最大时间。如果没有足够的时间处理所有数据,这个分片的结果可以是部分的,甚至是空数据。

routing

routing 能够在索引时提供来确保相关的文档,比如属于某个用户的文档被存储在某个分片上。 在搜索的时候,不用搜索索引的所有分片,而是通过指定几个 routing 值来限定只搜索几个相关的分片。

search_type

缺省的搜索类型是 query_then_fetch 。 在某些情况下,你可能想明确设置 search_type 为 dfs_query_then_fetch 来改善相关性精确度。搜索类型 dfs_query_then_fetch 有预查询阶段,这个阶段可以从所有相关分片获取词频来计算全局词频。 我们在 被破坏的相关度! 会再讨论它。

游标查询 Scroll

scroll 查询 可以用来对 Elasticsearch 有效地执行大批量的文档查询,而又不用付出深度分页那种代价。
游标查询允许我们 先做查询初始化,然后再批量地拉取结果。 这有点儿像传统数据库中的 cursor 。
游标查询会取某个时间点的快照数据。 查询初始化之后索引上的任何变化会被它忽略。 它通过保存旧的数据文件来实现这个特性,结果就像保留初始化时的索引 视图 一样。
深度分页的代价根源是结果集全局排序,如果去掉全局排序的特性的话查询结果的成本就会很低。 游标查询用字段 _doc 来排序。 这个指令让 Elasticsearch 仅仅从还有结果的分片返回下一批结果。

6.9 索引管理

创建一个索引

现在我们需要对这个建立索引的过程做更多的控制:我们想要确保这个索引有数量适中的主分片,并且在我们索引任何数据之前 ,分析器和映射已经被建立好
为了达到这个目的,我们需要手动创建索引,在请求体里面传入设置或类型映射,如下所示:

PUT /my_index
{
    "settings": { ... any settings ... },
    "mappings": {
        "type_one": { ... any mappings ... },
        "type_two": { ... any mappings ... },
        ...
    }
}

如果你想禁止自动创建索引,你 可以通过在 config/elasticsearch.yml 的每个节点下添加下面的配置:

action.auto_create_index: false

删除索引

用以下的请求来 删除索引:

DELETE /my_index

你也可以这样删除多个索引:

DELETE /index_one,index_two
DELETE /index_*

你甚至可以这样删除 全部 索引:

DELETE /_all
DELETE /*

索引设置(settings)

你可以通过修改配置来自定义索引行为,详细配置参照索引模块.
下面是两个最重要的设置:

  • number_of_shards :每个索引的主分片数,默认值是 5 。这个配置在索引创建后不能修改。
  • number_of_replicas :每个主分片的副本数,默认值是 1 。对于活动的索引库,这个配置可以随时修改。

    配置分析器

    第三个重要的索引设置是 analysis 部分,用来配置已存在的分析器或针对你的索引创建新的自定义分析器。

    自定义分析器

    虽然Elasticsearch带有一些现成的分析器,然而在分析器上Elasticsearch真正的强大之处在于,你可以通过在一个适合你的特定数据的设置之中组合字符过滤器、分词器、词汇单元过滤器来创建自定义的分析器。
    在 分析与分析器 我们说过,一个 分析器 就是在一个包里面组合了三种函数的一个包装器, 三种函数按照顺序被执行:
    字符过滤器(char_filter)
    字符过滤器 用来 整理 一个尚未被分词的字符串。
    一个分析器可能有0个或者多个字符过滤器。
    分词器(**tokenizer**)
    一个分析器 必须 有一个唯一的分词器。 分词器把字符串分解成单个词条或者词汇单元。
    词单元过滤器(filter)
    经过分词,作为结果的 词单元流 会按照指定的顺序通过指定的词单元过滤器 。
    词单元过滤器可以修改、添加或者移除词单元。

    创建一个自定义分析器

类型和映射

类型 在 Elasticsearch 中表示一类相似的文档。 类型由 名称 —比如 user 或 blogpost —和 映射 组成。

再次强调,ES 7之后已经没有类型的概念了,或者认为一个索引只有一个类型,名为_doc

映射, 就像数据库中的 schema ,描述了文档可能具有的字段或属性 、每个字段的数据类型—比如 string, integer 或 date —以及Lucene是如何索引和存储这些字段的。

根对象

映射的最高一层被称为 根对象 ,它可能包含下面几项:

  • 一个 properties 节点,列出了文档中可能包含的每个字段的映射
  • 各种元数据字段,它们都以一个下划线开头,例如 _type_id_source
  • 设置项,控制如何动态处理新的字段,例如 analyzerdynamic_date_formatsdynamic_templates
  • 其他设置,可以同时应用在根对象和其他 object 类型的字段上,例如 enableddynamicinclude_in_all

    动态映射

自定义动态映射

缺省映射

重新索引你的数据

尽管可以增加新的类型到索引中,或者增加新的字段到类型中,但是不能添加新的分析器或者对现有的字段做改动。 如果你那么做的话,结果就是那些已经被索引的数据就不正确, 搜索也不能正常工作。
对现有数据的这类改变最简单的办法就是重新索引:用新的设置创建新的索引并把文档从旧的索引复制到新的索引。
从Elasticsearch v2.3.0开始, Reindex API 被引入。它能够对文档重建索引而不需要任何插件或外部工具。

索引别名和零停机

6.10 分片内部原理

在 集群内的原理, 我们介绍了 分片, 并将它 描述成最小的 工作单元 。但是究竟什么 是 一个分片,它是如何工作的? 在这个章节,我们回答以下问题:

  • 为什么搜索是 实时的?
  • 为什么文档的 CRUD (创建-读取-更新-删除) 操作是 实时 的?
  • Elasticsearch 是怎样保证更新被持久化在断电时也不丢失数据?
  • 为什么删除文档不会立刻释放空间?
  • refresh, flush, 和 optimize API 都做了什么, 你什么情况下应该使用他们?

    使文本可被搜索