I have used SQL Server for a long time and just really learning MongoDB. I am trying to figure out how to do the aggregate finds to get just the data I want. Here is a sample from the database:
{
"_id": "1",
"user_id": "123-88",
"department_id": "1",
"type": "start",
"time": "2017-04-20T19:40:15.329Z"
}
{
"_id": "2",
"user_id": "123-88",
"department_id": "1",
"type": "stop",
"time": "2017-04-20T19:47:15.329Z"
}
What I want to do is find each unique user_id of department 1, only take the record with the latest time and tell me if they are oncall or not. So in the example above user 123-88 is not oncall now. How would you make this query? I know you will need something like this:
TimeCard.aggregate([
{ $match: {department_id: req.query.department_id}},
{ $sort: { user_id: 1, time: 1 }},
{ $group: { _id: "$user_id", current_type: "$type", lastTime: { $last: "$time" }}}
], function(err, docs){
if(err){console.log(err);}
else {res.json(docs);}
});
But I keep erroring so I know I am not correct in my logic. I know if I just have the match it works and if I add the sort it matches and sorts but the final group is not working. Also what would I add to then only show the people that are oncall. Thanks again for your help.
You can count how many "types" per user_id you have, this can be done by $sum, if it's odd, the user is oncall, because there is a start without a stop. This approach is correct only if you always have a stop for a start.
TimeCard.aggregate([
{ $match: { department_id: req.query.department_id } },
{ $sort: { user_id: 1, time: 1 } },
{ $group: { _id: "$user_id", count_types: { $sum: 1 }, lastTime: { $last: "$time" }}},
{ $match: { count_types: { $mod: [ 2, 1 ] } } },
], function(err, docs) {
if(err) { console.log(err); }
else { res.json(docs); }
});
Related
So what I want to do is group all documents having same hash whose count is more than 1 and only keep the oldest record according to startDate
My db structure is as follows:
[{
"_id": "82bacef1915f4a75e6a18406",
"Hash": "cdb3d507734383260b1d26bd3edcdfac",
"duration": 12,
"price": 999,
"purchaseType": "Complementary",
"startDate": {
"$date": {
"$numberLong": "1656409841000"
}
},
"endDate": {
"$date": {
"$numberLong": "1687859441000"
}
}
}]
I was using this query which I created
db.Mydb.aggregate([
{
"$group": {
_id: {hash: "$Hash"},
dups: { $addToSet: "$_id" } ,
count: { $sum : 1 }
}
},{"$sort":{startDate:-1}},
{
"$match": {
count: { "$gt": 1 }
}
}
]).forEach(function(doc) {
doc.dups.shift();
db.Mydb.deleteMany({
_id: {$in: doc.dups}
});
})
this gives a result like this:
{ _id: { hash: '1c01ef475d072f207c4485d0a6448334' },
dups:
[ '6307501ca03c94389f09b782',
'6307501ca03c94389f09b783',
'62bacef1915f4a75e6a18l06' ],
count: 3 }
The problem with this is that the _id's in dups array are random everytime I run this query i.e. not sorted according to startDate field.
What can be done here?
Any help is appreciated. Thanks!
After $group stage, startDate field will not pre present in the results, so you can not sort based on that field. So, as stated in the comments, you should put $sort stage first in the Aggregation pipeline.
db.Mydb.aggregate([
{
"$sort": { startDate: -1}
},
{
"$group": {
_id: {hash: "$Hash"},
dups: { $addToSet: "$_id" } ,
count: { $sum : 1 }
},
{
"$match": { count: { "$gt": 1 }
}
]
Got the solution. I was using $addToSet in the group pipeline stage which does not allow duplicate values. Instead, I used $push which allows duplicate elements in the array or set.
I'm trying to group values together in mongoose. I have a "Review" schema with the following fields:
{ userId, rating, comment }
There are many documents with the same userId. How can I retrieve them in the following format:
{userId: [...allRatings]
Or even better, is there a way to retrieve the averages for each userId? so like this: {userId: 2.8}
I know it's possible and very simple to do in node.js, but is there a way of doing it with mongoose?
Mongoose is really just a vehicle to pass commands to your mondoDB server, so accomplishing what you want in mongoose isn't dissimilar to accomplishing it in the mongo shell.
Here is the aggregation you're looking for:
db.collection.aggregate([
{
"$group": {
"_id": "$userId",
"ratings": {
$push: "$rating"
}
}
},
{
"$project": {
"_id": false,
"userId": "$_id",
"avgRating": {
"$avg": "$ratings"
}
}
}
])
The first stage of the pipeline groups all ratings by useId. The second stage calculates the ratings average and pretties up the key display. That's it. The result will be this:
[
{
"avgRating": 2.8,
"userId": 110
},
{
"avgRating": 3.275,
"userId": 100
}
]
Here is a playground for you: https://mongoplayground.net/p/yXVxk4klabB
As for how to specifically run this command in mongoose, well that's pretty straightforward:
const YourModel = mongoose.model('your_model');
...
YourModel.aggregate([
{
"$group": {
"_id": "$userId",
"ratings": {
$push: "$rating"
}
}
},
{
"$project": {
"_id": false,
"userId": "$_id",
"avgRating": {
"$avg": "$ratings"
}
}
}
])
.then(result => {
console.log(result);
})
This is the data from Mongodb
(Task.json)
{
"username": "john",
"taskId": "001",
"date": "2020-02-18T20:14:19.000Z",
},
{
"username": "david",
"taskId": "001",
"date": "2020-02-18T21:48:19.000Z",
},
{
"username": "john",
"taskId": "002",
"date": "2020-02-15T20:20:32.000Z",
}
... many more
What I am trying to acheive
I am trying a write a query that returns taskId with a list of the
latest users who executed the task (sorted by date - descending).
I only want the last three users who executed the task to show up on the list, thus the user array should not contain more the 3 users per task
Here is an example I created of how I want the response to be:
{
"tasks": [
{
"taskid": "001",
"users": [
{
"username": "david",
"date": "2020-02-18T21:48:19.000Z"
},
{
"username": "john",
"date": "2020-02-18T20:14:19.000Z"
}
]
},
{
"taskid": "002",
"users": [
{
"username": "john",
"date": "2020-02-15T20:20:32.000Z"
}
]
}
]
}
My progress so far:
router.route("/latest-tasks").get((req, res) => {
Task.find()
.sort({ date: "desc" })
.then(doc =>
res.status(200).json({
taskId: doc.taskId,
list: doc.map(doc => {
return {
username: doc.username,
date: doc.date
};
})
})
)
.catch(err => res.status(400).json("Error: " + err));
});
A bit of multi-query scenario here, I suggest you use MongoDB aggregation to do this. This query should work:
Todo.aggregate([
{
$sort: { taskId: 1, date: 1 }
},
{
$group: {
_id: "$taskId",
users: {
$push: { username: "$username", date: "$date" }
}
}
},
{
$project: {
_id: 0,
taskid: "$_id",
users: {
$filter: {
input: [
{ $arrayElemAt: ["$users", 0] },
{ $arrayElemAt: ["$users", 1] },
{ $arrayElemAt: ["$users", 2] }
],
as: "user",
cond: { $ne: ["$$user", null] }
}
},
}
}
]).exec()
.then(doc => { console.log(doc); })
.catch(err => { console.log(err); });
Aggregation Pipeline Explanation:
$sort: This sorts the tasks using the taskId and date field in an ascending order
$group: This group the tasks by taskId. This is how we get all users associated with a task
$project: This helps extract just the first 3 users associated with a task. The $filter operator used inside this pipeline stage helps to remove null values in case a task does not have up to three users associated with it.
Links: Aggregation Pipeline, Pipeline Stages, Pipeline Operators
You need to use .aggregate:
Mongoose code not tested
Task.aggregate([
{
$sort: {
taskId: 1,
date: -1
}
},
{
$group: {
_id: "$taskId",
users: {
$push: {
username: "$username",
date: "$date"
}
}
}
},
{
$facet: {
tasks: [
{
$sort: {
_id: 1
}
},
{
$project: {
_id: 0,
taskid: "$_id",
users: 1
}
}
]
}
}
]).exec()
.then( doc => res.status(200).json(doc))
.catch(err => res.status(400).json("Error: " + err));
MongoPlayground
Hi I am working on writing a Node/Express endpoint and I am trying to write a MongoDB query using Mongoose to return a sum value of my documents that have an expiration within the current day or month.
Currently, I am just trying to return the sum for the current day.
First I want to match all documents that contains the user's id.
Then I want to match all documents within the current day.
Lastly, I want to return the sum of the $amount field for all those
documents.
I know that the syntax for this is wrong and I am having a hard time find good examples matching what I am doing.
let now = Date.now(),
oneDay = 1000 * 60 * 60 * 24,
today = new Date(now - (now % oneDay)),
tomorrow = new Date(today.valueOf() + oneDay);
router.get("/user_current_month/:id", [jsonParser, jwtAuth], (req, res) => {
return Expense.aggregate([
{
$match: { user: new mongoose.Types.ObjectId(req.params.id) }
},
{
$match: {
$expiration: {
$gte: today,
$lt: tomorrow
}
}
},
{
$group: {
_id: "$month",
total: {
$sum: "$amount"
}
}
}
])
.then(sum => {
res.status(200).json(sum);
})
.catch(err => res.status(500).json({ message: "Something went terribly wrong!" }));
});
This is what a document looks like:
{
"_id": {
"$oid": "5daea018b3d92643fc2c8e6c"
},
"user": {
"$oid": "5d9929f065ef083d2cdbf66c"
},
"expense": "Post 1",
"expenseType": "One Time",
"notes": "",
"amount": {
"$numberDecimal": "100"
},
"expiration": {
"$date": "2019-10-22T06:22:01.628Z"
},
"status": "Active",
"createdAt": {
"$date": "2019-10-22T06:22:16.644Z"
},
"__v": 0
}
You need to pass the two match expressions in a AND clause and then group to perform the addition operation. You need to do something like this:
Expense.aggregate([
{ $match: { $and: [{ user: new mongoose.Types.ObjectId(req.params.id) },
{
$expiration: {
$gte: today,
$lt: tomorrow
}
} ]}},
{ $group: {
_id: "$month",
total: {
$sum: "$amount"
}
}}
]);
If I have a collection with documents looking more or less like this:
{
"_id": "52c06c86b78a091f26000001",
"desc": "Something somethingson",
"likes": [
{
"user_id": "52add4f4344e8ca867000001",
"username": "asd",
"created": 1390652212592
},
{
"user_id": "52add4f4344e8ca867000001",
"username": "asd",
"created": 1390652212592
}
],
"user_id": "52add4f4344e8ca867000001",
"username":"username"
}
Could I with mongodb (using nodejs & express) get a list of 10 users with the most liked posts?
I'm thinking it should be possible to do this Map-Reduce or with Aggregate, by grouping all posts by "user_id" and then count the total amount of items in the "likes" array. And then get the top 10 "user_id"s from that.
I'm guessing this is possible. Only problem is that I dont get the examples from the mongo website enough to put together my own thing.
Could anyone show me an example of doing this, or at least tell me its impossible if it is. =)
Try that out...
db.collection.aggregate([
{$unwind: "$likes"},
{$group: {_id: "$user_id", likesCount: {$sum: 1}}},
{$sort: {likesCount: -1}},
{$limit: 10},
{$project: {_id: 1}}
])
See this tutorial.
Yes, it is possible:
db.coll.aggregate([
{
$unwind: "$likes"
},
{
$group: {
_id: "$user_id",
likesPerUser: {
$sum:1
}
}
}
{
$sort: {
likesPerUser: -1
};
}
{
$limit: 10
}
])
If you need to count the number of likes element per parent user, you can do the following:
http://mongotry.herokuapp.com/#?bookmarkId=52fb8c604e86f9020071ed71
[
{
"$unwind": "$likes"
},
{
"$group": {
"_id": "$user_id",
"likesPerUser": {
"$sum": 1
}
}
},
{
"$sort": {
"likesPerUser": -1
}
},
{
"$limit": 10
}
]
Else if you need the number of liked user-id, counting 1 per sub-document cross all parents, you can do the following:
http://mongotry.herokuapp.com/#?bookmarkId=52fb8cf74e86f9020071ed72
[
{
"$unwind": "$likes"
},
{
"$group": {
"_id": "$likes.user_id",
"likesPerUser": {
"$sum": 1
}
}
},
{
"$sort": {
"likesPerUser": -1
}
},
{
"$limit": 1
}
]