vlambda博客
学习文章列表

读书笔记《elasticsearch-blueprints》管理关系内容

Chapter 4. Managing Relational Content

从某种意义上说,表示现实世界的数据非常具有决定性意义,通常直接的数据结构并不总是足以满足您的需求。通常,您会发现自己需要为数据采用复杂的结构,并在对象内建立某种关系。在本章中,我们将看到 Elasticsearch 如何提供管理此类关系内容的规定。以下是我们将在本章中介绍的主题:

  • 亲子搜索

  • 嵌套字段查询的限制

  • 解决方案 1 – 嵌套数据类型

  • 解决方案 2——父子数据类型

  • 存储问题和答案的模式设计

  • 根据答案标准搜索问题

  • 根据问题标准搜索答案

  • 术语查找

Elasticsearch 提供了多种管理关系内容的方法。其中, 最简单的方法是在结构中直接添加内部对象。让我们看看 Elasticsearch 中简单的内部对象是如何表现的。

The product-with-tags search problem


出现了一项新要求,即搜索具有特定属性值集的产品。

例如,用户应该能够根据其任何属性搜索特定产品。但是,这些属性不是固定的,可能每天都在变化。因此,不可能将其保留为键值对。更多的键意味着更多的字段,并且每个字段都在 Lucene 引擎盖下花费了一个反向索引。因此,更好的方法是将数据建模如下:

/products/product/LCD_TV

{
  "name" : "LCD TV",
  "tags" : [
    {
      "tagName" : "company" ,
      "value" : "Sony"
    },
    {
      "tagName" : "competitor" ,
      "value" : "Toshiba"
    }
  ]
}

使用这种方法,一切看起来都很好,但是,以下查询适用于前面的文档:

tags.tagName:company AND tags.value:Toshiba

这不是我们所期望的。该产品实际上属于 Sony 公司,但后来与 Toshiba 公司相匹配。

在这里,仔细观察该方法使我们意识到这个问题源于反向索引数据结构。发生的情况是,在反向索引中,对于每个字段,每个标记都映射到其文档 ID。

因此,标签在内部存储如下:

Tags.tagName => [ "company" , "competitor]
Tags.value => [ "sony" , "Toshiba" ]

这就是为什么 AND 对这两个条件的操作都是成功的,但从逻辑上讲,它不适合 我们。此问题称为文档拼合。

Nested types to the rescue


使用嵌套数据类型是内部对象的一种替代方法,它解决了 在 Elasticsearch 中文档扁平化的问题。让我们看看如何使用嵌套数据类型。让我们考虑以下示例:

curl -XPOST localhost:9200/authors/author/1 -d'{
  "name": "Multi G. Enre",
  "books": [
    {
      "name": "Guns and lasers",
      "genre": "scifi",
      "publisher": "orbit"
    },
    {
      "name": "Dead in the night",
      "genre": "thriller",
      "publisher": "penguin"
    }
  ]
}'
curl -XPOST localhost:9200/authors/author/2 -d'{
  "name": "Alastair Reynolds",
  "books": [
    {
      "name": "Revelation Space",
      "genre": "scifi",
      "publisher": "penguin"
    }
  ]
}'

如果您仔细检查,理想情况下,您会注意到与内部对象相比,文档的外观没有区别。这也是真的。为了让您的 Elasticsearch 实例了解您使用嵌套数据类型的意图,您必须明确提及映射作为嵌套。看看这是如何完成的:

curl -XPOST localhost:9200/authors/nested_author/_mapping -d '{
  "nested_author":{
    "properties":{
      "books": {
        "type": "nested"
      }
    }
  }
}'

在前面的 代码中,我们将书籍称为嵌套数据类型。一旦指定了这个映射,Elasticsearch 就会在内部处理嵌套结构。这使得在前一种情况下无法执行的查询能够在内部对象类型中完美地工作。让我们看看如何使用 "books.publisher": "penguin""books.genre": "scifi"

curl --XPOST localhost:9200/authors/nested_author/_search -d '
{
  "query": {
    "filtered": {
      "query": {"match_all": {}},
      "filter": {
        "nested": {
          "path": "books",
          "query":{
            "filtered": {
              "query": { "match_all": {}},
              "filter": {
                "and": [
                  {"term": {"books.publisher": "penguin"}},
                  {"term": {"books.genre": "scifi"}}
                ]
              }
            }
          }
        }
      }
    }
  }
}'

这将返回我们正在寻找的完美匹配的结果。

那么嵌套的类型能解决你所有的问题吗?使用嵌套数据类型的缺点是什么?我们将在下一节中看到。

Limitations on a query on nested fields


嵌套对象类型有一些限制。在简单内部对象类型中,每个嵌套的 JSON 对象都被独立处理。但是,将内部 对象的映射定义为嵌套类型可以克服这个缺点。此外,嵌套类型的使用也有其缺点,我们即将看到:

  • 嵌套类型对更新不友好。更新父元素信息或子元素信息意味着您需要重新索引整个文档。因此,如果新学生被学校录取或新学生离开学校,则必须重新索引整个文件以及其他学生和学校的信息。请记住,更新文档只不过是在后台软删除和添加文档。

  • 您不能单独检索嵌套对象。假设您需要获取单个学生的信息;这在这种方法中是不可能的。您需要获取文档的整个来源,包括学校和学生的信息,以检查特定学生的数据。

Using a parent-child approach


父子数据类型是建立关系数据的另一种选择。在父子方法中,我们在索引时间建立父子关系。在 map reduce 或 分片架构中,map 或分片之间不可能相互通信。这项工作必须由每个分片独立完成。请记住,关系数据不能作为交叉分片维护,这意味着我不能将学校信息文档放在一个分片中,将学生信息放在另一个分片中。

因此,要建立父子关系,需要父子文档在同一个分片中。让我们先看看如何索引父子文档,然后看看这个规则是如何施加的。

让我们先索引一些文档。索引名和类型名分别是posts和post,对应的ID手动给出为1, 2, and < code class="literal">3,如下代码所示:

curl -XPOST localhost:9200/posts/post/1 -d '{
   "question":" elasticsearch query to return all records"
}'
curl -XPOST localhost:9200/posts/post/2 -d '{
  "question":" Elasticsearch, Sphinx, Lucene, Solr, Xapian. Which fits for which usage?"
}'
curl -XPOST localhost:9200/posts/post/3 -d '{
  "question":" Queries vs. Filters"
}

前面的 片段将作者部分索引为父文档。现在,让我们为子文档类型配置映射:

curl -XPOST localhost:9200/posts/rating/_mapping -d '{
  "rating":{
    "_parent": {"type": "post"}
  }
}'

我们在名为 的索引中创建了两种类型,即 "post""rating" “发布”。前面的代码片段所做的是将类型 "post" 映射为另一个类型 "rating" 的父级。

现在,每当我们索引一个子文档时,我们都会提到一个标识符以将其表示为特定父文档的子文档,如以下代码所示:

curl -XPOST localhost:9200/posts/rating/1?parent=1 -d '{
  "accepted":"yes",
  "rating":72,
  "answer":"http://localhost:9200/foo/_search?pretty=true&q=*:*"
}'
curl -XPOST localhost:9200/posts/rating/2?parent=1 -d '{
  "accepted":"yes",
  "rating":3,
  "answer":"curl -XGET localhost:9200/foo/_search?"
}'
curl -XPOST localhost:9200/posts/rating/3?parent=1 -d '{
  "accepted":"yes",
  "rating":2,
  "answer":http://127.0.0.1:9200/foo/_search/?size=1000&pretty=1\
}'
curl -XPOST localhost:9200/posts/rating/4?parent=1 -d '{
  "accepted":"yes",
  "rating":1,
  "answer":"elasticsearch.org/guide/reference/api/search/uri-request"
}'
curl -XPOST localhost:9200/posts/rating/5?parent=2 -d '{
  "accepted":"yes",
  "rating":10,
  "answer":"An experiment to compare Elasticsearch and Solr"
}'
curl -XPOST localhost:9200/posts/rating/6?parent=2 -d '{
  "accepted":"yes",
  "rating":8,
  "answer":"http://blog.socialcast.com/realtime-search-solr-vs-elasticsearch/"
}'
curl -XPOST localhost:9200/posts/rating/7?parent=2 -d '{
  "accepted":"yes",
  "rating":6,
  "answer":"Solr vs elasticsearch Deathmatch!"
}'
curl -XPOST localhost:9200/posts/rating/8?parent=3 -d '{
  "accepted":"yes",
  "rating":54,
  "answer":"filters are cached and dont influence the score, therefore faster than queries"
}'
curl -XPOST localhost:9200/posts/rating/9?parent=3 -d '{
  "accepted":"yes",
  "rating":2,
  "answer":"filters should be used instead of queries for binary yes/no searches and for queries on exact values"
}'

通过用父ID索引子文档,建立父子关系。现在,在底层,Elasticsearch 需要确保父文档和子文档在同一个分片中被索引。为此,Elasticsearch 使用路由。当给定一个文档进行索引时,Elasticsearch 需要找到该文档应该映射到哪个分片。根据文档的文档 ID,Elasticsearch 使用模函数来查找分片。这称为路由, 默认情况下,路由键是文档 ID。在父子场景中,子文档的路由键取自父 ID,Elasticsearch 确保父文档和子文档进入同一个分片。

The has_parent filter/the has_parent query

查询/过滤器对父文档 起作用并返回子文档。

The has_child query/the has_child filter

has_child 过滤器接受查询和子类型以针对查询运行,这会导致 父文档具有与查询匹配的子文档。 has_child 查询也像 has_child 过滤器一样工作。

The top_children query

查询返回前 n 个子级。

Elasticsearch 中的父文档和子文档之间没有强连接。这使您可以在不影响整个文档的情况下更新子级或父级。因此,可以避免更新文档时的大内存开销。但是,这会带来最低的性能成本,因为它们没有被定向到同一个分片。为了利用缓存,子文档都被路由到同一个分片。

Schema design to store questions and answers


使用 父子文档,我们可以轻松找到问题的答案,类似地,可以找到具有特定答案的问题。让我们看看如何在本节中设计这样的模式。

想象一下,我们有一个类似 stackoverflow 的 论坛,用户可以在其中提问。假设我们在名为 "post" 的索引中设置了这样的模式。

首先,让我们创建索引 "post"

curl -XPOST localhost:9200/posts 

我们创建了一个帖子索引,我们将用户的帖子索引到该索引中。现在,让我们通过定义映射来让我们的 Elasticsearch 实例了解我们将要做什么:

curl -XPOST localhost:9200/posts/rating/_mapping -d '{
  "rating":{
    "_parent": {"type": "post"}
  }
}'

如前所述,这里我们将类型 "post" 映射为类型 "rating" 的父级。

现在,让我们索引我们的父文档和子文档:

// Already indexed

有了这个,我们已经设置了模式设计来存储问题和答案。

现在,让我们研究一个需求。公司需要创建一个像 stackoverflow 这样的问答论坛,可以在其中发布问题并给出答案。任何人都可以回来回答这些问题。一个或多个人可以回答相同的问题。因此,发布问题的人可以看到它们并将最合适的答案标记为 已接受 答案。此外,用户可以对答案给予赞成或反对。以下几点总结了网站的要求:

  • 找出答案被接受的所有问题

  • 查找对答案的支持率最高的热门问题

  • 按问题投票的顺序查找已接受的问题答案

  • 找出至少有三个答案的问题

Searching questions based on a criteria of answers


假设使用索引的文档集,我们需要找出特定 question 有多少答案的评分大于 50。让我们尝试通过发出查询找出答案:

curl -XPOST localhost:9200/posts/post/_search -d '{
  "query": {
    "filtered": {
      "query": {
        "text": {"question": " elasticsearch query to return all records "}
      },
      "filter":{
        "has_child": {
          "type": "rating",
          "query" : {
            "filtered": {
              "query": { "match_all": {}},
              "filter" : {
                "and": [
                  {"term": {"accepted": yes}},
                  {"range": {"rating": {"gt" : 50}}}
                ]
              }
            }
          }
        }
      }
    }
  }
}'

Searching answers based on a criteria of questions


现在让我们尝试通过查询子文档来找到父文档,这与我们在上一节中所做的相反。看看这个查询:

curl -XPOST localhost:9200/posts/rating/_search?pretty=true -d '{
  "query": {
    "filtered": {
      "query": {"match_all": {}},
      "filter": {
        "and": [
          {"term": {"accepted": yes}},
          {
            "has_parent": {
              "type": "post",
              "query": {
                "term": {"question": " Queries vs. Filters "}
              }
            }
          }
        ]
      }
    }
  },
  "sort": [
    { "rating" : {"order" : "desc"} }
  ]
}'

在这里,我们按降序获取问题中包含术语Queries vs. Filters的所有评分。

The score of questions based on the score of each answer


由于每个问题都有不同的答案,并且每个查询都与每个答案匹配,因此应该有某种机制来汇总每个答案的分数以形成最终分数对于父母,也就是问题:

curl -XPOST 'localhost:9200/posts/post/_search' -d '{
  "query": {
    "has_child": {
      "type": "rating",
      "query": {
        "function_score": {
          "functions": [
            {
              "field_value_factor": {
                "field": "rating"
              }
            }
          ]
        }
      },
      "score_mode": "sum"
    }
  }
}'

输出如下:

{
  "took" : 5,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 3,
    "max_score" : 78.0,
    "hits" : [ {
      "_index" : "posts",
      "_type" : "post",
      "_id" : "1",
      "_score" : 78.0,
      "_source":{ "question": " elasticsearch query to return all records " }
    }, {
      "_index" : "posts",
      "_type" : "post",
      "_id" : "3",
      "_score" : 56.0,
      "_source":{ "question": " Queries vs. Filters " }
    }, {
      "_index" : "posts",
      "_type" : "post",
      "_id" : "2",
      "_score" : 24.0,
      "_source":{ "question" : " Elasticsearch, Sphinx, Lucene, Solr, Xapian. Which fits for which usage? " }
    } ]
  }
}

响应中,我们可以看到有一个名为 _score 的字段,我们使用前面的查询,我们将其作为排序标准。

Filtering questions with more than four answers


接下来,让我们看看如何过滤至少有四个答案的问题。为此,有一个 规定可根据父文档拥有的子文档数量过滤父文档。我们可以使用 min_children 属性来实现这个限制:

curl -XPOST 'localhost:9200/posts/post/_search' -d '{
  "query": {
    "has_child": {
      "type": "rating",
      "min_children": 4,
      "query": {
        "match_all": {}
      }
    }
  }
}'

在这里,我们告诉 Elasticsearch 为所有父文档提供至少四个子文档。

有了这个,我们得到以下结果:

{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 1.0,
    "hits" : [ {
      "_index" : "posts",
      "_type" : "post",
      "_id" : "1",
      "_score" : 1.0,
      "_source":{ "question": " elasticsearch query to return all records " }
    } ]
  }
}

结果中,我们可以观察到只有至少有四个答案的问题出现了。

同样,我们可以使用 max_children 属性来标记孩子的上限。

Displaying the best questions and their accepted answers

现在,让我们根据投票数获取热门问题其接受的答案。

我们可以在父子关系中使用内部对象功能来实现这一点。此内部对象功能可以将 inner_hits 参数添加到查询中,如下所示:

curl -XPOST 'localhost:9200/posts/post/_search' -d '{
  "query": {
    "has_child": {
      "type": "rating",
      "query": {
        "function_score": {
          "functions": [
            {
              "field_value_factor": {
                "field": "rating"
              }
            }
          ]
        }
      },
      "score_mode": "sum",
      "inner_hits": {}
    }
  }
}'

前面的 查询的 输出如下所示:

{
  "took": 11,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 3,
    "max_score": 78,
    "hits": [
      {
        "_index": "posts",
        "_type": "post",
        "_id": "1",
        "_score": 78,
        "_source": {
          "question": " elasticsearch query to return all records "
        },
        "inner_hits": {
          "rating": {
            "hits": {
              "total": 4,
              "max_score": 72,
              "hits": [
                {
                  "_index": "posts",
                  "_type": "rating",
                  "_id": "1",
                  "_score": 72,
                  "_source": {
                    "accepted": "yes",
                    "rating": 72,
                    "answer": "http://localhost:9200/foo/_search?pretty=true&q=*:*"
                  }
                },
                {
                  "_index": "posts",
                  "_type": "rating",
                  "_id": "2",
                  "_score": 3,
                  "_source": {
                    "accepted": "yes",
                    "rating": 3,
                    "answer": "curl -XGET localhost:9200/foo/_search?"
                  }
                },
                {
                  "_index": "posts",
                  "_type": "rating",
                  "_id": "3",
                  "_score": 2,
                  "_source": {
                  "accepted": "yes",
                  "rating": 2,
                  "answer": "http://127.0.0.1:9200/foo/_search/?size=1000&pretty=1"
                }
              }
            ]
          }
        }
      }
    },
    {
      "_index": "posts",
      "_type": "post",
      "_id": "3",
      "_score": 56,
      "_source": {
        "question": " Queries vs. Filters "
      },
      "inner_hits": {
        "rating": {
          "hits": {
            "total": 2,
            "max_score": 54,
            "hits": [
              {
                "_index": "posts",
                "_type": "rating",
                "_id": "8",
                "_score": 54,
                "_source": {
                  "accepted": "yes",
                  "rating": 54,
                  "answer": "filters are cached and dont influence the score, therefore faster than queries"
                }
              },
            {
              "_index": "posts",
              "_type": "rating",
              "_id": "9",
              "_score": 2,
              "_source": {
                "accepted": "yes",
                "rating": 2,
                "answer": "filters should be used instead of queries for binary yes/no searches and for queries on exact values"
              }
            }
          ]
        }
      }
    }
  }
  {
    "_index": "posts",
      "_type": "post",
      "_id": "2",
      "_score": 24,
      "_source": {
        "question": " ElasticSearch, Sphinx, Lucene, Solr, Xapian. Which fits for which usage? "
      },
      "inner_hits": {
        "rating": {
          "hits": {
            "total": 3,
            "max_score": 10,
            "hits": [
              {
                "_index": "posts",
                "_type": "rating",
                "_id": "5",
                "_score": 10,
                "_source": {
                  "accepted": "yes",
                  "rating": 10,
                  "answer": "An experiment to compare ElasticSearch and Solr"
                }
              },
              {
                "_index": "posts",
                "_type": "rating",
                "_id": "6",
                "_score": 8,
                "_source": {
                  "accepted": "yes",
                  "rating": 8,
                  "answer": "http://blog.socialcast.com/realtime-search-solr-vs-elasticsearch/"
                }
              },
              {
                "_index": "posts",
                "_type": "rating",
                "_id": "7",
                "_score": 6,
                "_source": {
                  "accepted": "yes",
                  "rating": 6,
                  "answer": "Solr vs elasticsearch Deathmatch!"
                }
              }
            ]
          }
        }
      }
    }
    ]
  }
}

在这里,我们可以看到结果是在a的基础上整理出来的得分,并且对于每个结果,都会显示内部匹配项以及子项,这些子项也是根据分数进行排序的。

请注意,inner_hits 参数仅在 Elasticsearch 1.5 及其更高版本中受支持。

Summary


在 NoSQL 中,很难建立关系数据,因为数据分散在各种排除的存储桶中,您无法将它们互连以处理数据。

然而,Elasticsearch 为我们提供了多种选择来实现这一点。

我们看到嵌套和父子结构为建立关系数据提供了一些很好的机制。这两种方法的交叉比较如下:

  • 嵌套方法:

    • 数据由设计连接

    • 这对于孩子人数较少的情况非常理想

    • 这比父子方法快

    • 添加或删除子项意味着必须重新索引整个文档,这在 Lucene 中成本很高

    • 编辑父级的信息也需要重新索引,这又是非常昂贵的

  • 亲子方法:

    • 这适用于大量儿童

    • This is ideal for large relations with a large number of children

    • 添加和删​​除孩子是无缝且便宜的

    • 无需对子文档进行任何更新即可编辑父信息

    • 父子查询对于每个父查询来说有点昂贵,所有子文档都必须加载到主内存中

    • 我们还看到了在 Elasticsearch 1.5 及更高版本中添加的 inner-hits 参数,这使我们不仅可以获取父文档,还可以获取它们各自的子文档

在下一章中,我们将通过几个用例场景来了解 Elasticsearch 在分析领域的能力和使用情况。