Unfortunately $redact is not a viable option here based on the fact that with the recursive $$DESCEND it is basically looking for a field called "id" at all levels of the document. You cannot possibly ask to do this only at a specific level of embedding as it's all or nothing.
This means you need alternate methods of filtering the content rather than $redact. All "id" values are unique so their is no problem filtering via "set" operations.
So the most efficient way to do this is via the following:
db.docs.aggregate([
{ "$match": {
"packs.billingPts.topUps.id": "55fbc7f6b0ce97a309b3cea9"
}},
{ "$project": {
"packs": {
"$setDifference": [
{ "$map": {
"input": "$packs",
"as": "pack",
"in": {
"$let": {
"vars": {
"billingPts": {
"$setDifference": [
{ "$map": {
"input": "$$pack.billingPts",
"as": "billing",
"in": {
"$let": {
"vars": {
"topUps": {
"$setDifference": [
{ "$map": {
"input": "$$billing.topUps",
"as": "topUp",
"in": {
"$cond": [
{ "$eq": [ "$$topUp.id", "55fbc7f6b0ce97a309b3cea9" ] },
"$$topUp",
false
]
}
}},
[false]
]
}
},
"in": {
"$cond": [
{ "$ne": [{ "$size": "$$topUps"}, 0] },
{
"id": "$$billing.id",
"name": "$$billing.name",
"expiryVal": "$$billing.expiryVal",
"amount": "$$billing.amount",
"topUps": "$$topUps"
},
false
]
}
}
}
}},
[false]
]
}
},
"in": {
"$cond": [
{ "$ne": [{ "$size": "$$billingPts"}, 0 ] },
{
"id": "$$pack.id",
"name": "$$pack.name",
"packDispVal": "$$pack.packDispVal",
"billingPts": "$$billingPts"
},
false
]
}
}
}
}},
[false]
]
}
}}
])
Where after digging down to the innermost array that is being filtered, that then the size of each resulting array going outwards is tested to see if it is zero, and omitted from results where it is.
It's a long listing but it is the most efficient way since each array is filtered down first and within each document.
A not so efficient way is to pull apart with $unwind and the $group back the results:
db.docs.aggregate([
{ "$match": {
"packs.billingPts.topUps.id": "55fbc7f6b0ce97a309b3cea9"
}},
{ "$unwind": "$packs" },
{ "$unwind": "$packs.billingPts" },
{ "$unwind": "$packs.billingPts.topUps"},
{ "$match": {
"packs.billingPts.topUps.id": "55fbc7f6b0ce97a309b3cea9"
}},
{ "$group": {
"_id": {
"_id": "$_id",
"packs": {
"id": "$packs.id",
"name": "$packs.name",
"packDispVal": "$packs.packDispVal",
"billingPts": {
"id": "$packs.billingPts.id",
"name": "$packs.billingPts.name",
"expiryVal": "$packs.billingPts.expiryVal",
"amount": "$packs.billingPts.amount"
}
}
},
"topUps": { "$push": "$packs.billingPts.topUps" }
}},
{ "$group": {
"_id": {
"_id": "$_id._id",
"packs": {
"id": "$_id.packs.id",
"name": "$_id.packs.name",
"packDispVal": "$_id.packs.packDispVal"
}
},
"billingPts": {
"$push": {
"id": "$_id.packs.billingPts.id",
"name": "$_id.packs.billingPts.name",
"expiryVal": "$_id.packs.billingPts.expiryVal",
"amount": "$_id.packs.billingPts.amount",
"topUps": "$topUps"
}
}
}},
{ "$group": {
"_id": "$_id._id",
"packs": {
"$push": {
"id": "$_id.packs.id",
"name": "$_id.packs.name",
"packDispVal": "$_id.packs.packDispVal",
"billingPts": "$billingPts"
}
}
}}
])
The listing looks a lot more simple but of course there is a lot of overhead introduced by $unwind here. The process of grouping back is basically keeping a copy of everything outside of the current array level being reconstructed, and then push that content back into the array in the next stage, until you get back to the root _id.
Please note that unless you intend such a search to match more than one document or if you are going to have significant gains from reduced network traffic by effectively reducing down the response size from a very large document, then it would be advised to do neither of these but follow much of the same design as the first pipeline example but in client code.
Whilst the first example would be still okay performance wise, it's still a mouthful to send to the server and as a general listing, that is typically written with the same operations in a cleaner way in client code to process and filter the resulting structure.
{
"_id" : ObjectId("56038c8cca689261baca93eb"),
"packs" : [
{
"id" : "55fbc7f6b0ce97a309b3cead",
"name" : "Classic",
"packDispVal" : "PACK",
"billingPts" : [
{
"id" : "55fbc7f6b0ce97a309b3ceab",
"name" : "Classic 1 month",
"expiryVal" : 1,
"amount" : 20,
"topUps" : [
{
"id" : "55fbc7f6b0ce97a309b3cea9",
"name" : "1 extra",
"amount" : 8
}
]
}
]
}
]
}