文章目录
1、Nested 类型介绍
Elasticsearch 中的 nested 类型用于处理复杂数据结构中的嵌套对象。默认情况下,Elasticsearch 会将对象数组扁平化处理,导致查询时无法区分不同对象中的字段。nested 类型解决了这一问题,允许对嵌套对象进行独立索引和查询。
在 Elasticsearch 中,对象数组默认被扁平化。例如:
{
"user": [
{
"name": "Alice",
"age": 25
},
{
"name": "Bob",
"age": 30
}
]
}
默认情况下,Elasticsearch 会将其转换为:
{
"user.name": ["Alice", "Bob"],
"user.age": [25, 30]
}
这种扁平化使得无法区分 name 和 age 的对应关系,导致查询时无法准确匹配。
本文需要读者对 nested
类型有足够的了解,如有需要,请移步:
2、场景案例
2.1 场景描述
假设在ES中有order
索引存储商品订单信息,和order_goods
,用以存储订单相关商品信息,商品数量可以是多个,一个订单可能对应多个商品,order_goods
使用nested
类型存储,现在我希望对订单及订单商品进行检索,下面的所有内容将围绕这一场景讲解。
2.2 测试数据
order 索引:其中 order_goods 为 nested 类型
PUT /order
{
"mappings": {
"properties": {
"order_id": {
"type": "keyword"
},
"user_id": {
"type": "keyword"
},
"total_amount": {
"type": "float"
},
"status": {
"type": "keyword"
},
"created_at": {
"type": "date"
},
"order_goods": {
"type": "nested",
"properties": {
"goods_id": {
"type": "keyword"
},
"goods_name": {
"type": "text"
},
"price": {
"type": "float"
},
"quantity": {
"type": "integer"
}
}
}
}
}
}
为索引添加两条测试数据,如下:
POST /order/_doc/1
{
"order_id": "2001",
"user_id": "3001",
"total_amount": 1049.98,
"status": "completed",
"created_at": "2023-10-02T10:00:00Z",
"order_goods": [
{
"goods_id": "1001",
"goods_name": "iPhone 13",
"price": 799.99,
"quantity": 1
},
{
"goods_id": "1003",
"goods_name": "AirPods Pro",
"price": 249.99,
"quantity": 1
}
]
}
POST /order/_doc/2
{
"order_id": "2002",
"user_id": "3002",
"total_amount": 1999.99,
"status": "pending",
"created_at": "2023-10-02T11:00:00Z",
"order_goods": [
{
"goods_id": "1002",
"goods_name": "MacBook Pro",
"price": 1999.99,
"quantity": 1
}
]
}
2.3 需求描述及DSL实现
查询订单 id
为1
的订单,并且查看订单中与手机
相关的商品
GET order/_search
{
"query": {
"bool": {
"must": [
{
"term": {
"order_id": 2001
}
},
{
"nested": {
"path": "order_goods",
"query": {
"match": {
"order_goods.goods_name": "iPhone"
}
}
}
}
]
}
}
}
查询结果如下:
3、问题和剖析
3.1 坑
上述查询的语义其实是这样的:查询订单为1,且订单的商品与手机
相关的记录,
这样其实会把订单中的其他商品也会查询出来,因为查询中并未对order_goods
进行筛选,order_goods
的查询,仅仅是对文档做筛选。其实上述查询,和下面查询本质上没什么区别,因为bool query中的两个查询子句查询的文档是重合的
# 下面查询和 3.2 中的查询,在本案例中,结果是没有区别的
GET order/_search
{
"query": {
"bool": {
"must": [
{
"term": {
"order_id": 2001
}
}
]
}
}
}
3.2 解决方案
本质上,第二个查询子句,是希望对
如果需要筛选 order_goods
中的商品,可以利用 nested
查询结合inner_hits
来实现:
{
"query": {
"bool": {
"must": [
{
"match": {
"id": 1
}
},
{
"nested": {
"path": "order_goods",
"query": {
"match": {
"order_goods.goods_name": "iPhone"
}
},
"inner_hits": {}
}
}
]
}
}
}
查询结果如下:
3.3 问题剖析
查询结果中,多出了inner_hits
,并且结结果中 _source 已经是筛选之后的结果了,也就是说,以下这个查询逻辑:
"match": {
"order_goods.goods_name": "iPhone"
}
是对 nested 中嵌套的 order_goods 生效的,而非上层查询生效,致辞即实现了第一小节中描述的场景的查询。
4、总结
4.1 使用场景
nested 类型适用于以下场景:
-
对象数组的独立查询:当需要对数组中的每个对象进行独立查询时,例如查询博客的评论、订单的商品等。
-
复杂数据结构的精确匹配:当需要确保数组中的对象字段之间的关联性时,例如查询某个用户的特定评论。
-
嵌套对象的聚合操作:当需要对嵌套对象进行聚合分析时,例如计算评论的平均评分。
典型场景示例:
-
博客系统中的评论(每条评论是一个嵌套对象)。
-
电商系统中的订单商品(每个商品是一个嵌套对象)。
-
用户系统中的用户地址(每个地址是一个嵌套对象)。
4.2 特点
4.2.1 优点
-
精确查询:能够对嵌套对象中的字段进行精确匹配,避免默认扁平化导致的字段关联丢失问题。
-
独立索引:每个嵌套对象被独立索引,支持复杂的查询和聚合操作。
-
灵活性:支持嵌套对象的多层嵌套,适合处理复杂的文档结构。
4.2.2 缺点
- 性能开销:
- 索引开销:每个嵌套对象都会被独立存储和索引,增加了索引的大小和写入的开销。
- 查询开销:nested 查询比普通查询更复杂,可能导致查询性能下降,尤其是在嵌套对象数量较多时。
- 复杂性:nested 查询和聚合的语法相对复杂,增加了开发和维护的难度。
- 存储成本:由于嵌套对象被独立存储,可能会增加存储空间的占用。
4.3 建议
-
仅在必要时使用:如果对象数组中的字段不需要独立查询或聚合,可以使用默认的扁平化处理,避免不必要的性能开销。
-
控制嵌套层级:尽量避免多层嵌套,因为嵌套层级越深,查询和索引的开销越大。
-
优化查询性能:
-
使用 nested 查询时,尽量限制查询范围(例如通过 path 和 score_mode 参数优化)。
-
对嵌套字段使用合适的索引设置(例如 doc_values 和 index 配置)。
-
-
数据建模:
-
在设计数据模型时,评估是否可以将嵌套对象拆分为独立的文档,通过父子关系(join 类型)或外部关联来替代嵌套结构。
-
如果嵌套对象数量较少且查询需求简单,可以考虑使用 flattened 类型替代 nested 类型。
-
4.4 避坑
-
避免过度嵌套:多层嵌套会导致查询性能急剧下降,建议将嵌套层级控制在 2 层以内。
-
注意查询性能:
-
避免在大规模嵌套对象上执行复杂的 nested 查询。
-
使用 inner_hits 时,注意返回的嵌套对象数量,避免返回过多数据。
-
-
索引设计:
-
在索引设计阶段评估是否需要 nested 类型,避免后期重构。
-
对嵌套字段的字段类型和索引设置进行优化,例如关闭不必要的字段索引。
-
-
聚合性能:
-
在嵌套对象上进行聚合时,注意聚合的深度和范围,避免性能问题。
-
使用 reverse_nested 聚合时,确保聚合路径正确。
-