先把结论写在前面

nested 不能用 term 组合代替,核心原因不在查询层,而在索引层:

  • term 只负责「查什么值」
  • nested 决定「这些值是否属于同一个对象」

普通 object 类型会把对象数组扁平化存储,数组元素之间的字段关联会丢失。在这种数据上,无论你怎么组合 term,ES 都无法保证「Alice 和 Smith 来自同一条 user 记录」——它只会看到两个独立的多值字段里各自出现了目标词项。

只有 nested 类型把数组元素存成独立的隐藏 Lucene 子文档,查询时才能在正确的对象边界内做匹配。


这篇文章要解决什么

很多人第一次遇到 ES 对象数组查询时,会自然想到:

我要查 first = Alice 且 last = Smith,写两个 term 放在 bool.must 里不就行了?为什么还要 nested?

在简单场景下,这样写有时能跑、有时还会命中。问题就在于「有时命中」的那部分——命中了,但语义是错的。


根本原因:底层存储方式完全不同

nested 和普通 object(默认类型)的差异,不在查询语法,而在 Lucene 怎么存这份数据。

普通 object:扁平化存储

Lucene 没有「内部对象」的概念。ES 遇到对象数组时,会把它拍扁成多值字段。

假设文档如下:

{
  "user": [
    { "first": "Alice", "last": "White" },
    { "first": "John",  "last": "Smith" }
  ]
}

在 ES 内部大致会变成:

"user.first": ["Alice", "John"],
"user.last":  ["White", "Smith"]

关键后果:first 和 last 之间的归属关系丢失了。ES 不再知道「Alice」和「White」原本属于同一个对象。

nested 类型:独立子文档存储

当你把 user 声明为 nested 类型时,ES 会为数组中的每一个对象创建一个独立的隐藏 Lucene 文档:

父文档: { group: "fans" }
子文档 1: { user.first: "Alice", user.last: "White" }
子文档 2: { user.first: "John",  user.last: "Smith" }

每个子对象内部的字段关联被完整保留。查询时,nested 查询会进入这些子文档,在单个子文档的范围内执行条件判断。

映射示例:

{
  "mappings": {
    "properties": {
      "user": {
        "type": "nested",
        "properties": {
          "first": { "type": "keyword" },
          "last":  { "type": "keyword" }
        }
      }
    }
  }
}

为什么 term 组合无法代替?

term 查询只是在倒排索引里精确查找某个词项。它不改变底层存储结构,因此无法修复扁平化带来的语义丢失。

致命缺陷:跨对象错误匹配

目标:查「first 是 Alice 且 last 是 Smith」的人。

用 term 组合:

{
  "bool": {
    "must": [
      { "term": { "user.first": "Alice" } },
      { "term": { "user.last":  "Smith" } }
    ]
  }
}

在扁平化的 object 数据上,这个查询会错误命中上面的示例文档:

条件是否满足实际来源
user.first 含 Alice✅子对象 1
user.last 含 Smith✅子对象 2

ES 只检查到两个字段的数组里分别出现了目标值,并不知道它们来自不同的对象。业务上你要的是「同一个人同时叫 Alice Smith」,但 ES 返回的是「文档里碰巧有 Alice 也有 Smith,只是不在同一条 user 里」。

这就是典型的跨对象错误匹配(cross-object matching)。

nested 查询为什么有效

nested 查询告诉 ES:把条件限定在每个独立的子文档内部执行。

{
  "query": {
    "nested": {
      "path": "user",
      "query": {
        "bool": {
          "must": [
            { "term": { "user.first": "Alice" } },
            { "term": { "user.last":  "Smith" } }
          ]
        }
      }
    }
  }
}

同样的 term 组合,在 nested 路径下只会在同一个子文档里同时满足 first=Alice 且 last=Smith 时才命中。对上面的示例文档,不会误匹配——因为没有任何一个子文档同时满足这两个条件。


一张图串起来

扁平 object                         nested
─────────────────                   ─────────────────
user.first: [Alice, John]           子文档 A: Alice + White
user.last:  [White, Smith]          子文档 B: John  + Smith
        │                                    │
        ▼                                    ▼
term 组合:数组里都有就行              nested 查询:同一子文档内都要满足
→ Alice + Smith 误命中 ❌            → Alice + Smith 不命中 ✅

什么时候必须用 nested?

满足以下特征时,应考虑 nested 而不是普通 object + term 组合:

  1. 字段是对象数组(array of objects)
  2. 查询需要数组内多个字段的联合条件(AND / 范围组合等)
  3. 这些字段的归属关系对业务语义很重要

常见例子:订单里的商品明细、用户的多个地址、嵌套的权限列表、SKU 属性组合查询等。

如果只是对数组内单个字段做过滤(例如「是否存在 first=Alice 的 user」),普通 object + term 通常够用——因为你没有要求「同一条记录里的多个字段同时满足」。


一句话总结

term 只是「查什么值」,nested 决定「值之间是否有关联」。

在扁平化数据上,term 组合会丢失对象边界,产生错误匹配;只有 nested 的独立文档存储,才能让 term 在正确的范围内工作。


延伸阅读