I have two functions to get an array from monga using aggregation. The functions are completely the same, except for one pipeline - "match ({startTime: {$ gte: start}})". How can I leave one function and add a "math" only by the presence of the "start" variable, which is a date filter?
let groupedByUserSessions
if (lastDay) {
const start = getDateFromString(lastDay);
groupedByUserSessions = await getValuesByDate(start)
} else {
groupedByUserSessions = await getAllValues();
}
The functions are completely the same,
function getValuesByDate(start) {
return Sessions.aggregate()
.match({ startTime: { $gte: start } })
.group({
_id: { departament: "$departament", userAdName: "$userAdName" },
cleanTime: { $sum: { $subtract: ["$commonTime", "$idlingTime"] } }
})
.group({
_id: { departament: "$_id.departament"},
users: { $push: {value: '$cleanTime', name: '$_id.userAdName'} },
commonCleanTime: { $sum: "$cleanTime" }
})
.project({
departament: '$_id.departament',
users: '$users',
commonCleanTime: '$commonCleanTime',
performance: { $divide: [ "$commonCleanTime", { $size: "$users" }] }
});
}
You can break the pipeline into a "head" and a "tail" and construct the head differently when the start argument is given:
function getValuesByDate(start) {
let agg = Sessions.aggregate();
if (start) {
agg = agg.match({ startTime: { $gte: start } });
}
return agg
.group({
_id: { departament: '$departament', userAdName: '$userAdName' },
cleanTime: { $sum: { $subtract: ['$commonTime', '$idlingTime'] } },
})
.group({
_id: { departament: '$_id.departament' },
users: { $push: { value: '$cleanTime', name: '$_id.userAdName' } },
commonCleanTime: { $sum: '$cleanTime' },
})
.project({
departament: '$_id.departament',
users: '$users',
commonCleanTime: '$commonCleanTime',
performance: { $divide: ['$commonCleanTime', { $size: '$users' }] },
});
}
Related
My MongoDB schema (simplified):
user: ObjectID
calories: Number
meals:[{
calories: Number
name:String
}]
And I have a updateMany query:
await Meals.updateMany(
{ user: user, 'meals.name': extraMealName },
{ $inc: { calories: 'meals.$.calories' } },
{multi : true},
function(error, result) {
console.log(error);
}
);
The query throws me this error:
CastError: Cast to Number failed for value "meals.$.calories" at path "calories"
I have tried changing the query for the last hour, but nothing worked... I also browsed stackoverflow, but found nothing I could work with
Does someone have an idea how to fix this?
Using pipelined update,
$reduce, go through the meals array and add up the calories where name=extraMealName
$subtract from calories, the sum from previous step
mongoplayground
db.Meals.update({
user: "user", "meals.name": "extraMealName"
},
[
{
$set: {
calories: {
$subtract: [
"$calories",
{
$reduce: {
input: "$meals",
initialValue: 0,
in: {
$add: [
"$$value",
{
$cond: [
{$eq: ["$$this.name", "extraMealName"]},
"$$this.calories",
0
]
}
]
}
}
}
]
}
}
}
]);
Updated for multiple fields.
db.collection.update({
user: "user", "meals.name": "extraMealName"
},
[
{
$addFields: {
reducedValues: {
$reduce: {
input: "$meals",
initialValue: {
calories: 0, fat: 0
},
in: {
calories: {
$add: [
"$$value.calories",
{
$cond: [
{$eq: ["$$this.name", "extraMealName"]},
"$$this.calories",
0
]
}
]
},
fat: {
$add: [
"$$value.fat",
{
$cond: [
{$eq: ["$$this.name", "extraMealName"]},
"$$this.fat",
0
]
}
]
}
}
}
}
}
},
{
$set: {
"calories": {
$subtract: ["$calories", "$reducedValues.calories"]
},
"fat": {
$subtract: ["$fat", "$reducedValues.fat"]
},
}
}
]);
Playground
the $inc has a syntax error, $inc expects a number not string so try some like this.
await Meals.updateMany(
{ user: user, 'meals.name': extraMealName },
{ $inc: { calories: { $sum: '$meals.$.calories' } } },
{ multi: true },
function(error, result) {
console.log(error);
}
);
I currently have a Mongo query that looks like this:
const user = await User.findOne({ userId }).lean() || []
const contributions = await Launch.aggregate([
{ $sort: { addedAt: -1 } },
{ $limit: 10 },
{
$match: {
_id: { $in: user.contributions }
}
},
{
$addFields: {
activity: 'contribution',
launchName: '$name',
launchId: '$_id',
date: '$addedAt',
content: '$description'
}
}
])
But instead of having two different Mongo queries (findOne and aggregate), how can I combine them into one query?
I tried this but it just errors out immediately in the lookup part:
const contributions = await Launch.aggregate([
{ $sort: { addedAt: -1 } },
{ $limit: 10 },
{
$lookup: {
from: 'user',
let: { id: $user.contributions },
pipeline: [
{ $match: { $expr: { $in: [$_id, $$user.contributions] } } }
],
localField: '_id',
foreignField: 'userId',
as: 'user'
}
},
{
$addFields: {
activity: 'contribution',
launchName: '$name',
launchId: '$_id',
date: '$addedAt',
content: '$description'
}
}
])
I've never used the pipeline option so a little confused onn how to fix this problem?
Enclose these $user.contributions, $_id with quotes in order to make the query valid.
Since you declare the id variable with the value of user.contributions. You should use the variable with $$id instead of $$user.contributions.
I don't think the localField and foreignField are needed as you are mapping/joining with pipeline.
Your aggregation query should be looked as below:
const contributions = await Launch.aggregate([
{ $sort: { addedAt: -1 } },
{ $limit: 10 },
{
$lookup: {
from: 'user',
let: { id: "$user.contributions" },
pipeline: [
{ $match: { $expr: { $in: ["$_id", "$$id"] } } }
],
as: 'user'
}
},
{
$addFields: {
activity: 'contribution',
launchName: '$name',
launchId: '$_id',
date: '$addedAt',
content: '$description'
}
}
])
update so Mohammad Faisal has the best solution.However it breaks when a new document is added lol! so i learned a lot from his code and modified it and it Works! =) the code is all the way in the bottom.
But here's what i said..
So i have this document
{"_id":"5ddea2e44eb407059828d740",
"projectname":"wdym",
"username":"easy",
"likes":0,
"link":["ssss"]
}
{"_id":"5ddea2e44eb407059822d740",
"projectname":"thechosenone",
"username":"easy",
"likes":30,
"link":["ssss"]
}
{"_id":"5ddea2e44eb407059828d740",
"projectname":"thanos",
"username":"wiley",
"likes":10,
"link":["ssss"]
}
and basically what i want is the document that contains the highest
likes with it's associated project name
For example the output would be
"projectname":"thechosenone",
"username":"easy",
"likes":30
}
,
{
"projectname":"thanos",
"username":"wiley",
"likes":10,
}
the code i have for this is the following
db
.collection("projects")
.aggregate([
{
$group: {
_id: { username: "$username" },
likes: { $max: "$likes" }
}
},
{
$project:{projectname:1}
}
])
$project gives me a strange output. However,
the output was correct without the $project.
But i wanted to project the projectname, the user and the highest likes. Thanks for hearing me out :)
heres the solution =)
db
.collection("projects")
.aggregate([
{
$sort: {
likes: -1
}
},
{
$group: {
_id: {
username: "$username"
},
likes: {
$max: "$likes"
},
projectname: {
$push: "$projectname"
},
link: {
$push: "$link"
}
}
},
{
$project: {
username: "$_id.username",
projectname: {
$arrayElemAt: ["$projectname", 0]
},
link: {
$arrayElemAt: ["$link", 0]
}
}
}
])
.toArray()
If you don't have to use $group this will solve your problem:
db.projects.aggregate([
{$sort:{likes:-1}},
{$limit:1}
]).pretty()
the result would be
{
"_id" : ObjectId("5ddee7f63cee7cdf247059db"),
"projectname" : "thechosenone",
"username" : "easy",
"likes" : 30,
"links" : ["ssss"]
}
Try this:-
db.collection("projects").aggregate([
{
$group: {
_id: { username: "$username" },
likes: { $max: "$likes" },
projectname: { $push : { $cond: [ { $max: "$likes" }, "$projectname", "" ]}}
}
}
,
{
$project:{
username:"$_id.username",
projectname:{"$reduce": {
"input": "$projectname",
"initialValue": { "$arrayElemAt": ["$projectname", 0] },
"in": { "$cond": [{ "$ne": ["$$this", ""] }, "$$this", "$$value"] }
}},
likes:1
}
}
])
I'm working on a node.js project to display some data using charts and tables on the front end.
I have the two following queries on my route:
atendimentos.find({})
.then(atendimentos => {
final = atendimentos.filter(atendimentos => atendimentos.status === 'F')
testeea = atendimentos.filter(atendimentos => atendimentos.status === 'EA')
res.render('home', {user: req.user, fin: final.length, ea: testeea.length});
//Funciona
console.log(final.length)
})
.catch(err => console.error(err));
atendimentos.aggregate([
{ $project:
{ _id: "$month",
year: {$year: "$date" },
month: { $month: "$date"},
amount: 1
}
},
{ $group:
{ _id: { year: "$year", month: ("$month")},
sum: { $sum: 1}
}
}]).exec(function(error, items){
if(error){return next(error);}
console.log(items);
});
EDIT 1:
So, the input data... I guess that I don't have any because I'm actually fetching everything from the database through my queries. The data that I expect, are the documents/object with the status F or EA which I'm rendering on my chart.
The database has around 8.5k documents, the F one returns 8041 documents and the EA returns 351, it is a simple array with the number that is returned using .length on my route. Those numbers are rendered on the chart.
Now, related to the aggregation part, I'm trying to make a table using the collection. I intend to show the number of support calls (atendimentos) per month. It's actually logging the correct data like this:
[ { _id: { year: 2018, month: 6 }, sum: 4005 },
{ _id: { year: 2018, month: 7 }, sum: 43 },
{ _id: { year: 2018, month: 5 }, sum: 3996 },
{ _id: { year: 2018, month: 4 }, sum: 434 } ]
And I want to use this data to render the table on my view.
END OF EDIT 1
EDIT 2
router.get('/home', isAuthenticated, async (req, res, next) => {
let final;
let testeea;
atendimentos.find({})
.then(atendimentos => {
final = atendimentos.filter(atendimentos => atendimentos.status === 'F')
testeea = atendimentos.filter(atendimentos => atendimentos.status === 'EA')
res.render('home', {user: req.user, fin: final.length, ea: testeea.length});
//Funciona
console.log(final.length)
})
.catch(err => console.error(err));
So, here's the route, the other part is just the aggregation query that I've tried to do and closing brackets. As you can see, I get the data and use Array.filter to filter the results fetched, using status = F or = EA.
It returns me the length of the array, so it counts the number of status with each letter. This number is rendered in the chart, because I'm sending it to the front end as fin: final.length and ea: testeea.length. No formatted data or something like that in here. It's okay this way.
Related to the aggregation part where it returns the calls per month, I want to use just the number of calls, month and year. In this part I expected the data like: [ { _id: { year: 2018, month: 6 }, sum: 4005 }
I wish I could fetch the data the same way as I've fetched the fin and ea, using .length to count and put it into the view.
END OF EDIT 2
Both are returning exactly what I need, the problem is that I can't just put the aggregation query before the find query and add items: items to the render method. I would like to know how do I do these queries to display the same that that I'm fetching on these two queries. Thanks in advance!
MongoDB Server 3.2 and below
You need to run two aggregate queries and merge the objects in the results. This can be done in a multiple ways but can show you the Promise way and the async/await approach.
1. Using Promises
router.get('/home', isAuthenticated, (req, res, next) => {
const counts = atendimentos.aggregate([
{ '$group': {
'_id': null,
'fin': {
'$sum': {
'$cond': [ { '$eq': [ '$status', 'F' ] }, 1, 0 ]
}
},
'ea': {
'$sum': {
'$cond': [ { '$eq': [ '$status', 'EA' ] }, 1, 0 ]
}
}
} }
]).exec();
const monthly = atendimentos.aggregate([
{ '$group': {
'_id': {
'year': { '$year': '$date' },
'month': { '$month': '$date' }
},
'sum': { '$sum': 1 }
} },
{ '$group': {
'_id': null,
'back': { '$push': '$$ROOT' }
} },
]).exec();
Promise.all([ counts, monthly ]).then(([ counts, monthly ]) => {
const statusData = counts[0];
const monthlyData = monthly[0];
const data = {...statusData, ...monthlyData, user: req.user};
console.log(JSON.stringify(data, null, 4));
res.render('home', data);
}).catch(err => next(err));
});
2. Using async/await
router.get('/home', isAuthenticated, async (req, res, next) => {
try {
const counts = await atendimentos.aggregate([
{ '$group': {
'_id': null,
'fin': {
'$sum': {
'$cond': [ { '$eq': [ '$status', 'F' ] }, 1, 0 ]
}
},
'ea': {
'$sum': {
'$cond': [ { '$eq': [ '$status', 'EA' ] }, 1, 0 ]
}
}
} }
]).exec();
const monthly = await atendimentos.aggregate([
{ '$group': {
'_id': {
'year': { '$year': '$date' },
'month': { '$month': '$date' }
},
'sum': { '$sum': 1 }
} },
{ '$group': {
'_id': null,
'back': { '$push': '$$ROOT' }
} },
]).exec();
const statusData = counts[0];
const monthlyData = monthly[0];
const data = {...statusData, ...monthlyData, user: req.user};
console.log(JSON.stringify(data, null, 4));
res.render('home', data);
} catch (err) {
next(err);
}
});
MongoDB Server 3.4.4 and above
The aggregation pipeline can also handle filtering, you just need to use the $facet pipeline step which is capable of processing multiple aggregation pipelines within a single stage on the same set of input documents. Each sub-pipeline has its own field in the output document where its results are stored as an array of documents.
Consider running the following pipeline:
atendimentos.aggregate([
{ '$facet': {
'counts': [
{ '$group': {
'_id': null,
'fin': {
'$sum': {
'$cond': [ { '$eq': [ '$status', 'F' ] }, 1, 0 ]
}
},
'ea': {
'$sum': {
'$cond': [ { '$eq': [ '$status', 'EA' ] }, 1, 0 ]
}
}
} }
],
'monthly': [
{ '$group': {
'_id': {
'year': { '$year': '$date' },
'month': { '$month': '$date' }
},
'sum': { '$sum': 1 }
} },
{ '$group': {
'_id': null,
'items': { '$push': '$$ROOT' }
} },
]
} },
{ '$replaceRoot': {
'newRoot': {
'$mergeObjects': {
'$concatArrays': ['$counts', '$monthly']
}
}
} }
]).exec((err, results) => {
const data = results[0];
console.log(data);
res.render('home', { user: req.user, ...data });
})
Model.aggregate([
{
'$group': {
'_id': '$id',
'name': { '$first': '$name' },
'tof': { $sum: { $eq:["$tof",true] } },
}
}
]) .... // rest of the code.
I am trying to sum the tof (true or false) field, but only if the value is true, but is not working like the way i am trying..
What i am doing wrong here? How proceed?
Thanks!!
Instead of extracting and iterating, as you would in this case, documents you don't need using $cond you can just omit them from the find:
Model.aggregate([
{
'$match': {'tof': true}
},
{
'$group': {
'_id': '$id',
'name': { '$first': '$name' },
'tof': { $sum: 1 },
}
}
])
However, since you are using multiple counts you need $cond ( http://docs.mongodb.org/manual/reference/operator/aggregation/cond/ ) so here is an example:
Model.aggregate([
{
'$group': {
'_id': '$id',
'name': { '$first': '$name' },
'tof': { $sum: {$cond: [{$eq:['$tof', true]}, 1, 0]} },
}
}
])