为什么需要 nested
普通 object 数组在索引时会被扁平化:元素内部的层级关系被抹掉,只留下字段名与值的列表。于是 user.first=John 与 user.last=Smith 可能来自数组里两个不同元素,跨字段条件会串位命中。声明为 nested 后,每个元素成为独立的 Lucene 文档,成组匹配与按元素返回才成立。
基本信息
| 适用版本 | Elasticsearch 7.x / 8.x |
|---|---|
| 映射声明 | { "type": "nested", "properties": { ... } } |
| 查询写法 | nested 查询包裹 path + query,可选 inner_hits |
| 索引代价 | 每个嵌套对象都是独立文档,体积与写入放大 |
| 能力限制 | 不能像普通字段那样直接排序与聚合 |
典型使用场景
- 用户对象数组:姓名、手机号必须来自同一元素,不能跨元素拼出假命中
- 订单明细行:按「同一行内 sku 与数量」组合筛选,而不是全表任一命中
- 商品多规格:颜色与尺码成组匹配,避免颜色来自 A 规格、尺码来自 B 规格
- 权限与标签分组:需要判断某个标签组合出现在同一元素中
- 展示命中的具体子对象:用 inner_hits 把匹配到的元素一起返回给前端
示例
示例
PUT /my-index/_mapping # 先声明 nested 类型,普通 object 无法事后改
{
"properties": {
"user": {
"type": "nested",
"properties": {
"first": { "type": "keyword" },
"last": { "type": "keyword" }
}
}
}
}
GET /my-index/_search
{
"query": {
"nested": {
"path": "user",
"query": {
"bool": {
"must": [ { "term": { "user.first": "John" } },
{ "term": { "user.last": "Smith" } } ]
}
},
"inner_hits": { "size": 2 } # 返回具体命中的子文档,方便展示与排查
}
}
}行为对照
| 写法 | 行为 | 注意点 |
|---|---|---|
| 普通 object 数组 | 字段被扁平化 | 跨元素条件会串位,产生假命中 |
| nested 类型 | 每个元素独立成文档 | 索引体积与写入开销随元素数放大 |
| nested 查询 | path + query 成组匹配 | 查询路径必须与 mapping 的 nested 路径一致 |
| inner_hits | 额外返回命中的子文档 | 大数组时返回体膨胀,需显式设 size |
| nested 排序聚合 | 需 nested 上下文 | 不能把嵌套字段当普通字段直接用 |
nested 文档是独立的 Lucene 文档,因此它的相关度与主文档分开计算,主文档的得分是这些子查询得分的合成结果;理解这一点才能解释为什么同一条查询在不同数组长度下得分不同。
字段一旦建成 nested 就不能事后改回普通字段,只能重建索引(_reindex);设计阶段就要想清楚哪些数组需要成组语义。
nested 的元素数量直接决定索引体积与查询代价:数组有几百个元素时要评估是否改用父子文档模型,或把数组拆到独立索引由应用层关联。