Mongo DB: How to aggregate on self Collection to pull Value(s) associated to the IDs in Hierarchical Dataset - nosql

I'm trying to write an aggregation in Mongo which would result in something similar to SQL.
I'm now trying to achieve the same in Mongo with the above collection.
Please Suggest me how to build Mongo Aggregation in order to achieve my output.

Unwind both module_details and module_child then match them.
[
{
"$unwind": "$module.module_details.data"
},
{
"$unwind": "$module.module_child.data"
},
{
"$match": {
"$expr": {
"$eq": [
"$module.module_details.data.module_child_id",
"$module.module_child.data.module_child_id"
]
}
}
},
{
"$project": {
"_id": 0,
"module_id:": "$module.module_details.data.module_id",
"name": "$module.module_child.data.name",
"value": "$module.module_details.data.value"
}
}
]
You probably need to match on module_id as well. However, it was not a part of the question.
[
{
"$match": {
"module_id": "9898"
}
},
{
"$unwind": "$module.module_details.data"
},
{
"$unwind": "$module.module_child.data"
},
{
"$match": {
"$expr": {
"$eq": [
"$module.module_details.data.module_child_id",
"$module.module_child.data.module_child_id"
]
}
}
},
{
"$project": {
"_id": 0,
"module_id:": "$module.module_details.data.module_id",
"name": "$module.module_child.data.name",
"value": "$module.module_details.data.value"
}
}
]

Related

Can I get the count of subdocuments that match a filter?

I have the following document
[
{
"_id": "624713340a3d2901f2f5a9c0",
"username": "fotis",
"exercises": [
{
"_id": "624713530a3d2901f2f5a9c3",
"description": "Sitting",
"duration": 60,
"date": "2022-03-24T00:00:00.000Z"
},
{
"_id": "6247136a0a3d2901f2f5a9c6",
"description": "Coding",
"duration": 999,
"date": "2022-03-31T00:00:00.000Z"
},
{
"_id": "624713a00a3d2901f2f5a9ca",
"description": "Sitting",
"duration": 999,
"date": "2022-03-30T00:00:00.000Z"
}
],
"__v": 3
}
]
And I am trying to get the count of exercises returned with the following aggregation (I know it is way easier to do it in my code, but I am trying to understand how to use mongodb queries)
db.collection.aggregate([
{
"$match": {
"_id": "624713340a3d2901f2f5a9c0"
}
},
{
"$project": {
"username": 1,
"exercises": {
"$slice": [
{
"$filter": {
"input": "$exercises",
"as": "exercise",
"cond": {
"$eq": [
"$$exercise.description",
"Sitting"
]
}
}
},
1
]
},
"count": {
"$size": "exercises"
}
}
}
])
When I try to access the exercises field using "$size": "exercises", I get an error query failed: (Location17124) Failed to optimize pipeline :: caused by :: The argument to $size must be an array, but was of type: string.
But when I access the subdocument exercises using "$size": "$exercises" I get the count of all the subdocuments contained in the document.
Note: I know that in this example I use $slice and I set the limit to 1, but in my code it is a variable.
You are actually on the right track. You don't really need the $slice. You can just use $reduce to perform the filtering. The reason that your count is not working is that the filtering and the $size are in the same stage. In such case, it will take the pre-filtered array to do the count. You can resolve this by adding a $addFields stage.
db.collection.aggregate([
{
"$match": {
"_id": "624713340a3d2901f2f5a9c0"
}
},
{
"$project": {
"username": 1,
"exercises": {
"$filter": {
"input": "$exercises",
"as": "exercise",
"cond": {
"$eq": [
"$$exercise.description",
"Sitting"
]
}
}
}
}
},
{
"$addFields": {
"count": {
$size: "$exercises"
}
}
}
])
Here is the Mongo playground for your reference.

Mongodb lookup for not equal fields

I want to join two collections and find the documents where has one equal field and one unequal field!
This is what I was tried, But not work
db.collectionOne.aggregate[
{
"$match": {
"$and": [
{ "$text": { "$search": "this is my query" } },
{ "b": { "$eq": "60e849054d2f0d409041b6a2" } }
]
}
},
{ "$addFields": { "pID": { "$toString": "$_id" }, "score": { "$meta": "textScore" } } },
{
"$lookup": {
"from": "collectionsTwo",
"as": "collectionsTwoName",
"pipeline": [{
"$match": {
"$expr": {
"$and": [{
"$ne": ["$fieldOne", "60dd0f98d10f072e2a225502"] // This one is unqual field
}, { "$eq": ["$pID", "$fieldTwo"] }] // This one is equal field
}
}
}]
}
},
{ "$sort": { "score": -1 } },
{ "$limit": 1 }
])
Fields in the source document, i.e. $pID are not available inside the lookup pipeline.
In order to reference those values, you would need to define a variable using let, such as:
{
"$lookup": {
"from": "collectionsTwo",
"as": "collectionsTwoName",
"let": { "srcpID":"$pID" },
"pipeline": [{
"$match": {
"$expr": {
"$and": [{
"$ne": ["$fieldOne", "60dd0f98d10f072e2a225502"] // This one is unqual field
}, { "$eq": ["$$srcpID", "$fieldTwo"] }] // This one is equal field
}
}
}]
}
},
See https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/#join-conditions-and-uncorrelated-sub-queries

Get documents with nested objects matching count condition

I am a mongo noob and am working with a mongo collection with records that look like so:
{
"cats" [
{
"name": "fluffy",
"color": "red",
},
{
"name": "snowball",
"color": "white",
},
]
{
I would like to perform a query that gets all records that have more than 1 white cats. MapReduce looks promising, but seems like overkill. Any help is appreciated.
You can use the aggregation framework to do this. You don't need to use the $where operator.
db.collection.aggregate([
{ "$match": { "cats.color": "white" }},
{ "$project": {
"nwhite": { "$map": {
"input": "$cats",
"as": "c",
"in": { "$cond": [
{ "$eq": [ "$$c.color", "white" ] },
1,
0
]}
}},
"cats": 1
}},
{ "$unwind": "$nwhite" },
{ "$group": {
"_id": "$_id",
"cats": { "$first": "$cats" },
"nwhite": { "$sum": "$nwhite" }
}},
{ "$match": { "nwhite": { "$gte" :2 } } }
])
Use $where. It is an especially powerful operator as it allows you to execute arbitrary javascript.
For your specific case, try this:
db.collection.find({$where: function() {
return this.cats.filter(function(cat){
// Filter only white cats
return cat.color === 'white';
}).length >= 2;
}});

MongoDB distinct values on subdocuments

I have a little weird database structure it is as follows:
I have a document with normal properties, then I have a metadata property which is an array of objects.
metadata: {[
{
key: [key],
value: [value]
},
...
]}
Edit: There will never be a metadata sub-document which has a duplicate key
It was done this way to retain the order of the metadata objects
Now I want to get distinct values of a metadata object with a given key.
I want to find every distinct [value] where [key] = "x" using MongoDB. And have the distinct values returned in an array (not the document)
I guess this is not possible using the distinct command, but is this possible using an aggregation pipeline or do I have to use Map-Reduce?
Any suggestions?
Thanks in advance! :)
I presume you mean this:
{
"metadata": [
{ "key": "abc", "value": "borf" },
{ "key": "cdc", "value": "biff" }
]
},
{
"metadata": [
{ "key": "bbc", "value": "barf" },
{ "key": "abc", "value": "borf" },
{ "key": "abc", "value": "barf" }
]
}
Where if you filter for "abc" and get the distinct "value" entries like this:
db.collection.aggregate([
{ "$match": { "metadata.key": "abc" } },
{ "$unwind": "$metadata" },
{ "$match": { "metadata.key": "abc" } },
{ "$group": {
"_id": "$metadata.value"
}}
])
Or even better:
db.collection.aggregate([
{ "$match": { "metadata.key": "abc" } },
{ "$redact": {
"$cond": {
"if": { "$eq": [ { "$ifNull": [ "$key", "abc" ] }, "abc" ] },
"then": "$$DESCEND",
"else": "$$PRUNE"
}
}},
{ "$unwind": "$metadata" },
{ "$group": {
"_id": "$metadata.value",
"count": { "$sum": 1 }
}}
])
Which would basically give:
{ "_id": "barf", "count": 1 },
{ "_id": "borf", "count": 2 }
But it is not possible for this to just be an array of "barf" and "borf". The distinct() method does an array of keys only, but it is also very limited. Therefore it can only do this:
db.collection.distinct("metadata.value",{ "metadata.key": "abc" })
[ "biff", "borf", "barf" ]
Which is incorrect as a result. So just take the "document" results from above and apply some "post processing":
db.collection.aggregate([
{ "$match": { "metadata.key": "abc" } },
{ "$redact": {
"$cond": {
"if": { "$eq": [ { "$ifNull": [ "$key", "abc" ] }, "abc" ] },
"then": "$$DESCEND",
"else": "$$PRUNE"
}
}},
{ "$unwind": "$metadata" },
{ "$group": {
"_id": "$metadata.value"
}}
]).map(function(doc) {
return doc._id;
})
And that result is a plain array of just the distinct values:
[ "borf", "barf" ]

How to find match in documents in Mongo and Mongo aggregation?

I have following json structure in mongo collection-
{
"students":[
{
"name":"ABC",
"fee":1233
},
{
"name":"PQR",
"fee":345
}
],
"studentDept":[
{
"name":"ABC",
"dept":"A"
},
{
"name":"XYZ",
"dept":"X"
}
]
},
{
"students":[
{
"name":"XYZ",
"fee":133
},
{
"name":"LMN",
"fee":56
}
],
"studentDept":[
{
"name":"XYZ",
"dept":"X"
},
{
"name":"LMN",
"dept":"Y"
},
{
"name":"ABC",
"dept":"P"
}
]
}
Now I want to calculate following output.
if students.name = studentDept.name
so my result should be as below
{
"name":"ABC",
"fee":1233,
"dept":"A",
},
{
"name":"XYZ",
"fee":133,
"dept":"X"
}
{
"name":"LMN",
"fee":56,
"dept":"Y"
}
Do I need to use mongo aggregation or is it possible to get above given output without using aggregation???
What you are really asking here is how to make MongoDB return something that is actually quite different from the form in which you store it in your collection. The standard query operations do allow a "limitted" form of "projection", but even as the title on the page shared in that link suggests, this is really only about "limiting" the fields to display in results based on what is present in your document already.
So any form of "alteration" requires some form of aggregation, which with both the aggregate and mapReduce operations allow to "re-shape" the document results into a form that is different from the input. Perhaps also the main thing people miss with the aggregation framework in particular, is that it is not just all about "aggregating", and in fact the "re-shaping" concept is core to it's implementation.
So in order to get results how you want, you can take an approach like this, which should be suitable for most cases:
db.collection.aggregate([
{ "$unwind": "$students" },
{ "$unwind": "$studentDept" },
{ "$group": {
"_id": "$students.name",
"tfee": { "$first": "$students.fee" },
"tdept": {
"$min": {
"$cond": [
{ "$eq": [
"$students.name",
"$studentDept.name"
]},
"$studentDept.dept",
false
]
}
}
}},
{ "$match": { "tdept": { "$ne": false } } },
{ "$sort": { "_id": 1 } },
{ "$project": {
"_id": 0,
"name": "$_id",
"fee": "$tfee",
"dept": "$tdept"
}}
])
Or alternately just "filter out" the cases where the two "name" fields do not match and then just project the content with the fields you want, if crossing content between documents is not important to you:
db.collection.aggregate([
{ "$unwind": "$students" },
{ "$unwind": "$studentDept" },
{ "$project": {
"_id": 0,
"name": "$students.name",
"fee": "$students.fee",
"dept": "$studentDept.dept",
"same": { "$eq": [ "$students.name", "$studentDept.name" ] }
}},
{ "$match": { "same": true } },
{ "$project": {
"name": 1,
"fee": 1,
"dept": 1
}}
])
From MongoDB 2.6 and upwards you can even do the same thing "inline" to the document between the two arrays. You still want to reshape that array content in your final output though, but possible done a little faster:
db.collection.aggregate([
// Compares entries in each array within the document
{ "$project": {
"students": {
"$map": {
"input": "$students",
"as": "stu",
"in": {
"$setDifference": [
{ "$map": {
"input": "$studentDept",
"as": "dept",
"in": {
"$cond": [
{ "$eq": [ "$$stu.name", "$$dept.name" ] },
{
"name": "$$stu.name",
"fee": "$$stu.fee",
"dept": "$$dept.dept"
},
false
]
}
}},
[false]
]
}
}
}
}},
// Students is now an array of arrays. So unwind it twice
{ "$unwind": "$students" },
{ "$unwind": "$students" },
// Rename the fields and exclude
{ "$project": {
"_id": 0,
"name": "$students.name",
"fee": "$students.fee",
"dept": "$students.dept"
}},
])
So where you want to essentially "alter" the structure of the output then you need to use one of the aggregation tools to do. And you can, even if you are not really aggregating anything.