db.logs.aggregate([{$match: { _device: { $in: devices }, takenTimestamp: {$gt: 1547650800 } }}, { $lookup: {
from: "users",
localField: "_deviceTakenByUser",
foreignField: "_id",
as: "user"
}}, { $project: { _id: 0, user: { email: 1 }}} ])
I know want to get distinct values from the user email, is it possible to do so? Can I do it with grouping?
After your $lookup, user is a list of users.
You can do an $unwind to split it
And then use a $group an the email field to have a list of all the emails
...
{
"$unwind": "$user"
},
{
"$group": {
"_id": "$user.email"
}
}
Related
I have MongoDB model called candidates
appliedJobs: [
{
job: { type: Schema.ObjectId, ref: "JobPost" },
date:Date
},
],
candidate may have multiple records in appliedJobs array. There I refer to the jobPost.
jobPost has the companyName, property.
companyName: String,
What I want is to get the company names with send job applications counts. For an example
|Company|Applications|
|--------|---------------|
|Facebook|10 applications|
|Google|5 applications|
I created this query
Candidate.aggregate([
{
$match: {
appliedJobs: { $exists: true },
},
},
{ $group: { _id: '$companyName', count: { $sum: 1 } } },
])
The problem here is I can't access the companyName like this. Because it's on another collection. How do I solve this?
In order to get data from another collection you can use $lookup (nore efficient) or populate (mongoose - considered more organized), so one option is:
db.candidate.aggregate([
{$match: {appliedJobs: {$exists: true}}},
{$unwind: "$appliedJobs"},
{$lookup: {
from: "JobPost",
localField: "appliedJobs.job",
foreignField: "_id",
as: "appliedJobs"
}
},
{$project: {companyName: {$first: "$appliedJobs.companyName"}}},
{$group: {_id: {candidate: "$_id", company: "$companyName"}, count: {$sum: 1}}},
{$group: {
_id: "$_id.candidate",
appliedJobs: {$push: {k: "$_id.company", v: "$count"}}
}},
{$project: {appliedJobs: {$arrayToObject: "$appliedJobs"}}}
])
See how it works on the playground example
Simply $unwind the appliedJobs array. Perform $lookup to get the companyName. Then, $group to get count of applications by company.
db.Candidate.aggregate([
{
$match: {
appliedJobs: {
$exists: true
}
}
},
{
$unwind: "$appliedJobs"
},
{
"$lookup": {
"from": "JobPost",
"localField": "appliedJobs._id",
"foreignField": "_id",
"as": "JobPostLookup"
}
},
{
$unwind: "$JobPostLookup"
},
{
"$group": {
"_id": "$JobPostLookup.companyName",
"Applications": {
"$sum": 1
}
}
}
])
Here is the Mongo Playground for your reference.
I have two collections:
employees
_id
office
jobTitle
offices
_id
city
I am trying to receive a list of all office locations with the job titles of employees of the respecitve office.
The end result would look like this:
[{
_id: ObjectId('6086f617cc0824cc4ce7c9f0'),
city: "Berlin",
jobTitles: ['SOFTWARE ENGINEER', 'CEO', 'CFO']
}, {
_id: ObjectId('60c08d36f925f3083488ea79'),
city: "Prague",
jobTitles: ['UX DESIGNER', 'BUSINESS ANALYST']
}]
This is the aggregation I've tried, with no success:
db.offices.aggregate([{
$lookup: {
from: 'employees',
localField: '_id',
foreignField: 'office',
as: 'jobTitles',
project: [{
$group: { _id: '$jobTitle'}
}]
}
}]);
One office can have thousands of employees, so I'm trying to make the query as efficient as possible.
Thank you for your ideas! :)
use $lookup
db.offices.aggregate([
{
"$lookup": {
"from": "employees",
"localField": "_id",
"foreignField": "office",
"as": "jobTitles"
}
},
{
"$set": {
"jobTitles": "$jobTitles.jobTitles"
}
},
{
"$unwind": "$jobTitles"
},
{
"$group": {
"_id": "$_id",
"city": {
"$first": "$city"
},
jobTitles: {
$addToSet: "$jobTitles"
}
}
}
])
mongoplayground
I am new to NoSQL databases, and I got a little confused with collection aggregation. Here is what I am trying to do: I have three collections: productCollection, detailsCollection and brandCollection.
productCollection has the following fields: _id, name, details
detailsCollection has _id and brand
brandCollection has _id and name
These collections also have other fields, but these are the most interesting for me. As you may guess, details in productCollection is a reference to _id in detailsCollection, while brand in detailsCollection is a reference to _id in brandCollection. What I need is to get the collection with products and their brands. So, basically, I need to join these three collections and extract name from productCollection and name from brandCollection
So far, I managed to write this script:
db.productCollection.aggregate([
{
$lookup: {
from: "detailsCollection",
localField: "details",
foreignField: "_id",
as: "det"
}
},
{
$replaceRoot: { newRoot: { $mergeObjects: [ { $arrayElemAt: [ "$det", 0 ] }, "$$ROOT" ] } }
},
{ $project: { "det": 0 } },
{
$lookup: {
from: "brandCollection",
localField: "brand",
foreignField: "_id",
as: "br"
}
},
{
$replaceRoot: { newRoot: { $mergeObjects: [ { $arrayElemAt: [ "$br", 0 ] }, "$$ROOT" ] } }
},
{ $project: { "br": 0 } }
])
It shows me all the fields in all three collections, but it does not show me the brand's name. I think it might be because the field name appears in both productCollection and brandCollection. All other fields are fine.
Hence, my question is: how do I make name from brandCollection appear in the result too? Maybe I can rename it in the process to be shown under another name? And is there an easier way to join these three collections? Or is the script above fine?
Thank you for any help!
$lookup with detailsCollection collection
$lookup with brandCollection and pass localField as brand id
$arrayElemAt to get first element from brand result
remove details field its no longer needed
db.productCollection.aggregate([
{
$lookup: {
from: "detailsCollection",
localField: "details",
foreignField: "_id",
as: "brand"
}
},
{
$lookup: {
from: "brandCollection",
localField: "brand.brand",
foreignField: "_id",
as: "brand"
}
},
{
$addFields: {
brand: {
$arrayElemAt: ["$brand.name", 0]
},
details: "$$REMOVE"
}
}
])
Playground
I have found a few questions that relate to this (here and here) but I have been unable to interpret the answers in a way that I can understand how to do what I need.
I have 3 collections: Organisations, Users, and Projects. Every project belongs to one user, and every user belongs to one organisation. From the user's id, I need to return all the projects that belong to the organisation that the logged-in user belongs to.
Returning the projects from the collection that belong to the user is easy, with this query:
const projects = await Project.find({ user: req.user.id }).sort({ createdAt: -1 })
Each user has an organisation id as a foreign key, and I think I need to do something with $lookup and perhaps $unwind mongo commands, but unlike with SQL queries I really struggle to understand what's going on so I can construct queries correctly.
EDIT: Using this query
const orgProjects = User.aggregate(
[
{
$match: { _id: req.user.id }
},
{
$project: { _id: 0, org_id: 1 }
},
{
$lookup: {
from: "users",
localField: "organisation",
foreignField: Organisation._id,
as: "users_of_org"
}
},
{
$lookup: {
from: "projects",
localField: "users_of_org._id",
foreignField: "user",
as: "projects"
}
},
{
$unset: ["organisation", "users_of_org"]
},
{
$unwind: "$projects"
},
{
$replaceWith: "$projects"
}
])
Seems to almost work, returning the following:
Aggregate {
_pipeline: [
{ '$match': [Object] },
{ '$project': [Object] },
{ '$lookup': [Object] },
{ '$lookup': [Object] },
{ '$unset': [Array] },
{ '$unwind': '$projects' },
{ '$replaceWith': '$projects' }
],
_model: Model { User },
options: {}
}
assuming your documents have a schema like this, you could do an aggregation pipeline like below with 2 $lookup stages.
db.users.aggregate(
[
{
$match: { _id: "user1" }
},
{
$project: { _id: 0, org_id: 1 }
},
{
$lookup: {
from: "users",
localField: "org_id",
foreignField: "org_id",
as: "users_of_org"
}
},
{
$lookup: {
from: "projects",
localField: "users_of_org._id",
foreignField: "user_id",
as: "projects"
}
},
{
$unset: ["org_id", "users_of_org"]
},
{
$unwind: "$projects"
},
{
$replaceWith: "$projects"
}
])
I have those collection schemas
Schema.users = {
name : "string",
username : "string",
[...]
}
Schema.rooms = {
name : "string",
hidden: "boolean",
user: "string",
sqmt: "number",
service: "string"
}
Schema.room_price = {
morning : "string",
afternoon: "string",
day: "string",
room:'string'
}
I need to aggregate the users with the rooms and foreach room the specific room prices.
the expected result would be
[{
_id:"xXXXXX",
name:"xyz",
username:"xyz",
rooms:[
{
_id: 1111,
name:'room1',
sqmt: '123x',
service:'ppp',
room_prices: [{morning: 123, afternoon: 321}]
}
]}]
The first part of the aggregate could be
db.collection('users').aggregate([
{$match: cond},
{$lookup: {
from: 'rooms',
let: {"user_id", "$_id"},
pipeline: [{$match:{expr: {$eq: ["$user", "$$user_id"]}}}],
as: "rooms"
}}])
but I can't figure out how to get the room prices within the same aggregate
Presuming that room from the room_prices collection has the matching data from the name of the rooms collection, then that would the expression to match on for the "inner" pipeline of the $lookup expression with yet another $lookup:
db.collection('users').aggregate([
{ $match: cond },
{ $lookup: {
from: 'rooms',
let: { "user_id": "$_id" },
pipeline: [
{ $match:{ $expr: { $eq: ["$user", "$$user_id"] } } },
{ $lookup: {
from: 'room_prices',
let: { 'name': '$name' },
pipeline: [
{ $match: { $expr: { $eq: [ '$room', '$$name'] } } },
{ $project: { _id: 0, morning: 1, afternoon: 1 } }
],
as: 'room_prices'
}}
],
as: "rooms"
}}
])
That's also adding a $project in there to select only the fields you want from the prices. When using the expressive form of $lookup you actually do get to express a "pipeline", which can be any aggregation pipeline combination. This allows for complex manipulation and such "nested lookups".
Note that using mongoose you can also get the collection name from the model object using something like:
from: RoomPrice.collection.name
This is generally future proofing against possible model configuration changes which might possibly change the name of the underlying collection.
You can also do pretty much the same with the "legacy" form of $lookup prior to the sub-pipeline syntax available from MongoDB 3.6 and upwards. It's just a bit more processing and reconstruction:
db.collection('users').aggregate([
{ $match: cond },
// in legacy form
{ $lookup: {
from: 'rooms',
localField: 'user_id',
foreignField: 'user',
as: 'rooms'
}},
// unwind the output array
{ $unwind: '$rooms' },
// lookup for the second collection
{ $lookup: {
from: 'room_prices',
localField: 'name',
foreignField: 'room',
as: 'rooms.room_prices'
}},
// Select array fields with $map
{ $addFields: {
'rooms': {
'room_prices': {
$map: {
input: '$rooms.room_prices',
in: {
morning: '$this.morning',
afternoon: '$this.afternoon'
}
}
}
}
}},
// now group back to 'users' data
{ $group: {
_id: '$_id',
name: { $first: '$name' },
username: { $first: '$username' },
// same for any other fields, then $push 'rooms'
rooms: { $push: '$rooms' }
}}
])
That's a bit more overhead mostly from usage of $unwind and also noting that the "field selection" does actually mean you did return the "whole documents" from room_prices "first", and only after that was complete can you select the fields.
So there are advantages to the newer syntax, but it still could be done with earlier versions if you wanted to.