首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Elasticsearch使用:父-子关系文档(上)

Elasticsearch使用:父-子关系文档(上)

原创
作者头像
HLee
修改于 2021-10-15 08:34:50
修改于 2021-10-15 08:34:50
4K00
代码可运行
举报
文章被收录于专栏:房东的猫房东的猫
运行总次数:0
代码可运行

简介

官网地址:https://www.elastic.co/guide/cn/elasticsearch/guide/current/parent-child.html

父-子关系文档 在实质上类似于 nested model :允许将一个对象实体和另外一个对象实体关联起来。而这两种类型的主要区别是:nested objects 文档中,所有对象都是在同一个文档中,而在父-子关系文档中,父对象和子对象都是完全独立的文档。

父-子关系的主要作用是允许把一个 type 的文档和另外一个 type 的文档关联起来,构成一对多的关系:一个父文档可以对应多个子文档 。与 nested objects 相比,父-子关系的主要优势有:

  • 更新父文档时,不会重新索引子文档。
  • 创建,修改或删除子文档时,不会影响父文档或其他子文档。
  • 子文档可以作为搜索结果独立返回。

Elasticsearch 维护了一个父文档和子文档的映射关系,得益于这个映射,父-子文档关联查询操作非常快。但是这个映射也对父-子文档关系有个限制条件:父文档和其所有子文档,都必须要存储在同一个分片中。

父-子文档ID映射存储在 Doc Values 中。当映射完全在内存中时, Doc Values 提供对映射的快速处理能力,另一方面当映射非常大时,可以通过溢出到磁盘提供足够的扩展能力

父-子关系文档映射

建立父-子文档映射关系时只需要指定某一个文档 type 是另一个文档 type 的父亲。

举例说明,有一个公司在多个城市有分公司,并且每一个分公司下面都有很多员工。有这样的需求:按照分公司、员工的维度去搜索,并且把员工和他们工作的分公司联系起来。针对该需求,用嵌套模型是无法实现的。当然,如果使用 application-side-joins 或者 data denormalization 也是可以实现的,但是为了演示的目的,在这里我们使用父-子文档。

我们需要告诉Elasticsearch,在创建员工 employee 文档 type 时,指定分公司 branch 的文档 type 为其父亲。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
PUT /company
{
  "mappings": {
    "branch": {},
    "employee": {
      "_parent": {
        "type": "branch"   # employee 文档 是 branch 文档的子文档。
      }
    }
  }
}

构建父-子文档索引

为父文档创建索引与为普通文档创建索引没有区别。父文档并不需要知道它有哪些子文档。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
POST /company/branch/_bulk
{ "index": { "_id": "london" }}
{ "name": "London Westminster", "city": "London", "country": "UK" }
{ "index": { "_id": "liverpool" }}
{ "name": "Liverpool Central", "city": "Liverpool", "country": "UK" }
{ "index": { "_id": "paris" }}
{ "name": "Champs Élysées", "city": "Paris", "country": "France" }

创建子文档时,用户必须要通过 parent 参数来指定该子文档的父文档 ID:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
PUT /company/employee/1?parent=london   # 当前 employee 文档的父文档 ID 是 london 。
{
  "name":  "Alice Smith",
  "dob":   "1970-10-24",
  "hobby": "hiking"
}

父文档 ID 有两个作用:创建了父文档和子文档之间的关系,并且保证了父文档和子文档都在同一个分片上。

路由一个文档到一个分片中 中,我们解释了 Elasticsearch 如何通过路由值来决定该文档属于哪一个分片,路由值默认为该文档的_id 。分片路由的计算公式如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
shard = hash(routing) % number_of_primary_shards

如果指定了父文档的 ID,那么就会使用父文档的 ID 进行路由,而不会使用当前文档_id 。也就是说,如果父文档和子文档都使用相同的值进行路由,那么父文档和子文档都会确定分布在同一个分片上。

在执行单文档的请求时需要指定父文档的 ID,单文档请求包括:通过 GET 请求获取一个子文档;创建、更新或删除一个子文档。而执行搜索请求时是不需要指定父文档的ID,这是因为搜索请求是向一个索引中的所有分片发起请求,而单文档的操作是只会向存储该文档的分片发送请求。因此,如果操作单个子文档时不指定父文档的ID,那么很有可能会把请求发送到错误的分片上。

父文档的 ID 应该在bulk API 中指定

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
POST /company/employee/_bulk
{ "index": { "_id": 2, "parent": "london" }}
{ "name": "Mark Thomas", "dob": "1982-05-16", "hobby": "diving" }
{ "index": { "_id": 3, "parent": "liverpool" }}
{ "name": "Barry Smith", "dob": "1979-04-01", "hobby": "hiking" }
{ "index": { "_id": 4, "parent": "paris" }}
{ "name": "Adrien Grand", "dob": "1987-05-11", "hobby": "horses" }

如果你想要改变一个子文档的parent值,仅通过更新这个子文档是不够的,因为新的父文档有可能在另外一个分片上。因此,你必须要先把子文档删除,然后再重新索引这个子文档。

通过子文档查询父文档

has_child的查询和过滤可以通过子文档的内容来查询父文档。例如,我们根据如下查询,可查出所有80后员工所在的分公司:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
GET /company/branch/_search
{
  "query": {
    "has_child": {
      "type": "employee",
      "query": {
        "range": {
          "dob": {
            "gte": "1980-01-01"
          }
        }
      }
    }
  }
}

类似于 nested queryhas_child 查询可以匹配多个子文档,并且每一个子文档的评分都不同。但是由于每一个子文档都带有评分,这些评分如何规约成父文档的总得分取决于score_mode 这个参数。该参数有多种取值策略:默认为 none ,会忽略子文档的评分,并且会给父文档评分设置为 1.0 ; 除此以外还可以设置成 avgminmaxsum

下面的查询将会同时返回 londonliverpool ,不过由于 Alice Smith 要比 Barry Smith 更加匹配查询条件,因此 london 会得到一个更高的评分。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
GET /company/branch/_search
{
  "query": {
    "has_child": {
      "type":       "employee",
      "score_mode": "max",
      "query": {
        "match": {
          "name": "Alice Smith"
        }
      }
    }
  }
}

score_mode为默认的none时,会显著地比其模式要快,这是因为Elasticsearch不需要计算每一个子文档的评分。只有当你真正需要关心评分结果时,才需要为score_mode设值,例如设成avgminmaxsum

min_children 和 max_children

has_child的查询和过滤都可以接受这两个参数:min_childrenmax_children。 使用这两个参数时,只有当子文档数量在指定范围内时,才会返回父文档。

如下查询只会返回至少有两个雇员的分公司:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
GET /company/branch/_search
{
  "query": {
    "has_child": {
      "type":         "employee",
      "min_children": 2,   # 至少有两个雇员的分公司才会符合查询条件。
      "query": {
        "match_all": {}
      }
    }
  }
}

带有min_childrenmax_children参数的has_child查询或过滤,和允许评分的has_child查询的性能非常接近。

has_child Filter

has_child查询和过滤在运行机制上类似,区别是 has_child过滤不支持score_mode参数。has_child 过滤仅用于筛选内容。​如内部的一个filtered查询​和其他过滤行为类似:包含或者排除,但没有进行评分。

has_child 过滤的结果没有被缓存,但是 has_child 过滤内部的过滤方法适用于通常的缓存规则。

通过父文档查询子文档

虽然 nested 查询只能返回最顶层的文档 ,但是父文档和子文档本身是彼此独立并且可被单独查询的。我们使用 has_child 语句可以基于子文档来查询父文档,使用 has_parent 语句可以基于父文档来查询子文档。

has_parenthas_child 非常相似,下面的查询将会返回所有在 UK 工作的雇员:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
GET /company/employee/_search
{
  "query": {
    "has_parent": {
      "type": "branch",   # 返回父文档 type 是 branch 的所有子文档
      "query": {
        "match": {
          "country": "UK"
        }
      }
    }
  }
}

has_parent查询也支持score_mode这个参数,但是该参数只支持两种值:none(默认)和score。每个子文档都只有一个父文档,因此这里不存在将多个评分规约为一个的情况,score_mode的取值仅为scorenone

has_parent Filter

has_parent查询用于非评分模式(比如 filter 查询语句)时, score_mode参数就不再起作用了。因为这种模式只是简单地包含或排除文档,没有评分,那么score_mode参数也就没有意义了。

子文档聚合

在父-子文档中支持 子文档聚合,这一点和 嵌套聚合 类似。但是,对于父文档的聚合查询是不支持的(和 reverse_nested 类似)。

我们通过下面的例子来演示按照国家维度查看最受雇员欢迎的业余爱好:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
GET /company/branch/_search
{
  "size" : 0,
  "aggs": {
    "country": {
      "terms": { 
        "field": "country"  # country 是 branch 文档的一个字段。
      },
      "aggs": {
        "employees": {
          "children": { 
            "type": "employee"  # 子文档聚合查询通过 employee type 的子文档将其父文档聚合在一起。
          },
          "aggs": {
            "hobby": {
              "terms": { 
                "field": "hobby"  # hobby 是 employee 子文档的一个字段。
              }
            }
          }
        }
      }
    }
  }
}

祖辈与孙辈关系

父子关系可以延展到更多代关系,比如生活中孙辈与祖辈的关系。唯一的要求是满足这些关系的文档必须在同一个分片上被索引。

让我们把上一个例子中的 country 类型设定为 branch 类型的父辈:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
PUT /company
{
  "mappings": {
    "country": {},
    "branch": {
      "_parent": {
        "type": "country"   # branch 是 country 的子辈。
      }
    },
    "employee": {
      "_parent": {
        "type": "branch"  # employee 是 branch 的子辈。
      }
    }
  }
}

country 和 branch 之间是一层简单的父子关系,所以我们的操作步骤与之前保持一致:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
POST /company/country/_bulk
{ "index": { "_id": "uk" }}
{ "name": "UK" }
{ "index": { "_id": "france" }}
{ "name": "France" }

POST /company/branch/_bulk
{ "index": { "_id": "london", "parent": "uk" }}
{ "name": "London Westmintster" }
{ "index": { "_id": "liverpool", "parent": "uk" }}
{ "name": "Liverpool Central" }
{ "index": { "_id": "paris", "parent": "france" }}
{ "name": "Champs Élysées" }

parentID 使得每一个branch文档被路由到与其父文档country相同的分片上进行操作。然而,当我们使用相同的方法来操作employee这个孙辈文档时,会发生什么呢?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
PUT /company/employee/1?parent=london
{
  "name":  "Alice Smith",
  "dob":   "1970-10-24",
  "hobby": "hiking"
}

employee 文档的路由依赖其父文档 ID — 也就是 london — 但是 london 文档的路由却依赖 其本身的 父文档 ID — 也就是 uk 。此种情况下,孙辈文档很有可能最终和父辈、祖辈文档不在同一分片上,导致不满足祖辈和孙辈文档必须在同一个分片上被索引的要求。

解决方案是添加一个额外的 routing 参数,将其设置为祖辈的文档 ID ,以此来保证三代文档路由到同一个分片上。索引请求如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
PUT /company/employee/1?parent=london&routing=uk   # routing 的值会取代 parent 的值作为路由选择。
{
  "name":  "Alice Smith",
  "dob":   "1970-10-24",
  "hobby": "hiking"
}

parent 参数的值仍然可以标识 employee 文档与其父文档的关系,但是 routing 参数保证该文档被存储到其父辈和祖辈的分片上。routing 值在所有的文档请求中都要添加。

联合多代文档进行查询和聚合是可行的,只需要一代代的进行设定即可。例如,我们要找到哪些国家的雇员喜欢远足旅行,此时只需要联合 country 和 branch,以及 branch 和 employee:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
GET /company/country/_search
{
  "query": {
    "has_child": {
      "type": "branch",
      "query": {
        "has_child": {
          "type": "employee",
          "query": {
            "match": {
              "hobby": "hiking"
            }
          }
        }
      }
    }
  }
}

实际使用中的一些建议

当文档索引性能远比查询性能重要的时候,父子关系是非常有用的,但是它也是有巨大代价的。其查询速度会比同等的嵌套查询慢5到10倍!

全局序号和延迟

父子关系使用了全局序数 来加速文档间的联合。不管父子关系映射是否使用了内存缓存或基于硬盘的 doc values,当索引变更时,全局序数要重建。

一个分片中父文档越多,那么全局序数的重建就需要更多的时间。父子关系更适合于父文档少、子文档多的情况。

全局序数默认情况下是延迟构建的:在refresh后的第一个父子查询会触发全局序数的构建。而这个构建会导致用户使用时感受到明显的迟缓。你可以使用全局序数预加载 来将全局序数构建的开销由query阶段转移到refresh阶段,设置如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
PUT /company
{
  "mappings": {
    "branch": {},
    "employee": {
      "_parent": {
        "type": "branch",
        "fielddata": {
          "loading": "eager_global_ordinals" # 在一个新的段可搜索前,_parent字段的全局序数会被构建。
        }
      }
    }
  }
}

当父文档过多时,全局序数的构建会耗费很多时间。此时可以通过增加refresh_interval来减少 refresh 的次数,延长全局序数的有效时间,这也很大程度上减小了全局序数每秒重建的cpu消耗。

多代使用和结语

多代文档的联合查询(查看 祖辈与孙辈关系)虽然看起来很吸引人,但必须考虑如下的代价:

  • 联合越多,性能越差。
  • 每一代的父文档都要将其字符串类型的_id字段存储在内存中,这会占用大量内存。

当你考虑父子关系是否适合你现有关系模型时,请考虑下面这些建议:

  • 尽量少地使用父子关系,仅在子文档远多于父文档时使用。
  • 避免在一个查询中使用多个父子联合语句。
  • 在 has_child 查询中使用 filter 上下文,或者设置 score_mode 为 none 来避免计算文档得分。
  • 保证父IDs 尽量短,以便在doc values中更好地压缩,被临时载入时占用更少的内存。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
小伙伴想写个 IDEA 插件么?这些 API 了解一下!
" 在看完 IDEA 插件开发简易教程后,小伙伴们是否迫不及待的想自己上手整一个插件了?心里规划好了一二三,但是却不知道从哪里开始下手。下面我分享下自己整理的一些常用的 API。 "
程序员小航
2020/11/23
2.3K0
小伙伴想写个 IDEA 插件么?这些 API 了解一下!
Idea插件开发
https://www.w3cschool.cn/intellij_idea_doc/
码客说
2024/08/04
4200
Idea插件开发
《IntelliJ IDEA 插件开发》第六节:选定对象批量织入“x.set(y.get)”代码,自动生成vo2dto
这些年从事编程开发以来,我好像发现了大部分研发那些不愿意干的事,都成就了别人。就像部署服务麻烦,有了Docker、简单CRUD不想开发,有了低代码、给方法代码加监控繁琐、有了非入侵的全链路监控。
小傅哥
2021/12/15
8590
《IntelliJ IDEA 插件开发》第六节:选定对象批量织入“x.set(y.get)”代码,自动生成vo2dto
为 TheRouter 开发一个 IDEA 插件
这篇文章是假定你已经有了 idea 插件开发的入门知识,来教你如何实现一个实际项目的功能。如果你还不知道如何开发一个插件,建议先从这个链接查看官网相关文档 https://plugins.jetbrains.com/docs/intellij/welcome.html。
用户1907613
2023/10/19
3790
为 TheRouter 开发一个 IDEA 插件
开发属于自己的插件 | IDEA & Android Studio插件开发指南
谷轩宇——从事安卓开发,目前效力于通天塔技术开放组是否曾经被ide重复繁琐的操作所困扰,又或者没有心仪的UI控件而难受。那么请阅读这篇文章,掌握idea插件的开发流程,开发属于自己的插件,造福开源社区。
京东技术
2018/09/28
5K0
开发属于自己的插件 | IDEA & Android Studio插件开发指南
idea插件开发记录
插件开发示例 ---- 功能开发代码示例 java package com.cjl.plugins.code.hints; import com.cjl.plugins.code.code.NavigatorPanel; import com.cjl.plugins.code.http.HttpUtils; import com.cjl.plugins.code.json.Json; import com.intellij.codeInsight.hint.HintManager; import com.
司夜
2023/03/31
7610
idea插件开发记录
只需三步实现Databinding插件化
首先为何我要实现Databinding这个小插件,主要是在日常开发中,发现每次通过Android Studio的Layout resource file来创建xml布局文件时,布局文件的格式都没有包含Databinding所要的标签。导致的问题就是每次都要重复手动修改布局文件,添加标签等。
Rouse
2019/07/22
1K0
只需三步实现Databinding插件化
《IntelliJ IDEA 插件开发》第四节:扩展创建工程向导步骤,开发DDD脚手架
你做这个东西的价值是什么?有竞品调研吗?能赋能业务吗?那不已经有同类的了,你为什么还自己造轮子?
小傅哥
2021/12/01
1.2K0
《IntelliJ IDEA 插件开发》第四节:扩展创建工程向导步骤,开发DDD脚手架
IDEA Cody 插件实现原理
近年来,智能编程助手 在开发者日常工作中变得越来越重要。IDEA Cody 插件是 JetBrains 生态中一个重要的插件,它可以帮助开发者 快速生成代码、自动补全、并提供智能提示,从而大大提升开发效率。今天我们将深入探讨 Cody 插件的实现原理,看看它是如何工作的。
井九
2024/10/12
3120
IDEA Cody 插件实现原理
Android MVP 代码自动生成插件开发
本文会出现的原因是,lucio在遵循Google的Android MVP示例代码的模式开发一个小的程序,发现我们会需要写很多重复的代码,更加麻烦的是,我们需要创建很多重复的文件。每开发一个小的模块,至少会需要创建Activity、Contract、Fragment和Presenter四个文件。
luciozhang
2023/04/22
5590
Android MVP 代码自动生成插件开发
IntelliJ插件开发-京东工程师教你改造你的IDE
王帅廷,京东 Android高级开发工程师,6年以上开发经验,对设计框架有着深刻的认识,负责京东商城研发工具的开发,设计并完成了多个IntelliJ插件的开发工作。
京东技术
2018/07/30
3.3K1
IntelliJ插件开发-京东工程师教你改造你的IDE
你们要的Intellij IDEA 插件开发秘籍,来了!
王昭霞,软件开发工程师,先后从事脚本工具编写、工具开发、Android基础模块开发等工作。
京东技术
2018/09/28
57.2K13
你们要的Intellij IDEA 插件开发秘籍,来了!
《IntelliJ IDEA 插件开发》第三节:开发工具栏和Tab页,展示股票行情和K线
以前,我不懂。写的技术就是技术内容,写的场景就是场景分析,但从读者的阅读我发现,大家更喜欢的是技术与场景结合,尤其是用技术结合那些羞羞答答的场景,虽然嘴上都不说。
小傅哥
2021/11/19
2.6K0
让代码自动补全的全套流程
作者:熊唯,黄飞 ,腾讯 PCG/QQ研发中心/CV应用研究组 AI 如果真的可以写代码了,程序员将何去何从?近几年,NLP 领域的生成式任务有明显的提升,那通过 AI 我们可以让代码自动完成后续补全吗?本文主要介绍了如何使用 GPT2 框架实现代码自动补全的功能。 如果 AI 真的可以自己写代码了,程序员将何去何从? 我去年做过一个代码补全的小功能,打包为 androidStudio 插件,使用效果如下: 代码补全模型预测出的结果有时的确会惊吓到我,这也能学到~ 那如果给它见识了全世界的优秀
腾讯技术工程官方号
2020/07/31
2.4K0
《IntelliJ IDEA 插件开发》第 五 节:IDEA工程右键菜单,自动生成ORM代码
几年前,大家并不是这样,那时候还有很多东西可以创新,乱世出英雄总能在一个方向深耕并做出一款款好用的产品功能、框架服务、技术组件等。但后来好像这样的情况开始减少了,取而代之的是重复、复刻、照搬,换个新的皮肤、换个新的样式、换个新的名字,就是取巧的新东西了。
小傅哥
2021/12/13
2.5K0
《IntelliJ IDEA 插件开发》第 五 节:IDEA工程右键菜单,自动生成ORM代码
p3c 插件,是怎么检查出你那屎山的代码?
虽然我们都被称为码农,也都是写着代码,但因为所处场景需求的不同,所以各类码农也都做着不一样都事情。
小傅哥
2021/10/08
1.1K0
IDEA插件:快速删除Java代码中的注释
本文针对Java语言,介绍一种利用第三方库的方式,可以方便快速地移除代码中的注释。
xiaoxi666
2021/02/17
3.1K0
IntelliJ IDEA/Android Studio插件开发指南
目前在为安卓手机QQ做自动化的相关工作,包括UI自动化,逻辑层自动化等。使用到的uiautomator等框架,需要在Android Studio进行编码工作。 其中很多工作如果做到插件化的话,可以有效地节省时间成本,提升大家的自动化效率。 比如运行自动化的时候,需要用到我们自定义的shell命令。我们可以通过插件来实现一键运行。 在运行adb shell am instrument命令的时候,需要编译出test APK和target APK。手Q整体的git仓库很大,编译耗时很久。我们想着通过一些方法来优化这个耗时。其中一个步骤就是,把我们代码目录下的变更,同步到一个编译目录下。 这个小功能的最合适的形态,自然就是Android Studio上的一个插件。点击一个按钮,一键同步,那可真是在米奇妙妙屋吃妙脆角——妙到家了! Android Studio是基于Intellij IDEA开发的,所以开发Android Studio的插件,其实就是开发IDEA的插件。 根据官方推荐,使用IDEA IDE来开发IDEA插件。
于果
2021/08/25
2.8K0
《IntelliJ IDEA 插件开发》第一节:两种方式创建插件工程
对于码农这一行业的编程学习生涯来说,会遇到很多的不会,不会搭建IDEA工程、不会写老师的案例、不会完成书中的效果、不会做项目的需求、不会实现复杂的逻辑、不会抽象工程的结构等等。但这些不会当中并不是所有的不会,都因为太复杂学不会,而是很大一部分内容因为找不到好的资料、没有清晰的文档、缺少完整的案例,导致不知道所以不会。
小傅哥
2021/10/20
3.1K0
技术调研,IDEA 插件怎么开发「脚手架、低代码可视化编排、接口生成测试」?
你觉得肯德基全家桶是什么?一家人一起吃的桶吗,就那么一点点?不是,肯德基全家桶说的是,鸡的全家桶!
小傅哥
2021/09/14
1.6K0
推荐阅读
相关推荐
小伙伴想写个 IDEA 插件么?这些 API 了解一下!
更多 >
LV.1
这个人很懒,什么都没有留下~
目录
  • 简介
  • 父-子关系文档映射
  • 构建父-子文档索引
  • 通过子文档查询父文档
    • min_children 和 max_children
    • has_child Filter
  • 通过父文档查询子文档
    • has_parent Filter
  • 子文档聚合
  • 祖辈与孙辈关系
  • 实际使用中的一些建议
    • 全局序号和延迟
    • 多代使用和结语
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档