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));
});
Related
I'm doing a simple follow friend functionality.
Please see my codes below:
Following schema:
{
"userId": { type: String },
"followers": [{ "followerId": type: String }],
"followings": [{ "followingId": type: String }],
}
User schema:
{
"fullName": { type: String }
}
Note: user 8 has 1 follower and 2 followings.
Now, my expected output should be like this:
"userId": 8,
"followers": [
{
"followerId": 4,
"fullName": "Rose Marriott",
},
{
"followerId": 5,
"fullName": "James Naismith",
}
],
"followings": [
{
"followingId": 1,
"fullName": "Russell Oakham",
},
{
"followingId": 5,
"fullName": "James Naismith",
}
]
This is what I tried so far:
db.followings.aggregate([
{ $unwind: "$followers" },
{
$lookup: {
from: "users",
localField: "followers.followerId",
foreignField: "_id",
as: "users"
}
},
{
$addFields:
{
users: { $arrayElemAt: ["$users", 0] },
},
},
{ $unwind: "$followings" },
{
$lookup: {
from: "users",
localField: "followings.followingId",
foreignField: "_id",
as: "users2"
}
},
{
$addFields:
{
users2: { $arrayElemAt: ["$users2", 0] },
},
},
{ $match: {"userId": mongoose.Types.ObjectId(userId) } },
{
$group: {
_id: "$_id",
userId: { $first: "$userId" },
followers: {
$push: {
followerId: "$followers.followerId",
fullName: "$users.fullName",
}
},
followings: {
$push: {
followingId: "$followings.followingId",
fullName: "$users2.fullName",
}
}
}
}
]);
But I'm getting 2 followers and 2 followings. I wonder what's causing this issue. Appreciate any help. Thanks!
You can try,
$addFields to make a unique array called userIds form both arrays followers and followings, $setUnion to get unique ids,
$lookup with users collection
$project to show fields,
followers get fullName, $map to iterate loop of followers and get the name of followerId from users array using $reduce and $cond
followings get fullName, $map to iterate loop of followings and get the name of followingId from users array using $reduce and $cond
db.followings.aggregate([
{
$addFields: {
userIds: {
$setUnion: [
{
$map: {
input: "$followers",
in: "$$this.followerId"
}
},
{
$map: {
input: "$followings",
in: "$$this.followingId"
}
}
]
}
}
},
{
$lookup: {
from: "users",
localField: "userIds",
foreignField: "_id",
as: "users"
}
},
{
$project: {
userId: 1,
followers: {
$map: {
input: "$followers",
as: "f",
in: {
$mergeObjects: [
"$$f",
{
fullName: {
$reduce: {
input: "$users",
initialValue: "",
in: {
$cond: [
{ $eq: ["$$this._id", "$$f.followerId"] },
"$$this.fullName",
"$$value"
]
}
}
}
}
]
}
}
},
followings: {
$map: {
input: "$followings",
as: "f",
in: {
$mergeObjects: [
"$$f",
{
fullName: {
$reduce: {
input: "$users",
initialValue: "",
in: {
$cond: [
{ $eq: ["$$this._id", "$$f.followingId"] },
"$$this.fullName",
"$$value"
]
}
}
}
}
]
}
}
}
}
}
])
Playground
when working with relations on Mongoose you should create the relationship based on a unique id, and then reference the document. In your case, it would be:
followingSchema = new mongoose.Schema({
{
"followers": [{type: mongoose.Schema.types.ObjectId, ref="User"}],
"followings": [{type: mongoose.Schema.types.ObjectId, ref="User"}],
}
})
userSchema = new mongoose.Schema({
fullname: String,
})
be aware that the userId will be created automatically by Mongoose with a field called _id. so, the end result of creating a new following documents would be:
{
_id: "klajdsfñalkdjf" //random characters created by mongoose,
followers: ["adfadf134234", "adfadte345"] //same as before, these are Ids of the users randomly generated by mongoose
followers: ["adfadf134234", "adfadte345"]
}
{
_id: adfadf134234,
fullName: "alex",
}
now, because there is no use for us to have a random number as information in the fields of following and followers in the following object, we can now use the method .populate() that can be used over the document itself to transform those Ids into actual information. You can see more about it here: mongoose documentation
our final result would be something like this:
{
_id: "añfakdlsfja",
followers : [{_id: "adlfadsfj", fullName: "alex"}],
following : [{_id: "adfadfad" , fullName: "other"}, {_id: "dagadga", fullName: "another"}]
}
I am trying to using aggregate function for fetching data from two documents. I am able to do it but i am finding a solution how can i apply $project in lookup table only
below is my approach
app.get('/getAllDetailById',(req,res)=>{
if(db){
// lookup
db.collection("points").aggregate(
[
{ "$addFields": { "enquiry_by": { "$toObjectId": "$enquiry_by" }}},
{
"$lookup" : {
from: "user",
localField: "enquiry_by",
foreignField: "_id",
as: "userDetails"
}
},
{ $unwind: "$userDetails"},
]
).toArray()
.then(result=>{
console.log(result[0])
}).catch(err=>{
res.send(err)
})
}
})
What i want is get all fields from points table and from user table i just want name and username. I have used $project but than its return only fields defined in this.
{ $project: {"userDetails.name":1, "userDetails.username":1,"_id":0} }
Is there any way that $project can be applied separately for user table
You can use pipeline in the lookup if you are using mongodb >= 3.6: https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/#join-conditions-and-uncorrelated-sub-queries
So your code will look like:
app.get('/getAllDetailById',(req,res)=>{
if(db){
// lookup
db.collection("points").aggregate(
[
{ "$addFields": { "enquiry_by": { "$toObjectId": "$enquiry_by" }}},
{
"$lookup" : {
from: "user",
let: { "enquiry_by": "$enquiry_by" },
pipeline: [
{
"$match": {
"$expr": {
"$eq": ["$_id", "$$enquiry_by"]
}
},
"$project": {
"$name": 1,
"$username": 1,
}
},
],
as: "userDetails"
}
},
{ $unwind: "$userDetails"},
]
).toArray()
.then(result=>{
console.log(result[0])
}).catch(err=>{
res.send(err)
})
}
})
I have a visits collection where I successfully count the number of visits per location
Visits model:
{
"_id": {
"$oid": "5a3969e2f4ea3e33ac5a523d"
},
"locationId": "5a395ccf210a1d35d0df4a58"
}
locationId above is of type 'Object' as I learned lookup localField and foreignField must be of same type
nodejs code =>
let sort = { "count": -1, "locationId": 1 };
Visit.aggregate([
{
$match:{
$and: [
{ accountId: req.session.passport.user },
{
'details.dateTimeIn': {
$gte: new Date(dateFrom), $lte: new Date(dateTo)
}
}
]
}
},
{
"$group": {
//_id: name,
_id: "$locationId",
count: { $sum: 1 }
}
},
{ $sort: sort }
])
Output is half ok:
[
{
"_id":"5a395ccf210a1d35d0df4a58",
"count":20
}
]
Instead of showing location id id like to show location name. Schema for locations collection is:
{
"_id": {
"$oid": "5a395ccf210a1d35d0df4a58"
"name": "Tower A",
"__v": 0
}
}
Research suggests I need to use $lookup to get that JOIN effect
So I tried
{
"$lookup": {
from: "locations",
localField: "_id",
foreignField: "_id",
as: "locationdetails"
}
}
but the match seems broken. The closest I got was a list of all locations in 'locationdetails'
But with code above here is the empty locationdetails
[
{
"_id":"5a395ddf1d221918d0041313",
"count":20,
"locationdetails":[
]
}
]
What am I missing ?
I have a mongoose model defined as such:
freelancerSchema = mongoose.Schema({
_id: { type: String, default: shortid.generate},
fname: String,
lname: String;
ratings: [{
rating: Number,
employer: {
type: String,
ref: 'Employer'
}
}],
...
}]
This schema represents a mongoose model for a Freelancer collection. My question is: in a certain query I need to find all freelancers with all their data and calculate the average rating for each of them. In the end, I would get an array of freelancers, each having their own calculated average rating preferably stored in a new field "avg_rating" or something like that.
I've tried looking into the mongodb Aggregate but I honestly didn't understand much.
Thanks in advance and sorry if my explanation wasn't precise enough.
If we are going to play code golf here, then the expression can be shortened:
Freelancer.aggregate([
{ "$addFields": {
"rating_avg": {
"$reduce": {
"input": "$ratings",
"initialValue": 0,
"in": {
"$add": [
"$$value",
{ "$divide": [ "$$this.rating", { "$size": "$ratings" } ] }
]
}
}
}
}},
{ "$sort": { "rating_avg": -1 } }
],function(err, results) {
res.send(results)
})
Or even a bit shorter using $avg and $map:
Freelancer.aggregate([
{ "$addFields": {
"rating_avg": {
"$avg": {
"$map": {
"input": "$ratings",
"as": "el",
"in": "$$el.rating"
}
}
}
}},
{ "$sort": { "rating_avg": -1 } }
],function(err, results) {
res.send(results)
})
And of course the shortest yet, being allowed since MongoDB 3.2 (modifying with $project of course):
Freelancer.aggregate([
{ "$addFields": {
"rating_avg": { "$avg": "$ratings.rating" }
}},
{ "$sort": { "rating_avg": -1 } }
],function(err, results) {
res.send(results)
})
All also using $addFields as an alternate to $project when using MongoDB 3.4, which is where $reduce becomes available. The second form when modified with $project also becomes valid for MongoDB 3.2, as is also true ( and noted ) of the third.
After messing around with my code and reading some other stacks, I found a solution that works fine for my needs:
Freelancer.aggregate(
[{
$project: {
fname: "$fname",
lname: "$lname",
rating_avg: {
$divide: [{
$reduce: {
input: "$ratings.rating",
initialValue: 0,
in: {
$sum: ["$$value", "$$this"]
}
}
}, {
$size: "$ratings"
}]
}
}
},
{
$sort: {
rating_avg: -1
}
}
],
function (err, results) {
res.send(results);
});
});
Hope this can help somebody else in the future.
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);