How to match Array elements in Sub-Documents by Criteria - node.js

I have following Mongoose schemas :
EmployeeSchema :
var EmployeeSchema = new Schema({
name : String,
employeeDetailsId: {
type: Schema.Types.ObjectId,
ref: 'employeedetails'
}
});
EmployeeDetailSchema :
var EmployeeDetailSchema = new Schema({
employeeId: {
type: Schema.Types.ObjectId,
ref: 'employee'
},
primarySkills: [
{
type: Schema.Types.ObjectId,
ref: 'skills'
}
],
});
SkillsSchema :
var SkillsSchema = new Schema({
name: {
type: String,
required: true
}
});
EmployeeDetailSchema data gets saved on demand, like when a particular Skill is assigned to Employee. Once EmployeeDetail document is saved then corresponding EmployeeDetailID is saved back to EmployeeSchema as employeeDetailsId.
Now there is bi-directional relationship between EmployeeSchema and EmployeeDetailSchema.
NOTE :
Multiple Skills can be associated to an Employee and they are stored as an array of ObjectID's in EmployeeDetails Schema.
UseCase :
I want to fetch all Employees who have particular Skill associated with them, Skill will be input to the Mongoose / Mongo query.
Say input Skill ID is 1234 then i want to fetch all employees who have Skill id 1234 in EmployeeDetail > PrimarySkills array.
Following is the approach which i tried using Mongoose :
EmployeeModel.aggregate([
{
$lookup: {
from: 'employeedetails',
localField: 'employeeDetailsId',
foreignField: '_id',
as: 'details'
}
},
{
$match: {
$and: [
{ "details.primarySkills": { "$exists": true } },
{
"details.primarySkills": {
$in: [mongoose.Types.ObjectId(req.params.skillId)]
}
}
]
}
}
]).exec(function (err, result) {
if (err) return res.send('400', {
message: 'Unable to fetch employees data by status. Please try again later'
});
return res.jsonp(result);
});
Result : Empty array.
I have no clue where am going wrong, need some help.

My bad, original approach which i followed was all fine except a small mistake. I should have used req.query.skillId instead of req.params.skillId
For those wondering the difference b/w query and params, check this answer
This is the final solution, thought it may help others :
EmployeeModel.aggregate([
{
$lookup: {
from: 'employeedetails',
localField: 'employeeDetailsId',
foreignField: '_id',
as: 'details'
}
},
{
$match: {
$and: [
{ "details.primarySkills": { "$exists": true } },
{
"details.primarySkills": {
$in: [mongoose.Types.ObjectId(req.query.skillId)]
}
}
]
}
}
]).exec(function (err, result) {
if (err) return res.send('400', {
message: 'Unable to fetch employees data by status. Please try again later'
});
return res.jsonp(result);
});

One approach that you could take is apply the $lookup to the Skills model on the EmployeeDetails model first and then do another lookup to the Employee model
EmployeeDetails.aggregate([
{ "$match": { "primarySkills": req.query.skillId } },
{ "$unwind": "$primarySkills" }, // skip this pipeline step for MongoDB 3.4 and above
{
"$lookup": {
"from": "skills",// ensure this is the correct collection name
"foreignField": "_id",
"localField": "primarySkills",
"as": "skills"
}
},
{ "$unwind": "$skills" }
{
"$group": {
"_id": "$_id",
"employeeId": { "$first": "$employeeId" },
"primarySkills": { "$push": "$skills" }
}
},
{
"$lookup": {
"from": "employees",// ensure this is the correct collection name
"foreignField": "_id",
"localField": "employeeId",
"as": "employee"
}
}
]).exec(callback);

Related

lookup with add extra field in mongodb

My OBJ
[{
_id:XXXXXXXXXX,
role:admin
},
{
_id:XXXXXXXXXX,
role:superUser
}]
and need results using aggregation how to solve this using aggregation
[{
name:'username'
role:'test'
}
]
I suppose you need the following
let db1 = db.get().collection(`temp1`);
let db2 = db.get().collection(`temp2`);
await db1.aggregate([
{
$lookup: {
from: "temp2",
localField: "_id", // field in the orders collection
foreignField: "_id", // field in the items collection
as: "users"
}
},
{
$replaceRoot: { newRoot: { $mergeObjects: [{ $arrayElemAt: ["$users", 0] }, "$$ROOT"] } }
},
{ $project: { users: 0 } }
]).toArray()

mongodb (aggregation) - $lookup with the lookup result

i am new in aggregation framework and have the following problem/question.
My collections looks like this:
users
_id: ObjectId('604caef28aa89e769585f0c4')
baseData: {
firstName: "max"
username: "max_muster"
...
...
}
activitiesFeed
_id: ObjectId('604cb97c99bbd77b54047370')
userID: "604caef28aa89e769585f0c4" // the user id
activity: "604caf718aa89e769585f0c8" // the activity id
viewed: false
activities
_id: ObjectId('604caf718aa89e769585f0c8')
"baseData": {
"userID":"604caef28aa89e769585f0c4",
"type":0,
"title":"Alessandro send you a friend request.",
"description":"",
"actionID":"604caef28aa89e769585f0c4"
},
aggregate function
const response = ActivityFeed.aggregate([
{
$match: { userID: ObjectId(args.user) },
},
{
$lookup: {
from: 'activities',
localField: 'activity',
foreignField: '_id',
as: 'lookup_activities',
},
},
{ $unwind: '$activities' },
{
$lookup: {
from: 'users',
localField: 'lookup_activities.baseData.actionID',
foreignField: '_id',
as: 'lookup_users',
},
},
]);
How i can made a lookup with the first lookup result?
I made a lookup to the activities collection. This collection holds the id for the second lookup baseData.userID. But with this approach i have no result with the second lookup.
The reason why i made a join to the activities collection is that the actionID can hold a userID or a document id from another collection. It depend on the type in the activities collection.
Is it in the aggregation framework possible to made a lookup which depends on the type and of the previous lookup?
Thank you in advance.
All I did is fixing your aggregation. You unwind the wrong field and please make sure all your ID fields are ObjectId. I highly recommend using MongoDB Compass to assist your aggregation if you are new to the aggregation function. They have stage by stage assistance GUI which will really make your life easier.
mongoplayground
db.activitiesFeed.aggregate([
{
$match: {
userID: ObjectId("604caef28aa89e769585f0c4")
},
},
{
$lookup: {
from: "activities",
localField: "activity",
foreignField: "_id",
as: "lookup_activities",
},
},
{
$unwind: "$lookup_activities"
},
{
$lookup: {
from: "users",
localField: "lookup_activities.baseData.actionID",
foreignField: "_id",
as: "lookup_users",
},
},
])
Sample Used
db={
"users": [
{
_id: ObjectId("604caef28aa89e769585f0c4"),
baseData: {
firstName: "max",
username: "max_muster"
}
}
],
"activitiesFeed": [
{
_id: ObjectId("604cb97c99bbd77b54047370"),
userID: ObjectId("604caef28aa89e769585f0c4"),
activity: ObjectId("604caf718aa89e769585f0c8"),
viewed: false
}
],
"activities": [
{
_id: ObjectId("604caf718aa89e769585f0c8"),
"baseData": {
"userID": ObjectId("604caef28aa89e769585f0c4"),
"type": 0,
"title": "Alessandro send you a friend request.",
"description": "",
"actionID": ObjectId("604caef28aa89e769585f0c4")
},
}
]
}

Mongodb lookup aggregation not getting all field values

I have two collections which are of schema like
driver
_id:5f9c1d897ea5e246945cd73a
agent_name:"Ratnabh Kumar Rai"
agent_email:"ra2614#gmail.com"
agent_phone:"70****63331"
and
reviews
_id:5f9d54cb3a3ee10c6829c0a4
order_id:"5f9d40f096e4a506e8684aba"
user_id:"5f9bcb66f7a5bf4be0ad9973"
driver_id:"5f9c1d897ea5e246945cd73a"
rating:3
text:""
so i want to calculate the avg driver rating . I tried to use lookup aggregation so that i can get all details and then later calculate the sum...what i did was
let doc = await db
.collection("drivers")
.aggregate([
{
$project: {
_id: {
$toString: "$_id",
},
},
},
{
$lookup: {
from: "reviews",
localField: "_id",
foreignField: "driver_id",
as: "driver_info",
},
},
{
$project: {
agent_email: 1,
orderReview: "$driver_info",
},
},
])
.toArray();
But i am getting result as
{
_id: '5f9d63eb8737e82fbc193dd9',
orderReview: [ [Object], [Object], [Object], [Object], [Object] ]
}
which is partially correct as i also need to get details from my localfield collection that is drivers field, as of now you can see i am only getting id of driver in my projection i also did "agent_email:1" but not getting email
You're actually only projecting _id in the first pipeline and hence only _id is passed to further pipelines, If you need email in further pipelines you need to project it too
let doc = await db.collection("drivers").aggregate([
{
$project: {
_id: {
$toString: "$_id",
},
agent_email: "$agent_email"
},
},
{
$lookup: {
from: "reviews",
localField: "_id",
foreignField: "driver_id",
as: "driver_info",
},
},
{
$project: {
agent_email: 1,
orderReview: "$driver_info",
},
},
])
MongoDB PlayGround : https://mongoplayground.net/p/h7D-tYJ7sLU
[Update]
I realized that you're doing this for getting average and if you need it to be done in a single aggregate query, here it is how you can do it.
Using unwind operator you can flat the reviews array as objects and then group by _id and use the $avg aggregation operator
db.collection("drivers").aggregate([
{
$project: {
_id: {
$toString: "$_id",
},
agent_email: "$agent_email"
},
},
{
$lookup: {
from: "reviews",
localField: "_id",
foreignField: "driver_id",
as: "driver_info",
},
},
// Makes the driver info flat with other information
{
"$unwind": "$driver_info"
},
{
// Now you can group them
$group: {
_id: "$_id",
// Calculates avg on rating field
avg: {
"$avg": "$driver_info.rating"
},
// To make email field appear in next pipeline.
// You can do it for other fields too like Name, etc
agent_email: {
$first: "$agent_email"
}
}
},
{
$project: {
// select the fields you want to display
agent_email: 1,
avg: 1
},
},
])
MonogoDb playground Link

MongoDB Aggregate limiting records returned

I have the following 3 collections schema in my Node.js app
Users{
_id
Name
}
Vendors{
_id
userId
CompanyName
}
CustomerFavoriteVendors{
_id
customerId
vendorId
}
Now I am trying to retrieve a list of all Vendors, while displaying their full user details as well as if they are Favorited by currently logged in customer, so I've built the following vendors aggregate function yet I can only retrieve vendors whom were Favorited, so I am wondering what I am missing here to retrieve all vendors despite if they were favorited or not? Thanks for your effort and support
let size = 10;
let offset = 0;
let query = [];
query.push(
{ $lookup: { from: "users", localField: "userId", foreignField: "_id", as: "userdetail" } },
{ $unwind: "$userdetail" },
{ $lookup: { from: "customerfavoritevendors", localField: "_id", foreignField: "vendorId", as: "customerfav" } },
{ $unwind: "$customerfav" },
{ $group: { _id: null, content: { $push: '$$ROOT' },count: { $sum: 1 } } },
{ $project: { content: { $slice: [ '$content', offset, size ] }, count: 1, _id: 1 } },
);
const vendorsList = await Vendors.aggregate(query);

$lookup on ObjectId's in an array of objects (Mongoose)

I have this two schema:
module.exports = mongoose.model('Article', {
title : String,
text : String,
lang : { type: String, default: 'en'},
user : { type : mongoose.Schema.Types.ObjectId, ref: 'User' },
});
var userSchema = mongoose.Schema({
email : String,
name : String,
rating : [{
_id: false,
articleID: {type: mongoose.Schema.Types.ObjectId, ref: 'Article'},
rate: Number
}]
});
module.exports = mongoose.model('User', userSchema);
and i want to calculate the average rating of an user (the average on all rating on its articles).
I tried this:
User.aggregate([
{ $unwind: "$rating" },
{
"$lookup": {
"from": "Article",
"localField": "rating.articleID",
"foreignField": "_id",
"as": "article-origin"
}
}//,
//{ $match: { "article-origin.user" : mongoose.Types.ObjectId(article.user) } }//,
//{ $group : {_id : "$rating.articleID", avgRate : { $avg : "$rating.rate" } } }
]).exec(function (err,result) {
console.log(err);
console.log(JSON.stringify(result));
});
but without success, the lockup always return the field article-origin null.
result:{"_id":"590747e1af02570769c875dc","name":"name","email":"email","rating":{"rate":5,"articleID":"59074a357fe6a307981e7925"},"__v":0,"article-origin":[]}]
Why this is not working ?
Certainly no need for the $lookup operator since the group aggregation operation does not make use of the documents from the articles collection, it only needs a single field i.e. articleID for grouping.
There are two ways you can go about this. If your MongoDB server version is 3.4 or greater, then the $divide, $reduce and $size operators can be applied here to calculate the average without resorting
to flatten the rating array first which can have some performance ramifications if the array is large.
Consider running the following pipeline:
User.aggregate([
{ "$match": { "_id" : mongoose.Types.ObjectId(article.user) } },
{
"$addFields": {
"avgRate": {
"$divide": [
{
"$reduce": {
"input": "$rating",
"initialValue": 0,
"in": { "$sum": ["$$value", "$$this.rate"] }
}
},
{
"$cond": [
{ "$ne": [{ "$size": "$rating" }, 0] },
{ "$size": "$rating" },
1
]
}
]
}
}
}
]).exec(function (err, result) {
console.log(err);
console.log(JSON.stringify(result));
});
If using MongoDB version 3.2 then you would need to $unwind the rating array first:
User.aggregate([
{ "$match": { "_id" : mongoose.Types.ObjectId(article.user) } },
{ "$unwind": "$rating" },
{
"$group": {
"_id": "$_id",
"avgRate": { "$avg": "$rating.rate" }
}
}
]).exec(function (err, result) {
console.log(err);
console.log(JSON.stringify(result));
});
If for some reason you need the $lookup operation, you need to reference the collection name, not the model name, thus the correct aggregate operation should be
User.aggregate([
{ "$unwind": "$rating" },
{
"$lookup": {
"from": "articles", /* collection name here, not model name */
"localField": "rating.articleID",
"foreignField": "_id",
"as": "article-origin"
}
},
{ "$match": { "article-origin.user" : mongoose.Types.ObjectId(article.user) } },
{
"$group": {
"_id": "$_id",
"avgRate": { "$avg": "$rating.rate" }
}
}
]).exec(function (err, result) {
console.log(err);
console.log(JSON.stringify(result));
});

Resources