vlambda博客
学习文章列表

读书笔记《elasticsearch-server-third-edition》扩展您的索引结构

Chapter 5. Extending Your Index Structure

我们通过学习如何处理 Elasticsearch 2.x 中修改后的过滤以及现在对它的期望来开始上一章。我们还探讨了突出显示以及它如何帮助我们改善用户的搜索体验。我们在 Elasticsearch 中发现了查询验证,并在 Elasticsearch 中学习了数据排序的方法。最后,我们讨论了查询重写以及它如何影响我们的查询。在本章结束时,您将学习以下主题:

  • 索引树状结构

  • 索引不平坦的数据

  • 使用嵌套对象和父子功能处理文档关系

  • 使用 Elasticsearch API 修改索引结构

Indexing tree-like structures


树木无处不在。如果您 开发一个电子商务商店应用程序,您的产品可能会使用类别来描述。关于类别的事情是,在大多数情况下,它们是分层的。有顶级类别,例如电子产品、音乐、书籍等。每个顶级类别都可以有许多子类别,例如小说和科学,这些类别可以更深入地研究科幻、爱情等。如果您查看文件系统,文件和目录也以树状结构排列。这本书也可以表示为一棵树:章节包含主题,主题分为子主题。所以我们周围的数据被排列成树状结构,正如你想象的那样,Elasticsearch 能够索引树状结构,以便我们可以更轻松地表示数据。让我们检查一下如何使用 path_analyzer 浏览此类数据。

Data structure

首先,让我们使用以下命令创建一个 简单的索引结构:

curl -XPUT 'localhost:9200/path?pretty' -d '{
  "settings" : {
    "index" : {
      "analysis" : {
        "analyzer" : {
          "path_analyzer" : { "tokenizer" : "path_hierarchy" }
        }
      }
    }
  },
  "mappings" : {
    "category" : {
      "properties" : {
        "category" : {
          "type" : "string",
          "fields" : {
            "name" : { "type" : "string", "index" : "not_analyzed" },
            "path" : { "type" : "string", "analyzer" : "path_analyzer", "store" : true }
          }
        }
      }
    }
  }
}'

如您所见,我们创建了一个类型——类别类型。我们将使用它来存储和索引有关我们的文档在树结构中的位置的信息。这个想法很简单——我们可以将文档的位置显示为路径,其方式与文件和目录在硬盘驱动器上的显示方式完全相同。例如,在汽车商店,我们可以有 /cars/passenger/sport/cars/passenger/camper,或/cars/delivery_truck/。然而,为了实现这一点,我们需要以两种不同的方式索引这条路径。首先,我们将使用一个名为 name 的未分析字段,以原始形式存储和索引路径名称。我们还将使用一个名为 path 的字段,它将使用我们定义的 path_analyzer 分析器来处理路径,因此它更容易搜索。

Analysis

现在,让我们看看 Elasticsearch 在分析过程中将如何处理类别路径。为了看到这一点,我们 将使用以下命令行,该命令行使用中讨论的分析API了解第 6 章的解释信息部分,让你更好地搜索

curl -XGET 'localhost:9200/path/_analyze?field=category.path&pretty' -d '/cars/passenger/sport'

Elasticsearch 将返回以下结果:

{
  "tokens" : [ {
    "token" : "/cars",
    "start_offset" : 0,
    "end_offset" : 5,
    "type" : "word",
    "position" : 0
  }, {
    "token" : "/cars/passenger",
    "start_offset" : 0,
    "end_offset" : 15,
    "type" : "word",
    "position" : 0
  }, {
    "token" : "/cars/passenger/sport",
    "start_offset" : 0,
    "end_offset" : 21,
    "type" : "word",
    "position" : 0
  } ]
}

可以看到,我们的类别路径 /cars/passenger/sport 被 Elasticsearch 处理并分为三个令牌。多亏了这一点,我们可以使用术语过滤器轻松找到属于给定类别或其子类别的每个文档。为了使示例完整,让我们使用以下命令索引一个简单的文档:

curl -XPUT 'localhost:9200/path/category/1' -d '{ "category" : "/cars/passenger/sport" }'

使用过滤器的示例如下:

curl -XGET 'localhost:9200/path/_search?pretty' -d '{
  "query" : {
    "bool" : {
      "filter" : {
        "term" : {
          "category.path" : "/cars"
        }
      }
    }
  }
}'

请注意,我们还在 category.name 字段中索引了原始值。当我们想要 从特定路径查找文档时,这很方便,忽略层次结构中更深的文档。

Indexing data that is not flat


并非所有数据都像我们迄今为止在书中使用的 示例一样平坦。您将遇到的大多数数据将在根 JSON 对象内具有一些结构和嵌套对象。当然,如果我们正在构建我们的系统,Elasticsearch 将成为其中的一部分,并且我们可以控制它的所有部分,我们可以创建一个便于 Elasticsearch 的结构。但即使在这种情况下,平面数据也不总是一种选择。值得庆幸的是,Elasticsearch 允许我们索引不平坦的数据,本节将向我们展示如何做到这一点。

Data

假设我们有 以下数据(我们将其存储在名为 structured_data.json 的文件中):

{
  "author" : {
    "name" : {
      "firstName" : "Fyodor",
      "lastName" : "Dostoevsky"
    }
  },
  "isbn" : "123456789",
  "englishTitle" : "Crime and Punishment",
  "year" : 1886,
  "characters" : [
    {
      "name" : "Raskolnikov"
    }, 
    {
      "name" : "Sofia"
    }
  ],
  "copies" : 0
}

如您所见,数据不是平面的——它包含数组和嵌套对象。如果我们想创建映射并使用我们目前掌握的知识,我们将不得不扁平化数据。然而,正如我们已经说过的,Elasticsearch 允许某种程度的结构,我们应该能够创建适用于前面示例的映射。

Objects

前面的示例 data 显示了结构化的 JSON 文件。正如您在示例中看到的,我们的根对象有一些额外的简单属性,例如englishTitle、isbn、year 和copies。这些将被索引为索引中的普通字段,我们已经知道如何处理它们(我们在 第 2 章索引您的数据)。除此之外,它还有字符数组类型和作者对象。 author 对象中嵌套了另一个对象 - name 对象,它有两个属性:firstNamelastName。如您所见,我们可以在彼此内部拥有多个嵌套对象。

Arrays

我们已经使用了array 类型的数据,但是我们没有谈论它。默认情况下,Lucene 和 Elasticsearch 中的所有字段都是多值的,这意味着它们可以存储多个值。为了将这些字段发送到 Elasticsearch 的索引,我们使用 JSON 数组类型,它嵌套在方括号 [] 中。正如您在前面的示例中所看到的,我们为书中的字符使用了数组类型。

Mappings

现在让我们看看我们之前展示的 book 对象的 映射是什么样子的。我们已经说过,索引数组不需要任何特殊的东西。因此,在 我们的例子中,要索引字符数据,我们需要添加类似于以下的字段定义:

"characters" : {
 "properties" : {
  "name" : {"type" : "string"}
 }
}

没什么奇怪的!我们只是将属性部分嵌套在数组名称中(在我们的例子中是字符),并在那里定义字段。作为上述映射的结果,我们将在索引中获得 characters.name 多值字段。

我们为作者对象做类似的事情。我们使用与数据中存在的名称相同的名称来调用该部分。我们有 author 对象,但它也有name< /code> 对象嵌套在其中,所以我们也这样做——我们只是在其中嵌套另一个对象。因此,我们对 author 字段的映射如下所示:

"author" : {
 "properties" : {
  "name" : {
   "properties" : {
    "firstName" : {"type" : "string"},
    "lastName" : {"type" : "string"}
   }
  }
 }
}

firstNamelastName 字段在索引中显示为 author.name.firstNameauthor.name.lastName

其余字段是简单的核心类型,因此我将跳过讨论它们,因为它们已经在 映射配置 部分中讨论过="link" href="#" linkend="ch02">第 2 章索引您的数据

Final mappings

所以我们的最终映射文件,我们已经 称为 structured_mapping.json, 如下所示:

{
 "book" : {
  "properties" : {
   "author" : {
    "type" : "object",
    "properties" : {
     "name" : {
      "type" : "object",
      "properties" : {
       "firstName" : {"type" : "string"},
       "lastName" : {"type" : "string"}
      }
     }
    }
   },
   "isbn" : {"type" : "string"},
   "englishTitle" : {"type" : "string"},
   "year" : {"type" : "integer"},
   "characters" : {
    "properties" : {
     "name" : {"type" : "string"}
    }
   },
   "copies" : {"type" : "integer"}
  }
 }
}

Sending the mappings to Elasticsearch

现在我们已经完成了映射,我们想测试我们所做的所有工作是否真的有效。这次我们将使用一种稍微不同的技术来创建索引和放置映射。首先,让我们使用以下命令创建库索引(如果您已经拥有,则需要删除 库索引):

curl -XPUT 'localhost:9200/library'

现在,让我们发送 book 类型的映射:

curl -XPUT 'localhost:9200/library/book/_mapping' -d @structured_mapping.json

现在我们可以索引我们的示例数据:

curl -XPOST 'localhost:9200/library/book/1' -d @structured_data.json

To be or not to be dynamic

我们已经知道,Elasticsearch 是无模式的,这意味着它可以索引数据而无需预先创建映射。当数据中遇到新的 字段时,Elasticsearch 会在后台执行映射更新——它会尝试猜测字段类型并将其添加到映射。默认情况下,Elasticsearch 的动态行为是打开的,但在某些情况下,您可能希望对索引的某些部分关闭它。为此,应将 dynamic 属性添加到给定字段并将其设置为 false。这应该在与对象的 type 属性相同的嵌套级别上完成,这不应该是动态的。例如,如果我们希望我们的作者和名称对象不是动态的,我们应该修改映射文件的相关部分,使其如下所示:

"author" : {
 "type" : "object",
 "dynamic" : false,
 "properties" : {
  "name" : {
   "type" : "object",
   "dynamic" : false,
   "properties" : {
    "firstName" : {"type" : "string", "index" : "analyzed"},
    "lastName" : {"type" : "string", "index" : "analyzed"}
   }
  }
 }
}

但是,请记住,为了为此类对象添加新字段,我们必须更新 映射。

Note

您还可以通过将 index.mapper.dynamic 属性添加到您的 elasticsearch.yml 配置文件和将其设置为 false

Disabling object indexing

当涉及到对象处理时,还有一个 我们想提一下——我们可以通过 使用 enabled 属性并将其设置为 false。这可能有多种原因,例如不希望某个字段被索引或不希望整个 JSON 对象被索引。例如,如果我们想从作者对象中省略一个名为 information 的对象,我们将使作者对象定义如下所示:

"author" : {
 "type" : "object",
  "properties" : {
  "name" : {
   "type" : "object",
   "dynamic" : false,
   "properties" : {
    "firstName" : {"type" : "string", "index" : "analyzed"},
    "lastName" : {"type" : "string", "index" : "analyzed"},
    "information" : {"type" : "object", "enabled" : false}
   }
  }
 }
}

dynamic 参数也可以设置为 strict。这意味着新字段在出现时不会添加到文档中,并且此类文档的索引将失败。

Using nested objects


嵌套对象在某些情况下会派上用场。基本上,使用嵌套对象,Elasticsearch 允许我们将多个文档连接在一起——一个主文档和多个从属文档。主文档和嵌套文档被索引在一起,它们被放置在索引的同一段中(实际上,在段内的同一块中,彼此靠近),这保证了我们对于这样的数据结构可以获得最佳性能. 同样适用于更改文档;除非您使用更新 API,否则您需要同时索引父文档和所有其他嵌套文档。

Note

如果您想了解更多关于嵌套对象如何在 Apache Lucene 级别上工作的信息,请参阅 Mike McCandless 在 http://blog.mikemccandless.com/2012/01 /searching-relational-content-with.html

现在让我们继续我们的示例用例。想象一下,我们有一家卖衣服的商店,我们存储每件 T 恤的尺寸和颜色。我们的标准非嵌套映射将如下所示(存储在 cloth.json 中):

{
 "cloth" : {
  "properties" : {
   "name" : {"type" : "string"},
   "size" : {"type" : "string", "index" : "not_analyzed"},
   "color" : {"type" : "string", "index" : "not_analyzed"}
  }
 }
}

要在没有布料映射的情况下创建 shop 索引,我们运行以下命令:

curl -XPOST 'localhost:9200/shop'
curl -XPUT 'localhost:9200/shop/cloth/_mapping' -d @cloth.json

现在想象一下,我们店里有一件 T 恤,只有红色的 XXL 码和黑色的 XL 码。所以我们的示例文档索引命令将如下所示:

curl -XPOST 'localhost:9200/shop/cloth/1' -d '{
 "name" : "Test shirt",
 "size" : [ "XXL", "XL" ],
 "color" : [ "red", "black" ]
}'

然而,这样的数据结构存在一个问题。如果我们的一位客户搜索我们的商店以找到黑色的 XXL T 恤怎么办?让我们通过运行以下查询来检查(我们假设我们已经使用我们的映射来创建索引并且我们已经索引了我们的示例文档):

curl -XGET 'localhost:9200/shop/cloth/_search?pretty=true' -d '{
  "query" : {
  "bool" : {
  "must" : [
    {
     "term" : { "size" : "XXL" }
    },
    {
     "term" : { "color" : "black" }
    }
    ]
  }
  }
}'

我们应该没有 结果,对吧?但实际上 Elasticsearch 返回了以下文档:

{
  (…)
  "hits" : {
    "total" : 1,
    "max_score" : 0.4339554,
    "hits" : [ {
      "_index" : "shop",
      "_type" : "cloth",
      "_id" : "1",
      "_score" : 0.4339554,
      "_source" : {
        "name" : "Test shirt",
        "size" : [ "XXL", "XL" ],
        "color" : [ "red", "black" ]
      }
    } ]
  }
}

这是因为文档是匹配的——我们在大小字段和颜色字段中有我们正在搜索的值。当然,这不是我们想要得到的。

因此,让我们修改我们的映射以使用嵌套对象将颜色和大小分隔到不同的嵌套文档。最终的映射如下所示(我们将这些映射存储在 cloth_nested.json 文件中):

{
 "cloth" : {
  "properties" : {
   "name" : {"type" : "string", "index" : "analyzed"},
   "variation" : {
    "type" : "nested",
    "properties" : {
     "size" : {"type" : "string", "index" : "not_analyzed"},
     "color" : {"type" : "string", "index" : "not_analyzed"}
    }
   }
  }
 }
}

现在,我们将通过运行以下命令,使用修改后的映射创建名为 shop_nested 的第二个索引:

curl -XPOST 'localhost:9200/shop_nested'
curl -XPUT 'localhost:9200/shop_nested/cloth/_mapping' -d @cloth_nested.json

如您所见,我们在布料类型中引入了 一个新对象——变体之一,它是一个嵌套对象(type 属性设置为 nested)。它基本上说我们将要索引嵌套文档。现在,让我们修改我们的文档。我们将向其中添加变体对象,该对象将存储具有两个属性的对象——大小和颜色。因此,我们修改后的示例产品的 index 命令将如下所示:

curl -XPOST 'localhost:9200/shop_nested/cloth/1' -d '{
  "name" : "Test shirt",
  "variation" : [
  { "size" : "XXL", "color" : "red" },
  { "size" : "XL", "color" : "black" }
  ]
}'

我们已经对文档进行了结构化,以便每个尺寸及其匹配的颜色都是一个单独的文档。但是,如果您运行我们之前的查询,它不会返回任何文档。这是因为为了查询嵌套文档,我们需要使用专门的查询。所以现在我们的查询如下所示:

curl -XGET 'localhost:9200/shop_nested/cloth/_search?pretty=true' -d '{
  "query" : {
  "nested" : {
   "path" : "variation",
   "query" : {
    "bool" : {
     "must" : [
      { "term" : { "variation.size" : "XXL" } },
      { "term" : { "variation.color" : "black" } }
     ]
    }
    }
  }
  }
}'

现在,前面的查询将不会返回索引文档,因为我们没有大小等于 XXL 且颜色为黑色的嵌套文档。

让我们回到 查询上来简单讨论一下。如您所见,我们使用嵌套查询来搜索嵌套文档。 path 属性指定嵌套对象的名称(是的,我们可以有多个)。我们刚刚在嵌套类型下包含了一个标准查询部分。另请注意,我们为嵌套对象中的字段名称指定了完整路径,这在您进行多级嵌套时很方便,这也是可能的。

Scoring and nested queries

在查询期间处理嵌套文档时,还有一个附加属性。除了 path 属性,还有 score_mode 属性,它允许我们定义如何从嵌套的查询。 Elasticsearch 允许我们将 score_mode 属性 设置为以下值之一:

  • avg:这是默认值。将 用于 score_mode 属性将导致 Elasticsearch 采用从定义的分数计算的平均值嵌套查询。计算的平均值将包含在主查询的分数中。

  • sum:将这个值用于 score_mode 属性将导致 Elasticsearch 得到一个 sum 每个嵌套查询的分数,并将其包含在主查询的分数中。

  • min:将这个值用于 score_mode 属性将导致 Elasticsearch 获取最低评分嵌套查询的分数并将其包含在 主查询的得分。

  • max:将此值用于 score_mode 属性将导致 Elasticsearch 获取最大得分嵌套查询的得分,并将其包含在主查询的得分中。

  • none:对 score_mode 属性使用此值将导致不会从嵌套的 查询。

Using the parent-child relationship


在上一节中,我们讨论了使用 Elasticsearch 来索引嵌套文档和父文档。但是,即使嵌套文档在 索引中作为单独文档进行索引,我们也无法更改单个嵌套文档(除非我们使用更新 API) . Elasticsearch 允许我们拥有真正的父子关系,我们将在下一节中对其进行研究。

Index structure and data indexing

让我们使用我们在讨论嵌套文档时使用的相同示例——假设的布料商店。 我们希望能够更新大小和颜色,而无需在 每次更改。我们将看到 如何使用 Elasticsearch 父子功能来实现这一点。

Child mappings

首先,我们必须创建一个子索引定义。要创建子映射,我们需要添加带有父类型名称的 _parent 属性,在我们的例子中是布料。在子文档中,我们想要有布料的尺寸和颜色。因此,将创建商店索引和变体类型的命令如下所示:

curl -XPOST 'localhost:9200/shop'
curl -XPUT 'localhost:9200/shop/variation/_mapping' -d '{
  "variation" : {
    "_parent" : { "type" : "cloth" },
    "properties" : {
      "size" : { "type" : "string", "index" : "not_analyzed" },
      "color" : { "type" : "string", "index" : "not_analyzed" }
    }
   }
}'

就这样。您无需指定将使用哪个字段将子文档连接到父文档。默认情况下,Elasticsearch 将为此使用文档的唯一标识符。如果您还记得之前的章节,关于唯一标识符的信息默认存在于索引中。

Parent mappings

我们需要在父文档中拥有的唯一字段是名称。我们不需要更多的东西。因此,为了 在商店索引中创建我们的布料类型,我们将运行以下命令:

curl -XPUT 'localhost:9200/shop/cloth/_mapping' -d '{
  "cloth" : {
    "properties" : {
      "name" : { "type" : "string" }
    }
  }
}'

The parent document

现在我们要索引我们的父文档。由于我们想在子文档中存储有关大小和颜色的信息,所以我们在父文档中唯一需要的就是名称。当然,有一点要记住——我们的父文档需要是 类型的布料,因为_parent< /code> 子映射中的属性值。我们父文档的索引命令非常简单,如下所示:

curl -XPOST 'localhost:9200/shop/cloth/1' -d '{
  "name" : "Test shirt"
}'

如果您查看前面的命令,您会注意到我们的文档将被赋予标识符 1。

Child documents

要索引子文档,我们需要使用 parent request 参数提供有关父文档的信息。 parent 参数的值应该指向 标识符父 文件。因此,要将两个子文档索引到我们的父文档,我们需要运行以下命令行:

curl -XPOST 'localhost:9200/shop/variation/1000?parent=1' -d '{
  "color" : "red",
  "size" : "XXL"
}'
curl -XPOST 'localhost:9200/shop/variation/1001?parent=1' -d '{
  "color" : "black",
  "size" : "XL"
}'

就这样。我们已经索引了两个额外的文档,它们属于我们的 variation 类型,但是我们已经指定我们的文档有一个父文档,该文档的标识符为 1

Querying

我们已经为我们的数据建立了索引, 现在我们需要使用适当的查询来将文档与存储在其子项中的数据进行匹配。这是因为,默认情况下,Elasticsearch 搜索文档而不查看父子关系。例如,以下查询将匹配我们索引的所有三个文档(两个孩子和一个父母):

curl -XGET 'localhost:9200/shop/_search?q=*&pretty'

这不是我们想要实现的,至少在大多数情况下是这样。通常,我们对具有与查询匹配的子级的父文档感兴趣。当然,Elasticsearch 通过专门类型的查询来提供此类功能。

Note

但要记住的是,当对父母运行查询时,不会返回子文档,反之亦然。

Querying data in the child documents

想象一下,我们想要 获得 XXL 码的红色衣服。您还记得,布料的大小和颜色在子文档中都有索引,所以 我们需要一个专门的 has_child 查询,检查哪些父文档具有所需大小和颜色的子文档。因此,符合我们要求的示例查询如下所示:

curl -XGET 'localhost:9200/shop/_search?pretty' -d '{
  "query" : {
    "has_child" : {
      "type" : "variation",
      "query" : {
        "bool" : {
          "must" : [
            { "term" : { "size" : "XXL" } },
            { "term" : { "color" : "red" } }
          ]
        }
      }
    }
  }
}'

查询很简单;它属于 has_child 类型,它告诉 Elasticsearch 我们要在子文档中进行搜索。为了指定我们感兴趣的子类型,我们用子类型的名称指定 type 属性。使用 query 属性提供查询。我们使用了一个标准的 bool 查询,我们已经讨论过了。查询的结果将只包含那些有子与我们匹配的父文档bool 查询。在我们的例子中,返回的单个文档如下所示:

{
  "took" : 16,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 1.0,
    "hits" : [ {
      "_index" : "shop",
      "_type" : "cloth",
      "_id" : "1",
      "_score" : 1.0,
      "_source" : {
        "name" : "Test shirt"
      }
    } ]
  }
}

has_child 查询允许我们提供额外的参数来控制其行为。找到的每个父文档都可能与一个或多个子文档相关联。这意味着每个子文档都可以影响结果分数。默认情况下,查询不关心子文档,有多少匹配,以及它们的内容是什么——只关心它们是否匹配查询。这可以通过使用 score_mode 参数来更改,该参数控制 has_child 查询的分数计算。该参数可以取的值是:

  • none:默认一个,关系生成的score为 1.0

  • min:分数取自得分最低的孩子

  • max:分数取自得分最高的孩子

  • sum:分数是计算为子分数之和

  • avg:分数取为子分数的平均值

让我们看一个 示例:

curl -XGET 'localhost:9200/shop/_search?pretty' -d '{
  "query" : {
    "has_child" : {
      "type" : "variation",
      "score_mode" : "sum",
      "query" : {
        "bool" : {
          "must" : [
            { "term" : { "size" : "XXL" } },
            { "term" : { "color" : "red" } }
          ]
        }
      }
    }
  }
}'

我们使用 sum 作为 score_mode,这导致子文档对父文档的最终分数做出贡献 - contribution 是每个匹配查询的子文档的得分之和。

最后,我们可以限制需要匹配的子文档的数量;我们可以指定允许匹配的最大子文档数(max_children 属性)和最小子文档数(min_children 属性)需要匹配。说明这些参数用法的查询如下:

curl -XGET 'localhost:9200/shop/_search?pretty' -d '{
  "query" : {
    "has_child" : {
      "type" : "variation",
      "min_children" : 1,
      "max_children" : 3,
      "query" : {
        "bool" : {
          "must" : [
            { "term" : { "size" : "XXL" } },
            { "term" : { "color" : "red" } }
          ]
        }
      }
    }
  }
}'

Querying data in the parent documents

有时,我们对父文档不感兴趣,而是对子文档感兴趣。如果您想返回 中给定数据匹配的子文档父文档,Elasticsearch 为我们提供了一个查询 - has_parent 查询。它类似于 has_child 查询;但是,我们使用父文档类型的值指定 parent_type 属性,而不是 type 属性。例如,以下查询将返回我们已索引的两个子文档,但不返回父文档:

curl -XGET 'localhost:9200/shop/_search?pretty' -d '{
  "query" : {
    "has_parent" : {
      "parent_type" : "cloth",
      "query" : {
        "term" : { "name" : "test" }
      }
    }
  }
}'

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

{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 2,
    "max_score" : 1.0,
    "hits" : [ {
      "_index" : "shop",
      "_type" : "variation",
      "_id" : "1000",
      "_score" : 1.0,
      "_routing" : "1",
      "_parent" : "1",
      "_source" : {
        "color" : "red",
        "size" : "XXL"
      }
    }, {
      "_index" : "shop",
      "_type" : "variation",
      "_id" : "1001",
      "_score" : 1.0,
      "_routing" : "1",
      "_parent" : "1",
      "_source" : {
        "color" : "black",
        "size" : "XL"
      }
    } ]
  }
}

has_child 查询类似,has_parent 查询也为我们提供了调整查询分数计算的可能性。在这种情况下,score_mode只有两个选项:none,默认的一个,查询计算的分数等于1.0和score,根据父文档内容计算文档的 分数。在 has_parent 查询中使用 score_mode 的示例看起来 如下:

curl -XGET 'localhost:9200/shop/_search?pretty' -d '{
  "query" : {
    "has_parent" : {
      "parent_type" : "cloth",
      "score_mode" : "score",
      "query" : {
        "term" : { "name" : "test" }
      }
    }
  }
}'

与上一个示例的一个区别是 score_mode。如果您检查这些查询的结果,您会发现只有一个区别。第一个示例中所有文档的得分为 1.0,而前面查询返回的结果的得分等于 0.8784157。在这种情况下,找到的所有文档都具有相同的分数,因为它们具有共同的父文档。

Performance considerations

使用 Elasticsearch 父子功能时,您必须了解它对性能的影响。您需要记住的第一件事是父文档和子文档 需要存储在同一个分片中才能使查询工作。如果您碰巧有单亲的大量子代,那么您最终可能会得到没有相似数量文档的分片。因此,您的查询性能在其中一个节点上可能会降低,从而导致整个查询变慢。另外,请记住,父子查询将比针对它们之间没有关系的文档运行的查询要慢。有一种方法可以通过急切加载所谓的全局序数来加速父子查询的连接,但会以内存为代价;但是,我们将在 Elasticsearch 缓存部分讨论该方法第 9 章Elasticsearch 集群详解

最后,第一个查询将使用 doc 值预加载和缓存文档标识符。这需要时间。为了提高使用父子关系的初始查询的性能,可以使用Warmer API。您可以在 Warming up 部分找到有关如何向 Elasticsearch 添加预热查询的更多信息linkend="ch10">第 10 章管理您的集群

Modifying your index structure with the update API


在前面的章节中,我们 讨论了如何创建索引映射和索引数据。但是,如果您已经创建了映射并索引了数据,但您想修改索引的结构怎么办?当然有人可以说我们可以用新的映射创建一个新的索引,但这并不总是可行的,尤其是在生产环境中。这在某种程度上是可能的。例如,默认情况下,如果我们使用新字段索引文档,Elasticsearch 会将该字段添加到索引结构中。现在让我们看看如何手动修改索引结构。

Note

对于需要更改映射并且由于与当前索引结构冲突而无法更改的情况,使用别名非常好——包括读取和写入。我们将在 Chapter 10< 的 Index aliasing 部分讨论别名/a>,管理您的集群

The mappings

假设我们user.json 文件中存储了以下用户索引映射:

{
 "user" : {
  "properties" : {
   "name" : {"type" : "string"}
  }
 }
}

如您所见,它非常简单。它只有一个属性来保存用户名。现在让我们创建一个名为 usersindex 并使用前面的映射来创建我们的类型。为此,我们将运行以下命令:

curl -XPOST 'localhost:9200/users'
curl -XPUT 'localhost:9200/users/user/_mapping' -d @user.json

如果一切顺利,我们将创建索引(称为 users)和类型(称为 user)。所以现在让我们尝试向映射中添加一个新字段。

Adding a new field to the existing index

为了说明 如何将新字段添加到我们的映射中,我们假设我们希望将电话号码添加到为每个用户存储的数据中。为此,我们需要向 /index_name/type_name/_mapping REST 端点发送一个 HTTP PUT 命令,其中包含我们的新字段的正确正文。例如,要添加提到的电话字段,我们将 运行以下命令:

curl -XPUT 'http://localhost:9200/users/user/_mapping' -d '{
 "user" : {
  "properties" : {
   "phone" : {"type" : "string", index : "not_analyzed"}
  }
 }
}'

与我们之前运行的命令类似,如果一切顺利,我们应该在索引结构中添加一个新字段。

Note

当然,Elasticsearch 不会重新索引我们的数据或自动填充新添加的字段。它只会改变主节点持有的映射并将映射填充到集群中的所有其他节点,仅此而已。数据重新索引必须由我们或在我们的环境中索引数据的应用程序来完成。在那之前,旧文档不会有新添加的字段。记住这一点至关重要。如果您没有原始文档,您可以使用 _source 字段从 Elasticsearch 中获取原始数据并再次对其进行索引。

为确保一切正常,我们可以向 _mapping REST 端点运行 GET HTTP 请求,Elasticsearch 将返回适当的映射.在用户索引中为我们的用户类型获取映射 的示例命令如下所示:

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

Modifying fields of an existing index

我们的 users 索引结构包含两个字段:namephone。假设我们索引了一些数据,但一段时间后我们决定要在电话字段上进行搜索,并且我们想更改它的index 属性从 not_analyzed分析。因为我们已经知道如何更改索引结构,所以我们将运行以下命令:

curl -XPUT 'http://localhost:9200/users/user/_mapping?pretty' -d '{
 "user" : {
  "properties" : {
   "phone" : {"type" : "string", "store" : "yes", "index" : "analyzed"}
  }
 }
}'

Elasticsearch 会返回一个指示错误的响应,如下所示:

{
  "error" : {
    "root_cause" : [ {
      "type" : "illegal_argument_exception",
      "reason" : "Mapper for [phone] conflicts with existing mapping in other types:\n[mapper [phone] has different [index] values, mapper [phone] has different [store] values, mapper [phone] has different [omit_norms] values, cannot change from disable to enabled, mapper [phone] has different [analyzer]]"
    } ],
    "type" : "illegal_argument_exception",
    "reason" : "Mapper for [phone] conflicts with existing mapping in other types:\n[mapper [phone] has different [index] values, mapper [phone] has different [store] values, mapper [phone] has different [omit_norms] values, cannot change from disable to enabled, mapper [phone] has different [analyzer]]"
  },
  "status" : 400
}

这是因为我们无法将设置为 not_analyzed 的字段更改为 analyzed 的字段。而不仅仅是 ,在大多数情况下,您将无法更新字段映射。这是一件好事,因为如果我们被允许更改这些设置,我们会混淆 Elasticsearch 和 Lucene。想象一下,我们已经有许多电话字段设置为 not_analyzed 的文档,我们可以将映射更改为分析。 Elasticsearch 不会更改 已编入索引的数据,但分析的查询将使用不同的逻辑进行处理,因此您将无法正确找到您的数据。

但是,为了给您一些禁止和禁止的示例,我们决定提及这两种情况的一些操作。例如,可以安全地进行以下修改:

  • 添加新的类型定义

  • 添加新字段

  • 添加新的分析器

以下修改被禁止或无效:

  • 为一个领域启用规范

  • 更改要存储或不存储的字段

  • 更改字段的类型(例如,从文本到数字)

  • 将存储的字段更改为不存储,反之亦然

  • 更改索引属性的值

  • 更改已索引文档的分析器

请记住,前面提到的允许和不允许更新的示例并未提及更新 API 使用的所有可能性,如果您尝试执行的更新有效,您必须自己尝试。

Summary


你刚刚读完的那一章集中在索引操作和处理不平坦或文档之间有关系的数据上。我们从在 Elasticsearch 中索引树状结构和对象开始。我们还使用了嵌套对象并了解了何时可以使用它们。我们还使用了父子功能,并了解了这种方法与嵌套文档相比有何不同。最后,我们通过调用 API 修改了我们的索引结构,并了解了何时可以这样做。

在下一章中,我们将回到查询相关主题。我们将了解 Lucene 评分的工作原理、如何在 Elasticsearch 中使用脚本以及如何处理多语言数据。我们将使用提升来影响评分,我们将使用同义词来改善用户的搜索结果。最后,我们将看看我们可以做些什么来查看我们的文档是如何评分的。