第三阶段 25 · nested 嵌套对象与查询(数组对象最大的坑)

📅 2026/8/2 9:46:38 👁️ 阅读次数 📝 编程学习
第三阶段 25 · nested 嵌套对象与查询(数组对象最大的坑)

25 · nested 嵌套对象与查询(数组对象最大的坑)

阶段:第三阶段 / 查询 + 聚合
ES:nested类型 +nestedquery / agg | PostgreSQL:一对多子表 JOIN / JSONB 数组


1. 概念:为什么普通对象数组会“串味”

ES 默认把对象数组扁平化存储。假设一个订单里有多个明细:

{"order_no":"SO-1","items":[{"sku":"A","qty":10},{"sku":"B","qty":99}]}

默认(object类型)ES 内部会拆成:items.sku = [A, B]items.qty = [10, 99]——
字段间的对应关系丢了。于是你查「sku=A 且 qty=99」时,会错误命中这条文档
(因为 A 在 sku 列里、99 在 qty 列里,跨对象串味了)。

解决办法:把items声明为nested。nested 会把数组里每个对象作为独立的隐藏子文档
索引,字段对应关系得以保留——概念上就像一张一对多子表。


2. PostgreSQL 对照

-- nested 约等于一对多子表 JOINSELECTo.*FROMorders oJOINorder_items iONi.order_no=o.order_noWHEREi.sku='A'ANDi.qty=99;-- 条件作用在“同一行明细”上

关键点:nestedquery 的语义就是「同一个子对象内同时满足多个条件」,
对应 SQL 里条件落在 JOIN 后的同一行


3. ES DSL

3.1 定义 nested 字段(mapping)

PUT orders_idx { "mappings": { "properties": { "order_no": { "type": "keyword" }, "items": { "type": "nested", "properties": { "sku": { "type": "keyword" }, "qty": { "type": "integer" } } } } } }

3.2 nested 查询(同一子对象内满足多条件)

GET orders_idx/_search { "query": { "nested": { "path": "items", "query": { "bool": { "filter": [ { "term": { "items.sku": "A" } }, { "term": { "items.qty": 99 } } ] } }, "inner_hits": {} // 可选:回传到底是哪个子对象命中的 } } }

没有nested包裹时,上面这组条件会串味误命中;包了nested就只命中「同一条明细里 sku=A 且 qty=99」。

3.3 nested 聚合(先进子文档再聚合)

GET orders_idx/_search { "size": 0, "aggs": { "items_agg": { "nested": { "path": "items" }, "aggs": { "by_sku": { "terms": { "field": "items.sku" }, "aggs": { "total_qty": { "sum": { "field": "items.qty" } } } } } } } }

4. Spring Boot 实现

@ComponentpublicclassDoc25NestedQuery{@AutowiredprivateElasticsearchClientelasticsearchClient;/** 同一条明细里 sku=? 且 qty=?,并回传命中的子对象 */publicList<Map<String,Object>>byItem(StringindexName,Stringsku,intqty)throwsIOException{SearchResponse<Map>resp=elasticsearchClient.search(s->s.index(indexName).query(q->q.nested(n->n.path("items").query(nq->nq.bool(b->b.filter(f->f.term(t->t.field("items.sku").value(sku))).filter(f->f.term(t->t.field("items.qty").value(qty))))).innerHits(ih->ih))),// inner_hits:拿到具体命中的子对象Map.class);returnresp.hits().hits().stream().map(Hit::source).filter(Objects::nonNull).collect(Collectors.toList());}}

nestedpath必须与 mapping 里的 nested 字段名一致;子字段用「path.子字段」引用。
inner_hits拿命中的子对象:hit.innerHits().get("items").hits().hits()


5. 坑与最佳实践

  1. 必须先在 mapping 里声明nested:类型一旦建好不能从object原地改成nested,需 reindex(第 33 篇)。
  2. 查询/聚合都要用nested包裹:直接term("items.sku")不会串味报错,但语义错(跨对象命中)。
  3. nested 有成本:每个子对象是独立 Lucene 文档,数量大时占空间、变慢;用index.mapping.nested_objects.limit兜底。
  4. 数组内对象少而稳定才适合 nested;关系复杂、需独立更新子项,考虑 join(parent-child,代价更高)。
  5. inner_hits才能知道“哪条子对象命中”,做高亮/回显时很有用。

需要「独立文档的父子关系 / 跨文档关联 / 什么时候该反范式」,见第 51 篇《join 父子关系与关系建模》。


下一篇

26-highlight-高亮.md


← 上一篇:24-filter-bucket聚合 | 总览 | 下一篇:26-highlight-高亮 →