Calculating average value per document with sort - node.js

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.

Related

MongoDB (Mongoose) - Get only records where nested item is equal to a value

I searched a lot, tried several ways, but nothing works.
I have this in mongo:
{
id: ObjectId("1234567890"),
answers: [{
id: 111,
deleted:0
},
{
id: 222,
deleted:0
},
{
id: 333,
deleted:1
}]
},
{
id: ObjectId("0987654321"),
answers: [{
id: 111,
deleted:0
},
{
id: 222,
deleted:1
},
{
id: 333,
deleted:1
}]
}
I want the document with ObjectId("1234567890"), and only the answers with delete = 1 ( only the id 333).
I have tryied this:
var query = chatbotMongoModel.find(
{ _id: "1234567890" },
{ answers: {$elemMatch: {deleted: "1"}}}
)
but returns all the answers.. Could you give me some help plase?
thanks!
Rafael
One way of doing this is using mongodb aggregation framework.
var result = await chatbotMongoModel.aggregate([
{
$match: {
id: "1234567890"
}
},
{
"$unwind": {
path: "$answers"
}
},
{
$match: {
"answers.deleted": 1
}
},
{
$group: {
_id: "$id",
answers: {
$push: "$answers"
},
allFields: {
$first: "$$ROOT"
}
}
},
{
"$replaceRoot": {
"newRoot": {
"$mergeObjects": [
"$allFields",
{
"answers": "$answers"
}
]
}
}
}
])
Note that in your sample documents you have id but you use _id in your query, they must match. Also deleted data type is number in sample documents, but you use string value in your query.
Running mongoplayground.
Another alternative solution:
var result = await chatbotMongoModel.aggregate([
{
$match: {
id: "1234567890"
}
},
{
$addFields: {
"answers": {
$filter: {
input: "$answers",
as: "answer",
cond: {
$eq: [
"$$answer.deleted",
1
]
}
}
}
}
}
])
mongoplayground
Thanks for the answers! I found a solution, and im posting here if someone needs this too...
var query = model.aggregate([
{ $match: { '_id': mongoose.Types.ObjectId("1234567890") } },
{
$project: {
answer: {
$filter: {
input: "$answer",
as: "f",
cond: { $eq: ["$$f.deleted", 1] }
}
}
}
}
])
regards!

How to find the latest date in nested array of objects (MongoDB)

I am trying to find the latest "order" in "orders" array in the whole collection (Not only in the one object).
Data:
[
{
_id: 1,
orders: [
{
title: 'Burger',
date: {
$date: '2021-07-18T13:12:08.717Z',
},
},
],
},
{
_id: 2,
orders: [
{
title: 'Salad',
date: {
$date: '2021-07-18T13:35:01.586Z',
},
},
],
},
];
Code:
var restaurant = await Restaurant.findOne({
'orders.date': 1,
});
Rather simple:
db.collection.aggregate([
{ $project: { latest_order: { $max: "$orders.date" } } }
])
If you like to get the full order use this:
db.collection.aggregate([
{
$project: {
latest_order: {
$first: {
$filter: {
input: "$orders",
cond: { $eq: [ "$$this.date", { $max: "$orders.date" } ] }
}
}
}
}
},
{ $sort: { "latest_order.date": 1 } },
{ $limit: 1 }
])
Mongo Playground
You have to use aggregation for that
db.collection.aggregate([
{ $unwind: "$orders" },
{ $sort: { "orders.date": -1 } },
{ $limit: 1 },
{
"$group": {
"_id": "$_id",
"orders": { "$first": "$orders" }
}
}
])
Working Mongo playground

How to count filtered results in $match in mongodb?

I have an aggregation query like this
nSkip=4, count=25, sortOn='first_name' , order=-1, toMatch='biker' // all variables are dynamic
query={status: true, roles: { $regex: toMatchRole, $options: "m" }} // also dynamic
User.aggregate([
{
$match: query
},
// after this I need the total number of documents that matched the criteria,
// before sorting or skipping or limiting in "total_count" variable
{
$sort: {
[sortOn]: order
}
},
{
$skip: nSkip
},
{
$limit: count
},
{
$project: {
last_name: 1,
first_name: 1,
email: 1
}
}
])
User Collection
{
_id:60befdcfa4198332b728f9cd",
status:false,
roles:["biker"],
email:"john#textmercato.com",
last_name:"aggr",
first_name:"john",
}
I am not sure how to achieve this without disturbing the rest of the stages in aggregation. Can someone please help me out.
You can use $group
{
"$group": {
"_id": null,
"data": { "$push": "$$ROOT" },
"total_count": { $sum: 1 }
}
},
{ $unwind: "$data" },
{
"$replaceRoot": {
"newRoot": {
"$mergeObjects": [ "$$ROOT", "$data" ]
}
}
}
and finally Project the total_count
Working Mongo playground

$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));
});

How to match and sort documents based on array elements in common

var UserSchema = Schema (
{
android_id: String,
created: {type: Date, default:Date.now},
interests: [{ type: Schema.Types.ObjectId, ref: 'Interests' }],
});
Users.aggregate([
{ $match: {android_id: {$ne: userID}, interests: {$elemMatch: {$in: ids}} }},
{ $group: { _id: { android_id: '$android_id'},count: {$sum: 1}}},
{ $sort: {count: -1}},
{ $limit: 5 }],
I need the to find the top 5 android_ids of the users with the most interests in common with me (ids array). I can work with the array of only matched elements from the interests array too.
You seemed to be going along the right lines here but you do need to consider that arrays have special considerations for comparisons.
Your basic start here is to find all users that are not the current user, and that you also need at least the "interests" array of the current user as well. You seem to be doing that already, but for here let us consider that you have the whole user object for the current user which will be used in the listing.
This makes your "top 5" basically a product of "Not me, and the most interests in common", which means you basically need to count the "overlap" of interests on each user compared to the current user.
This is basically the $setIntersection of the two arrays or "sets" where the elements in common are returned. In order to count how many are in common, there is also the $size operator. So you apply like this:
Users.aggregate(
[
{ "$match": {
"android_id": { "$ne": user.android_id },
"interests": { "$in": user.interests }
}},
{ "$project": {
"android_id": 1,
"interests": 1,
"common": {
"$size": {
"$setIntersection": [ "$interests", user.interests ]
}
}
}},
{ "$sort": { "common": -1 } },
{ "$limit": 5 }
],
function(err,result) {
}
);
The result returned in "common" is the count of common interests between the current user and the user being examined in the data. This data is then processed by $sort in order to put the largest number of common interests on top, and then $limit returns only the top 5.
If for some reason your MongoDB version is presently lower than MongoDB 2.6 where both the $setIntersection and $size operators are introduced, then you can still do this, but it just takes a longer form of processing the arrays.
Mainly you need to $unwind the arrays and process each match individually:
{ "$match": {
"android_id": { "$ne": user.android_id },
"interests": { "$in": user.interests }
}},
{ "$unwind": "$interests" },
{ "$group": {
"_id": "$_id",
"android_id": { "$first": "$android_id" },
"interests": { "$push": "$interests" },
"common": {
"$sum": {
"$add": [
{ "$cond": [{ "$eq": [ "$interests", user.interests[0] ] },1,0 ] },
{ "$cond": [{ "$eq": [ "$interests", user.interests[1] ] },1,0 ] },
{ "$cond": [{ "$eq": [ "$interests", user.interests[2] ] },1,0 ] }
]
}
}
}},
{ "$sort": { "common": -1 }},
{ "$limit": 5 }
Which is more practically coded to generate the condtional matches in the pipeline:
var pipeline = [
{ "$match": {
"android_id": { "$ne": user.android_id },
"interests": { "$in": user.interests }
}},
{ "$unwind": "$interests" }
];
var group =
{ "$group": {
"_id": "$_id",
"android_id": { "$first": "$android_id" },
"interests": { "$push": "$interests" },
"common": {
"$sum": {
"$add": []
}
}
}};
user.interests.forEach(function(interest) {
group.$group.common.$sum.$add.push(
{ "$cond": [{ "$eq": [ "$interests", interest ] }, 1, 0 ] }
);
});
pipeline.push(group);
pipeline = pipeline.concat([
{ "$sort": { "common": -1 }},
{ "$limit": 5 }
])
User.aggregate(pipeline,function(err,result) {
});
The key elements there being that "both" the current user and the user being inspected have their "interests" separated out for comparison to see if they are "equal". The result from $cond attributes a 1 where this is true or 0 where false.
Any returns ( and only ever expected to be 1 at best, per pair ) are passed to the $sum accumulator which counts the matches in common. You can alternately $match with an $in condition again:
{ "$unwind": "$interests" },
{ "$match": { "interests": { "$in": user.interests } },
{ "$group": {
"_id": "$_id",
"android_id": { "$first": "$android_id" },
"common": { "$sum": 1 }
}}
But this is naturally destructive of the array content as non matches are filtered out. So it depends on what you would rather have in the response.
That is the basic process for getting the "common" counts for use in further processing like $sort and $limit in order to get your "top 5".
Just for fun, here is a basic node.js listing to show the effects of common matches:
var async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.connect('mongodb://localhost/sample');
var interestSchema = new Schema({
name: String
});
var userSchema = new Schema({
name: String,
interests: [{ type: Schema.Types.ObjectId, ref: 'Interest' }]
});
var Interest = mongoose.model( 'Interest', interestSchema );
var User = mongoose.model( 'User', userSchema );
var interestHash = {};
async.series(
[
function(callback) {
async.each([Interest,User],function(model,callback) {
model.remove({},callback);
},callback);
},
function(callback) {
async.each(
[
"Tennis",
"Football",
"Gaming",
"Cooking",
"Yoga"
],
function(interest,callback) {
Interest.create({ name: interest},function(err,obj) {
if (err) callback(err);
interestHash[obj.name] = obj._id;
callback();
});
},
callback
);
},
function(callback) {
async.each(
[
{ name: "Bob", interests: ["Tennis","Football","Gaming"] },
{ name: "Tom", interests: ["Football","Cooking","Yoga"] },
{ name: "Sue", interests: ["Tennis","Gaming","Yoga","Cooking"] }
],
function(data,callback) {
data.interests = data.interests.map(function(interest) {
return interestHash[interest];
});
User.create(data,function(err,user) {
//console.log(user);
callback(err);
})
},
callback
);
},
function(callback) {
async.waterfall(
[
function(callback) {
User.findOne({ name: "Bob" },callback);
},
function(user,callback) {
console.log(user);
User.aggregate(
[
{ "$match": {
"_id": { "$ne": user._id },
"interests": { "$in": user.interests }
}},
{ "$project": {
"name": 1,
"interests": 1,
"common": {
"$size": {
"$setIntersection": [ "$interests", user.interests ]
}
}
}},
{ "$sort": { "common": -1 } }
],
function(err,result) {
if (err) callback(err);
Interest.populate(result,'interests',function(err,result) {
console.log(result);
callback(err);
});
}
);
}
],
callback
);
}
],
function(err) {
if (err) throw err;
//console.dir(interestHash);
mongoose.disconnect();
}
);
Which will output:
{ _id: 55dbd7be0e5516ac16ea62d1,
name: 'Bob',
__v: 0,
interests:
[ 55dbd7be0e5516ac16ea62cc,
55dbd7be0e5516ac16ea62cd,
55dbd7be0e5516ac16ea62ce ] }
[ { _id: 55dbd7be0e5516ac16ea62d3,
name: 'Sue',
interests:
[ { _id: 55dbd7be0e5516ac16ea62cc, name: 'Tennis', __v: 0 },
{ _id: 55dbd7be0e5516ac16ea62ce, name: 'Gaming', __v: 0 },
{ _id: 55dbd7be0e5516ac16ea62d0, name: 'Yoga', __v: 0 },
{ _id: 55dbd7be0e5516ac16ea62cf, name: 'Cooking', __v: 0 } ],
common: 2 },
{ _id: 55dbd7be0e5516ac16ea62d2,
name: 'Tom',
interests:
[ { _id: 55dbd7be0e5516ac16ea62cd, name: 'Football', __v: 0 },
{ _id: 55dbd7be0e5516ac16ea62cf, name: 'Cooking', __v: 0 },
{ _id: 55dbd7be0e5516ac16ea62d0, name: 'Yoga', __v: 0 } ],
common: 1 } ]

Resources