elasticsearch连接查询
Elasticsearch 连接查询:嵌套查询与父子查询详解
在 Elasticsearch 中,处理关联数据(如一对多关系)时,传统的对象类型(object)会因内部扁平化存储导致查询异常。为此,ES 提供了两种专门的关联查询方式:嵌套查询(nested query) 和 父子查询(parent-child query),分别适用于不同的业务场景。
对象类型的局限性:为何需要连接查询?
对象类型(object)用于存储 JSON 对象数组,但 ES 会在内部将其扁平化处理,导致对象边界丢失,从而引发查询逻辑错误。
问题示例
假设有一篇包含多个事件的文档:
{
"name": "技术大会",
"events": [
{ "date": "2022-10-10", "title": "java架构大会" },
{ "date": "2022-08-08", "title": "hadoop大会" },
{ "date": "2022-02-02", "title": "elasticsearch大会" }
]
}
ES 内部会将其扁平化为:
{
"name": "技术大会",
"events.date": ["2022-10-10", "2022-08-08", "2022-02-02"],
"events.title": ["java架构大会", "hadoop大会", "elasticsearch大会"]
}
此时查询 “title:hadoop 且 date 在 2022-10-01 至 2022-12-31 之间” 的事件,会错误匹配:
- 条件
title:hadoop匹配第二个事件(date:2022-08-08); - 条件
date范围匹配第一个事件(date:2022-10-10); - 扁平化存储导致 ES 认为 “存在同时满足两个条件的文档”,返回错误结果。
结论
对象类型仅适用于一对一关系,对于一对多关系,需使用嵌套查询或父子查询。
嵌套查询(Nested Query)
嵌套查询通过 nested 类型将对象数组存储为独立的 Lucene 文档,保留对象边界,确保多字段条件能正确关联到同一对象。
定义嵌套类型映射
需在映射中显式声明字段为 nested 类型:
PUT conference
{
"mappings": {
"default": {
"properties": {
"name": { "type": "text" },
"events": {
"type": "nested", // 声明为嵌套类型
"properties": {
"date": { "type": "date" },
"title": { "type": "text" }
}
}
}
}
}
}
插入嵌套文档
与普通对象数组格式一致,ES 会自动将 events 中的每个对象作为独立 Lucene 文档存储:
PUT conference/default/1
{
"name": "技术大会",
"events": [
{ "date": "2022-10-10", "title": "java架构大会" },
{ "date": "2022-08-08", "title": "hadoop大会" },
{ "date": "2022-02-02", "title": "elasticsearch大会" }
]
}
执行嵌套查询
使用 nested 子句指定嵌套路径(path)和查询条件,确保条件仅匹配同一嵌套对象:
GET conference/_search
{
"query": {
"nested": {
"path": "events", // 嵌套对象的路径
"query": {
"bool": {
"must": [
{ "term": { "events.title": "hadoop" } }, // 条件1:标题含hadoop
{ "range": { "events.date": { "gte": "2022-10-01", "lte": "2022-12-31" } } } // 条件2:日期在范围内
]
}
}
}
}
}
结果
该查询返回空结果(正确),因为 hadoop 事件的日期不在范围内。
嵌套查询的参数
path:嵌套对象在文档中的路径(如events)。query:作用于嵌套对象的查询条件(内部字段需用全路径,如events.title)。score_mode:嵌套对象的匹配得分如何影响父文档得分(默认avg,可选sum/max/min/none)。
优缺点与适用场景
| 优点 | 缺点 | 适用场景 |
|---|---|---|
| 保留对象边界,查询准确; 查询性能好(嵌套对象与父文档同分片); 支持聚合操作。 | 更新任一嵌套对象需重新索引整个文档; 嵌套层级过深可能影响性能。 | 嵌套对象不频繁更新的场景(如商品的规格参数、文章的评论)。 |
父子查询(Parent-Child Query)
父子查询通过 join 类型定义文档间的父子关系,父文档和子文档作为独立 ES 文档存储,更新子文档无需影响父文档,适合频繁更新的场景。
定义父子关系映射(6.x+)
6.x 后通过 join 类型替代旧版 _parent 字段,显式声明父子关系:
PUT Q&A // 索引名
{
"mappings": {
"default": {
"properties": {
"my_join_field": { // 用于定义父子关系的字段
"type": "join",
"relations": {
"question": "answer" // question为父类型,answer为子类型
}
}
}
}
}
}
插入父文档与子文档
父文档:需指定
join字段的名称为父类型:PUT Q&A/default/1?refresh // 父文档ID=1 { "text": "什么是Elasticsearch?", "my_join_field": { "name": "question" } // 声明为父类型 }子文档:需通过
routing参数确保与父文档同分片,并指定parent为父文档 ID:PUT Q&A/default/101?routing=1&refresh // 子文档ID=101,routing=父文档ID { "text": "Elasticsearch是一款搜索引擎", "my_join_field": { "name": "answer", // 声明为子类型 "parent": "1" // 关联父文档ID=1 } }注意:
routing确保父子文档存储在同一分片,否则无法查询关联关系。
常用父子查询
(1)has_child:通过子文档查询父文档
返回包含匹配子文档的父文档:
GET Q&A/_search
{
"query": {
"has_child": {
"type": "answer", // 子类型名
"query": { "term": { "text": "搜索引擎" } } // 子文档条件
}
}
}
- 结果:返回父文档(ID=1),因为其包含匹配 “搜索引擎” 的子文档。
(2)has_parent:通过父文档查询子文档
返回关联匹配父文档的子文档:
GET Q&A/_search
{
"query": {
"has_parent": {
"parent_type": "question", // 父类型名
"query": { "term": { "text": "elasticsearch" } } // 父文档条件
}
}
}
- 结果:返回子文档(ID=101),因为其关联的父文档包含 “elasticsearch”。
(3)parent_id:通过父 ID 查询子文档
返回指定父文档的所有子文档:
GET Q&A/_search
{
"query": {
"parent_id": {
"type": "answer", // 子类型名
"id": "1" // 父文档ID
}
}
}
- 结果:返回父文档 ID=1 的所有子文档(如 ID=101)。
优缺点与适用场景
| 优点 | 缺点 | 适用场景 |
|---|---|---|
| 子文档更新不影响父文档; 支持频繁更新的子文档。 | 查询性能低于嵌套查询(需跨文档匹配); 父子文档必须同分片(依赖 routing)。 |
子文档频繁更新的场景(如问答的回复、订单的物流记录)。 |
嵌套查询 vs 父子查询:如何选择?
| 维度 | 嵌套查询(Nested) | 父子查询(Parent-Child) |
|---|---|---|
| 存储方式 | 嵌套对象与父文档同 Lucene 文档 | 父子文档为独立 ES 文档 |
| 更新成本 | 更新子对象需重新索引整个文档 | 子文档更新不影响父文档 |
| 查询性能 | 高(同分片,无跨文档查找) | 中(需关联独立文档) |
| 适用场景 | 子对象不频繁更新(如商品属性) | 子对象频繁更新(如评论、日志) |