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
相关产品推荐
相关产品推荐

