【Elastic Engineering】Elasticsearch: nested 对象

本文涉及的产品
检索分析服务 Elasticsearch 版,2核4GB开发者规格 1个月
简介: Elasticsearch: nested 对象

作者:刘晓国


在处理大量数据时,关系数据库存在很多问题。 无论是速度,高效处理,有效并行化,可扩展性还是成本,当数据量开始增长时,关系数据库都会失败。该关系数据库的另一个挑战是必须预先定义关系和模式。Elasticsearch 也是一个 NoSQL 文档数据存储。 但是,尽管是一个 NoSQL 数据存储,Elasticsearch 在一定程度上提供了很多帮助来管理关系数据。 它支持类似 SQL 的连接,并且在嵌套和相关的数据实体上工作得非常棒。


比如,对于一个像下面的 blog 形式的文档:


image.png


一个 blog 可能对应于很多的 comments,或者一个员工对应于很多的经验。这种数据就是关系数据。使用 Elasticsearchimage.png,您可以通过保留轻松地工作与不同实体的关联以及强大的全文搜索和分析。 Elasticsearch 通过引入两种类型的文档关系模型使这成为可能:


nested 关系: 在这种关系中,这种一对多的关系存在于同一个文档之中

parent-child 关系:在这种关系中,它们存在于不同的文档之中。我将在另外一个文章中描述


这两种关系在同一个模式下工作,即一对多个的关系。一个 root 或 parent 可以有一个及多个子 object。


如上图所示,在嵌套关系中,有一个根对象,它是我们拥有的主文档,它包含一个称为嵌套文档的子文档数组。 根对象内的文档嵌套级别没有限制。 例如,查看以下 JSON 以进行多级嵌套:

 {
     "location_id": "axdbyu",
     "location_name": "gurgaon",
     "company": [
       {
         "name": "honda",
         "modelName": [
           { "name": "honda cr-v", "price": "2 million" }
         ]
}, {
         "name": "bmw",
         "modelName": [
           { "name": "BMW 3 Series", "price": "2 million"},
           { "name": "BMW 1 Series", "price": "3 million" }
         ]
  } ]
}

下面,我们来做一个例子来展示一下为什么 nested 对象可以解决我们的问题。


Object 数据类型


我们首先创建一个叫做 developer 的 index,并输入如下的两个数据:

POST developer/_doc/101
{
  "name": "zhang san",
  "skills": [
    {
      "language": "ruby",
      "level": "expert"
    },
    {
      "language": "javascript",
      "level": "beginner"
    }
   ]
}
POST developer/_doc/102
{
  "name": "li si",
  "skills": [
    {
      "language": "ruby",
      "level": "beginner"
    }
   ]
}

上面显示是一对多的一个 index。


Object Query


这个时候我们想搜一个 skills: language 是 ruby,并且 level 是 biginner 的文档。我们可能想到的方法是:

GET developer/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "match": {
            "skills.language": "ruby"
          }
        },
        {
          "match": {
            "skills.level": "beginner"
          }
        }
      ]
    }
  }
}

通过上面的搜寻,我们得到的结果是:

{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 0.0,
    "hits" : [
      {
        "_index" : "developer",
        "_type" : "_doc",
        "_id" : "101",
        "_score" : 0.0,
        "_source" : {
          "name" : "zhang san",
          "skills" : [
            {
              "language" : "ruby",
              "level" : "expert"
            },
            {
              "language" : "javascript",
              "level" : "beginner"
            }
          ]
        }
      },
      {
        "_index" : "developer",
        "_type" : "_doc",
        "_id" : "102",
        "_score" : 0.0,
        "_source" : {
          "name" : "li si",
          "skills" : [
            {
              "language" : "ruby",
              "level" : "beginner"
            }
          ]
        }
      }
    ]
  }
}

我们可以看到,我们得到两个结果。但是我们仔细查看一下发现得到的结果并不是我们想得到的。从我们的原意来说,我们想得到的是 li si,因为只有 li si 这个人的 language 是 ruby,并且他的 level 是biginner。zhang san 这个文档,应该不在搜寻之列。这是为什么呢?


原来,langauge 及 level 是 skills 的 JSON 内部数组项。当 JSON 对象被 Lucene 扁平化后,我们失去了 language 和 level 之间的对应关系。取而代之的是如下的这种关系:

{
  "name": "zhang san",
  "skills.language" :["ruby", "javascript"],
  "skills.level": ["expert", "beginner"]
}

如上所示,我们看到的是一个扁平化的数组。之前的那种 language 和 level 之间的那种对应关系已经不存在了。


Object aggregation


同样的问题也存在于 aggregation 中,比如我们想做一下的 aggregation:

GET developer/_search
{
  "size": 0,
  "aggs": {
    "languages": {
      "terms": {
        "field": "skills.language.keyword"
      },
      "aggs": {
        "level": {
          "terms": {"field": "skills.level.keyword"}
        }
      }
    }
  }
}

显示的结果是:

{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "languages" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "ruby",
          "doc_count" : 2,
          "level" : {
            "doc_count_error_upper_bound" : 0,
            "sum_other_doc_count" : 0,
            "buckets" : [
              {
                "key" : "beginner",
                "doc_count" : 2
              },
              {
                "key" : "expert",
                "doc_count" : 1
              }
            ]
          }
        },
        {
          "key" : "javascript",
          "doc_count" : 1,
          "level" : {
            "doc_count_error_upper_bound" : 0,
            "sum_other_doc_count" : 0,
            "buckets" : [
              {
                "key" : "beginner",
                "doc_count" : 1
              },
              {
                "key" : "expert",
                "doc_count" : 1
              }
            ]
          }
        }
      ]
    }
  }
}

显然,对于 key javascript 来说,它并没有 expert 对应的 level,但是在我们的 aggregation 里显示出来了。这个结果显然是错误的。


nested 数据类型


nested 数据类型能够让我们对 object 数组建立索引,并且分别进行查询。


如果需要维护数组中每个对象的关系,请使用 nested 数据类型


为了能够把我们的数据定义为nested,我们必须修改之前的索引 mapping 为:

DELETE developer
PUT developer
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text"
      },
      "skills": {
        "type": "nested",
        "properties": {
          "language": {
            "type": "keyword"
          },
          "level": {
            "type": "keyword"
          }
        }
      }
    }
  }
}

经过这样的改造之后,重新把我们之前的数据输入到 index 里:

POST developer/_doc/101
{
  "name": "zhang san",
  "skills": [
    {
      "language": "ruby",
      "level": "expert"
    },
    {
      "language": "javascript",
      "level": "beginner"
    }
   ]
}
POST developer/_doc/102
{
  "name": "li si",
  "skills": [
    {
      "language": "ruby",
      "level": "beginner"
    }
   ]
}

针对 101,在 Lucence 中的数据结构变为:

{
  "name": "zhang san",
  {
    "skills.language": "ruby",
    "skills.level": "expert"
  },
  {
    "skills.language": "javascript",
    "skills.level", "beginner"
  }
}


nested query


我们来重新做我们之前的搜索:

GET developer/_search
{
  "query": {
    "nested": {
      "path": "skills",
      "query": {
        "bool": {
          "filter": [
            {
              "match": {
                "skills.language": "ruby"
              }
            },
            {
              "match": {
                "skills.level": "beginner"
              }
            }
          ]
        }
      }
    }
  }
}

注意上面的 “nested” 字段。显示的结果是:

{
  "took" : 5,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.0,
    "hits" : [
      {
        "_index" : "developer",
        "_type" : "_doc",
        "_id" : "102",
        "_score" : 0.0,
        "_source" : {
          "name" : "li si",
          "skills" : [
            {
              "language" : "ruby",
              "level" : "beginner"
            }
          ]
        }
      }
    ]
  }
}

显然,我们只得到了一个我们想要的结果。


获取 inner hits


parent-joinnested 功能允许返回在不同范围内具有匹配项的文档。 在父/子情况下,根据子文档中的匹配返回父文档,或者根据父文档中的匹配返回子文档。 在 nested 的情况下,基于嵌套内部对象中的匹配返回文档。


在许多情况下,了解哪些内部嵌套对象(对于嵌套的情况)或子/父文档(对于父/子的情况)导致某些信息返回非常有用。 内部点击功能可用于此目的。 此功能会在搜索响应中为每个搜索命中返回附加的嵌套命中,这些嵌套命中会导致搜索命中在不同范围内匹配。


我们可以尝试如下的查询:

GET developer/_search
{
  "query": {
    "nested": {
      "path": "skills",
      "query": {
        "bool": {
          "filter": [
            {
              "match": {
                "skills.language": "ruby"
              }
            },
            {
              "match": {
                "skills.level": "beginner"
              }
            }
          ]
        }
      },
      "inner_hits": {}
    }
  }
}

在上面,我们添加了 inner_hits。上面写的命令返回的结果为:

{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.0,
    "hits" : [
      {
        "_index" : "developer",
        "_type" : "_doc",
        "_id" : "102",
        "_score" : 0.0,
        "_source" : {
          "name" : "li si",
          "skills" : [
            {
              "language" : "ruby",
              "level" : "beginner"
            }
          ]
        },
        "inner_hits" : {
          "skills" : {
            "hits" : {
              "total" : {
                "value" : 1,
                "relation" : "eq"
              },
              "max_score" : 0.0,
              "hits" : [
                {
                  "_index" : "developer",
                  "_type" : "_doc",
                  "_id" : "102",
                  "_nested" : {
                    "field" : "skills",
                    "offset" : 0
                  },
                  "_score" : 0.0,
                  "_source" : {
                    "language" : "ruby",
                    "level" : "beginner"
                  }
                }
              ]
            }
          }
        }
      }
    ]
  }
}


nested aggregation


同样,我们可以对我们的索引来做一个 aggregation:

GET developer/_search
{
  "size": 0,
  "aggs": {
    "nested_skills": {
      "nested": {
        "path": "skills"
      },
      "aggs": {
        "languages": {
          "terms": {
            "field": "skills.language"
          },
          "aggs": {
            "levels": {
              "terms": {
                "field": "skills.level"
              }
            }
          }
        }
      }
    }
  }
}

显示的结果是:

{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "nested_skills" : {
      "doc_count" : 3,
      "languages" : {
        "doc_count_error_upper_bound" : 0,
        "sum_other_doc_count" : 0,
        "buckets" : [
          {
            "key" : "ruby",
            "doc_count" : 2,
            "levels" : {
              "doc_count_error_upper_bound" : 0,
              "sum_other_doc_count" : 0,
              "buckets" : [
                {
                  "key" : "beginner",
                  "doc_count" : 1
                },
                {
                  "key" : "expert",
                  "doc_count" : 1
                }
              ]
            }
          },
          {
            "key" : "javascript",
            "doc_count" : 1,
            "levels" : {
              "doc_count_error_upper_bound" : 0,
              "sum_other_doc_count" : 0,
              "buckets" : [
                {
                  "key" : "beginner",
                  "doc_count" : 1
                }
              ]
            }
          }
        ]
      }
    }
  }
}

从上面显示的结果,可以看出来对于 ruby 来说,它分别对应于一个 bigginer 及一个 expert。这个和我们之前的数据是一样的。对于 javascript 来说,它只有一个 beginner 的 level。


相关实践学习
使用阿里云Elasticsearch体验信息检索加速
通过创建登录阿里云Elasticsearch集群,使用DataWorks将MySQL数据同步至Elasticsearch,体验多条件检索效果,简单展示数据同步和信息检索加速的过程和操作。
ElasticSearch 入门精讲
ElasticSearch是一个开源的、基于Lucene的、分布式、高扩展、高实时的搜索与数据分析引擎。根据DB-Engines的排名显示,Elasticsearch是最受欢迎的企业搜索引擎,其次是Apache Solr(也是基于Lucene)。 ElasticSearch的实现原理主要分为以下几个步骤: 用户将数据提交到Elastic Search 数据库中 通过分词控制器去将对应的语句分词,将其权重和分词结果一并存入数据 当用户搜索数据时候,再根据权重将结果排名、打分 将返回结果呈现给用户 Elasticsearch可以用于搜索各种文档。它提供可扩展的搜索,具有接近实时的搜索,并支持多租户。
相关文章
|
4天前
|
运维 架构师 搜索推荐
7 年+积累、 Elastic 创始人Shay Banon 等 15 位专家推荐的 Elasticsearch 8.X新书已上线...
7 年+积累、 Elastic 创始人Shay Banon 等 15 位专家推荐的 Elasticsearch 8.X新书已上线...
22 4
|
4天前
|
搜索推荐 JavaScript Java
Elasticsearch 8.X 如何依据 Nested 嵌套类型的某个字段进行排序?
Elasticsearch 8.X 如何依据 Nested 嵌套类型的某个字段进行排序?
22 0
|
4天前
|
存储 安全 数据处理
Elastic 中国开发者大会2023最新干货——Elasticsearch 7、8 新功能一网打尽
Elastic 中国开发者大会2023最新干货——Elasticsearch 7、8 新功能一网打尽
13 0
|
4天前
|
数据建模 索引
干货 | Elasticsearch Nested 数组大小求解,一网打尽!
干货 | Elasticsearch Nested 数组大小求解,一网打尽!
8 0
|
12天前
|
SQL JSON DataWorks
DataWorks产品使用合集之DataWorks 数据集成任务中,将数据同步到 Elasticsearch(ES)中,并指定 NESTED 字段中的 properties 类型如何解决
DataWorks作为一站式的数据开发与治理平台,提供了从数据采集、清洗、开发、调度、服务化、质量监控到安全管理的全套解决方案,帮助企业构建高效、规范、安全的大数据处理体系。以下是对DataWorks产品使用合集的概述,涵盖数据处理的各个环节。
21 0
|
5月前
|
存储 缓存 监控
Elasticsearch elastic io 100%,但磁盘的iops和吞吐量没爆没啥原因吗?
Elasticsearch elastic io 100%,但磁盘的iops和吞吐量没爆没啥原因吗?
59 2
|
5月前
|
搜索推荐 索引
Elasticsearch elastic io 100%,但磁盘的iops和吞吐量没爆没啥原因吗?
Elasticsearch elastic io 100%,但磁盘的iops和吞吐量没爆没啥原因吗?
76 3
|
7月前
|
存储 自然语言处理 监控
ElasticSearch第三讲:ES详解 - Elastic Stack生态和场景方案
ElasticSearch第三讲:ES详解 - Elastic Stack生态和场景方案
|
9月前
|
存储 JSON 数据建模
Elasticsearch数据建模实战之基于nested object实现博客与评论嵌套关系
Elasticsearch数据建模实战之基于nested object实现博客与评论嵌套关系
|
12月前
|
数据采集 数据可视化 搜索推荐
带你读《Elastic Stack 实战手册》之3:——3.1.1.从 Elasticsearch 到 Elastic Stack(上)
带你读《Elastic Stack 实战手册》之3:——3.1.1.从 Elasticsearch 到 Elastic Stack(上)
212 0

相关产品

  • 检索分析服务 Elasticsearch版