You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

MongoDB聚合匹配多层嵌套对象数组的实现方案

问题背景

存储的MongoDB文档为两层嵌套对象数组结构:文档的parts字段为对象数组,数组内每个对象包含part字段,以及嵌套的progressions对象数组;progressions数组内的对象包含progression、key、_id字段,省略无关字段后的2条示例文档结构如下:

{
  title: 'abc',
  parts: [
    {
      part: "verse",
      progressions: [
        {
          progression: "62a4a87da7fdbdabf787e47f",
          key: "Ab",
          _id: "62b5aaa0c9e9fe8a7d7240d3"
        },
        {
          progression: "62adf477ed11cbbe156d5769",
          key: "C",
          _id: "62b5aaa0c9e9fe8a7d7240d3"
        },
      ],
      _id: "62b5aaa0c9e9fe8a7d7240d2"
    },
    {
      part: "chorus",
      progressions: [
        {
          progression: "62a4a51b4693c43dce9be09c",
          key: "E",
          _id: "62b5aaa0c9e9fe8a7d7240d9"
        }
      ],
      _id: "62b5aaa0c9e9fe8a7d7240d8"
    }
  ],
}

{
  title: 'def',
  parts: [
    {
      part: "verse",
      progressions: [
        {
          progression: "33a4a87da7fopvvbf787erwe",
          key: "E",
          _id: "62b5aaa0c9e9fe8a7d7240d3"
        },
        {
          progression: "98opf477ewfscbbe156d5442",
          key: "Bb",
          _id: "62b5aaa0c9e9fe8a7d7240d3"
        },
      ],
      _id: "12r3aaa0c4r5me8a7d72oi8u"
    },
    {
      part: "bridge",
      progressions: [
        {
          progression: "62a4a51b4693c43dce9be09c",
          key: "C#",
          _id: "62b5aaa0c9e9fe8a7d7240d9"
        }
      ],
      _id: "62b5aaa0rwfvse8a7d7240d8"
    }
  ],
}

客户端请求传入的过滤参数为对象数组,格式如下:

[
  { part: 'verse', progressions: ['62a4a87da7fdbdabf787e47f', '62a4a51b4693c43dce9be09c'] },
  { part: 'chorus', progressions: ['62adf477ed11cbbe156d5769'] }
]

需要实现的匹配规则:文档只要匹配输入过滤数组中任意一个对象的规则即可返回,单条规则逻辑为:文档parts数组中存在元素的part字段值等于过滤对象的part值,且该元素下的progressions数组中存在对象的progression字段值属于过滤对象指定的progressions ID列表。以上述示例数据为例,title为abc的第一条文档符合匹配要求。

原有基于Mongoose aggregate编写的管道代码执行失败,已知问题为$elemMatch操作符仅可在$match阶段使用,且原有实现逻辑冗余,错误代码如下:

// parsedProgressions = [
//   { part: 'verse', progressions: ['62a4a87da7fdbdabf787e47f', '62a4a51b4693c43dce9be09c'] },
//   { part: 'chorus', progressions: ['62adf477ed11cbbe156d5769'] }
// ]
songs.aggregate([
  {
    $addFields: {
      "tempMapResults": {
        $map: {
          input: parsedProgressions,
          as: "parsedProgression",
          in: {
            $cond: {
              if: { parts: { $elemMatch: { part: "$$parsedProgression.part", "progressions.progression": mongoose.Types.ObjectId("$$parsedProgression.progression") } } },
              then: true, else: false
            }
          }
        }
      }
    }
  },
  {
    $addFields: {
      "isMatched": { $anyElementTrue: ["$tempMapResults"] }
    }
  },
  { $match: { isMatched: true } },
  { $project: { title: 1, "parts.part": 1, "parts.progressions.progression": 1 } }
]);
解决方案

直接在$match阶段通过$expr表达式完成所有匹配逻辑,无需生成临时中间字段,执行效率更高,可直接运行的代码如下:

// 入参预处理:如果progression字段存储为ObjectId类型,先把字符串ID转为对应类型,避免类型不匹配
const parsedFilters = parsedProgressions.map(filter => ({
  part: filter.part,
  progressions: filter.progressions.map(id => new mongoose.Types.ObjectId(id))
}))

songs.aggregate([
  {
    $match: {
      $expr: {
        // 任意一个过滤规则匹配即返回文档
        $anyElementTrue: {
          $map: {
            input: parsedFilters,
            as: "filter",
            in: {
              // 遍历parts数组,判断是否存在匹配当前规则的part元素
              $anyElementTrue: {
                $map: {
                  input: "$parts",
                  as: "partItem",
                  in: {
                    $and: [
                      // part字段值匹配
                      { $eq: ["$$partItem.part", "$$filter.part"] },
                      // 嵌套progressions数组中存在ID命中过滤列表的元素
                      {
                        $anyElementTrue: {
                          $map: {
                            input: "$$partItem.progressions",
                            as: "progItem",
                            in: { $in: ["$$progItem.progression", "$$filter.progressions"] }
                          }
                        }
                      }
                    ]
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  // 按需返回指定字段
  {
    $project: {
      title: 1,
      "parts.part": 1,
      "parts.progressions.progression": 1
    }
  }
])

注意:如果业务中progression字段存储为字符串类型,省略入参预处理的ID转换步骤,直接传入字符串ID即可。

方案逻辑说明:

  • 完全规避了在非$match阶段使用$elemMatch的错误写法,所有匹配逻辑前置到查询第一阶段,可利用索引优化查询性能
  • 三层$anyElementTrue+$map的组合完全贴合匹配规则:外层遍历所有传入的过滤规则,中层遍历文档的parts数组,内层遍历每个part下的progressions数组,逐层校验匹配条件
  • 针对示例数据运行,会正确返回title: 'abc'的文档,过滤掉title: 'def'的文档(该文档verse下的progression不在过滤列表、bridge的part名称不匹配过滤规则)

内容的提问来源于stack exchange,提问作者Omer Setty

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 11:48:26