MongoDB nested query using aggregate function - mongodb

I have a collection "superpack", which has the nested objects. The sample document looks like below.
{
"_id" : ObjectId("56038c8cca689261baca93eb"),
"name": "Test sub",
"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
},
{
"id": "55fbc7f6b0ce97a309b3ceaa",
"name": "2 extra",
"amount": 12
}
]
},
{
"id": "55fbc7f6b0ce97a309b3ceac",
"name": "Classic 2 month",
"expiryVal": 1,
"amount": 30,
"topUps": [
{
"id": "55fbc7f6b0ce97a309b3cea8",
"name": "3 extra",
"amount": 16
}
]
}
]
}
]
}
I need to query for the nested object topups with the id field and result should have only the selected topup object and its associated parent. I am expecting the output to like below, when i query it on topup id 55fbc7f6b0ce97a309b3cea9.
{
"_id" : ObjectId("56038c8cca689261baca93eb"),
"name": "Test sub",
"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
}
]
}
]
}
]
}
I tried with the below aggregate query for the same. However its not returning any result. Can you please help me, what is wrong in the query?
db.superpack.aggregate( [{ $match: { "id": "55fbc7f6b0ce97a309b3cea9" } }, { $redact: {$cond: { if: { $eq: [ "$id", "55fbc7f6b0ce97a309b3cea9" ] }, "then": "$$KEEP", else: "$$PRUNE" }}} ])

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
}
]
}
]
}
]
}

Related

MongoDB pull first matching nested array item

I have the following documents...
{ "_id": 2, "name": "Jane Doe", "phones": [ { "type": "Mobile", "digits": [ { "val": 1 }, { "val": 2 } ] }, { "type": "Mobile", "digits": [ { "val": 3 }, { "val": 4 } ] }, { "type": "Land", "digits": [ { "val": 5 }, { "val": 6 } ] } ] }
{ "_id": 1, "name": "John Doe", "phones": [ { "type": "Land", "digits": [ { "val": 1 }, { "val": 2 } ] }, { "type": "Mobile", "digits": [ { "val": 0 }, { "val": 3 }, { "val": 4 } ] }, { "type": "Mobile", "digits": [ { "val": 3 }, { "val": 4 }, { "val": 9 } ] } ] }
...and the following MongoDB query...
db.getCollection("persons").updateOne({"name": "John Doe"},
{
"$pull":
{
"phones.$[condition1].digits":
{
"val: { $in: [ 3, 4 ] }
}
}
},
{
arrayFilters:
[
{ "condition1.type": "Mobile" }
]
})
My problem is that the query removes the last two elements of the array: "phones" of the second document (John Doe) and I want to remove only the first one (and not the last one that have a "9" among the digits). How I can delete only the first matching nested array item?
Query
pipeline update
reduce on phones, starting with {"phones": [], "found": false}
if [3,4] subset of digits.val and not found => ignore it
else keep it (concat arrays to add the member)
$getField to get the phones from the reduced {"phones" : [...]}
*$pull removes all elements that satisfy the condition, maybe there is a way with update operators and not pipeline update, but this works if you dont find more compact way
*alternative to reduce, could be 2 filters, one to keep the values that dont contain the [3,4] and one to keep those that contain, from those that contained, and then concat those arrays removing only one of those that contain the [3,4]
Playmongo
update(
{"name": {"$eq": "John Doe"}},
[{"$set":
{"phones":
{"$getField":
{"field": "phones",
"input":
{"$reduce":
{"input": "$phones",
"initialValue": {"phones": [], "found": false},
"in":
{"$cond":
[{"$and":
[{"$not": ["$$value.found"]},
{"$setIsSubset": [[3, 4], "$$this.digits.val"]}]},
{"phones": "$$value.phones", "found": true},
{"phones": {"$concatArrays": ["$$value.phones", ["$$this"]]},
"found": "$$value.found"}]}}}}}}}])
I have no real sense of motivation for this update, so I am unsure about the details of the logic. I think I have taken the OP's words and partial demonstration literally and I've implemented an update pipeline to fix the stated problem. Given the number of possibilities, this may not be what you are looking for. My pipeline is very similar to the #Takis answer, but the logic is slightly different and therefore the output is different. I look forward to the OP's comments/questions to identify/clarify any discrepancies and/or ambiguities.
db.collection.update({
"name": "John Doe"
},
[
{
"$set": {
"phones": {
"$getField": {
"field": "phones",
"input": {
"$reduce": {
"input": "$phones",
"initialValue": { "phones": [], "pullDone": false },
"in": {
"$cond": [
{
"$and": [
{ "$eq": [ "$$this.type", "Mobile" ] },
{ "$not": "$$value.pullDone" }
]
},
{
"pullDone": true,
"phones": {
"$concatArrays": [
"$$value.phones",
[
{
"$mergeObjects": [
"$$this",
{
"digits": {
"$filter": {
"input": "$$this.digits",
"as": "digit",
"cond": {
"$not": [ { "$in": [ "$$digit.val", [ 3, 4 ] ] } ]
}
}
}
}
]
}
]
]
}
},
{
"pullDone": "$$value.pullDone",
"phones": {
"$concatArrays": [ "$$value.phones", [ "$$this" ] ]
}
}
]
}
}
}
}
}
}
}
])
Try it on mongoplayground.net.

Is there a way in mongodb to group at multiple levels

I have a document which contains an array of array as given below.
This is the first document.
{
"_id": "5d932a2178fdfc4dc41d75da",
"data": [
{
"nestedData": [
{
"_id": "5d932a2178fdfc4dc41d75e1",
"name": "Special 1"
},
{
"_id": "5d932a2178fdfc4dc41d75e0",
"name": "Special 2"
}
]
}
]
}
I need to lookup(join) to another collection with the _id in the nestedData array in the aggregation framework.
The 2nd document from which I need to lookup is
{
"_id": "5d8b1ac3b15bc72d154408e1",
"status": "COMPLETED",
"rating": 4
}
I know I need to $unwind it twice to convert nestedData array into object.
But how do I group back again to form the same object like given below
{
"_id": "5d932a2178fdfc4dc41d75da",
"data": [
{
"array": [
{
"_id": "5d932a2178fdfc4dc41d75e1",
"name": "Special 1",
"data": {
"_id": "5d8b1ac3b15bc72d154408e1",
"status": "COMPLETED",
"rating": 4
},
{
"_id": "5d932a2178fdfc4dc41d75e0",
"name": "Special 2",
"data": {
"_id": "5d8b1ac3b15bc72d154408e0",
"status": "COMPLETED",
"rating": 4
},
}
]
}
]
}
Try this query
db.testers.aggregate([
{$lookup: {
from: 'demo2',
pipeline: [
{ $sort: {'_id': 1}},
],
as: 'pointValue',
}},
{
$addFields:{
"data":{
$map:{
"input":"$data",
"as":"doc",
"in":{
$mergeObjects:[
"$$doc",
{
"nestedData":{
$map:{
"input":"$$doc.nestedData",
"as":"nestedData",
"in":{
$mergeObjects:[
{ $arrayElemAt: [ {
"$map": {
"input": {
"$filter": {
"input": "$pointValue",
"as": "sn",
"cond": {
"$and": [
{ "$eq": [ "$$sn._id", "$$nestedData._id" ] },
]
}
}
},"as": "data",
"in": {
"name": "$$nestedData.name",
"data":"$$data",
}}
}, 0 ] },'$$nestedData'
],
}
}
}
}
]
}
}
}
}
},
{$project: { pointValue: 0 } }
]).pretty()

MongoDB multiple counts, single document, arrays

I have been searching on stackoverflow and cannot find exactly what I am looking for and hope someone can help. I want to submit a single query, get multiple counts back, for a single document, based on array of that document.
My data:
db.myCollection.InsertOne({
"_id": "1",
"age": 30,
"items": [
{
"id": "1",
"isSuccessful": true,
"name": null
},{
"id": "2",
"isSuccessful": true,
"name": null
},{
"id": "3",
"isSuccessful": true,
"name": "Bob"
},{
"id": "4",
"isSuccessful": null,
"name": "Todd"
}
]
});
db.myCollection.InsertOne({
"_id": "2",
"age": 22,
"items": [
{
"id": "6",
"isSuccessful": true,
"name": "Jeff"
}
]
});
What I need back is the document and the counts associated to the items array for said document. In this example where the document _id = "1":
{
"_id": "1",
"age": 30,
{
"totalIsSuccessful" : 2,
"totalNotIsSuccessful": 1,
"totalSuccessfulNull": 1,
"totalNameNull": 2
}
}
I have found that I can get this in 4 queries using something like this below, but I would really like it to be one query.
db.test1.aggregate([
{ $match : { _id : "1" } },
{ "$project": {
"total": {
"$size": {
"$filter": {
"input": "$items",
"cond": { "$eq": [ "$$this.isSuccessful", true ] }
}
}
}
}}
])
Thanks in advance.
I am assuming your expected result is invalid since you have an object literal in the middle of another object and also you have totalIsSuccessful for id:1 as 2 where it seems they should be 3. With that said ...
you can get similar output via $unwind and then grouping with $sum and $cond:
db.collection.aggregate([
{ $match: { _id: "1" } },
{ $unwind: "$items" },
{ $group: {
_id: "_id",
age: { $first: "$age" },
totalIsSuccessful: { $sum: { $cond: [{ "$eq": [ "$items.isSuccessful", true ] }, 1, 0 ] } },
totalNotIsSuccessful: { $sum: { $cond: [{ "$ne": [ "$items.isSuccessful", true ] }, 1, 0 ] } },
totalSuccessfulNull: { $sum: { $cond: [{ "$eq": [ "$items.isSuccessful", null ] }, 1, 0 ] } },
totalNameNull: { $sum: { $cond: [ { "$eq": [ "$items.name", null ]}, 1, 0] } } }
}
])
The output would be this:
[
{
"_id": "_id",
"age": 30,
"totalIsSuccessful": 3,
"totalNameNull": 2,
"totalNotIsSuccessful": 1,
"totalSuccessfulNull": 1
}
]
You can see it working here

MongoDB: Get all $matched elements individually from an array

I'm trying to get all matched elements individually, here is the sample data and the query.
// json
[
{
"name": "Mr Cool",
"ican": [
{
"subcategory": [
{
"id": "5bffdba824488b182ec86f8d", "name": "Cricket"
},
{
"id": "5bffdba824488b182ec86f8c", "name": "Footbal"
}
],
"category": "5bffdba824488b182ec86f88",
"name": "Sports"
}
]
}
]
// query
db.collection.aggregate([
{
"$match": {
"ican.subcategory.name": { $in: ["Cricket","Football"] }
}
},
{
"$project": { "_id": 1, "name": 1, }
}
])
I'm getting the combined result, I need the individual match record. I tried $all and $elementMatch but getting the same response. how can I get the results as below. I'm using $aggregate because I will be using $geoNear pipeline for getting the nearby users.
// current result
[
{
"_id": ObjectId("5a934e000102030405000000"),
"name": "Mr Cool"
}
]
// expected result
[
{
"_id": ObjectId("5a934e000102030405000000"),
"name": "Mr Cool",
"subcategory: "Cricket"
},
{
"_id": ObjectId("5a934e000102030405000000"),
"name": "Mr Cool",
"subcategory: "Footbal"
}
]
Thank you
Try this Mongo Playground
db.col.aggregate([
{"$unwind" : "$ican"},
{"$unwind" : "$ican.subcategory"},
{"$match" : {"ican.subcategory.name": { "$in": ["Cricket","Football"] }}},
{"$group" : {"_id" : null,"data" : {"$push" : {"_id" : "$_id","name" : "$name","subcategory" : "$ican.subcategory.name"}}}},
{"$unwind" : "$data"},
{"$replaceRoot" : {"newRoot" : "$data"}}
])
You can use below aggregation without the $unwind and for better performance
db.collection.aggregate([
{ "$match": { "ican.subcategory.name": { "$in": ["Cricket","Football"] }}},
{ "$project": {
"ican": {
"$reduce": {
"input": "$ican",
"initialValue": [],
"in": {
"$concatArrays": [
{ "$filter": {
"input": {
"$map": {
"input": "$$this.subcategory",
"as": "s",
"in": { "name": "$name", "subcategory": "$$s.name" }
}
},
"as": "fil",
"cond": { "$in": ["$$fil.subcategory", ["Football"]] }
}},
"$$value"
]
}
}
}
}},
{ "$unwind": "$ican" },
{ "$replaceRoot": { "newRoot": "$ican" }}
])

Mongo Shell - Order by array item + Filter by Culture

I have the offer collection in MongoDb.
[
{
"Name": "item01",
"Descriptions": [
{
"Name": "(es) Item01 Name",
"Culture": "es"
},
{
"Name": "(en) Item01 Name",
"Culture": "en"
},
{
"Name": "(de) Item01 Name",
"Culture": "de"
}
]
},
{
"Name": "item02",
"Descriptions": [
{
"Name": "(en) Item02 Name",
"Culture": "en"
},
{
"Name": "(de) Item03 Name",
"Culture": "de"
}
]
}
]
I need to sort the list of items by description.
Must be ordered by the user culture. If there is no such culture must use English by default.
I'm trying to solve this problem using mongo aggregation. But can't find how.
db.Offer.aggregate(
[
{$unwind:'$Descriptions'},
{$group: {
'_id': '$_id',
'Culture': '$Culture',
'ElementNameComp': {$first: {$cond:[
{$eq:['$Descriptions.Culture', 'es']},
'$Descriptions.Name',
{$cond:[
{$eq:['$Descriptions.Culture', 'en']},
'$Descriptions.Name',
'no exists EN'
]}
]} }
}}
]
)
Some ideas?
UPDATE
expected result when culture is (ES). I have also modified the data to cover more examples.
[
{
"Name": "item01",
"Descriptions": "(es) Item01 Name"
},
{
"Name": "item02",
"Descriptions": "(en) Item02 Name"
}
]
You need something to assign a "score" value to the possible matches in order to determine which is best, as the basic process.
An approach that can "filter" the array content before you $unwind would be:
var locale = "es";
var result = db.Offer.aggregate([
{ "$project": {
"Name": 1,
"Descriptions": {
"$setDifference": [
{ "$map": {
"input": "$Descriptions",
"as": "el",
"in": {
"$cond": [
{ "$eq": [ "$$el.Culture", locale ] },
{ "name": "$$el.Name", "score": { "$literal": 2 } },
{ "$cond": [
{ "$eq": [ "$$el.Culture", "en" ] },
{ "name": "$$el.Name", "score": { "$literal": 1 } },
false
]}
]
}
}},
[false]
]
}
}},
{ "$unwind": "$Descriptions" },
{ "$sort": { "Descriptions.score": -1 }},
{ "$group": {
"_id": "$_id",
"Name": { "$first": "$Name" },
"Description": { "$first": "$Descriptions.name" }
}}
]);
This strips out some of the structure of "Descriptions" but it basically seems to be what you want. The reasoning is to take only the value for the selected locale or otherwise fall back to the "default" English locale if no other items match.
The $cond operator assigns a "score" to the matched values, then you sort them accordingly so that the "highest" score is returned.
Then you sort and group back to your array.
You can do the same sort of thing prior to MongoDB 2.6 which gives the additional operators:
var locale = "es";
var result = db.Offer.aggregate([
{ "$unwind": "$Descriptions" },
{ "$project": {
"Name": 1,
"Descriptions": {
"$cond": [
{ "$eq": [ "$Descriptions.Culture", locale ] },
{ "name": "$Descriptions.Name", "score": { "$const": 2 } },
{ "$cond": [
{ "$eq": [ "$Descriptions.Culture", "en" ] },
{ "name": "$Descriptions.Name", "score": { "$const": 1 } },
false
]}
]
}
}},
{ "$match": { "Descriptions": { "$ne": false } }},
{ "$sort": { "Descriptions.score": -1 } },
{ "$group": {
"_id": "$_id",
"Name": { "$first": "$Name" },
"Description": { "$first": "$Descriptions.name" }
}}
]);
In either case your result should be this when the locale is matched:
{
"_id" : ObjectId("539f91f831d29097dc43e8ae"),
"Name" : "item02",
"Description" : "(es) Item02 Name"
},
{
"_id" : ObjectId("539f91f831d29097dc43e8ad"),
"Name" : "item01",
"Description" : "(es) Item01 Name"
}
Or when setting a locale that does not exist
{
"_id" : ObjectId("539f91f831d29097dc43e8ae"),
"Name" : "item02",
"Description" : "(en) Item02 Name"
},
{
"_id" : ObjectId("539f91f831d29097dc43e8ad"),
"Name" : "item01",
"Description" : "(en) Item01 Name"
}
Any results that did not even have a default "en" locale would be omitted.