vlambda博客
学习文章列表

读书笔记《elasticsearch-server-third-edition》为您的数据编制索引

Chapter 2. Indexing Your Data

在上一章中,我们了解了全文搜索是什么以及 Apache Lucene 如何适应全文搜索。我们了解了 Elasticsearch 的基本概念,现在我们熟悉了它的顶层架构,因此我们知道它是如何工作的。我们使用 REST API 来索引数据、更新数据、删除数据,当然还有检索数据。我们使用简单的 URI 查询搜索数据,并使用允许我们使用乐观锁定功能的版本控制。在本章结束时,您将学习以下主题:

  • 关于 Elasticsearch 索引的基本信息

  • 调整 Elasticsearch 无模式行为

  • 创建自己的映射

  • 使用开箱即用的分析仪

  • 配置您自己的分析器

  • 批量索引数据

  • 向索引添加额外的内部信息

  • 段合并

  • 路由

Elasticsearch indexing


到目前为止,我们的 Elasticsearch 集群已启动并运行。我们也知道如何使用 Elasticsearch REST API 来索引我们的数据,我们知道如何检索它,我们也知道如何删除我们没有的数据更长的需要。我们还学习了如何使用 URI 请求搜索和 Apache Lucene 查询语言在我们的 数据中进行搜索。然而,到目前为止,我们使用的 Elasticsearch 功能让我们不必关心索引、分片和数据结构。当您来自 SQL 数据库的世界时,您可能不习惯这样做,您需要预先创建所有列的数据库和表。通常,您需要描述能够将数据放入数据库的数据结构。 Elasticsearch 是无模式的,默认情况下会自动创建索引,因此我们可以安装它并索引数据而无需任何准备。但是,对于您希望控制数据分析的生产环境,这通常不是最佳情况。因为 我们将首先向您展示如何管理您的索引,然后我们将带您了解 Elasticsearch 中的映射世界。

Shards and replicas

第 1 章中,Elasticsearch 集群入门 ,我们告诉过您,Elasticsearch 中的索引是由一个或多个分片构建的。这些分片中的每一个都包含文档集的一部分,并且每个分片都是一个单独的 Lucene 索引。除此之外,每个分片都可以有副本——主分片本身的物理副本。当我们创建一个索引时,我们可以告诉Elasticsearch它应该有多少个分片建于。

Note

Elasticsearch 使用的默认 分片数量为 5,每个索引也将包含一个副本。可以通过在 index.number_of_shardsindex.number_of_replicas 属性来更改默认配置>elasticsearch.yml 配置文件。

当使用默认值时,我们最终会得到五个构建 Elasticsearch 索引的 Apache Lucene 索引,并且每个索引都有一个副本。所以,有五个分片和一个副本,我们实际上会得到 10 个分片。这是因为每个分片都有自己的副本,所以集群中的分片总数为 10。

以这种方式划分索引允许我们将分片分布在整个集群中。这样做的好处是所有分片将自动分布在整个集群中。如果我们只有一个节点,Elasticsearch 会将五个主分片放在该节点上,并且不会分配副本,因为 Elasticsearch 不会将分片及其副本分配给同一个节点。原因很简单——如果一个节点崩溃,我们将失去数据的主要来源和所有副本。因此,如果您有一个 Elasticsearch 节点,不必担心没有分配副本——这是意料之中的事情。当然,当您有足够的节点供 Elasticsearch 分配所有副本(除了分片)时,不分配它们是不好的,您应该寻找导致这种情况的可能原因。

要记住的是,拥有分片和副本不是免费的。首先,每个副本都需要额外的磁盘空间,与原始分片所需的空间量完全相同。因此,如果我们的索引有 3 个副本,我们实际上将需要 4 倍的空间。如果我们的主分片总重 100GB,有 3 个副本,我们将需要 400GB - 每个副本需要 100GB。然而,这并不是唯一的代价。每个副本本身就是一个 Lucene 索引,Elasticsearch 需要一些内存来处理它。集群中的分片越多,使用的内存就越多。最后,拥有副本意味着除了主分片上的索引之外,我们还必须对每个副本进行索引。 影子副本的概念可以复制整个二进制索引,但在大多数情况下,每个副本都会执行自己的索引。副本的好处是 Elasticsearch 会尝试 分散查询并在分片和它们的 副本,这意味着我们可以通过使用它们来水平扩展集群。

所以总结一下的结论:

  • 在索引中拥有更多的分片允许我们在更多的服务器之间分布索引并并行化索引操作,从而具有更好的索引吞吐量。

  • 根据您的部署,拥有更多分片可能会增加查询吞吐量并降低查询延迟——尤其是在每秒没有大量查询的环境中。

  • 与单个分片查询相比,拥有更多分片可能会更慢,因为 Elasticsearch 需要从多个服务器检索数据并将它们组合到内存中,然后才能返回最终查询结果。

  • 拥有更多副本会导致集群更具弹性,因为当主分片不可用时,其副本将扮演该角色。基本上,拥有一个副本可以让我们丢失一个分片的副本,但仍然可以提供整个数据。拥有两个副本允许我们丢失分片的两个副本,但仍然可以看到整个数据。

  • 副本数越高,集群的查询吞吐量就越高。这是因为每个副本都可以独立于所有其他副本提供其拥有的数据。

  • 更多的分片(主分片和副本分片)将导致 Elasticsearch 需要更多的内存。

当然,这些并不是 Elasticsearch 中分片数量和副本数量之间的唯一关系。我们将在本书后面部分讨论其中的大部分内容。

那么,我们的索引应该有多少分片和副本?那要看。我们相信默认设置非常好,但没有什么可以代替好的测试。请注意,副本的数量不是很重要,因为您可以在创建索引后在活动集群上调整它。如果需要并拥有运行它们的资源,您可以删除和添加它们。不幸的是,当涉及到分片数量时,情况并非如此。创建索引后,更改分片数量的唯一方法是创建另一个索引并重新索引您的数据。

Write consistency

Elasticsearch 允许我们控制写入一致性,以防止写入不应该发生。默认情况下,当在活动分片上的 quorum 上写入成功时,Elasticsearch 索引操作成功——这意味着 50% 的活动分片加一。我们可以通过将 action.write_consitency 添加到我们的 elasticsearch.yml 文件或通过将一致性参数添加到我们的索引请求来控制这种行为.提到的属性 可以采用以下值:

  • quorum:默认值,索引操作成功需要50%加上1个活跃分片才能成功

  • one:只需要一个活动分片才能成功,索引操作才能成功

  • all:要求所有活动分片成功,索引操作才能成功

Creating indices

当我们在第1章中为我们的文档编制索引时,< span class="emphasis">Elasticsearch Cluster 入门,我们根本不关心索引的创建。我们假设 Elasticsearch 会为我们做所有事情,实际上这是真的;我们刚刚使用了以下命令:

curl -XPUT 'http://localhost:9200/blog/article/1' -d '{"title": "New version of Elasticsearch released!", "content": "Version 1.0 released today!", "tags": ["announce", "elasticsearch", "release"] }'

这很好。如果这样的索引不存在,Elasticsearch 会自动为我们创建索引。但是,有时我们出于各种原因想要自己创建索引。也许我们想控制创建哪些索引以避免错误,或者我们有一些非默认设置,我们想在创建特定索引时使用这些设置。原因可能不同,但很高兴知道我们可以在不索引文档的情况下创建索引。

创建索引的最简单方法是使用我们要创建的索引的名称运行 PUT HTTP 请求。例如,要创建一个名为 blog 的索引,我们可以使用以下命令:

curl -XPUT http://localhost:9200/blog/

我们刚刚告诉 Elasticsearch 我们要创建名为 blog 的索引。如果一切顺利,您将看到来自 Elasticsearch 的以下响应:

{"acknowledged":true}

Altering automatic index creation

我们已经提到在某些情况下自动创建索引并不是最好的主意。例如,索引创建过程中的一个简单拼写错误可能会导致创建数百个未使用的索引,并使集群状态信息超出应有的范围,从而给 Elasticsearch 和底层 JVM 带来更大的压力。因此,我们可以通过在 elasticsearch.yml 配置文件中添加一个简单的属性来关闭自动索引创建:

action.auto_create_index: false

让我们停下来讨论一下 action.auto_create_index 属性,因为它允许我们做比仅仅允许更复杂的事情(将其设置为 true )并禁用(将其设置为 false)自动索引创建。提到的属性允许我们使用模式来指定应该允许自动创建和不允许自动创建的索引名称。例如,假设我们希望允许为以日志开头的索引自动创建索引,而我们希望禁止所有其他索引。为此,我们将 action.auto_create_index 属性设置如下:

action.auto_create_index: +logs*,-*

现在,如果我们想创建一个名为 logs_2015-10-01, 的索引,我们会成功。要创建这样的索引,我们将使用以下命令:

curl -XPUT http://localhost:9200/logs_2015-10-01/log/1 -d '{"message": "Test log message" }'

Elasticsearch 会响应:

{
  "_index" : "logs_2015-10-01",
  "_type" : "log",
  "_id" : "1",
  "_version" : 1,
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "created" : true
}

但是,假设我们现在尝试使用以下命令创建 blog

curl -XPUT http://localhost:9200/blog/article/1 -d '{"title": "Test article title" }'

Elasticsearch 将响应类似于以下错误的错误:

{
  "error" : {
    "root_cause" : [ {
      "type" : "index_not_found_exception",
      "reason" : "no such index",
      "resource.type" : "index_expression",
      "resource.id" : "blog",
      "index" : "blog"
    } ],
    "type" : "index_not_found_exception",
    "reason" : "no such index",
    "resource.type" : "index_expression",
    "resource.id" : "blog",
    "index" : "blog"
  },
  "status" : 404
}

要记住的一件事是模式定义的顺序很重要。 Elasticsearch 检查模式直到 第一个匹配的模式,所以如果我们将 -* 作为第一个模式,则根本不会使用 +logs* 模式。

Settings for a newly created index

手动创建索引 当我们想在创建索引时传递非默认配置选项时也是必要的;例如,分片和副本的初始数量。我们可以通过在 PUT HTTP 请求正文中包含带有设置的 JSON 有效负载来做到这一点。例如,如果我们想告诉 Elasticsearch 我们的博客索引最初应该只有一个分片和两个副本,可以使用以下命令:

curl -XPUT http://localhost:9200/blog/ -d '{
    "settings" : {
        "number_of_shards" : 1,
        "number_of_replicas" : 2
    }
}'

前面的命令将导致创建具有一个分片和两个副本的博客索引,总共创建三个物理 Lucene 索引——我们已经知道称为分片。当然,我们可以使用更多的设置,但是我们现在所做的已经足够了,我们将在整本书中了解其余的内容。

Index deletion

当然,类似于我们处理文档的方式,Elasticsearch 也允许我们删除索引。删除索引与创建索引非常相似,但我们不使用 PUT HTTP 方法,而是使用 DELETE 方法。例如,如果我们想删除之前创建的博客索引,我们将运行以下命令:

curl -XDELETE http://localhost:9200/blog

响应将与我们之前创建索引时看到的响应相同,应该如下所示:

{"acknowledged":true}

现在我们知道索引是什么、如何创建以及如何删除它,我们已经准备好使用我们定义的映射创建索引了。尽管 Elasticsearch 是无模式的,但在很多情况下,我们还是希望手动创建模式,以避免索引结构出现任何问题。

Mappings configuration


如果你习惯使用 SQL 数据库, 可能知道,在开始向数据库中插入数据之前,需要创建一个 schema,它会描述你的数据是什么样的。尽管 Elasticsearch 是一个无模式(我们宁愿称其为数据驱动模式)搜索引擎,并且可以即时计算出数据结构,但我们认为控制结构并自行定义是一种更好的方法。字段类型确定机制不会猜测未来。例如,如果您首先发送一个 integer 值,例如 60,然后您发送一个 float 值,例如 70.23 对于同一字段,可能会发生错误,或者 Elasticsearch 将截断 的小数部分float 值(实际上就是这样)。这是因为 Elasticsearch 将首先将字段类型设置为整数,并尝试将 float 值索引到 integer 字段,这将导致浮点数中的小数点被切除。在接下来的几页中,您将看到如何创建满足您的需求并匹配您的数据结构的映射。

Note

请注意,我们没有在本章中包含有关可用类型的所有信息,并且 Elasticsearch 的一些特性,例如嵌套类型、父子处理、存储地理点和搜索,将在本书的后续章节中介绍。

Type determining mechanism

在我们开始描述 如何手动创建映射之前,我们想回到Elasticsearch 中使用的自动类型确定算法。正如我们已经说过的,Elasticsearch 可以通过查看构建文档的 JSON 来尝试猜测文档的架构。因为 JSON 是结构化的,所以这似乎很容易做到。例如,字符串用引号括起来,布尔值使用特定的单词定义,数字只是几个数字。这是一个简单的技巧,但它通常有效。例如,让我们看一下以下文档:

{
  "field1": 10,
  "field2": "10"
}

前面的文档有两个字段。 field1 字段将被赋予一个类型编号(准确地说,该字段将被赋予一个长类型)。第二个字段称为 field2 将被赋予一个字符串类型,因为它是用引号括起来的。当然,对于某些用例,这可能是所需的行为。但是,如果我们以某种方式使用引号将所有数据括起来(无论如何这不是最好的主意),我们的索引结构将只包含字符串类型的字段。

Note

不要担心您不熟悉什么是数字类型、字符串类型等等。在向您展示如何调整 Elasticsearch 中的自动类型确定机制后,我们将对其进行描述。

Disabling the type determining mechanism

第一个解决方案是完全 禁用 Elasticsearch 中的无模式行为。我们可以通过将 index.mapper.dynamic 属性添加到我们的索引属性并将其设置为 false 来做到这一点。我们可以通过运行以下命令来创建索引来做到这一点:

curl -XPUT 'localhost:9200/sites' -d '{
  "index.mapper.dynamic": false
}'

通过这样做,我们告诉 Elasticsearch 我们不希望它猜测我们在站点索引中的文档类型,我们将自己提供映射。如果我们尝试将一些示例文档索引到站点的索引中,我们将收到以下错误:

{
  "error" : {
    "root_cause" : [ {
      "type" : "type_missing_exception",
      "reason" : "type[[doc, trying to auto create mapping, but dynamic mapping is disabled]] missing",
      "index" : "sites"
    } ],
    "type" : "type_missing_exception",
    "reason" : "type[[doc, trying to auto create mapping, but dynamic mapping is disabled]] missing",
    "index" : "sites"
  },
  "status" : 404
}

这是因为我们没有创建任何映射——没有创建文档模式。 Elasticsearch 无法为我们创建一个,因为我们不允许它并且索引命令失败。

当然,在配置类型确定机制的工作方式时,我们可以做的不仅仅是。我们还可以在对象级别为给定类型调整或禁用它。我们将在Chapter 5中讨论第二种情况,扩展你的索引结构 。现在,让我们看看在 Elasticsearch 中调整类型确定机制的可能性。

Tuning the type determining mechanism for numeric types

JSON 文档和类型猜测问题的解决方案之一是我们并不总是能够控制数据。我们索引的文档可能来自多个地方,并且我们环境中的某些系统可能包含文档中所有字段的引号。这可能会导致问题和错误的猜测。因此,Elasticsearch 允许我们通过设置 numeric_detection对数字字段启用更积极的字段值检查属性到映射定义中的 true。例如,假设我们要创建一个名为 users 的索引,并且我们希望它具有用户类型,我们希望在该用户类型上进行更积极的数字字段解析。为此,我们将使用以下命令:

curl -XPUT http://localhost:9200/users/?pretty -d '{ 
  "mappings" : {
    "user": {
      "numeric_detection" : true
    }
  }
}'

现在让我们运行以下命令将单个文档索引到 users 索引:

curl -XPOST http://localhost:9200/users/user/1 -d '{"name": "User 1", "age": "20"}'

早些时候,使用默认设置,年龄字段将设置为字符串类型。 numeric_detection 属性设置为 true,年龄字段的类型将设置为 long。我们可以通过运行以下命令来检查(它将检索用户索引中所有类型的映射):

curl -XGET 'localhost:9200/users/_mapping?pretty'

上述命令应导致 Elasticsearch 返回以下响应:

{
  "users" : {
    "mappings" : {
      "user" : {
        "numeric_detection" : true,
        "properties" : {
          "age" : {
            "type" : "long"
          },
          "name" : {
            "type" : "string"
          }
        }
      }
    }
  }
}

正如我们所见,age 字段 确实设置为

Tuning the type determining mechanism for dates

另一种引起麻烦的数据类型是带有日期的字段。日期可以有不同的风格,例如,2015-10-01 11:22:33 是合适的日期,2015-10 也是如此-01T11:22:33+00。因为,Elasticsearch 会尝试将字段匹配到与某些给定日期格式匹配的时间戳或字符串。如果该匹配操作成功,则该字段被视为基于日期的字段。如果我们知道日期字段的外观,我们可以通过使用 dynamic_date_formats 属性提供可识别的日期格式列表来帮助 Elasticsearch,该属性允许我们指定格式数组。让我们看看以下用于创建索引的命令:

curl -XPUT 'http://localhost:9200/blog/' -d '{ 
  "mappings" : {
    "article" : {
      "dynamic_date_formats" : ["yyyy-MM-dd hh:mm"]
    }
  }
}'

前面的命令将导致创建一个名为 blog 的索引,其类型为 article。我们还使用了具有单一日期格式的 dynamic_date_formats 属性,这将导致 Elasticsearch 使用日期核心类型(请参阅 核心类型< /code> 章节中有关字段类型的更多信息)用于匹配定义格式的字段。 Elasticsearch 使用 joda-time 库来定义日期格式,所以访问 http://joda-time.sourceforge.net/ api-release/org/joda/time/format/DateTimeFormat.html 如果您有兴趣了解它们。

Note

请记住,dynamic_date_format 属性接受一个值数组。这意味着我们可以同时处理多种日期格式。

使用前面的 索引,我们现在可以尝试使用以下命令为新文档建立索引:

curl -XPUT localhost:9200/blog/article/1 -d '{"name": "Test", "test_field":"2015-10-01 11:22"}'

Elasticsearch 当然会索引该文档,但让我们看看为我们的索引创建的映射:

curl -XGET 'localhost:9200/blog/_mapping?pretty'

上述命令的响应如下:

{
  "blog" : {
    "mappings" : {
      "article" : {
        "dynamic_date_formats" : [ "yyyy-MM-dd hh:mm" ],
        "properties" : {
          "name" : {
            "type" : "string"
          },
          "test_field" : {
            "type" : "date",
            "format" : "yyyy-MM-dd hh:mm"
          }
        }
      }
    }
  }
}

正如我们所见,test_field 字段被赋予了一个日期类型,所以我们的调优是有效的。

不幸的是,如果我们想猜测布尔类型,问题仍然存在。没有选项可以强制从文本中猜测布尔类型。在这种情况下,当源格式的改变 是不可能的,我们只能在映射定义中直接定义该字段。

Index structure mapping

每个数据都有自己的结构——有些非常简单,有些包括复杂的对象关系、子文档和嵌套属性。在每种情况下,我们都需要在 Elasticsearch 中有一个称为映射的模式,用于定义数据的外观。当然,我们可以使用 Elasticsearch 的无模式特性,但我们可以而且我们通常希望预先准备映射,因此我们知道如何处理数据。

出于本章的目的,我们将在索引中使用单一类型。当然,Elasticsearch 作为多租户系统允许我们在一个索引中拥有多种类型,但我们希望简化示例,使其更易于理解。因此,为了接下来几页的目的,我们将创建一个名为posts 的索引,它将保存post 类型中文档的数据。我们还假设该索引将包含以下信息:

  • 博文的唯一标识符

  • 博文名称

  • 发布日期

  • 内容 - 帖子本身的文本

在 Elasticsearch 中,与几乎所有通信一样,映射在请求正文中作为 JSON 对象发送。因此,如果我们想创建符合我们需要的最简单的映射,它看起来如下(我们将映射存储在 posts.json 文件中,因此我们可以轻松发送它):

{
  "mappings": {
    "post": {
      "properties": {
        "id": { "type":"long" },
        "name": { "type":"string" },
        "published": { "type":"date" },
        "contents": { "type":"string" }
      }
    }
  }
}

要使用前面的映射文件创建我们的帖子索引,我们只需运行以下命令:

curl -XPOST 'http://localhost:9200/posts' -d @posts.json

Note

请注意,您可以存储映射并将文件名设置为您喜欢的任何名称。 curl 命令只会获取它的内容。

同样,如果一切顺利,我们会看到以下响应:

{"acknowledged":true}

Elasticsearch 报告我们的索引已经创建。如果我们查看 Elasticsearch 节点——在当前主节点上,我们将看到如下内容:

[2015-10-14 15:02:12,840][INFO ][cluster.metadata         ] [Shalla-Bal] [posts] creating index, cause [api], templates [], shards [5]/[1], mappings [post]

我们可以看到 posts 索引已经创建,有 5 个分片和 1 个副本 (shards [5]/[1 ])和单个帖子类型的映射(映射[post])。现在让我们讨论 posts.json 文件的内容以及映射的可能性。

Type and types definition

Elasticsearch 中的映射定义只是另一个 JSON 对象,因此它需要正确地以大括号开始和结束。所有映射定义都嵌套在单个映射对象中。在我们的示例中,我们有一个帖子类型,但我们可以有多个。例如,如果我们想在我们的映射中有多个类型,我们只需要用逗号分隔它们。假设我们希望在我们的帖子索引中有一个额外的用户类型。这种情况下的映射定义如下所示(我们将其存储在 posts_with_user.json 文件中):

{
  "mappings": {
    "post": {
      "properties": {
        "id": { "type":"long" },
        "name": { "type":"string" },
        "published": { "type":"date" },
        "contents": { "type":"string" }
      }
    },
    "user": {
      "properties": {
        "id": { "type":"long" },
        "name": { "type":"string" }
      }
    }
  }
}

如您所见,我们可以使用我们想要的名称来命名类型。在每种类型下,我们在 中都有 properties 对象,我们存储字段的实际名称 及其定义。

Fields

mappings 定义中的每个字段只是一个名称和一个描述字段属性的对象。例如,我们可以将一个字段定义如下:

"body": { "type":"string", "store":"yes", "index":"analyzed" }

前面的字段定义以名称开头——body。之后我们有了一个具有三个属性的对象——字段的类型(type 属性),如果应该存储原始字段值(store 属性),以及该字段是否应该被索引以及如何(index 属性)。当然,多个字段定义使用逗号分隔,就像其他 JSON 对象一样。

Core types

Elasticsearch 中的每个字段类型都可以 指定为提供的核心类型之一。 Elasticsearch 中的核心类型为 如下:

  • 细绳

  • 数字(integer, long, float, )

  • 日期

  • 布尔值

  • 二进制

除了核心类型之外,Elasticsearch 还提供了可以处理更复杂数据的其他类型——例如嵌套文档、对象等。我们将在 第 5 章扩展您的索引结构中讨论它们

Common attributes

在继续所有核心类型描述之前,我们想讨论一些常用属性,您可以使用这些属性描述所有类型(二进制类型除外) :

  • index_name:该属性定义了将存储在索引中的字段的名称。如果未定义,则名称将设置为定义字段的对象的名称。通常,您不需要设置此属性,但在某些情况下它可能很有用;例如,当您无法控制发送到 Elasticsearch 的 JSON 文档中的字段名称时。

  • index:该属性可以取值 analyzedno 并且,对于基于字符串的字段,也可以 设置为附加的not_analyzed 值。如果设置为 analyzed,该字段将被编入索引并因此可搜索。如果设置为 no,您将无法搜索此类字段。默认值为 analyzed。对于基于字符串的字段, 是一个附加选项,not_analyzed。这在设置时意味着该字段将被索引但不被分析。因此,该字段在发送到 Elasticsearch 时被写入索引中,并且在搜索期间只会计算完美匹配 - 查询必须包含与索引中的值完全相同的值。如果我们将其与 SQL 数据库世界进行比较,将字段的索引属性设置为 not_analyzed 就像使用 where field = value 。还请记住,将 index 属性设置为 no 将导致在 include_in_all 中禁用包含该字段(讨论了 include_in_all 属性作为列表中的最后一个属性)。

  • store:这个属性可以取值yesno 并指定是否应将字段的原始值写入索引。默认值为no,表示Elasticsearch不会存储字段的原始值,会尝试使用_source 字段(表示已发送到 Elasticsearch 的原始文档的 JSON)当您要检索字段值时。存储的字段不用于搜索,但如果启用,它们可以用于突出显示(如果它很大,加载 _source 字段可能更有效)。

  • doc_values:该属性可以true 和 false。当设置为 true, Elasticsearch 将在索引期间为未标记化的字段(如未分析的字符串字段、基于数字的字段、布尔字段和日期字段)创建特殊的磁盘结构。这种结构非常高效,Elasticsearch 将其用于需要非反转数据的操作,例如聚合、排序或脚本。从 Elasticsearch 2.0 开始,对于未标记化的字段,默认值为 true。将此值设置为 false 将导致 Elasticsearch 使用字段数据缓存而不是 doc 值,这具有更高的内存需求,但在某些极少数情况下可能会更快。

  • boost:这个属性定义了字段在文档中的重要性;提升越高,字段中的值越重要。这个属性的默认值是1,这意味着一个中性值——任何大于1的值都会使字段更重要,小于1的值会变得不那么重要。

  • null_value:该属性指定一个值,如果该字段是 不是索引文档的一部分。默认行为将忽略该字段。

  • copy_to:该属性指定一个字段数组,原始值将被复制到该数组中。这允许对相同数据进行不同类型的分析。例如,你可以想象有两个字段——一个叫做title,一个叫做title_sort,每个都有相同的值,但处理方式不同。我们可以使用 copy_to 将标题字段值复制到 title_sort

  • include_in_all:此属性 指定字段是否应包含在 _all 字段。 _all 字段是 Elasticsearch 使用的一个特殊字段,用于轻松搜索整个索引文档的内容。 Elasticsearch 通过复制那里的所有文档字段来创建 _all 字段的内容。默认情况下,如果使用 _all 字段,则所有字段都将包含在其中。

String

字符串是基本的文本类型,它允许我们在其中存储一个或多个字符。此类 字段的示例定义如下:

"body" : { "type" : "string", "store" : "yes", "index" : "analyzed" }

除了常用属性外,还可以为基于字符串的字段设置以下属性:

  • term_vector:该属性可以取值no(默认),yes with_offsetswith_positionswith_positions_offsets。它定义是否计算该字段的 Lucene 术语向量。如果您使用突出显示(区分在 查询期间在文档中匹配的术语),您将需要计算所谓的快速向量的术语向量突出显示 - 更有效的突出显示版本。

  • analyzer:这个属性定义了用于索引和搜索的分析器的名称。它默认为全局定义的分析器名称。

  • search_analyzer:此属性定义用于处理查询部分的分析器的名称 字符串,即发送到特定领域。

  • norms.enabled:此属性 指定是否应为字段加载规范。默认情况下,分析字段设置为 true(这意味着将为此类字段加载规范)和 false 对于非分析字段。规范是 Lucene 索引内的值,用于计算文档的分数——通常不需要未分析的字段,仅在查询时使用。为存在的单个字段禁用规范的示例索引创建命令如下所示:

    curl -XPOST 'localhost:9200/essb' -d '{
     "mappings" : {
      "book" : {
       "properties" : {
        "name" : { 
         "type" : "string", 
         "norms" : {
          "enabled" : false
         }
        }
       }
      }
     }
    }'
    
  • norms.loading:这个属性取值eagerlazy 并定义 Elasticsearch 将如何加载规范。第一个值意味着始终加载此类字段的规范。第二个值意味着仅在需要时才加载规范。规范对于评分很有用,但对于大型数据集可能需要大量内存。急切地加载规范(属性设置为 eager)意味着在查询期间的工作更少,但会导致更多的内存消耗。一个为单个字段急切加载规范的示例索引创建命令如下所示:

    curl -XPOST 'localhost:9200/essb_eager' -d '{
     "mappings" : {
      "book" : {
       "properties" : {
        "name" : { 
         "type" : "string", 
         "norms" : {
          "loading" : "eager"
         }
        }
       }
      }
     }
    }'
    
  • position_offset_gap:此属性默认为 0,并指定 给定字段的实例之间的索引间隙同名。如果您希望基于位置的查询(例如短语查询)仅在字段的单个实例内匹配 ,则将其设置为更高的值可能会很有用。

  • index_options:这个属性定义了帖子列表的索引选项——包含 术语的结构(我们谈论在本章的Postings format部分中了解更多信息)。可能的值是 docs(仅索引文档编号)、freqs(索引文档编号和词频)、positions (文档编号、词频及其位置被索引)和 offsets(文档编号、词频、它们的位置和偏移被索引)。此属性的默认值是 positions 用于已分析字段,而 docs 用于已编入索引但未分析的字段。

  • ignore_above:这个属性定义了字段的最大字符大小。分析器将忽略大小高于指定值的字段。

    Note

    在即将发布的 Elasticsearch 版本之一中,字符串类型可能会被弃用,并且可能会被两种新类型(文本和关键字)替换,以更好地指示基于字符串的字段所代表的内容。文本类型将用于分析的文本字段,关键字类型将用于未分析的文本字段。如果您对传入的更改感兴趣,请参阅 以下 GitHub 问题:https://github.com/elastic/elasticsearch/issues/12394

Number

这是一些核心 类型的通用名称,这些类型收集了所有可用且等待使用的数字字段类型。 Elasticsearch 中提供了以下类型(我们使用 type 属性指定它们):

  • byte:这个类型定义了一个byte值;例如,1。它允许 -128127 之间的值。

  • short:这个类型定义了一个short值;例如,12。它允许介于 -3276832767 之间的值。

  • integer:这个类型定义了一个integer值;例如,134。它允许 -231231-1 之间的值(包括 Java 7)和 0232-1 在 Java 8 中。

  • long:该类型定义了一个long值;例如,123456789。它允许 -263263-1 之间的值(包括 Java 7)和 0264-1 在 Java 8 中。

  • float:这个类型定义了一个float值;对于 示例,12.23。有关可能值的 信息,请参阅https://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html# jls-4.2.3

  • double:这种类型定义了一个double值;对于 示例,123.45。有关可能值的信息,请参阅https://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html# jls-4.2.3

    Note

    您可以在 提到的 Java 类型/java/nutsandbolts/datatypes.html" target="_blank">http://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html。

基于其中一种数值类型的字段的示例定义如下:

"price" : { "type" : "float", "precision_step" : "4" }

除常用属性外,还可以为数值字段设置以下属性:

  • precision_step:此属性定义为数字字段中的每个值生成的术语数。值越低,生成的术语数越多。对于每个值的术语数量较多的字段,范围查询会更快,但索引会稍大一些。默认值为 16 表示 long 和 double, 8 表示 integer、short 和 float, 2147483647 表示字节。

  • coerce:这个属性默认为true,可以取的值truefalse。它定义了 Elasticsearch 是否应该尝试将字符串值转换为给定字段的数字,以及是否应该为基于整数的字段截断 float 值的小数部分。

  • ignore_malformed:这个属性可以取值truefalse(这是默认设置)。它应该设置为 true 以省略格式错误的值。

Boolean

boolean 核心类型设计用于索引布尔值(truefalse)。基于boolean类型的字段的示例定义如下:

"allowed" : { "type" : "boolean", "store": "yes" }
Binary

二进制字段是存储在索引中的二进制数据的 BASE64 表示。您可以使用它来存储通常 以二进制形式编写的数据,例如图像。基于这种类型的字段默认是存储的而不是索引的,所以你只能检索它们而不能对它们执行搜索操作。二进制类型只支持index_nametype、store和doc_values特性。基于二进制字段的示例字段定义可能如下所示:

"image" : { "type" : "binary" }
Date

日期核心类型被设计为用于日期索引。字段中的日期允许我们指定 Elasticsearch 可以识别的格式。值得注意的是,所有日期都以 UTC 编制索引,并在内部索引为长值。除此之外,对于基于日期的字段,Elasticsearch 接受表示自纪元以来的 UTC 毫秒的长值,而不管为日期字段指定的格式如何。

Elasticsearch 识别的默认日期格式非常通用,允许我们提供日期和可选的时间;例如,2012-12-24T12:10:22。基于日期类型的字段示例定义如下:

"published" : { "type" : "date", "format" : "YYYY-mm-dd" }

使用指定格式的上述日期字段的示例文档如下:

{
  "name" : "Sample document",
  "published" : "2012-12-22"
}

除了常用属性外,还可以为基于date类型的字段设置以下属性:

  • format:这个属性指定日期的格式。默认值为 dateOptionalTime。如需格式的完整列表,请访问https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-date-format.html .

  • precision_step:此 属性定义为数字字段中的每个值生成的术语数。有关此参数的更多信息,请参阅数字核心类型说明。

  • numeric_resolution:这个 属性定义了当 数字值被传递给基于日期的字段,而不是格式后面的日期。默认情况下,Elasticsearch 使用毫秒值,这意味着该数值将被视为自纪元以来的毫秒数。另一个值是秒。

  • ignore_malformed:这个属性可以取值truefalse。默认值为 false。它应该设置为 true 以忽略格式错误的值。

Multi fields

在某些情况下,我们需要对相同的字段进行不同的分析。例如,一种用于排序,一种用于搜索,一种用于聚合分析,但都使用相同的字段值,只是索引不同。我们当然可以使用前面描述的字段值复制,但我们也可以使用所谓的多字段。为了能够使用 Elasticsearch 的该功能,我们需要在字段定义中定义一个名为 fields 的附加属性。 fields 是一个对象,它可以包含一个或多个附加字段,这些字段将出现在我们的索引中,并且具有分配给它们的字段的值。例如,如果我们想对 name 字段进行聚合,并且除了在该字段上进行搜索之外,我们将其定义如下:

"name": {
  "type": "string",
  "fields": {
    "agg": { "type" : "string", "index": "not_analyzed" }
  }
}

前面的定义 将创建两个字段——一个称为name,第二个称为name.agg。当然,您不必在发送到 Elasticsearch 的数据中指定两个单独的字段——一个名为 name 的字段就足够了。 Elasticsearch 将完成剩下的工作,这意味着将字段的值复制到前面定义的所有字段中。

The IP address type

ip 字段类型已添加到 Elasticsearch 以简化以数字形式使用 IPv4 地址。此字段类型 允许我们搜索索引为 IP 地址的数据,对此类数据进行排序,并使用 IP 值使用范围查询。

基于其中一种数值类型的字段的示例定义如下:

"address" : { "type" : "ip" }

除了通用属性外,还可以为基于 ip 类型的字段设置 precision_step 属性。有关该属性的更多信息,请参阅数字类型说明。

使用基于 ip 的字段的示例文档如下所示:

{
  "name" : "Tom PC",
  "address" : "192.168.2.123"
}

Token count type

token_count 字段类型允许我们存储和索引有关给定字段有多少令牌的信息,而不是 存储和索引提供给字段的文本。它接受与数字类型相同的配置选项,但除此之外,我们需要指定用于将字段值划分为标记的分析器。我们通过使用 analyzer 属性来做到这一点。

基于 token_count 字段类型的字段的示例定义如下所示:

"title_count" : { "type" : "token_count", "analyzer" : "standard" }

Using analyzers

Elasticsearch 的伟大之处在于它利用了 Apache Lucene 的分析功能。这意味着对于 基于 string 类型的字段,我们可以指定 Elasticsearch 应该使用哪个分析器.您还记得 第 1 章的 全文搜索 部分< /a>,Elasticsearch Cluster 入门,分析器是一种用于以我们想要的方式分析数据或查询的功能。例如,当我们根据空格和小写字符划分单词时,我们不必担心 用户发送的单词是小写还是大写.这意味着 Elasticsearch、elasticsearch 和 ElAstIcSeaRCh 将被视为同一个词。更重要的是,Elasticsearch 不仅允许我们使用开箱即用的分析器,还可以创建我们自己的配置。我们还可以在索引时使用不同的分析器,在查询时使用不同的分析器——我们可以选择在搜索过程的每个阶段如何处理我们的数据。现在让我们看一下 Elasticsearch 提供的分析器和一般的 Elasticsearch 分析功能。

Out-of-the-box analyzers

Elasticsearch 允许我们使用默认定义的众多分析器之一。以下分析仪开箱即用:

Defining your own analyzers

除了前面提到的分析器之外,Elasticsearch 还允许我们定义新的分析器,而无需 编写一行 Java 代码。为此,我们需要在映射文件中添加一个附加部分;即设置部分,其中包含 Elasticsearch 在创建索引期间使用的附加信息。以下代码片段显示了我们如何定义自定义设置部分:

"settings" : {
  "index" : {
    "analysis": {
      "analyzer": {
        "en": {
          "tokenizer": "standard",
          "filter": [
            "asciifolding",
            "lowercase",
            "ourEnglishFilter"
          ]
        }
      },
      "filter": {
        "ourEnglishFilter": {
          "type": "kstem"
        }
      }
    }
  }
}

我们指定我们希望出现一个名为 en 的新分析器。每个分析器都是由单个标记器和多个过滤器构建的。 默认过滤器和标记器的完整列表可以在 https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-tokenizers.html。我们的 en 分析器包括标准分词器和三个过滤器:asciifoldinglowercase ,这是默认可用的,还有一个自定义的 ourEnglishFilter,这是我们定义的过滤器。

要定义过滤器,我们需要提供其名称、类型(type 属性)以及任意数量的附加 该过滤器类型所需的参数。 Elasticsearch 中可用的过滤器类型的完整列表可以在 https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-tokenfilters.html。请注意,我们不会讨论每个过滤器,因为过滤器列表会不断 变化。如果您对完整的过滤器列表感兴趣,请参阅文档中提到的页面。

因此,定义了我们的自定义分析器的最终映射文件将如下所示:

{
  "settings" : {
    "index" : {
      "analysis": {
        "analyzer": {
          "en": {
            "tokenizer": "standard",
            "filter": [
             "asciifolding",
             "lowercase",
             "ourEnglishFilter"
            ]
          }
        },
        "filter": {
          "ourEnglishFilter": {
            "type": "kstem"
          }
        }
      }
    }
  },
  "mappings" : {
    "post" : {
      "properties" : { 
        "id": { "type" : "long" },
        "name": { "type" : "string", "analyzer": "en" } 
      }
    }
  }
}

如果我们将前面的映射保存到一个名为 posts_mappings.json 的文件中, 我们可以运行以下命令来创建 posts 索引:

curl -XPOST 'http://localhost:9200/posts' -d @posts_mappings.json

我们可以使用 分析 API (https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-analyze.html)。例如,让我们看一下以下命令:

curl -XGET 'localhost:9200/posts/_analyze?pretty&field=name' -d 'robots cars'

该命令要求 Elasticsearch 使用为帖子类型及其名称定义的分析器 显示给定短语(机器人汽车)的分析内容场地。我们将从 Elasticsearch 得到的响应如下:

{
  "tokens" : [ {
    "token" : "robot",
    "start_offset" : 0,
    "end_offset" : 6,
    "type" : "<ALPHANUM>",
    "position" : 0
  }, {
    "token" : "car",
    "start_offset" : 7,
    "end_offset" : 11,
    "type" : "<ALPHANUM>",
    "position" : 1
  } ]
}

如您所见,robots cars 短语分为两个标记。除此之外,robots 一词已更改为 robotcars 字改为汽车。

Default analyzers

关于分析仪,还有一件事要说。如果没有定义分析器,Elasticsearch 允许我们指定默认使用的分析器。这与我们在映射文件的设置部分中配置自定义分析器的方式相同,但不是为分析器指定自定义 name,应使用默认关键字。因此,为了使我们之前定义的分析器成为默认分析器,我们可以将 en 分析器更改为以下内容:

{
  "settings" : {
    "index" : {
      "analysis": {
        "analyzer": {
          "default": {
            "tokenizer": "standard",
            "filter": [
             "asciifolding",
             "lowercase",
             "ourEnglishFilter"
            ]
          }
        },
        "filter": {
          "ourEnglishFilter": {
            "type": "kstem"
          }
        }
      }
    }
  }
}

我们还可以选择不同的默认分析器进行搜索,并选择不同的分析器进行索引。如果我们想 这样做而不是使用分析器名称的默认关键字,我们应该使用 default_searchdefault_index 分别。

Different similarity models

随着 2012 年 Apache Lucene 4.0 的发布,这个伟大的全文搜索库的所有用户都有机会 更改默认的基于 TF/IDF算法并使用不同的算法(我们在 全文搜索 部分中提到了它linkend="ch01">第 1 章Elasticsearch 集群入门)。因此,我们能够在 Elasticsearch 中选择相似性模型,这基本上允许我们对文档使用不同的评分公式。

Note

请注意,相似性模型主题范围从中级到高级,在大多数情况下,基于 TF/IDF 的算法足以满足您的用例。但是,我们决定在书中对其进行描述,这样您就知道如果需要,您可以更改评分算法的行为。

Setting per-field similarity

从 Elasticsearch 0.90 开始,我们可以为 mappings 文件中的每个字段设置不同的相似度。例如,假设我们使用以下简单映射来索引博客文章:

{
  "mappings" : {
    "post" : {
      "properties" : {
        "id" : { "type" : "long" },
        "name" : { "type" : "string" },
        "contents" : { "type" : "string" }
      }
    }
  }
}

为此,我们将对名称字段和内容字段使用 BM25 相似性模型。为此,我们需要扩展我们的字段定义并添加具有所选相似性名称的值 的相似性属性。我们更改后的映射将如下所示:

{
  "mappings" : {
    "post" : {
      "properties" : {
        "id" : { "type" : "long" },
        "name" : { "type" : "string", "similarity" : "BM25" },
        "contents" : { "type" : "string", "similarity" : "BM25" }
      }
    }
  }
}

仅此而已,不再需要任何东西。经过上述更改后,Apache Lucene 将使用 BM25 相似度来计算名称和内容字段的得分因子。

Available similarity models

至少有五个新的相似模型可用。对于大多数用例,除了默认的用例,您可能会发现以下模型很有用:

  • Okapi BM25 模型:此相似性模型基于一个概率模型,该模型估计给定查询找到文档的概率。为了在 Elasticsearch 中使用这种相似性,您需要使用 BM25 名称。据说 Okapi BM25 相似性在处理短文本文档时表现最好,其中术语重复对整体文档得分特别有害。要使用这种相似性,需要将字段的相似性属性设置为 BM25。这种相似性是开箱即用的,不需要设置其他属性。

  • Divergence from randomness model:这个相似度模型是基于概率模型同名。为了在 Elasticsearch 中使用这种相似性,您需要使用 DFR 名称。据说散度与随机性相似性模型在与自然语言相似的文本上表现良好。

  • 基于信息的模型:这是新引入的最后一个模型相似性模型,与随机性模型的分歧非常相似。为了在 Elasticsearch 中使用这种 相似性,您需要使用 IB 名称。与 DFR 相似度类似,据说基于信息的模型在类似于自然语言文本的数据上表现良好。

目前可用的另外两个相似度模型是 LM Dirichlet 相似度(要使用它,将 type 属性设置为 LMDirichlet)和 LM Jelinek Mercer 相似度(要使用它,请将 type 属性设置为 LMJelinekMercer)。您可以在 Apache Lucene JavadocsMastering Elasticsearch Second Edition 中找到有关这些相似性模型的更多信息,已发布由 Packt Publishing 或在 Elasticsearch 的官方 文档中提供,可在 https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-similarity.html< /一>。

Configuring default similarity

默认相似度 允许我们提供额外的 discount_overlaps 属性。它允许我们控制在分数计算期间是否省略令牌流中相同位置上的令牌(位置增量为 0)。默认设置为true,即省略相同位置的token;如果您希望它们被计算在内,您可以将该属性设置为 false。例如,以下命令显示了如何创建一个索引,其中 discount_overlaps 属性已更改为默认相似性:

curl -XPUT 'localhost:9200/test_similarity' -d '{
 "settings" : {
  "similarity" : {
   "altered_default": {
    "type" : "default",
    "discount_overlaps" : false
   }
  }
 },
 "mappings": {
  "doc": {
   "properties": {
    "name": { "type" : "string", "similarity": "altered_default" }
   }
  }
 }
}'
Configuring BM25 similarity

即使我们不需要配置 BM25 相似性,我们也可以提供一些额外的选项来调整其行为。 BM25 相似度允许我们提供 discount_overlaps 属性,类似于 默认相似度和两个附加属性:< code class="literal">k1 和 bk1 属性指定词频归一化因子,b 属性值确定文档长度将词频归一化的程度价值观。

Configuring DFR similarity

在DFR相似的情况下,我们可以配置basic_model属性(可以取值be, d, g, if< /code>、inpine)、< code class="literal">after_effect 属性(值为 nobl),以及 normalization 属性(可以是 noh1h2h3z)。如果我们选择 no 以外的 normalization 值,我们需要设置 normalization 因素。

根据选择的规范化值,我们应该为 normalization.h1.cfloat 值) literal">h1 规范化,normalization.h2.cfloat 值)用于 h2 规范化,normalization.h3.cfloat 值)用于 h3 规范化,以及 normalization.z.zfloat 值)用于 z 标准化。例如,以下是示例相似性配置的外观(我们将其放入映射文件的设置部分):

      "similarity" : {
        "esserverbook_dfr_similarity" : {
          "type" : "DFR",
          "basic_model" : "g",
          "after_effect" : "l",
          "normalization" : "h2",
          "normalization.h2.c" : "2.0"
        }
      }
Configuring IB similarity

在 IB 相似度的情况下,我们有以下参数可以配置分布属性(可以取 llspl< /code>)和 lambda 属性(可以取 dftff 的值)。除此之外,我们可以选择归一化因子,它与 DFR 相似度的 相同,因此我们将省略第二次描述。以下是示例 IB 相似性配置的外观(我们将其放入映射文件的设置部分):

      "similarity" : {
        "esserverbook_ib_similarity" : {
          "type" : "IB",
          "distribution" : "ll",
          "lambda" : "df",
          "normalization" : "z",
          "normalization.z.z" : "0.25"
        }
      }

Batch indexing to speed up your indexing process


第 1 章中,Elasticsearch 集群入门 ,我们看到了如何将特定文档索引到 Elasticsearch 中。它需要打开 HTTP 连接、发送文档并关闭连接。当然,当我们使用 curl 命令时,我们不负责其中的大部分,但在后台发生了这样的事情。但是,逐个发送 文档效率不高。正因为如此,现在是时候找出如何以比逐个索引更方便、更高效的方式索引大量 文档了。

Preparing data for bulk indexing

Elasticsearch 允许我们 将许多请求合并到一个包中。此包可以作为单个请求发送。更重要的是,我们不仅限于在所谓的批量中拥有单一类型的请求——我们可以将不同类型的操作混合在一起,包括:

  • 添加或替换索引中的现有文档(index

  • 从索引中删除 个文档(delete

  • 当索引中没有文档的其他定义时,将新文档添加到索引中(create

  • 如果文档不存在,则修改文档或创建新文档(update

选择请求的格式是为了提高处理效率。它假定请求的每一行都包含一个带有操作描述的 JSON 对象,然后是带有文档的第二行——另一个 JSON 对象本身。我们可以将第一条线视为一种信息线,将第二条线视为数据线。此规则的例外是 delete 操作,它只包含信息行,因为不需要文档。让我们看下面的例子:

{ "index": { "_index": "addr", "_type": "contact", "_id": 1 }}
{ "name": "Fyodor Dostoevsky", "country": "RU" }
{ "create": { "_index": "addr", "_type": "contact", "_id": 2 }}
{ "name": "Erich Maria Remarque", "country": "DE" }
{ "create": { "_index": "addr", "_type": "contact", "_id": 2 }}
{ "name": "Joseph Heller", "country": "US" }
{ "delete": { "_index": "addr", "_type": "contact", "_id": 4 }}
{ "delete": { "_index": "addr", "_type": "contact", "_id": 1 }}

将每个文档或操作描述放在一行中(以换行符结尾)非常重要。这意味着该文档不能被漂亮地打印出来。批量索引文件的 大小有一个默认限制,设置为 100 MB,可以通过指定 http.max_content_length 属性在 Elasticsearch 配置文件中。这让我们在 处理太大的请求时避免了可能的请求超时和内存问题。

Note

请注意,使用单个批量索引文件,我们可以将数据加载到多个索引中,并且批量请求中的文档可以具有不同的类型。

Indexing the data

为了执行 批量请求,Elasticsearch 提供了 _bulk 端点。这可以用作 /_bulk 或索引名称为 /index_name/_bulk ,甚至类型和索引名称为/index_name/type_name/_bulk。第二种和第三种形式定义了索引名称和类型名称的默认值。我们可以在请求的信息行中省略这些属性,Elasticsearch 将使用 URI 中的默认值。还值得知道的是,默认 URI 值可以被信息行中的值覆盖。

假设我们已经将数据存储在 documents.json 文件中,我们可以运行以下命令将这些数据发送到 Elasticsearch:

curl -XPOST 'localhost:9200/_bulk?pretty' --data-binary @documents.json

?pretty 参数当然不是必须的。我们使用这个参数只是为了便于分析前面命令的响应。在这种情况下,重要的是使用带有 --data-binary 参数的 curl 而不是使用 -d。这是因为标准的 –d 参数会忽略换行符,正如我们之前所说,这对于 Elasticsearch 解析批量请求内容很重要。现在让我们看一下 Elasticsearch 返回的响应:

{
  "took" : 469,
  "errors" : true,
  "items" : [ {
    "index" : {
      "_index" : "addr",
      "_type" : "contact",
      "_id" : "1",
      "_version" : 1,
      "_shards" : {
        "total" : 2,
        "successful" : 1,
        "failed" : 0
      },
      "status" : 201
    }
  }, {
    "create" : {
      "_index" : "addr",
      "_type" : "contact",
      "_id" : "2",
      "_version" : 1,
      "_shards" : {
        "total" : 2,
        "successful" : 1,
        "failed" : 0
      },
      "status" : 201
    }
  }, {
    "create" : {
      "_index" : "addr",
      "_type" : "contact",
      "_id" : "2",
      "status" : 409,
      "error" : {
        "type" : "document_already_exists_exception",
        "reason" : "[contact][2]: document already exists",
        "shard" : "2",
        "index" : "addr"
      }
    }
  }, {
    "delete" : {
      "_index" : "addr",
      "_type" : "contact",
      "_id" : "4",
      "_version" : 1,
      "_shards" : {
        "total" : 2,
        "successful" : 1,
        "failed" : 0
      },
      "status" : 404,
      "found" : false
    }
  }, {
    "delete" : {
      "_index" : "addr",
      "_type" : "contact",
      "_id" : "1",
      "_version" : 2,
      "_shards" : {
        "total" : 2,
        "successful" : 1,
        "failed" : 0
      },
      "status" : 200,
      "found" : true
    }
  } ]
}

正如我们所见,每个结果都是 items 数组的一部分。让我们简单地将这些结果与我们的输入数据进行比较。前两个命令,名为 index 和 create,执行没有任何问题。第三个操作失败了,因为我们想创建一个具有 的标识符的记录,该标识符已经存在于索引中。接下来的两个操作是删除。两者都成功了。请注意,他们中的第一个试图删除一个不存在的文档;如您所见,这对 Elasticsearch 来说不是问题——但值得注意的是,对于不存在的文档,我们看到状态为 404,在 HTTP 响应中 代码表示未找到(http://www .w3.org/Protocols/rfc2616/rfc2616-sec10.html)。如您所见,Elasticsearch 会返回有关每个操作的信息,因此对于大批量请求,响应可能会很大。

The _all field

_all 字段被 Elasticsearch 用于将来自所有其他字段的数据存储在单个字段中以方便使用的搜索。当我们想要实现一个简单的搜索功能并且我们想要搜索所有数据(或仅我们复制到 _all 字段的字段)时,这种字段可能很有用,但是我们不想 考虑字段名称之类的东西。默认情况下,启用 _all 字段并包含文档中所有字段的所有数据。但是,此字段使索引更大一些,并且并不总是需要。

例如,当您在图书馆目录网站的搜索框中输入搜索词组时,您希望可以使用作者姓名、ISBN 号和书名包含的单词进行搜索,但搜索的是页数或者封面类型通常没有意义。我们可以完全禁用 _all 字段或排除某些字段的复制。为了不在 _all 字段中包含某个字段,我们使用本章前面讨论过的 include_in_all 属性.为了完全关闭 _all 字段功能,我们修改我们的映射文件如下:

{
  "book" : {
    "_all" : {
      "enabled" : false
     },
     "properties" : {
       .  .  .
     }
  }
}

除了 enabled 属性之外,_all字段支持以下:

  • 存储

  • term_vector

  • 分析器

有关 前面属性的信息,请参阅映射配置部分这一章。

The _source field

_source 字段允许我们 存储在索引期间发送到 Elasticsearch 的原始 JSON 文档。默认情况下,_source 字段是打开的,因为某些 Elasticsearch 功能依赖于它(例如,部分更新功能)。除此之外,_source 字段可以用作突出显示 功能的数据源,如果不存储字段。但是,如果我们不需要这样的功能,我们可以禁用 _source 字段,因为它会导致一些存储开销。为此,我们需要将 _source 对象的 enabled 属性设置为 false,如下所示:

{
  "book" : {

	"_source" : {
      "enabled" : false
    },
    "properties" : {
      . . .
    }
  }
}

我们还可以告诉 Elasticsearch 我们要从 _source 字段中排除哪些字段以及我们要包含哪些字段。我们通过将 includesexcludes 属性添加到 _source 字段来做到这一点 定义。例如,如果我们想从 _source 字段中排除作者路径中的所有 字段,我们的映射如下所示:

{
  "book" : {
    "_source" : {
      "excludes" : [ "author.*" ]
    },
    "properties" : {
      . . .
    }
  }
}

Additional internal fields

还有一些其他字段是 Elasticsearch 内部使用的,但我们无法配置。这些字段是:

  • _id:该字段用于保存索引内文档的标识符和类型

  • _uid:该字段用于保存索引中文档的唯一标识符,由_id_type (这允许在同一个索引中拥有具有相同标识符和不同类型的文档)

  • _type:该字段是文档的类型 名称

  • _field_names:该字段是文档中存在的字段的列表

Introduction to segment merging


第1章全文搜索部分, Elasticsearch Cluster 入门,我们提到了分段及其不变性。我们写道,Lucene 库以及 Elasticsearch 将数据写入某些结构,这些结构只写一次就不会改变。这允许一些简化,但也引入了额外工作的需要。一个这样的 示例是删除。由于段不能被更改,因此必须将有关删除的信息存储在旁边,并在搜索期间动态应用。这是通过从返回的结果集中过滤已删除的文档来完成的。另一个示例是无法修改文档(但是,可以进行一些修改,例如修改数字文档值)。当然,可以说 Elasticsearch 支持文档更新(请参阅 使用 REST API 处理数据部分="#" linkend="ch01">第 1 章Elasticsearch 集群入门)。但是,在后台,旧文档被标记为已删除,并且内容更新的文档被编入索引。

随着时间的推移,您继续索引或删除您的数据,越来越多的段被创建。根据您修改索引的频率,Lucene 创建具有不同数量文档的段 - 因此,段具有不同的大小。因此,搜索性能可能会降低,并且您的索引可能会比应有的更大——它仍然包含已删除的文档。这个等式很简单——索引的段越多,搜索速度就越慢。这是段合并发挥作用的时候。我们不想详细描述这个过程;在当前的 Elasticsearch 版本中,这部分引擎被简化了,但它仍然是一个相当高级的话题。我们决定提到合并是因为我们认为知道在哪里寻找与打开文件过多、可疑 CPU 使用率、扩展索引或搜索和索引速度随时间下降有关的问题的原因很方便。

Segment merging

段合并是 期间的过程,底层 Lucene 库采用几个段并根据其中找到的信息创建一个新段。结果段具有存储在原始段中的所有文档,除了标记为删除的文档。合并操作后,源段将从磁盘中删除。由于段合并在 CPU 和 I/O 使用方面的成本相当高,因此适当控制调用此过程的时间和频率至关重要。

The need for segment merging

你可能会问自己为什么你必须为段合并而烦恼。首先,建立索引的段越多,搜索就越慢,Lucene 使用的内存就越多。第二个是索引使用的磁盘空间和资源,例如文件描述符。如果您从索引中删除了许多文档,那么在合并发生之前,这些文档只会被标记为已删除,而不会被物理删除。因此,可能会发生大多数使用我们的 CPU 和内存的文档不存在的情况!幸运的是,Elasticsearch 为段合并使用了合理的默认值,并且很有可能不需要进行任何更改。

The merge policy

合并策略定义了何时应该执行合并过程。 Elasticsearch 合并大小大致相似的段,同时考​​虑每层允许的最大段数。合并算法可以找到合并成本最低、对结果段影响最大的段。

分层合并 策略的基本属性如下:

  • index.merge.policy.expunge_deletes_allowed:此属性告诉 Elasticsearch 合并段与 已删除文档的百分比高于此值,默认为 10

  • index.merge.policy.floor_segment:该属性默认为 2mb 并告诉 Elasticsearch 处理 较小的段,其大小等于该属性的值。它可以防止刷新小段以避免它们的数量过多。

  • index.merge.policy.max_merge_at_once:在此属性中,要合并的最大段数一次默认为 10

  • index.merge.policy.max_merge_at_once_explicit:在这个属性中,segments的最大数量expunge 删除或优化操作默认为 10

  • index.merge.policy.max_merged_segment:在该属性中,可以产生的最大段大小在正常合并期间默认为 5gb

  • index.merge.policy.segments_per_tier:该属性默认为10,大致定义了段数。较小的值意味着更多的合并但更少的 segments,这导致更高的搜索速度但更低的索引速度和更大的 I/O 压力。较高的属性值将导致较高的段数,因此搜索速度较慢但索引速度较高。

  • index.merge.policy.reclaim_deletes_weight - 该属性告诉 Elasticsearch 选择具有多个删除的文件。默认为 2.0

    例如,要更新已创建索引的合并策略设置,我们可以运行如下命令:

    curl -XPUT 'localhost:9200/essb/_settings' -d '{
    "index.merge.policy.max_merged_segment" : "10gb"
    }'
    

要深入了解分段合并,请参阅 Packt Publishing 出版的 Mastering Elasticsearch Second Edition 一书。您还可以在 https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-merge.html

Note

在 Elasticsearch 2.0 版本之前,我们可以在三种合并策略中进行选择:tieredlog_byte_size,log_doc。当前使用的合并策略是基于分层合并策略的,我们被迫使用它。

The merge scheduler

合并调度程序告诉 Elasticsearch 合并过程应该如何发生。当前实现基于并发合并调度程序, 在单独的线程中启动,并使用定义数量的线程并行执行合并。 Elasticsearch 允许您使用 index.merge.scheduler.max_thread_count 属性设置可用于同时合并的线程数。

Throttling

正如我们已经提到的,当涉及到服务器资源时,合并可能 成本很高。合并过程通常与其他操作并行工作,因此理论上它应该不会产生太大影响。在实践中,磁盘输入/输出操作的数量可能会很大,以至于会显着影响整体性能。在这种情况下,节流可能会有所帮助。事实上,这个特性可以用来限制合并的速度,但它也可以用于所有使用数据存储的操作。可以在 Elasticsearch 配置文件(elasticsearch.yml 文件)中设置限制,也可以使用设置 API 动态设置(参考 Chapter 9 的更新设置 API 部分, Elasticsearch 集群, 了解详细信息)。有两种设置可以调整限制:typevalue

要设置 限制类型,请设置 indices.store.throttle.type 属性,其中 允许我们使用以下值:

  • none:此值定义 没有开启限制

  • merge:该值定义限制只影响合并过程

  • all:此值 定义对所有数据存储活动使用限制

第二个属性 indices.store.throttle.max_bytes_per_sec, 描述了节流对 I/O 操作的限制程度。顾名思义,它告诉我们每秒可以处理多少字节。例如,我们看下面的配置:

indices.store.throttle.type: merge
indices.store.throttle.max_bytes_per_sec: 10mb

在此示例中,我们将合并操作限制为每秒 10 兆字节。默认情况下,Elasticsearch 使用 max_bytes_per_sec 属性设置为 20mb 的合并限制类型。这意味着所有合并操作都被限制为每秒 20 兆字节。

Introduction to routing


默认情况下,Elasticsearch 会尝试 将您的文档均匀地分布在索引的所有分片中。然而,这并不总是理想的情况。为了检索文档,Elasticsearch 必须查询所有分片并合并结果。如果我们可以在某些基础上(例如,客户端标识符)划分我们的数据并使用该信息将具有相同属性的数据放在集群中的相同位置会怎样。 Elasticsearch 允许我们通过公开强大的文档和查询分布控制机制路由来做到这一点。简而言之,它允许我们选择用于索引或搜索数据的分片。

Default indexing

在索引操作期间,当您发送一个文档进行索引时,Elasticsearch 会查看其标识符来选择应该在其中索引文档的分片。默认情况下,Elasticsearch 会计算文档标识符的哈希值, 在此基础上,它将文档放入可用的主分片之一。然后,这些文档被重新分发到副本。下图显示了默认情况下索引如何工作的简单说明:

读书笔记《elasticsearch-server-third-edition》为您的数据编制索引

Default searching

搜索 与索引有点不同,因为在大多数情况下您需要查询获取您感兴趣的数据的所有分片(我们将在 Chapter 3, 搜索您的数据),至少在查询的初始分散阶段。想象一下,当您有以下映射描述您的索引时:

   {
     "mappings" : {
       "post" : {
         "properties" : {
           "id" : { "type" : "long" },
           "name" : { "type" : "string" },
           "contents" : { "type" : "string" },
           "userId" : { "type" : "long" }
    } } 
} }

如您所见,我们的索引由四个字段组成:标识符(id 字段)、文档名称(name 字段)、文档内容(contents 字段)和文档的用户标识符属于(userId 字段)。要获取特定用户的所有文档,其中一个 userId 等于 12,您可以运行以下查询:

curl –XGET 'http://localhost:9200/posts/_search?q=userId:12'

取决于搜索的 类型(我们将在第3章中详细讨论搜索您的数据),Elasticsearch 将运行您的查询。这通常意味着它将首先向所有节点查询匹配文档的标识符和分数,然后 它会再次发送内部查询,但仅限于相关分片(包含所需文档的分片)以获取构建响应所需的文档。

下图显示了默认搜索在其初始阶段如何工作的非常简化的视图:

读书笔记《elasticsearch-server-third-edition》为您的数据编制索引

如果我们可以将单个用户的所有文档放入单个分片并在该分片上查询会怎样?这对性能来说不是明智的吗?是的,这很方便,这就是路由允许您做的事情。

Routing

路由可以控制 将您的文档和查询转发到哪个分片。到目前为止,您可能已经猜到我们可以在索引期间和查询期间指定路由值,事实上,如果您决定指定显式路由值,您可能希望在索引和搜索期间执行此操作。

在我们的例子中,我们将在索引期间使用 userId 值来设置 routing,并且在搜索期间将使用相同的值。因为我们将为单个用户的所有文档使用相同的路由值,所以将计算相同的哈希值,因此该特定用户的所有文档将被放置在同一个分片中。在搜索期间使用相同的值将导致搜索单个分片而不是整个索引。

在搜索时使用路由时,您应该记住一件事。搜索时,您应该添加一个查询部分,将返回的文档限制为给定用户的文档。路由是不够的。这是因为您可能拥有比构建索引所使用的分片数量更多的不同路由值。例如,您可以有 10 个分片来构建索引,但同时有数百个用户。将单个分片仅用于单个用户在物理上是不可能的。从缩放的角度来看,它通常也不好。因此,一些不同的值可以指向同一个分片——在我们的例子中,几个用户的数据将被放置在同一个分片中。因此,我们需要一个查询部分,它将数据限制为特定的用户标识符,例如术语查询。

下图显示了搜索如何使用提供的自定义路由值的非常简单的说明:

读书笔记《elasticsearch-server-third-edition》为您的数据编制索引

如您所见,Elasticsearch 会将我们的查询发送到单个分片。现在让我们看看如何指定 routing 值。

The routing parameters

这个想法很简单。 端点用于与在 Elasticsearch 中获取或存储文档相关的所有操作,允许我们使用称为路由的附加参数。您可以将其添加到您的 HTTP 或使用您选择的客户端库进行设置。

因此,为了将示例文档索引到之前显示的索引,我们将使用以下命令:

curl -XPUT 'http://localhost:9200/posts/post/1?routing=12' -d '{
  "id": "1",
  "name": "Test document",
  "contents": "Test document",
  "userId": "12"
}'

如果我们现在回到之前获取用户数据的查询并修改它以使用路由,它将如下所示:

curl -XGET 'http://localhost:9200/posts/_search?routing=12&q=userId:12'

如您所见,在索引和查询期间使用了相同的路由值。当使用路由时,这在大多数情况下是可能的。我们知道我们正在索引哪些用户数据,并且我们可能会知道哪个用户正在搜索数据。在我们的例子中,我们的假想用户被赋予了 12 的标识符,我们在索引和搜索过程中使用了这个值。

请注意,在搜索的过程中,您可以指定多个以逗号分隔的路由值。例如,如果我们希望通过 section 参数的值(如果存在)额外路由前面的查询,并且我们还希望按此参数进行过滤,我们的查询将如下所示:

curl -XGET 'http://localhost:9200/posts/_search?routing=12,6654&q=userId:12+AND+section:6654'

当然,前面的命令现在可以匹配多个分片,因为路由的值可以指向多个分片。因此,您需要在索引期间仅提供单个路由值(Elasticsearch 需要指向单个分片,否则索引将失败)。您当然可以同时查询多个分片,因此可以在搜索期间提供多个路由值。

Note

请记住,路由并不是为给定用户获取结果所需的唯一内容。那是因为通常我们很少有具有唯一路由值的分片。这意味着我们将在一个分片中拥有来自多个用户的数据。因此,在使用路由时,您还应该将结果范围缩小到给定用户的结果。您将在 第 3 章搜索您的数据

Routing fields

使用索引操作时,为每个请求指定路由值至关重要。没有它,Elasticsearch 使用默认的方式来确定文档应该存储在哪里——它使用文档标识符的哈希值。这可能会导致一个文档存在于不同分片上的多个版本中。在获取 文档时可能会出现类似的情况。当使用给定的路由值存储文档时,我们可能会命中错误的分片并且可能找不到该文档。

事实上,Elasticsearch 允许我们更改默认行为并强制我们在查询给定索引时使用路由。为此,我们需要在类型定义中添加以下部分:

   "_routing" : {
     "required" : true
   }

上述定义表示需要提供路由值("required": true属性) ;没有它,索引请求将失败。

Summary


在本章中,我们学到了很多关于 Elasticsearch 中的索引和数据处理的知识。我们从有关 Elasticsearch 的基本信息开始,然后继续调整 Elasticsearch 中的无模式行为。我们学习了如何配置我们的映射,使用 Elasticsearch 开箱即用的语言分析功能,以及创建我们自己的映射。我们研究了批量索引以加快索引速度,并在索引中的文档中添加了额外的内部信息。最后,我们查看了段合并和路由。

在下一章中,我们将完全专注于 Elasticsearch 的搜索和广泛的查询语言。我们将从如何查询 Elasticsearch 以及 Elasticsearch 查询过程如何工作开始。我们将了解所有基本查询和复合查询,以便能够在我们的应用程序中使用它们。最后,我们将看到应该为给定的用例选择哪个查询。