MongoDB lookup when foreign field is an array - mongodb

I've searched the internet and StackOverflow, but I cannot find the answer or even the question.
I have two collections, reports and users. I want my query to return all reports and indicate if the specified user has that report as a favorite in their array.
Reports Collection
{ _id: 1, name:"Report One"}
{ _id: 2, name:"Report Two"}
{ _id: 3, name:"Report Three"}
Users Collection
{_id: 1, name:"Mike", favorites: [1,3]}
{_id: 2, name:"Tim", favorites: [2,3]}
Desired Result for users.name="Mike"
{ _id: 1, name:"Report One", favorite: true}
{ _id: 2, name:"Report Two", favorite: false}
{ _id: 3, name:"Report Three", favorite: true}
All of the answers I can find use $unwind on the local (reports) field, but in this case the local field isn't an array. The foreign field is the array.
How can I unwind the foreign field? Is there a better way to do this?
I saw online that someone suggested making another collection favorites that would contain:
{ _id: 1, userId: 1, reportId: 1 }
{ _id: 2, userId: 1, reportId: 3 }
{ _id: 3, userId: 2, reportId: 2 }
{ _id: 4, userId: 2, reportId: 3 }
This method seems like it should be unnessesary. It should be simple to join onto an ID in a foreign array, right?

You can use $lookup with custom pipeline which will give you 0 or 1 result and then use $size to convert an array to single boolean value:
db.reports.aggregate([
{
$lookup: {
from: "users",
let: { report_id: "$_id" },
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: [ "$name", "Mike" ] },
{ $in: [ "$$report_id", "$favorites" ] }
]
}
}
}
],
as: "users"
}
},
{
$project: {
_id: 1,
name: 1,
favorite: { $eq: [ { $size: "$users" }, 1 ] }
}
}
])
Alternatively if you need to use MongoDB version lower than 3.6 you can use regular $lookup and then use $filter to get only those users where name is Mike:
db.reports.aggregate([
{
$lookup: {
from: "users",
localField: "_id",
foreignField: "favorites",
as: "users"
}
},
{
$project: {
_id: 1,
name: 1,
favorite: { $eq: [ { $size: { $filter: { input: "$users", as: "u", cond: { $eq: [ "$$u.name", "Mike" ] } } } }, 1 ] }
}
}
])

"_id" : ObjectId("611fc392cfadfbba65d4f4bd"),
"t_name" : "Bahadur",
"t_age" : "22",
"trch" : "java",
"StudentsDetails" : [
{
"_id" : ObjectId("611fc41ccfadfbba65d4f4be"),
"s_name" : "Asin",
"s_age" : "18",
"trch" : "java",
"tsid" : ObjectId("611fc392cfadfbba65d4f4bd")
},
{
"_id" : ObjectId("611fc8f1a815fb2c737ae31f"),
"s_name" : "sonu",
"s_age" : "18",
"tsid" : ObjectId("611fc392cfadfbba65d4f4bd")
},
{
"_id" : ObjectId("611fc915a815fb2c737ae320"),
"s_name" : "monu",
"s_age" : "19",
"tsid" : ObjectId("611fc392cfadfbba65d4f4bd")
}
]
}
Create Trainer Collection
Create Scholar Collection
//query
db.Trainer.aggregate(
[`enter code here`
{`enter code here`
$lookup:`enter code here`
{`enter code here`
from: "scholar",`enter code here`
localField: "_id",`enter code here`
foreignField: "tsid",`enter code here`
as: "StudentsDetails"`enter code here`
}`enter code here`
}`enter code here`
]`enter code here`
).pretty();

Related

MongoDB multiple $lookup and $group output

I'm quite a newbie with MongoDB and I'm trying to retrieve a kind-of leaderboard based on two related collections and a third one, referencing one of the two, based on its different property.
Schema can be found here
Consider a schema like the following one:
tree: { _id, company_id: string, company_name }
link: { _id, company_id: string, url: string }
analytics: { _id, tree_id: string, link_id: string, views: number, clicks: number, date: string }
A analytics document can have tree_id, views or link_id, clicks at once.
What I'm trying to achieve right now is a kind-of a "leaderboard" of the total clicks + views, starting from analytics collection, joining it with both tree and link, and finally retrieving the sum of clicks and views.
I have already managed to retrieve the sum of them for a specific company_id, with the following code
db.analytics.aggregate([{
$lookup: {
from: "trees",
as: "trees",
localField: "tree_id",
foreignField: "_id"
}
}, {
$lookup: {
from: "links",
as: "links",
localField: "link_id",
foreignField: "_id"
}
}, {
$match: {
$or: [
{"trees.company_id": "1"},
{"links.company_id": "1"}
]
}
}, {
$group: {
_id: null,
views_count: {
$sum: "$views"
},
clicks_count: {
$sum: "$clicks"
}
}
}])
But I can't find a way to get a list of results like
{ company_id: 1, company_name: "foo", clicks: 100, views: 200 },
{ company_id: 2, company_name: "bar", clicks: 200, views: 200 }
and so on.
What I've tried so far is grouping by different _id, which is not working as I would expect
db.analytics.aggregate([{
$lookup: {
from: "trees",
as: "trees",
localField: "tree_id",
foreignField: "_id"
}
}, {
$lookup: {
from: "links",
as: "links",
localField: "link_id",
foreignField: "_id"
}
}, {
$group: {
_id: "$trees.company_id",
views_count: {
$sum: "$views"
},
clicks_count: {
$sum: "$clicks"
}
}
}])
Which does not assign clicks_count to a specific entry, but outputs something like
{ "_id" : [ "1" ], "views_count" : 6, "clicks_count" : 0 }
{ "_id" : [ ], "views_count" : 0, "clicks_count" : 48 }
{ "_id" : [ "2" ], "views_count" : 10, "clicks_count" : 0 }
I'm not even sure that this schema could be the best solution, so I will also appreciate any design suggestions or similar stuff.
Based on the comment below, I tried to deconstruct trees before grouping results, but it ended outputting the company_id, views_count only, without counting clicks, as following
{ "_id" : "2", "views_count" : 10, "clicks_count" : 0 }
{ "_id" : "1", "views_count" : 6, "clicks_count" : 0 }
$addFields to add company field, check condition if trees.company_id not empty [] then return trees otherwise return links
$arrayElemAt to get first element from array
$group by company_id and sum your counts
db.analytics.aggregate([
{ $lookup: { //... } },
{ $lookup: { //... } },
{
$addFields: {
company: {
$arrayElemAt: [
{ $cond: [{ $ne: ["$trees.company_id", []] }, "$trees", "$links"] },
0
]
}
}
},
{
$group: {
_id: "$company.company_id",
company_name: { $first: "$company.company_name" },
views_count: { $sum: "$views" },
clicks_count: { $sum: "$clicks" }
}
}
])
Playground

mongodb aggregate to find,count and project unique documnets

Below are the sample collection.
col1:
"_id" : ObjectId("5ec293782bc00b43b463b67c")
"status" : ["running"],
"name" : "name1 ",
"dcode" : "dc001",
"address" : "address1",
"city" : "city1"
col2:
"_id" : ObjectId("5ec296182bc00b43b463b68f"),
"scode" : ObjectId("5ec2933df6079743c0a2a1f8"),
"ycode" : ObjectId("5ec293782bc00b43b463b67c"),
"city" : "city1",
"lockedDate" : ISODate("2020-05-20T00:00:00Z"),
"_id" : ObjectId("5ec296182bc00b43b463688b"),
"scode" : ObjectId("5ec2933df6079743c0a2a1ff"),
"ycode" : ObjectId("5ec293782bc00b43b463b67c"),
"city" : "city1",
"lockedDate" : ISODate("2020-05-20T00:00:00Z"),
"_id" : ObjectId("5ec296182bc00b43b44fc6cb"),
"scode" :null,
"ycode" : ObjectId("5ec293782bc00b43b463b67c"),
"city" : "city1",
"lockedDate" : ISODate("2020-05-20T00:00:00Z"),
problemStatement:
I want to display name from col1 & count of documents from col2 according to ycode where scode is != null
Tried attempt:
db.col1.aggregate([
{'$match':{
city:'city1'
}
},
{
$lookup:
{
from: "col2",
let: {
ycode: "$_id",city:'$city'
},
pipeline: [
{
$match: {
scode:{'$ne':null},
lockedDate:ISODate("2020-05-20T00:00:00Z"),
$expr: {
$and: [
{
$eq: [
"$ycode",
"$$ycode"
]
},
{
$eq: [
"$city",
"$$city"
]
}
]
},
},
},
], as: "col2"
}
},
{'$unwind':'$col2'},
{'$count':'ycode'},
{
$project: {
name: 1,
status: 1,
}
},
])
now problem with this query is it either displays the count or project the name & status i.e if i run this query in the current format it gives {} if I remove {'$count':'ycode'} then it project the values but doesn't give the count and if I remove $project then i do get the count {ycode:2} but then project doesn't work but I want to achieve both in the result. Any suggestions
ORM: mongoose v>5, mongodb v 4.0
You can try below query :
db.col1.aggregate([
{ "$match": { city: "city1" } },
{
$lookup: {
from: "col2",
let: { id: "$_id", city: "$city" }, /** Create local variables from fields of `col1` but not from `col2` */
pipeline: [
{
$match: { scode: { "$ne": null }, lockedDate: ISODate("2020-05-20T00:00:00Z"),
$expr: { $and: [ { $eq: [ "$ycode", "$$id" ] }, { $eq: [ "$city", "$$city" ] } ] }
}
},
{ $project: { _id: 1 } } // Optional, But as we just need count but not the entire doc, holding just `_id` helps in reduce size of doc
],
as: "col2" // will be an array either empty (If no match found) or array of objects
}
},
{
$project: { _id: 0, name: 1, countOfCol2: { $size: "$col2" } }
}
])
Test : mongoplayground

Count articles grouping by tags mongodb

I had a lot of articles with a field called tags, and is an array of tags _ids, and for statistics purpose I want to count how many articles we had by each tag. If tags were a simple tag _id, it's easy because I could group by tag, but is an array of tags, and I can't group by that field.
First I try with this:
db.note.aggregate([{$match: {
publishedAt: {
$gte: ISODate('2018-01-01'),
$lte: ISODate('2019-01-01')
}
}}, {$group: {
_id: "$tags",
"total": {
"$sum": 1
}
}}, {$lookup: {
from: 'tags',
localField: '_id',
foreignField: '_id',
as: 'tag'
}}, {$unwind: {
path: "$tag"
}}, {$project: {
total: 1,
"tag.name": 1
}}, {$sort: {
total: -1
}}])
But that doesn't work, that query, group by tags group, so I try to do this:
{
'$match': {
'publishedAt': {
'$gte': new Date(req.body.gte),
'$lte': new Date(req.body.lte)
}
}
},
{
'$unwind': {
'path': '$tags'
}
}, {
'$group': {
'_id': '$tags',
'total': {
'$sum': 1
}
}
}, {
'$lookup': {
'from': 'tags',
'localField': '_id',
'foreignField': '_id',
'as': 'tag'
}
}, {
'$project': {
'total': 1,
'tag.name': 1
}
}, {
'$sort': {
'total': -1
}
},
{
'$unwind': {
'path': '$tag'
}
}
)
But the problem with this, that group for the first tag from the array and I miss all other tags in that array.
What do you think will be the solution?
I had a lot of articles with a field called tags, and is an array of
tags _ids, and for statistics purpose I want to count how many
articles we had by each tag.
You can try this (I am assuming the following input documents):
notes:
{ _id: 1, name: "art-1", author: "ab", tags: [ "t1", "t2" ] },
{ _id: 2, name: "art-2", author: "cd", tags: [ "t1", "t3" ] },
{ _id: 3, name: "art-3", author: "wx", tags: [ "t4", "t3" ] },
{ _id: 4, name: "art-4", author: "yx", tags: [ "t1" ] }
tags:
{ _id: 1, id: "t1", name: "t1's name" },
{ _id: 2, id: "t2", name: "t2's name" },
{ _id: 3, id: "t3", name: "t3's name" },
{ _id: 4, id: "t4", name: "t4's name" }
The Query:
db.tags.aggregate( [
{
$lookup: {
from: "notes",
localField: "id",
foreignField: "tags",
as: "tag_matches"
}
},
{ $project: { id: 1, name: 1, _id: 0, count: { $size: "$tag_matches" } } }
] )
The Output:
{ "id" : "t1", "name" : "t1's name", "count" : 3 }
{ "id" : "t2", "name" : "t2's name", "count" : 1 }
{ "id" : "t3", "name" : "t3's name", "count" : 2 }
{ "id" : "t4", "name" : "t4's name", "count" : 1 }

Issues with merging arrays of objects in MoongoDb

I trying to build an aggregation quarry in MoongoDb that will merge arrays from
2 different collection (one of the collections is of type TTL). And I facing with 2 issues that I can’t resolve.
First Issue:
I would like to merge the TakenSeats fields of my temp collations and permanent collection and set the result instead of my correct TakenSeats field, Using my aggregation in the bottom i manage to merge the arrays with the $push operator, But I cant replace the result field with the TakenSeats field that is in my permanent document.
Second Issue:
In case that I don’t have any documents in my temp collection, how can I still receive the document from the permanent one?
Sample of document in the permanent collection: (extracting data from one document)
{
"_id" : ObjectId("5b6b656818883ec018d1542d"),
"showsHall" : [
ObjectId("5b64cb758ad5f81a6cb7e6ae")
],
"movie" : [
ObjectId("5b6b614218883ec018d15428")
],
"takenSeats" : [
{
"id" : 11
},
{
"id" : 12
}
],
"showDate" : "8/14/2018",
"showStartTime" : "3:00 PM",
"showEndTime" : "5:00 PM",
"creteDate" : ISODate("2018-08-08T21:49:28.020Z"),
"__v" : 0
}
From the TTL collection: (extracting data from multiple documents)
{
"_id" : ObjectId("5b6f35023f64851baa70c61b"),
"createdAt" : ISODate("2018-08-11T19:12:02.951Z"),
"showId" : [
ObjectId("5b6b656818883ec018d1542d")
],
"takenSeats" : [
{
"id" : 22
},
{
"id" : 25
}
]
}
This is the aggregation that I used:
db.getCollection('shows').aggregate([
{ $match: { _id: ObjectId("5b6b656818883ec018d1542d") } },
{
$lookup: {
from: "temp",
localField: "_id",
foreignField: "showId",
as: "fromItems"
}
},
{ $unwind: "$fromItems" },
{ "$project": {"takenSeats": { "$setUnion": ["$takenSeats", "$fromItems.takenSeats"]}, _id: 1, showsHall: 1, movie: 1, takenSeats: 1 , showDate: 1, showStartTime: 1, showEndTime: 1 }},
{$unwind:"$takenSeats"},
{$group:{_id: "$_id", takenSeats: {$push : "$takenSeats"} }},
])
Result:
[Edit]
I manage to maintain my original data with $first operator.
But now i cant resolve issue no 2 (prevent result if null), I tried to use preserveNullAndEmptyArrays
in both of the unwind stages but the result is that it pushes an empty array.
My wanted result is that it should push to a new array only if there is values to push
This is my aggregation :
db.getCollection('shows').aggregate([
{ $match: { _id: ObjectId("5b6b656818883ec018d1542d") } },
{
$lookup: {
from: "temp",
localField: "_id",
foreignField: "showId",
as: "fromItems"
}
},
{ $unwind:{path:"$fromItems" ,preserveNullAndEmptyArrays:true}},
{ "$project": {"takenSeats": { "$setUnion": ["$takenSeats", "$fromItems.takenSeats"]}, _id: 1, showsHall: 1, movie: 1, showDate: 1, showStartTime: 1, showEndTime: 1 }},
{$unwind:{path:"$takenSeats" ,preserveNullAndEmptyArrays:true}},
,
{$group:{
_id: "$_id",
showsHall : { $first: '$showsHall' },
movie : { $first: '$movie' },
showDate : { $first: '$showDate' },
showStartTime : { $first: '$showStartTime' },
showEndTime : { $first: '$showEndTime' },
takenSeats: {$push : "$takenSeats"}
}
}
])
This is the result that i getting if there is no documents in the temp collection
{
"_id" : ObjectId("5b6b656818883ec018d1542d"),
"showsHall" : [
ObjectId("5b64cb758ad5f81a6cb7e6ae")
],
"movie" : [
ObjectId("5b6b614218883ec018d15428")
],
"showDate" : "8/14/2018",
"showStartTime" : "3:00 PM",
"showEndTime" : "5:00 PM",
"takenSeats" : [
null
]
}
Here Please add ifNull Condition for solution 2
db.getCollection('shows').aggregate([
{ $match: { _id: ObjectId("5b6b656818883ec018d1542d") } },
{
$lookup: {
from: "tempShows",
localField: "_id",
foreignField: "showId",
as: "fromItems"
}
},
{ $unwind:{path:"$fromItems" ,preserveNullAndEmptyArrays:true}},
{ "$project": {"takenSeats": { $ifNull: [{ "$setUnion": ["$takenSeats", "$fromItems.takenSeats"]}, '$takenSeats'] } ,_id: 1, showsHall: 1, movie: 1, showDate: 1, showStartTime: 1, showEndTime: 1 }},
{$unwind:{path:"$takenSeats" ,preserveNullAndEmptyArrays:true}},
{$group:{
_id: "$_id",
showsHall : { $first: '$showsHall' },
movie : { $first: '$movie' },
showDate : { $first: '$showDate' },
showStartTime : { $first: '$showStartTime' },
showEndTime : { $first: '$showEndTime' },
takenSeats: {$push : "$takenSeats"}
}
}
])

Mongodb aggregation with referred collection

I want to get a list of my second level affiliate users, I've designed the collection like
user
{
"_id" : ObjectId("5a1b9df7bfdbfef2d4f1e9f3"),
"name" : "name 1",
"affKey" : "H1g-CfFxG",
}
{
"_id" : ObjectId("5a1bce5e9a2918f71a9ac4fb"),
"name" : "name 2",
"affKey" : "K1gKJfFxG",
}
affilites
{
"affKey" : "H1g-CfFxG",
"affUsers" : [
ObjectId("5a1bce5e9a2918f71a9ac4fb")
],
}
{
"affKey" : "K1gKJfFxG",
"affUsers" : [
ObjectId("5a1b9e43bfdbfef2d4f1e9f8"),
ObjectId("5a1b9e43bfdbfef2d4f1e911"),
],
}
Here I am saving a new set in affiliate.affKey is based on Users.affKey
Now I want to get a list of my first and second level affiliates, i.e. the list of affiliates of 5a1b9e43bfdbfef2d4f1e9f6 and 5a1bce5e9a2918f71a9ac4fb along with first level affiliate.
expecting the results like
{
first: [first level affiliates] // 1 result
second: [second level affiliates] // 2 results
}
For this situation you have to some steps to get expected result. can follow bellow steps
$lookup the affiliets collection for affKey
Then you have to $lookup the users collection to get the reference user information
After that you have to $lookup again the affiliets collection for second level user affKey
so query can be like bellow
db.users.aggregate([
{
$lookup: {
from: "affilites",
localField: "affKey",
foreignField: "affKey",
as: "affUsers"
}
},
{
$project: {
name: 1,
affKey: 1,
first: {$arrayElemAt: ["$affUsers.affUsers", 0]},
secondLevelUserId: {$arrayElemAt: ["$affUsers.affUsers", 0]}
}
},
{ $unwind: { path: "$secondLevelUserId", "preserveNullAndEmptyArrays": true }},
{
$lookup: {
from: "users",
localField: "secondLevelUserId",
foreignField: "_id",
as: "secondLevelUser"
}
},
{
$project: {
name: 1,
affKey: 1,
first: 1,
secondLevelUser: {$arrayElemAt: ["$secondLevelUser", 0]}
}
},
{
$lookup: {
from: "affilites",
localField: "secondLevelUser.affKey",
foreignField: "affKey",
as: "secondLevelUser"
}
},
{
$project: {
name: 1,
affKey: 1,
first: 1,
second: {$arrayElemAt: ["$secondLevelUser.affUsers", 0]}
}
},
{
$unwind: {
"path": "$second",
"preserveNullAndEmptyArrays": true
}
},
{
$group: {
_id: "$_id",
name: {$first: "$name"},
affKey: {$first: "$affKey"},
first: {$first: "$first"},
second: {$addToSet: "$second"}
}
}
]);
After execute the query you will get result like bellow
first document:
{
"_id" : ObjectId("5a1b9df7bfdbfef2d4f1e9f3"),
"name" : "name 1",
"affKey" : "H1g-CfFxG",
"first" : [
ObjectId("5a1bce5e9a2918f71a9ac4fb")
],
"second" : [
ObjectId("5a1b9e43bfdbfef2d4f1e911"),
ObjectId("5a1b9e43bfdbfef2d4f1e9f8")
]
}
second document
{
"_id" : ObjectId("5a1bce5e9a2918f71a9ac4fb"),
"name" : "name 2",
"affKey" : "K1gKJfFxG",
"first" : [
ObjectId("5a1b9e43bfdbfef2d4f1e9f8"),
ObjectId("5a1b9e43bfdbfef2d4f1e911")
],
"second" : []
}