How to add a condition inside $lookup in MongoDb .aggregate()? - javascript

persons collection:
{
"_id": ObjectId("5f3258cfbaaccedaa5dd2c96"),
"gender": "male",
"name": {
"title": "mr",
"first": "victor",
"last": "pedersen",//... more properties
}
persondetails collection:
{
"_id": ObjectId("5f3a91e68b1c26e68f9ed3ad"),
"country": "India",
"personid": ObjectId("5f3258cfbaaccedaa5dd2c96")
}
Get documents from persons along with associated persondetails where associated country in persondetails is "India
So if out of 10 persons only 3 are from India. I should get 3 person documents in resultset along with their associated persondetails
Query:
[
{
"$match": {
"$or": [
{
"$expr": {
"$eq": [
"$gender",
"male"
]
}
}
]
}
},
{
"$facet": {
"totalCount": [
{
"$count": "value"
}
],
"data": [
{
"$project": {
"_id": "$_id",
"fname": "$name.first",
"lname": "$name.last",
"dobage": "$dob.age",
"registeredAge": "$registered.age"
}
},
{
"$sort": {
"name.first": 1
}
} ,
{
"$lookup": {
"from": "persondetails",
"localField": "_id",
"foreignField": "personid", // how to add where clause in this lookup?
"as": "persondetail"
}
}
]
}
}
]
EDIT:
https://mongoplayground.net/p/3vBs6Frt-aK
Expected Result:
[
{
"data": [
{
"_id": ObjectId("5f3258cfbaaccedaa5dd2c96"),
"fname": "victor",
"lname": "pedersen",
"persondetail": [
{
"_id": ObjectId("5f3a91e68b1c26e68f9ed3ad"),
"country": "India",
"personid": ObjectId("5f3258cfbaaccedaa5dd2c96")
}
]
}],
"totalCount": [
{
"value": 1
}
]
}
]

There is a second $lookup syntax which allows you to specify custom filtering condition:
{
"$lookup": {
"from": "persondetails",
"let": { person_id: "$_id" },
"pipeline": [
{
$match: {
$expr: {
$and: [
{ $eq: [ "$$person_id", "$personid" ] },
{ $eq: [ "$country", "India" ] },
]
}
}
}
],
"as": "persondetail"
}
}
EDIT:
You also need to add $match to filter out people with empty persondetail and if you want this to be included in your count then you need to run $facet as the last operation:
[
{
"$match": {
"$or": [ {"$expr": { "$eq": [ "$gender", "male" ] } } ]
}
},
{
"$project": {
"_id": "$_id",
"fname": "$name.first",
"lname": "$name.last",
"dobage": "$dob.age",
"registeredAge": "$registered.age"
}
},
{
"$lookup": {
"from": "persondetails",
"let": { person_id: "$_id" },
"pipeline": [
{
$match: {
$expr: {
$and: [
{ $eq: [ "$$person_id", "$personid" ] },
{ $eq: [ "$country", "India" ] },
]
}
}
}
],
"as": "persondetail"
}
},
{
$match: {
persondetail: { $ne: [] }
}
},
{
$facet: {
totalCount: [ { $count: "value" } ],
data: [ { $sort: { "name.first": 1 } }, ]
}
}
]
Mongo Playground

Related

MongoDB - Structure an array without using key field in Aggregration

I'm having an issue with making count for items returned from an array without assuming or using those fields in my aggregration.
Data structure looks like this:
[
{
"_id": "1",
"title": "Vanella Icream",
"contain": "sugar",
"details": [
{
"flavour": "Vanella"
},
{
"weight": "10KG"
},
{
"sugar": "15KG"
}
]
},
{
"_id": "2",
"title": "Pretzels",
"contain": "salt",
"details": [
{
"flavour": "Wheat"
},
{
"weight": "10KG"
},
{
"sugar": "15KG"
}
]
},
{
"_id": "3",
"title": "Rasmalai Icream",
"contain": "sugar",
"details": [
{
"flavour": "Vanella"
},
{
"weight": "15KG"
},
{
"sugar": "12KG"
}
]
},
{
"_id": "4",
"title": "Vanella Icream",
"contain": "sugar",
"details": [
{
"flavour": "Vanella"
},
{
"weight": "15KG"
},
{
"sugar": "12KG"
}
]
}
]
Output I want:
[
{
"details": {
"flavour": {
"Vanella": 3, //Number of times Vanella present in each document.
"Wheat": 1,
},
"weight": {
"10KG": 2,
"15KG": 2
},
"sugar": {
"12KG": 2,
"15KG": 2
}
}
}
]
Query:
db.collection.aggregate([
{
"$unwind": {
"path": "$details"
}
},
{
"$replaceRoot": {
"newRoot": {
"$mergeObjects": [
"$details",
"$$ROOT"
]
}
}
},
{
"$facet": {
"flavour": [
{
"$group": {
"_id": "$flavour",
"sum": {
"$sum": 1
}
}
},
{
"$addFields": {
"flavour": "$_id"
}
},
{
"$project": {
"_id": 0
}
}
],
"weight": [
{
"$group": {
"_id": "$weight",
"sum": {
"$sum": 1
}
}
},
{
"$addFields": {
"weight": "$_id"
}
},
{
"$project": {
"_id": 0
}
}
]
}
},
{
"$addFields": {
"flavour": {
"$reduce": {
"input": {
"$filter": {
"input": {
"$map": {
"input": "$flavour",
"as": "w",
"in": {
"$cond": [
{
"$ne": [
"$$w.flavour",
null
]
},
{
"$let": {
"vars": {
"o": [
[
"$$w.flavour",
"$$w.sum"
]
]
},
"in": {
"$arrayToObject": "$$o"
}
}
},
null
]
}
}
},
"as": "f",
"cond": {
"$ne": [
"$$f",
null
]
}
}
},
"initialValue": {},
"in": {
"$let": {
"vars": {
"d": "$$value",
"p": "$$this"
},
"in": {
"$mergeObjects": [
"$$d",
"$$p"
]
}
}
}
}
},
"weight": {
"$reduce": {
"input": {
"$filter": {
"input": {
"$map": {
"input": "$weight",
"as": "w",
"in": {
"$cond": [
{
"$ne": [
"$$w.weight",
null
]
},
{
"$let": {
"vars": {
"o": [
[
"$$w.weight",
"$$w.sum"
]
]
},
"in": {
"$arrayToObject": "$$o"
}
}
},
null
]
}
}
},
"as": "f",
"cond": {
"$ne": [
"$$f",
null
]
}
}
},
"initialValue": {},
"in": {
"$let": {
"vars": {
"d": "$$value",
"p": "$$this"
},
"in": {
"$mergeObjects": [
"$$d",
"$$p"
]
}
}
}
}
}
}
},
{
"$project": {
"details": "$$ROOT"
}
}
])
Here I'm trying to get the flavour and weight with their count, with manually adding those fields in $filter stage. I want to do it without assuming those keys. So, even if there is 20 items present in array details it will map those items and shows me output with their counts respectively.
I hope you guys understand.
Playground:https://mongoplayground.net/p/j1mzgWvcmvd
You need to change the schema, the thing you want to do is easy, and both those queries are so complicated and slow, even the second that is much smaller has 2 $unwind and 3 $group with 3 $arrayToObject and 8 stages total because of the schema and the schema of the answer.
Don't store data in the keys of the documents, people that are new to MongoDB do those, i was doing it also, but it makes all things harder.(i can't say like never do it but you dont need it here)
Your schema should be something like
{
"_id": "2",
"title": "Pretzels",
"contain": "salt",
"details": [
{
"type" : "flavour",
"value" : "Wheat"
},
{
"type" : "weight",
"value" : "10KG"
},
{
"type" : "sugar",
"value" : "15KG"
}
]
}
See this example
Converts your schema, to the new schema and produce the results you
want but without data in keys (the first part you wouldnt need it you would need only the bellow query if you had that schema from start)
Query with the new Schema (no data in keys)
[{"$unwind": { "path": "$details"}},
{"$replaceRoot": {"newRoot": "$details"}},
{
"$group": {
"_id": {
"type": "$type",
"value": "$value"
},
"sum": {"$sum": 1}
}
},
{
"$replaceRoot": {
"newRoot": {"$mergeObjects": ["$_id","$$ROOT"]}
}
},
{"$project": {"_id": 0}},
{
"$group": {
"_id": "$type",
"values": {
"$push": {
"value": "$value",
"sum": "$sum"
}
}
}
},
{"$addFields": {"type": "$_id"}},
{"$project": {"_id": 0}}
]
MongoDB operators are not made to support for data in keys or dynamic keys(uknown keys) (to do it you do complicated things like the above)
If you want to change your schema, either do it with update in the database,
Or take the documents to the application and do it with javascript, and re-insert.
Even if you solve this question in the next one, you will have again problems.
I'm the guy from Mongodb Forum:
Try this out https://mongoplayground.net/p/tfyfpIkHilQ

MongoDB get max date inside double nested array

Assume I have the following document:
[
{
"callId": "17dac51e-125e-499e-9064-f20bd3b1a9d8",
"caller": {
"firstName": "Test",
"lastName": "Testing",
"phoneNumber": "1231231234"
},
"inquiries": [
{
"inquiryId": "b0d14381-ce75-49aa-a66a-c36ae20b72a8",
"routeHistory": [
{
"assignedUserId": "cfa0ffe9-c77d-4eec-87d7-4430f7772e81",
"routeDate": "2020-01-01T06:00:00.000Z",
"status": "routed"
},
{
"assignedUserId": "cfa0ffe9-c77d-4eec-87d7-4430f7772e81",
"routeDate": "2020-01-03T06:00:00.000Z",
"status": "ended"
}
]
},
{
"inquiryId": "9d743be9-7613-46d7-8f9b-a04b4b899b56",
"routeHistory": [
{
"assignedUserId": "cfa0ffe9-c77d-4eec-87d7-4430f7772e81",
"routeDate": "2020-01-01T06:00:00.000Z",
"status": "routed"
},
{
"assignedUserId": "cfa0ffe9-c77d-4eec-87d7-4430f7772e81",
"routeDate": "2020-01-03T06:00:00.000Z",
"status": "ended"
}
]
}
]
}
]
I want to get results where inquiries.routeHistory.routeDate is equal to the $max routeDate value in routeHistory. I would expect my results to look like the following:
[
{
"callId": "17dac51e-125e-499e-9064-f20bd3b1a9d8",
"caller": {
"firstName": "Test",
"lastName": "Testing",
"phoneNumber": "1231231234"
},
"inquiries": [
{
"inquiryId": "b0d14381-ce75-49aa-a66a-c36ae20b72a8",
"routeHistory": [
{
"assignedUserId": "cfa0ffe9-c77d-4eec-87d7-4430f7772e81",
"routeDate": "2020-01-03T06:00:00.000Z",
"status": "ended"
}
]
},
{
"inquiryId": "9d743be9-7613-46d7-8f9b-a04b4b899b56",
"routeHistory": [
{
"assignedUserId": "cfa0ffe9-c77d-4eec-87d7-4430f7772e81",
"routeDate": "2020-01-03T06:00:00.000Z",
"status": "ended"
}
]
}
]
}
]
Is there a clean way to do this in a single aggregate, so that additional $match criteria can be applied? One caveat is that I can only use operators supported by DocumentDB: https://docs.aws.amazon.com/documentdb/latest/developerguide/mongo-apis.html
I have tried the following code, but to no avail:
{
$addFields: {
maxDate: {
$max: '$inquiries.routeHistory.routeDate',
},
},
},
{
$addFields: {
routeHistory: [
{
$arrayElemAt: [
{
$filter: {
input: '$inquiries.routeHistory',
cond: {
$eq: ['$maxDate', '$$this.routeDate'
],
},
},
},
0,
],
},
],
},
}
You have to use $map to scan outer array and to $filter to compare inner array's elements against $max date:
db.collection.aggregate([
{
$addFields: {
inquiries: {
$map: {
input: "$inquiries",
as: "inquiry",
in: {
inquiryId: "$$inquiry.inquiryId",
routeHistory: {
$filter: {
input: "$$inquiry.routeHistory",
cond: {
$eq: [ { $max: "$$inquiry.routeHistory.routeDate" }, "$$this.routeDate" ]
}
}
}
}
}
}
}
}
])
Mongo Playground
EDIT: by looking at your link I've noticed that $map is not supported, you can use below combination as a workaround:
db.collection.aggregate([
{
$unwind: "$inquiries"
},
{
$addFields: {
"inquiries.routeHistory": {
$filter: {
input: "$inquiries.routeHistory",
cond: {
$eq: [ { $max: "$inquiries.routeHistory.routeDate" }, "$$this.routeDate" ]
}
}
}
}
},
{
$group: {
_id: "$_id",
callId: { $first: "$callId" },
caller: { $first: "$caller" },
inquiries: { $push: "$inquiries" }
}
}
])
Mongo Playground (2)

How to change field from subdocument a parent field in Mongoose

I am trying to export Mongo data to XLSX which requires all the data to be in the parent map but currently I have data in this format:
[
{
"_id": "eatete",
"competition": {
"_id": "eatete"
"name": "Some competition name"
},
"members": [
{
"_id": "eatete",
"name": "Saad"
},
{
"_id": "eatete",
"name": "Saad2"
}
],
"leader": {
"name": "Saad",
"institute": {
"_id": "eatete",
"name": "Some institute name"
}
},
}
]
Ideally, the data should be:
[
{
"_id": "eatete",
"competition": "Some competition name"
"member0name": "Saad",
"member1name": "Saad2",
"leadername": "Saad",
"institute": "Some institute name"
}
]
So basically what I want is to refer the data of fields of subdocuments as if those were part of parent document, like competitions = competitions.name.
Can you please help me how can I do so using Mongoose.
Thanks
With some more aggregation trick
db.collection.aggregate([
{ "$unwind": { "path": "$members", "includeArrayIndex": "i" }},
{ "$group": {
"_id": "$_id",
"competition": { "$first": "$competition.name" },
"leadername": { "$first": "$leader.name" },
"institute": { "$first": "$leader.institute.name" },
"data": {
"$push": {
"k": { "$concat": ["members", { "$toLower": "$i" }, "name"] },
"v": "$members.name"
}
}
}},
{ "$replaceRoot": {
"newRoot": {
"$mergeObjects": ["$$ROOT", { "$arrayToObject": "$data" }]
}
}},
{ "$project": { "data": 0 }}
])
You can try below aggregation on your Model:
let resultt = await Model.aggregate([
{
$project: {
_id: 1,
competition: "$competition.name",
leadername: "$leader.name",
institute: "$leader.institute.name",
members: {
$map: {
input: { $range: [ 0, { $size: "$members" } ] },
in: {
k: { $concat: [ "member", { $toString: "$$this" }, "name" ] },
v: {
$let: {
vars: { current: { $arrayElemAt: [ "$members", "$$this" ] } },
in: "$$current.name"
}
}
}
}
}
}
},
{
$replaceRoot: {
newRoot: {
$mergeObjects: [ "$$ROOT", { $arrayToObject: "$members" } ]
}
}
},
{
$project: {
members: 0
}
}
])
Since you need to dynamically evaluate your keys based on indexes you can use $map with $range to map a list of indexes into keys of a new object. Then you can use $arrayToObject to get an object from those keys and $mergeObjects with $replaceRoot to flatten this object structure.

Wrong Result by $lookup mongodb

I am using $lookup to get the data by joining data from two or three collections, Below is my aggregate query.
let condition = {status:{$ne:config.PROJECT_STATUS.completed}, assignId:mongoose.Types.ObjectId(req.params.id)};
Project.aggregate([
{
"$match": condition
},
{
"$group": { "_id": "$_id" }
},
{
"$lookup": {
"from": "worksheets",
"let": { "projectId": "$_id" },
"pipeline": [
{
"$match": { "$expr": { "$eq": ["$projectId", "$$projectId"] } }
},
{
"$group": { "_id": "$projectId", "totalHours": { "$sum": "$hours" } }
},
{
"$lookup": {
"from": "projects",
"let": { "projectId": "$_id" },
"pipeline": [
{ "$match": { "$expr": { "$eq": ["$_id", "$$projectId"] } } },
{
"$lookup": {
"from": "users",
"let": { "developers": "$developers" },
"pipeline": [
{ "$match": { "$expr": { "$in": ["$_id", "$$developers"] } } },
{ "$project":{"firstName":1,"lastName":1}}
],
"as": "developers"
}
},
{
"$lookup": {
"from": "billing_accounts",
"let": { "upworkId": "$upworkId" },
"pipeline": [
{ "$match": { "$expr": { "$eq": ["$_id", "$$upworkId"] } } },
{"$project":{"name":1,"username":1}}
],
"as": "upworkId"
}
},
{
"$project": {
"projectName": 1, "upworkId": 1, "status": 1, "developers": 1, "hoursApproved": 1
}
}
],
"as": "project"
}}
],
"as": "projects"
}
}
])
And it is giving me the below result:
[
{
"_id": "5c188a9959f6cf1258f4cb01",
"projects": [
{
"_id": "5c188a9959f6cf1258f4cb01",
"totalHours": 8,
"project": [
{
"_id": "5c188a9959f6cf1258f4cb01",
"hoursApproved": 192,
"developers": [
{
"_id": "5c0a29e597e71a0d28b910aa",
"lastName": "kumar",
"firstName": "Amit"
}
],
"projectName": "Jims fitness",
"status": "ongoing",
"upworkId": [
{
"_id": "5c17a1cec1a7681f7c54bb2d",
"name": "Heena Ln",
"username": "heena_ln"
}
]
}
]
}
]
},
{
"_id": "5c17a253c1a7681f7c54bb2f",
"projects": []
}
]
But what i want to get is:
[
{
"_id": "5c188a9959f6cf1258f4cb01",
"projects": [
{
"_id": "5c188a9959f6cf1258f4cb01",
"totalHours": 0,
"project": [
{
"_id": "5c188a9959f6cf1258f4cb01",
"hoursApproved": 192,
"developers": [
{
"_id": "5c0a29e597e71a0d28b910aa",
"lastName": "kumar",
"firstName": "Amit"
}
],
"projectName": "Project1",
"status": "ongoing",
"upworkId": [
{
"_id": "5c17a1cec1a7681f7c54bb2d",
"name": "Heena Ln",
"username": "heena_ln"
}
]
}
]
}
]
},
{
"_id": "5c17a253c1a7681f7c54bb2f",
"projects": [
{
"_id": "5c17a253c1a7681f7c54bb2f",
"totalHours": 0,
"project": [
{
"_id": "5c17a253c1a7681f7c54bb2f",
"hoursApproved": 192,
"developers": [
{
"_id": "5c0a29e597e71a0d28b910a9",
"lastName": "kumar",
"firstName": "Rajat"
}
],
"projectName": "project2",
"status": "ongoing",
"upworkId": [
{
"_id": "5c17a1cec1a7681f7c54bb2d",
"name": "Heena Ln",
"username": "heena_ln"
}
]
}
]
}
]
}
]
As you can see that now i have totalHours equals to 0 instead of empty array and have the project details.
Actually I have four collections: projects, worksheets, users and billings and i am executing aggregate query on the projects collection to get the projects of a project manager and for this i am also joining worksheets collection to get the data for how many hours the employees worked on this project, because worksheets collection contains the projectId, userId and hours.
Query: You can see in the result that, i am getting the empty array of projects, this is because i don't have any record of second project projectId into the worksheet collection, so for this it is giving me empty array, but i want to get the projects details as it is and totalHours equals to 0.

$lookup with deeply nested object

I am new to MongoDB and currently working on a recipe App for school which suggests diet plans. Therefore I need to join the "meal" ObjectId in the diet plan of the user (collection "Users") with the ObjectIds in the collections "Meals".
Afterwards I need to join an "ingredient" ObjectID in the "Meals" collection with the ID of the "ingredient" in the "Ingredients" collection. The problem is, that the "ingredient" ObjectID in collection "Meals" is situated in an Object with another integer variable "amount". This object is nested in an array called "ingredients" with many objects such as the one just described.
Below is my Structure:
Users
{
"_id": ObjectId("5b28cab902f28e18b863bd36"),
"username: "testUser1",
"password": "$2a$08$KjddpaSQPjp6aF/gseOhVeddYdqWJCJ4DpFwxfNgsk81G.0TOtN5i",
"dietPlans": Object
{
"dietPlanCurrent":Object
{
"monday":Object
{
"breakfast":Object
{
"meal": ObjectId("5b2b9a8bbda339352cc39ec4")
},
…
},
…
},
…
},
}
Meals
{
"_id" : ObjectId("5b2b9a8bbda339352cc39ec4"),
"name": "Gulasch-breakfast",
"cuisine": "International",
"ingredients":[
{
"ingredient": ObjectId("5b1ec0f939b55efcd4e28a2d"),
"amount": 20
},
{
"ingredient": ObjectId("5b1ec42474fc1f58d84264d4"),
"amount": 20
},
{
"ingredient": ObjectId("5b1ec42474fc1f58d84264d5"),
"amount": 20
},
…
],
"comments": [
…
]
}
Ingredients
{
{
"_id": ObjectId("5b1ec0f939b55efcd4e28a2d"),
"name": "Walnut",
"calories": 654
…
}
{
"_id": ObjectId("5b1ec0f939b55efcd4e28a3d"),
"name": "Apple",
"calories": 123
…
}
…
}
What I am trying to get is:
{
"_id": ObjectId("5b28cab902f28e18b863bd36"),
"username: "testUser1",
"password": "$2a$08$KjddpaSQPjp6aF/gseOhVeddYdqWJCJ4DpFwxfNgsk81G.0TOtN5i",
"dietPlans": Object
{
"dietPlanCurrent":Object
{
"Monday":Object
{
"breakfast":Object
{
"meal": ObjectId("5b2b9a8bbda339352cc39ec4")
"matchedIngredients": [
{
"_id": ObjectId("5b1ec0f939b55efcd4e28a2d"),
"name": "Walnut",
"calories": 654
…
}
…
]
},
…
},
…
},
…
},
}
My approach which is not working (only returning empty matchedIngredients Array)
{
$match: {
'_id': mongoose.Types.ObjectId(req.params.userId)
}
},
{
$lookup: {
from: 'meals',
localField: 'dietPlans.dietPlanCurrent.monday.breakfast.meal',
foreignField: '_id',
as: "dietPlans.dietPlanCurrent.monday.breakfast.mealObject"
}
},
{
$unwind: {
path: "$dietPlans.dietPlanCurrent.monday.breakfast.mealObject",
preserveNullAndEmptyArrays: true
}
},
{
$unwind: {
path: "$dietPlans.dietPlanCurrent.monday.breakfast.mealObject.ingredients",
preserveNullAndEmptyArrays: true
}
},
{
$lookup: {
from: 'ingredients',
localField: 'dietPlans.dietPlanCurrent.monday.breakfast.mealObject.ingredients.ingredient',
foreignField: '_id',
as: "dietPlans.dietPlanCurrent.monday.breakfast.matchedIngredients"
}
}
Help is very much appreciated. I already checked out this approach, but it somehow didn't work:
Approach that didn't work for me
Thank you very much!
What you are trying to do is not possible with mongodb version 3.4 but if you upgrade to 3.6 then you can try below aggregation
db.collection.aggregate([
{ "$match": { "_id": mongoose.Types.ObjectId(req.params.userId) } },
{ "$lookup": {
"from": Meals.collection.name,
"let": { "meal_id": "$dietPlans.dietPlanCurrent.monday.breakfast.meal" },
"pipeline": [
{ "$match": { "$expr": { "$eq": [ "$_id", "$$meal_id" ] } } },
{ "$unwind": "$ingredients" },
{ "$lookup": {
"from": Ingredients.collection.name,
"let": { "ingredient_id": "$ingredients.ingredient" },
"pipeline": [
{ "$match": { "$expr": { "$eq": [ "$_id", "$$ingredient_id" ] } } }
],
"as": "matchedIngredients"
}},
{ "$unwind": "$ingredients.matchedIngredients" },
{ "$group": {
"_id": "$_id",
"name": { "$first":"$name" },
"cuisine": { "$first":"$cuisine" },
"ingredients": { "$push":"$ingredients" }
}}
],
"as": "dietPlans.dietPlanCurrent.monday.breakfast.mealObject"
}},
{ "$unwind": "$dietPlans.dietPlanCurrent.monday.breakfast.mealObject" }
])

Categories

Resources