Remove duplicates from array without $unwind - mongodb

Using query aggregation I want to create a new array by a filter of another array, so that the filtered result will be done by a specific field of the preliminary array.
In this case, I want to filter by the field "fieldName".
I will always want to filter out the last occur
example:
I have one document:
{
"fullyQualifiedName" : "MongoDB",
"items" : [
{
"fieldName" : "_id",
"fieldCount" : 7,
"confidence_level" : 1,
"fieldClassifications" : [
"LineageGuid"
],
},
{
"fieldName" : "_id",
"fieldCount" : 7,
"fieldClassifications" : [
{
"classificationName" : "LineageGuid",
}
]
},
{
"fieldName" : "details",
"fieldCount" : 7,
},
{
"fieldName" : "state",
"fieldCount" : 7,
}
]
}
I want to create a new array like:
"items" : [
{
"fieldName" : "_id",
"fieldCount" : 7,
"confidence_level" : 1,
"fieldClassifications" : [
"LineageGuid"
],
},
{
"fieldName" : "details",
"fieldCount" : 7,
},
{
"fieldName" : "state",
"fieldCount" : 7,
}
]
The simple solution is to $unwind and $group again but I can't do it because of performance issue.
I am using MongoDB 3.4

You can use below aggregation
db.collection.aggregate([
{ "$addFields": {
"items": {
"$map": {
"input": {
"$setUnion": [
{ "$map": {
"input": "$items",
"in": { "$indexOfArray": ["$items.fieldName", "$$this.fieldName"] }
}}
]
},
"as": "i",
"in": {
"fieldName": { "$arrayElemAt": ["$items.fieldName", "$$i"] },
"fieldCount": { "$arrayElemAt": ["$items.fieldCount", "$$i"] },
"confidence_level": { "$arrayElemAt": ["$items.confidence_level", "$$i"] },
"fieldClassifications": { "$arrayElemAt": ["$items.fieldClassifications", "$$i"] }
}
}
}
}}
])

Related

How to query Child Reference Pattern collection with $graphLookup

I have a collection with child reference. Each document can have multiple parents.
How can I query it with $graphLookup in order to make a result prepared for a treeview?
Example:
{
"_id" : ObjectId("6143450cc0318c23d8f18424"),
"id" : "3",
"name" : "prod03",
"children" : [
{
"_id" : "6143440ac0318c23d8f1841f",
"qty" : 10
},
{
"_id" : "614344b1c0318c23d8f18422",
"qty" : 100
}
],
"totalQty" : 110
},
{
"_id" : ObjectId("614344b1c0318c23d8f18422"),
"id" : "2",
"name" : "prod02",
"children" : [ ],
"totalQty" : 100
},
{
"_id" : ObjectId("6143440ac0318c23d8f1841f"),
"id" : "1",
"name" : "prod01",
"children" : [ ],
"totalQty" : 10
}
Prod03 is formed from prod01 and prod02
The desired result would be like:
{
id: '3',
name: 'prod03',
totalQty: 110
children: [
{
id: '1',
name: 'prod01',
qty: 10
},
{
id: '2',
name: 'prod02',
qty: 100
},
],
}
The query must go multiple levels down until find no more children.
Final result would be a tree with all history of product manufacture components.
You are actually on the right track to use $graphLookup. You just need to convert children._id back to objectId from String for lookup.
db.collection.aggregate([
{
"$match": {
"id": "3"
}
},
{
"$addFields": {
"children": {
"$map": {
"input": "$children",
"as": "c",
"in": {
"_id": {
"$toObjectId": "$$c._id"
},
"qty": "$$c.qty"
}
}
}
}
},
{
"$graphLookup": {
"from": "collection",
"startWith": "$children._id",
"connectFromField": "children._id",
"connectToField": "_id",
"as": "children"
}
},
{
"$addFields": {
"children": {
"$map": {
"input": "$children",
"as": "c",
"in": {
"id": "$$c.id",
"name": "$$c.name",
"qty": "$$c.totalQty"
}
}
}
}
}
])
Here is the Mongo playground for your reference.

Mongo DB aggregation with $project and $filter: $add and $subtract return null

So I'm running a pretty big aggregation query in mongo shell (just for testing purpose)
in my last $project step, i use $filter to select a range of elements.
$filter: {
"input": "$users",
"as": "users",
"cond": {
$and: [
{
$lte: [
"$$users.ranking",
{$add: ["$myUser[0].ranking", 5]}
]
},
{
$gte: [
"$$users.ranking",
{$subtract: ["$myUser[0].ranking", 5]}
]
}
]
}
}
$subtract and $add both return null, any idea how i get it correct?
MongoVersion: 3.6.3, running in a docker container using the mongo 3.6.3 image.
Correct output should be:
"users" : [
{
"_id" : ObjectId("5ba3c2089a3a3e26a859f11b"),
"sgId" : ObjectId("5b76c1040c3aa5000559e6b3"),
"score" : 30,
"ranking" : NumberLong("0")
},
{
"_id" : ObjectId("5ba3c1d89a3a3e26a859f11a"),
"sgId" : ObjectId("5b76c1000c3aa500060e0fd2"),
"score" : 20,
"ranking" : NumberLong("1")
},
{
"_id" : ObjectId("5ba4fa3b71936b33e46569b9"),
"sgId" : ObjectId("5b76c8a3f7d606000566b652"),
"score" : 10,
"ranking" : NumberLong("2")
},
{
"_id" : ObjectId("5ba4fa4c71936b33e46569ba"),
"sgId" : ObjectId("5b76cafbf7d6060006270c90"),
"score" : 9,
"ranking" : NumberLong("3")
},
{
"_id" : ObjectId("5ba4fe6e71936b33e46569bb"),
"sgId" : ObjectId("5b7a4e69f7d606000566b65f"),
"score" : 8,
"ranking" : NumberLong("4")
},
{
"_id" : ObjectId("5ba4fe7471936b33e46569bc"),
"sgId" : ObjectId("5b7a4f47f7d6060006270cc4"),
"score" : 7,
"ranking" : NumberLong("5")
},
{
"_id" : ObjectId("5ba4fe8871936b33e46569bd"),
"sgId" : ObjectId("5b7a5265f7d606000566b67e"),
"score" : 6,
"ranking" : NumberLong("6")
}
]
Complete Query:
db.highscore.aggregate([
{
$sort: {score: -1}
},
{
$group: {
"_id": false,
"users": {
$push: {
"_id": "$_id",
"sgId": "$sgId",
"score": "$score",
}
}
}
},
{
$unwind: {
"path": "$users",
"includeArrayIndex": "ranking"
}
},
{
$group: {
"_id": false,
"users": {
$push: {
"_id": "$users._id",
"sgId": "$users.sgId",
"score": "$users.score",
"ranking": "$ranking"
}
}
}
},
{
$project: {
"users": "$users",
"myUser": {
$filter: {
"input": "$users",
"as": "user",
"cond": {
$eq: ["$$user.sgId", ObjectId("5b76c1000c3aa500060e0fd2")]
}
}
}
}
},
{
$project: {
"myUser": "$myUser",
"users" : {
$filter: {
"input": "$users",
"as": "users",
"cond": {
$and: [
{
$lte: [
"$$users.ranking",
{$add: ["$myUser[0].ranking", NumberLong("5")]}
]
},
{
$gte: [
"$$users.ranking",
{$subtract: ["$myUser[0].ranking", NumberLong("5")]}
]
}
]
}
}
}
}
},
])
Used Documents:
{
"_id" : ObjectId("5ba3c1d89a3a3e26a859f11a"),
"sgId" : ObjectId("5b76c1000c3aa500060e0fd2"),
"type" : "a",
"score" : 20,
"created" : ISODate("2018-09-20T17:50:48.024+02:00")
},
{
"_id" : ObjectId("5ba3c2089a3a3e26a859f11b"),
"sgId" : ObjectId("5b76c1040c3aa5000559e6b3"),
"type" : "a",
"score" : 30,
"created" : ISODate("2018-09-20T17:51:36.258+02:00")
},
{
"_id" : ObjectId("5ba4fa3b71936b33e46569b9"),
"sgId" : ObjectId("5b76c8a3f7d606000566b652"),
"type" : "a",
"score" : 10,
"created" : ISODate("2018-09-20T17:50:48.024+02:00")
},
{
"_id" : ObjectId("5ba4fa4c71936b33e46569ba"),
"sgId" : ObjectId("5b76cafbf7d6060006270c90"),
"type" : "a",
"score" : 9,
"created" : ISODate("2018-09-20T17:50:48.024+02:00")
}
Found it,
i just needed to add an $unwind before the last $project to convert the myUser Array into an object - then i was able to reach it for the add.
So full pipeline to get rankings of a highscore list and a range with your given user as source.
db.highscore.aggregate([
{
$sort: {score: -1}
},
{
$group: {
"_id": false,
"users": {
$push: {
"_id": "$_id",
"sgId": "$sgId",
"score": "$score",
}
}
}
},
{
$unwind: {
"path": "$users",
"includeArrayIndex": "ranking"
}
},
{
$group: {
"_id": false,
"users": {
$push: {
"_id": "$users._id",
"sgId": "$users.sgId",
"score": "$users.score",
"ranking": "$ranking"
}
}
}
},
{
$project: {
"users": "$users",
"myUser": {
$filter: {
"input": "$users",
"as": "user",
"cond": {
$eq: ["$$user.sgId", ObjectId("5b76c1000c3aa500060e0fd2")]
}
}
}
}
},
{
$unwind: {
path: '$myUser'
}
},
{
$project: {
"myUser": "$myUser",
"users" : {
$filter: {
"input": "$users",
"as": "users",
"cond": {
$and: [
{
$lte: [
"$$users.ranking",
{$add: ["$myUser.ranking", NumberLong("2")]}
]
},
{
$gte: [
"$$users.ranking",
{$subtract: ["$myUser.ranking", NumberLong("2")]}
]
}
]
}
}
}
}
},
], {'allowDiskUse': true})

Match Items In Multi-Level Embedded Arrays

I use the following collection which represents sports > categories > tournaments.
{
"_id" : ObjectId("597846358bbbc4440895f2e8"),
"Name" : [
{ "k" : "en-US", "v" : "Soccer" },
{ "k" : "fr-FR", "v" : "Football" }
],
"Categories" : [
{
"Name" : [
{ "k" : "en-US", "v" : "France" },
{ "k" : "fr-FR", "v" : "France" }
],
"Tournaments" : [
{
"Name" : [
{ "k" : "en-US", "v" : "Ligue 1" },
{ "k" : "fr-FR", "v" : "Ligue 1" }
],
},
{
"Name" : [
{ "k" : "en-US", "v" : "Ligue 2" },
{ "k" : "fr-FR", "v" : "Ligue 2" }
],
}
]
},
{
"Name" : [
{ "k" : "en-US", "v" : "England" },
{ "k" : "fr-FR", "v" : "Angleterre" }
],
"Tournaments" : [
{
"Name" : [
{ "k" : "en-US", "v" : "Premier League" },
{ "k" : "fr-FR", "v" : "Premier League" }
],
},
{
"Name" : [
{ "k" : "en-US", "v" : "Championship" },
{ "k" : "fr-FR", "v" : "Championnat" }
],
}
]
},
]
}
I want to query the collection using the category’s name and the tournament’s name. I’ve successfully use “$elemMatch” with the following code:
db.getCollection('Sport').find({
Categories: {
$elemMatch: {
Name: {
$elemMatch: { v: "France" }
},
Tournaments: {
$elemMatch: {
Name: {
$elemMatch: { v: "Ligue 1" }
}
}
}
}
} },
{ "Categories.$": 1, Name: 1 })
However, I cannot receive only the matching tournament in the category object.
Using the answer in this question: MongoDB Projection of Nested Arrays, I’ve built an aggregation:
db.getCollection('Sport').aggregate([{
"$match": {
"Categories": {
"$elemMatch": {
"Name": {
"$elemMatch": {
"v": "France"
}
},
"Tournaments": {
"$elemMatch": {
"Name": {
"$elemMatch": {
"v": "Ligue 1"
}
}
}
}
}
}
}
}, {
"$addFields": {
"Categories": {
"$filter": {
"input": {
"$map": {
"input": "$Categories",
"as": "category",
"in": {
"Tournaments": {
"$filter": {
"input": "$$category.Tournaments",
"as": "tournament",
"cond": {
// stuck here
}
}
}
}
}
},
"as": "category",
"cond": {
// stuck here
}
}
}
}
}
])
I tried to use a condition but MongoDB doesn’t recognize (Use of undefined variable:) $$KEEP and $$PRUNE ($redact) when I use $anyElementTrue then $map on the “Name” property.
My question: how can I check that the collection of names contains my string?
I'm more surprised that on the answer you reference I did not not "strongly recommend you do not nest arrays" like this. Nesting in this way is impossible to update atomically until the next release of MongoDB, and they are notoriously difficult to query.
For this particular case you would do:
db.getCollection('Sport').aggregate([
{ "$match": {
"Categories": {
"$elemMatch": {
"Name.v": "France",
"Tournaments.Name.v": "Ligue 1"
}
}
}},
{ "$addFields": {
"Categories": {
"$filter": {
"input": {
"$map": {
"input": "$Categories",
"as": "c",
"in": {
"Name": {
"$filter": {
"input": "$$c.Name",
"as": "n",
"cond": { "$eq": [ "$$n.v", "France" ] }
}
},
"Tournaments": {
"$filter": {
"input": {
"$map": {
"input": "$$c.Tournaments",
"as": "t",
"in": {
"Name": {
"$filter": {
"input": "$$t.Name",
"as": "n",
"cond": {
"$eq": [ "$$n.v", "Ligue 1" ]
}
}
}
}
}
},
"as": "t",
"cond": {
"$ne": [{ "$size": "$$t.Name" }, 0]
}
}
}
}
}
},
"as": "c",
"cond": {
"$and": [
{ "$ne": [{ "$size": "$$c.Name" },0] },
{ "$ne": [{ "$size": "$$c.Tournaments" },0] }
]
}
}
}
}}
])
Which returns the result:
/* 1 */
{
"_id" : ObjectId("597846358bbbc4440895f2e8"),
"Name" : [
{
"k" : "en-US",
"v" : "Soccer"
},
{
"k" : "fr-FR",
"v" : "Football"
}
],
"Categories" : [
{
"Name" : [
{
"k" : "en-US",
"v" : "France"
},
{
"k" : "fr-FR",
"v" : "France"
}
],
"Tournaments" : [
{
"Name" : [
{
"k" : "en-US",
"v" : "Ligue 1"
},
{
"k" : "fr-FR",
"v" : "Ligue 1"
}
]
}
]
}
]
}
The whole point is that each array needs a $filter, and at the outer levels you are looking for $size not being 0 as a result of "inner" $filter operations on contained arrays.
Since the "inner" arrays can change in content as a result, the "outer" arrays need a $map in order to return the "changed" elements.
So in terms of the structure "Categories" needs a $map because it has inner elements. And the "inner" "Tournaments" needs a $map for the same reason. Every array all the way to the final properties need $filter, and each wrapping array with a $map has a $filter with a $size condition.
That's the general logic pattern, and it works by repeating that pattern for each nested level. As stated though, it's pretty horrible. Which is why you really should avoid "nesting" like this at all costs. The increased complexity just about always outweighs any perceived gains.
I should also note you went a little overboard with $elemMatch, You really only need it at the "Categories" array level since that's the only thing that has multiple conditions to be met for it's element.
The sub-elements can use plain "Dot Notation" since they are only "singular" conditions within their respective arrays. So that does cut down on the terse syntax somewhat and still matches exactly the same documents.

MongoDB join data inside an array of objects

I have document like this in a collection called diagnoses :
{
"_id" : ObjectId("582d43d18ec3f432f3260682"),
"patientid" : ObjectId("582aacff3894c3afd7ad4677"),
"doctorid" : ObjectId("582a80c93894c3afd7ad4675"),
"medicalcondition" : "high fever, cough, runny nose.",
"diagnosis" : "Viral Flu",
"addmissiondate" : "2016-01-12",
"dischargedate" : "2016-01-16",
"bhtno" : "125",
"prescription" : [
{
"drug" : ObjectId("58345e0e996d340bd8126149"),
"instructions" : "Take 2 daily, after meals."
},
{
"drug" : ObjectId("5836bc0b291918eb42966320"),
"instructions" : "Take 1 daily, after meals."
}
]
}
The drug id inside the prescription object array is from a separate collection called drugs, see sample document below :
{
"_id" : ObjectId("58345e0e996d340bd8126149"),
"genericname" : "Paracetamol Tab 500mg",
"type" : "X",
"isbrand" : false
}
I am trying to create a mongodb query using the native node.js driver to get a result like this:
{
"_id" : ObjectId("582d43d18ec3f432f3260682"),
"patientid" : ObjectId("582aacff3894c3afd7ad4677"),
"doctorid" : ObjectId("582a80c93894c3afd7ad4675"),
"medicalcondition" : "high fever, cough, runny nose.",
"diagnosis" : "Viral Flu",
"addmissiondate" : "2016-01-12",
"dischargedate" : "2016-01-16",
"bhtno" : "125",
"prescription" : [
{
"drug" :
{
"_id" : ObjectId("58345e0e996d340bd8126149"),
"genericname" : "Paracetamol Tab 500mg",
"type" : "X",
"isbrand" : false
},
"instructions" : "Take 2 daily, after meals."
},
...
]
}
Any advice on how to approach a similar result like this is much appreciated, thanks.
Using MongoDB 3.4.4 and newer
With the aggregation framework, the $lookup operators supports arrays
db.diagnoses.aggregate([
{ "$addFields": {
"prescription": { "$ifNull" : [ "$prescription", [ ] ] }
} },
{ "$lookup": {
"from": "drugs",
"localField": "prescription.drug",
"foreignField": "_id",
"as": "drugs"
} },
{ "$addFields": {
"prescription": {
"$map": {
"input": "$prescription",
"in": {
"$mergeObjects": [
"$$this",
{ "drug": {
"$arrayElemAt": [
"$drugs",
{
"$indexOfArray": [
"$drugs._id",
"$$this.drug"
]
}
]
} }
]
}
}
}
} },
{ "$project": { "drugs": 0 } }
])
For older MongoDB versions:
You can create a pipeline that first flattens the prescription array using the $unwind operator and a $lookup subsequent pipeline step to do a "left outer join" on the "drugs" collection. Apply another $unwind operation on the created array from the "joined" field. $group the previously flattened documents from the first pipeline where there $unwind operator outputs a document for each element in the prescription array.
Assembling the above pipeline, run the following aggregate operation:
db.diagnoses.aggregate([
{
"$project": {
"patientid": 1,
"doctorid": 1,
"medicalcondition": 1,
"diagnosis": 1,
"addmissiondate": 1,
"dischargedate": 1,
"bhtno": 1,
"prescription": { "$ifNull" : [ "$prescription", [ ] ] }
}
},
{
"$unwind": {
"path": "$prescription",
"preserveNullAndEmptyArrays": true
}
},
{
"$lookup": {
"from": "drugs",
"localField": "prescription.drug",
"foreignField": "_id",
"as": "prescription.drug"
}
},
{ "$unwind": "$prescription.drug" },
{
"$group": {
"_id": "$_id",
"patientid" : { "$first": "$patientid" },
"doctorid" : { "$first": "$doctorid" },
"medicalcondition" : { "$first": "$medicalcondition" },
"diagnosis" : { "$first": "$diagnosis" },
"addmissiondate" : { "$first": "$addmissiondate" },
"dischargedate" : { "$first": "$dischargedate" },
"bhtno" : { "$first": "$bhtno" },
"prescription" : { "$push": "$prescription" }
}
}
])
Sample Output
{
"_id" : ObjectId("582d43d18ec3f432f3260682"),
"patientid" : ObjectId("582aacff3894c3afd7ad4677"),
"doctorid" : ObjectId("582a80c93894c3afd7ad4675"),
"medicalcondition" : "high fever, cough, runny nose.",
"diagnosis" : "Viral Flu",
"addmissiondate" : "2016-01-12",
"dischargedate" : "2016-01-16",
"bhtno" : "125",
"prescription" : [
{
"drug" : {
"_id" : ObjectId("58345e0e996d340bd8126149"),
"genericname" : "Paracetamol Tab 500mg",
"type" : "X",
"isbrand" : false
},
"instructions" : "Take 2 daily, after meals."
},
{
"drug" : {
"_id" : ObjectId("5836bc0b291918eb42966320"),
"genericname" : "Paracetamol Tab 100mg",
"type" : "Y",
"isbrand" : false
},
"instructions" : "Take 1 daily, after meals."
}
]
}
In MongoDB 3.6 or later versions
It seems that
$lookup will overwrite the original array instead of merging it.
A working solution (a workaround, if you prefer) is to create a different field,
and then merge two fields, as shown below:
db.diagnoses.aggregate([
{ "$lookup": {
"from": "drugs",
"localField": "prescription.drug",
"foreignField": "_id",
"as": "prescription_drug_info"
} },
{ "$addFields": {
"merged_drug_info": {
"$map": {
"input": "$prescription",
"in": {
"$mergeObjects": [
"$$this",
{ "$arrayElemAt": [
"$prescription_drug_info._id",
"$$this._id"
] }
]
}
}
}
} }
])
This would add two more fields and the name of the desired field
will be merged_drug_info. We can then add $project stage to filter
out excessive fields and $set stage to rename the field:
...
{ "$set": { "prescription": "$merged_drug_info" } },
{ "$project": { "prescription_drug_info": 0, "merged_drug_info": 0 } }
...

Nested filters: $filter array, then $filter child array

Essentially I'm trying to filter OUT subdocuments and sub-subdocuments that have been "trashed". Here's a stripped-down version of my schema:
permitSchema = {
_id,
name,
...
feeClassifications: [
new Schema({
_id,
_trashed,
name,
fees: [
new Schema({
_id,
_trashed,
name,
amount
})
]
})
],
...
}
So I'm able to get the effect I want with feeClassifications. But I'm struggling to find a way to have the same effect for feeClassifications.fees as well.
So, this works as desired:
Permit.aggregate([
{ $match: { _id: mongoose.Types.ObjectId(req.params.id) }},
{ $project: {
_id: 1,
_name: 1,
feeClassifications: {
$filter: {
input: '$feeClassifications',
as: 'item',
cond: { $not: {$gt: ['$$item._trashed', null] } }
}
}
}}
])
But I also want to filter the nested array fees. I've tried a few things including:
Permit.aggregate([
{ $match: { _id: mongoose.Types.ObjectId(req.params.id) }},
{ $project: {
_id: 1,
_name: 1,
feeClassifications: {
$filter: {
input: '$feeClassifications',
as: 'item',
cond: { $not: {$gt: ['$$item._trashed', null] } }
},
fees: {
$filter: {
input: '$fees',
as: 'fee',
cond: { $not: {$gt: ['$$fee._trashed', null] } }
}
}
}
}}
])
Which seems to follow the mongodb docs the closest. But I get the error:
this object is already an operator expression, and can't be used as a document expression (at 'fees')
Update: -----------
As requested, here's a sample document:
{
"_id" : ObjectId("57803fcd982971e403e3e879"),
"_updated" : ISODate("2016-07-11T19:24:27.204Z"),
"_created" : ISODate("2016-07-09T00:05:33.274Z"),
"name" : "Single Event",
"feeClassifications" : [
{
"_updated" : ISODate("2016-07-11T19:05:52.418Z"),
"_created" : ISODate("2016-07-11T17:49:12.247Z"),
"name" : "Event Type 1",
"_id" : ObjectId("5783dc18e09be99840fad29f"),
"fees" : [
{
"_updated" : ISODate("2016-07-11T18:51:10.259Z"),
"_created" : ISODate("2016-07-11T18:41:16.110Z"),
"name" : "Basic Fee",
"amount" : 156.5,
"_id" : ObjectId("5783e84cc46a883349bb2339")
},
{
"_updated" : ISODate("2016-07-11T19:05:52.419Z"),
"_created" : ISODate("2016-07-11T19:05:47.340Z"),
"name" : "Secondary Fee",
"amount" : 50,
"_id" : ObjectId("5783ee0bad7bf8774f6f9b5f"),
"_trashed" : ISODate("2016-07-11T19:05:52.410Z")
}
]
},
{
"_updated" : ISODate("2016-07-11T18:22:21.567Z"),
"_created" : ISODate("2016-07-11T18:22:21.567Z"),
"name" : "Event Type 2",
"_id" : ObjectId("5783e3dd540078de45bbbfaf"),
"_trashed" : ISODate("2016-07-11T19:24:27.203Z")
}
]
}
And here's the desired output ("trashed" subdocuments are excluded from BOTH feeClassifications AND fees):
{
"_id" : ObjectId("57803fcd982971e403e3e879"),
"_updated" : ISODate("2016-07-11T19:24:27.204Z"),
"_created" : ISODate("2016-07-09T00:05:33.274Z"),
"name" : "Single Event",
"feeClassifications" : [
{
"_updated" : ISODate("2016-07-11T19:05:52.418Z"),
"_created" : ISODate("2016-07-11T17:49:12.247Z"),
"name" : "Event Type 1",
"_id" : ObjectId("5783dc18e09be99840fad29f"),
"fees" : [
{
"_updated" : ISODate("2016-07-11T18:51:10.259Z"),
"_created" : ISODate("2016-07-11T18:41:16.110Z"),
"name" : "Basic Fee",
"amount" : 156.5,
"_id" : ObjectId("5783e84cc46a883349bb2339")
}
]
}
]
}
Since we want to filter both the outer and inner array fields, we can use the $map variable operator which return an array with the "values" we want.
In the $map expression, we provide a logical $conditional $filter to remove the non matching documents from both the document and subdocument array field.
The conditions are $lt which return true when the field "_trashed" is absent in the sub-document and or in the sub-document array field.
Note that in the $cond expression we also return false for the <false case>. Of course we need to apply filter to the $map result to remove all false.
Permit.aggregate(
[
{ "$match": { "_id": mongoose.Types.ObjectId(req.params.id) } },
{ "$project": {
"_updated": 1,
"_created": 1,
"name": 1,
"feeClassifications": {
"$filter": {
"input": {
"$map": {
"input": "$feeClassifications",
"as": "fclass",
"in": {
"$cond": [
{ "$lt": [ "$$fclass._trashed", 0 ] },
{
"_updated": "$$fclass._updated",
"_created": "$$fclass._created",
"name": "$$fclass.name",
"_id": "$$fclass._id",
"fees": {
"$filter": {
"input": "$$fclass.fees",
"as": "fees",
"cond": { "$lt": [ "$$fees._trashed", 0 ] }
}
}
},
false
]
}
}
},
"as": "cls",
"cond": "$$cls"
}
}
}}
]
)
In the upcoming MongoDB release (as of this writing and since MongoDB 3.3.5), You can replace the $cond expression in the the $map expression with a $switch expression:
Permit.aggregate(
[
{ "$match": { "_id": mongoose.Types.ObjectId(req.params.id) } },
{ "$project": {
"_updated": 1,
"_created": 1,
"name": 1,
"feeClassifications": {
"$filter": {
"input": {
"$map": {
"input": "$feeClassifications",
"as": "fclass",
"in": {
"$switch": {
"branches": [
{
"case": { "$lt": [ "$$fclass._trashed", 0 ] },
"then": {
"_updated": "$$fclass._updated",
"_created": "$$fclass._created",
"name": "$$fclass.name",
"_id": "$$fclass._id",
"fees": {
"$filter": {
"input": "$$fclass.fees",
"as": "fees",
"cond": { "$lt": [ "$$fees._trashed", 0 ] }
}
}
}
}
],
"default": false
}
}
}
},
"as": "cls",
"cond": "$$cls"
}
}
}}
]
)
For more complicated bigdats, it would be unnecessarily difficult.
Just edit it in $filter input by adding a dotted annotation field.You can search the document to any depth of JSON by dotted annotation without further complicated $filter mapping.
"$filter":{
"input": "$feeClassifications._trashed",
"as": "trashed",
"cond": { "$lt": [ "$$trashed._trashed", 0 ] }
}