7.2 父子文档 (Parent-Child Relationship 逐渐被 Join 数据类型替代) Elasticsearch 高级特性:从父子文档到 Join 数据类型的演进 父子文档 (Parent-Child Relationship):历史与局限 在 Elasticsearch 早期版本中,父子文档关系是实现文档之间一对多关系的主要方式。它允许你将某些文档指定为“父文档”,而其他文档则作为其“子文档”。这种关系常用于表示诸如博客文章和评论、订单和订单项等场景。 1.1 父子文档的工作原理 父子文档关系的核心在于在索引文档时指定 字段。当索引一个子文档时,你需要指定其父文档的 ID。
在 Elasticsearch 早期版本中,父子文档关系是实现文档之间一对多关系的主要方式。它允许你将某些文档指定为“父文档”,而其他文档则作为其“子文档”。这种关系常用于表示诸如博客文章和评论、订单和订单项等场景。
父子文档关系的核心在于在索引文档时指定 _parent 字段。当索引一个子文档时,你需要指定其父文档的 ID。Elasticsearch 会确保父文档和子文档被路由到相同的分片 (shard),这对于保证查询性能至关重要。
映射 (Mapping) 定义
在定义映射时,你需要为子文档类型指定 _parent 字段,并指定父文档的类型。例如,假设我们有两个类型: blog_post (父类型) 和 comment (子类型)。
PUT /my_index { "mappings": { "properties": { "comment": { "type": "join", // 在 7.x 版本中,即使是父子关系,底层也开始使用 join 类型,但早期版本使用 _parent 字段 "relations": { "blog_post": "comment" } }, "blog_post_title": { "type": "text" }, "comment_text": { "type": "text" } } } }
索引文档
索引父文档 blog_post:
POST /my_index/_doc/blog_post_1?routing=blog_post_1 // 为了演示 routing,显式指定 routing key { "blog_post_title": "Elasticsearch 父子文档详解" }
索引子文档 comment,并指定 parent 字段:
POST /my_index/_doc/comment_1?routing=blog_post_1&parent=blog_post_1 // routing 必须与父文档一致,并指定 parent { "comment_text": "这篇文章写得真好!", "comment": "comment" // 使用 join 字段,并标记为 "comment" 关系 }
注意: 在早期的父子文档实现中,你可能需要使用 _parent 字段和 parent 参数,但实际上在 7.x 版本,即使是父子文档的概念,底层也开始使用 join 数据类型来管理关系。为了更清晰地理解演进过程,我们这里先用 join 类型模拟早期的父子文档行为。
查询父子文档
要查询与特定父文档关联的子文档,可以使用 has_child 查询。
GET /my_index/_search { "query": { "has_child": { "type": "comment", "query": { "match": { "comment_text": "真好" } }, "inner_hits": {} // 获取子文档的具体内容 } } }
反之,要查询与特定子文档关联的父文档,可以使用 has_parent 查询。
GET /my_index/_search { "query": { "has_parent": { "parent_type": "blog_post", "query": { "match": { "blog_post_title": "Elasticsearch 父子文档详解" } }, "inner_hits": {} // 获取父文档的具体内容 } } }
mermaid 图示:父子文档关系
尽管父子文档关系在早期 Elasticsearch 版本中提供了一种处理文档关系的方法,但它存在一些固有的局限性,导致其逐渐被弃用:
性能瓶颈: 父子文档必须路由到相同的分片,这限制了数据分布的灵活性,并可能导致热点分片问题,尤其是在父文档拥有大量子文档时。查询性能也可能受到影响,因为需要跨分片进行协调。
复杂性: 父子关系的实现和管理相对复杂,包括路由配置、查询语法的特殊性等。
功能限制: 父子关系只支持一对多关系,对于更复杂的关系类型(例如多对多,或更细粒度的关系控制)无能为力。
维护成本: 随着 Elasticsearch 的发展,维护和优化父子文档关系变得越来越困难,社区也逐渐倾向于寻找更通用的解决方案。
正是由于这些局限性,Elasticsearch 团队开始着手寻找更灵活、更高效的方式来处理文档之间的关系,最终 Join 数据类型 应运而生,并逐渐取代了父子文档关系。
Elasticsearch 版本引入了 Join 数据类型,旨在提供更强大、更灵活的方式来处理文档之间的关系,它不仅解决了父子文档的诸多局限性,还提供了更广泛的应用场景。
Join 数据类型允许你在单个索引中定义多种类型的关系,例如父子关系、兄弟关系、甚至更复杂的关系。它通过在文档中添加一个特殊的 join 字段来标记文档在关系中所扮演的角色,并定义不同关系类型之间的连接。
映射 (Mapping) 定义
使用 Join 数据类型,你需要在映射中定义 join 字段,并指定 relations 属性来描述索引中存在的不同关系类型。
PUT /my_index_join { "mappings": { "properties": { "my_join_field": { "type": "join", "relations": { "question": "answer", // 定义 question -> answer 的父子关系 "blog_post": "comment", // 定义 blog_post -> comment 的父子关系 "department": "employee" // 定义 department -> employee 的父子关系 } }, "question_title": { "type": "text" }, "answer_text": { "type": "text" }, "blog_post_title": { "type": "text" }, "comment_text": { "type": "text" }, "department_name": { "type": "keyword" }, "employee_name": { "type": "text" } } } }
在这个例子中,我们定义了三种关系: question 到 answer, blog_post 到 comment,以及 department 到 employee。my_join_field 就是我们的 Join 数据类型字段。
索引文档
索引父文档 question:
POST /my_index_join/_doc/question_1 { "question_title": "Elasticsearch Join 数据类型详解", "my_join_field": { "name": "question" // 标记为 "question" 类型 } }
索引子文档 answer,并指定 parent 关系:
POST /my_index_join/_doc/answer_1?routing=question_1&parent=question_1 // routing 仍需与父文档一致,并指定 parent 参数 { "answer_text": "Join 数据类型非常强大!", "my_join_field": { "name": "answer", // 标记为 "answer" 类型 "parent": "question_1" // 指向父文档 ID } }
注意: 与早期的 _parent 字段不同,join 字段本身就包含了关系信息 (name 和 parent),使得关系定义更加明确和集中。 routing 和 parent 参数在索引子文档时仍然需要,以确保父子文档在同一分片。
查询 Join 数据类型
查询 Join 数据类型的方式与父子文档类似,仍然使用 has_child 和 has_parent 查询,但需要指定 join 字段的名称。
查询与 question 父文档关联的 answer 子文档:
GET /my_index_join/_search { "query": { "has_child": { "type": "answer", // 仍然使用 type,但 type 现在指的是 relations 中定义的子关系名称 "join": "my_join_field", // 指定 join 字段名称 "query": { "match": { "answer_text": "强大" } }, "inner_hits": {} } } }
查询与 answer 子文档关联的 question 父文档:
GET /my_index_join/_search { "query": { "has_parent": { "parent_type": "question", // 仍然使用 parent_type,指 relations 中定义的父关系名称 "join": "my_join_field", // 指定 join 字段名称 "query": { "match": { "question_title": "Join 数据类型详解" } }, "inner_hits": {} } } }
更灵活的关系查询
Join 数据类型不仅支持 has_child 和 has_parent 查询,还可以在聚合 (Aggregation) 中使用,实现更复杂的关系分析。例如,我们可以统计每个部门有多少员工:
GET /my_index_join/_search { "aggs": { "departments": { "terms": { "field": "department_name" }, "aggs": { "employee_count": { "children": { // 使用 children aggregation,指定 join 字段和子关系类型 "type": "employee", "join": "my_join_field" } } } } } }
mermaid 图示:Join 数据类型关系
相比于父子文档关系,Join 数据类型具有以下显著优势:
更灵活的关系定义: Join 数据类型允许在一个索引中定义多种关系类型,并为每种关系指定名称,使得数据建模更加灵活,可以适应更复杂的需求。
更清晰的关系表达: 关系信息被明确地定义在 join 字段中,使得文档之间的关系更加清晰易懂,维护性更高。
更广泛的应用场景: Join 数据类型不仅可以用于父子关系,还可以用于兄弟关系、多对多关系等,应用场景更加广泛。
更好的性能: 虽然 Join 数据类型底层仍然依赖于 routing 机制来保证父子文档在同一分片,但其更灵活的设计为未来的性能优化提供了更大的空间。
更强大的查询和聚合能力: Join 数据类型不仅支持父子文档的查询方式,还可以在聚合中使用,实现更复杂的关系分析和统计。
由于 Join 数据类型的优势明显,Elasticsearch 官方推荐使用 Join 数据类型来替代父子文档关系。虽然 Elasticsearch 版本仍然支持父子文档的语法,但官方已经明确表示将在未来的版本中逐步移除对父子文档的支持。
迁移策略
如果你现有的 Elasticsearch 集群中使用了父子文档关系,建议尽早迁移到 Join 数据类型。迁移过程主要包括以下步骤:
修改映射 (Mapping): 将原有的父子文档映射修改为使用 Join 数据类型,定义 join 字段和 relations 属性。
重新索引数据 (Reindexing): 由于映射发生了变化,你需要重新索引现有的数据。在重新索引的过程中,需要将原有的父子关系信息转换为 Join 数据类型所需的格式,即在文档中添加 join 字段,并设置 name 和 parent 属性。
修改查询和聚合 (Queries and Aggregations): 更新现有的查询和聚合语句,将针对父子文档的查询方式修改为针对 Join 数据类型的查询方式,例如在 has_child 和 has_parent 查询中指定 join 字段名称。
迁移示例
假设你之前使用父子文档关系存储博客文章和评论,现在要迁移到 Join 数据类型。
原有父子文档映射 (假设为 type-per-index 模式):
PUT /blog_posts_index { "mappings": { "blog_post": { "properties": { "blog_post_title": { "type": "text" } } } } } PUT /comments_index { "mappings": { "comment": { "_parent": { "type": "blog_post" }, "properties": { "comment_text": { "type": "text" } } } } }
迁移后的 Join 数据类型映射 (single-index 模式):
PUT /blog_index_join_migrated { "mappings": { "properties": { "my_join_field": { "type": "join", "relations": { "blog_post": "comment" } }, "blog_post_title": { "type": "text" }, "comment_text": { "type": "text" } } } }
数据迁移 (Reindex 过程 - 简化示例,实际需要考虑批量操作和滚动重启等):
从 blog_posts_index 迁移 blog_post 文档:
POST /blog_index_join_migrated/_doc/blog_post_1 { "blog_post_title": "Elasticsearch 父子文档迁移到 Join 数据类型", "my_join_field": { "name": "blog_post" } }
从 comments_index 迁移 comment 文档 (假设 comment_1 的父文档是 blog_post_1):
POST /blog_index_join_migrated/_doc/comment_1?routing=blog_post_1&parent=blog_post_1 { "comment_text": "迁移成功!", "my_join_field": { "name": "comment", "parent": "blog_post_1" } }
完成映射修改和数据重新索引后,你需要相应地修改你的应用程序代码,更新查询和聚合语句,以适应 Join 数据类型的语法。
父子文档关系是 Elasticsearch 早期版本中处理文档关系的一种尝试,但由于其固有的局限性,逐渐被更强大、更灵活的 Join 数据类型所取代。Join 数据类型不仅解决了父子文档的性能瓶颈和功能限制,还提供了更广泛的应用场景和更清晰的关系表达。
在 Elasticsearch 以后的版本中,强烈建议使用 Join 数据类型来处理文档之间的关系。迁移到 Join 数据类型不仅可以提升性能,还能为未来的扩展和更复杂的关系建模奠定基础。理解父子文档的历史背景和 Join 数据类型的优势,有助于你更好地选择和使用 Elasticsearch 的高级特性,构建更高效、更强大的搜索应用。
随着 Elasticsearch 的不断演进,掌握其最新的特性和最佳实践至关重要。Join 数据类型正是 Elasticsearch 在关系数据处理方面迈出的重要一步,值得每一位 Elasticsearch 用户深入学习和应用。