Fetch grouped items from database - node.js

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

Related

How to delete documents in MongoDb according to date

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.

MongoDB aggregation: Check if user has already rated a product

I'm having some trouble coming up with a solution to this problem by using mongo directly instead of node.
I have a collection of products that have several fields, including an array of ratings. The ratings consist of the id of the user who rated it and the score. They look like this:
{
"_id": {
"$oid": "62812fac06f29f5fe874c0db"
},
"model": "fire",
"allRatings": [
{
"user_id": "6282962e3aa8163084776092",
"rating": 9
},
{
"user_id": "62811d520f52b64990a94e99",
"rating": 6
}
],
}
I'd like to return all of the fields, but instead of all the ratings, I just want to return a boolean that shows if the current user, whose ID I have in the code, has already rated the product. I have tried with aggregations, but haven't found any solution that works.
Something like this:
{
"_id": {
"$oid": "62812fac06f29f5fe874c0db"
},
"model": "fire",
"hasRated": true,
}
If I've understood correctly you can use this aggregation query:
This query looks for the object you want (matching by _id) and then, into a $project stage it set the value hasRated if the desired id exists into the array.
db.collection.aggregate([
{
$match: {
_id: ObjectId("62812fac06f29f5fe874c0db")
}
},
{
$project: {
model: 1,
hasRated: {
"$in": [
"6282962e3aa8163084776092",
"$allRatings.user_id"
]
}
}
}
])
Example here
To do in JS you can simply create the object adding the id as a variable (something like this, is pseudocode, not tested):
const id = 'your_id_here'
const agg = [{
$match: {
_id: new ObjectId("62812fac06f29f5fe874c0db")
}
},
{
$project: {
model: 1,
hasRated: {
"$in": [
id,
"$allRatings.user_id"
]
}
}
}]
db.aggregate(agg)

Node.js calling MongoDB with aggregate find

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

Finding only an item in an array of arrays by value with Mongoose

Here is an example of my Schema with some data:
client {
menus: [{
sections: [{
items: [{
slug: 'some-thing'
}]
}]
}]
}
And I am trying to select it like this:
Schema.findOne({ client._id: id, 'menus.sections.items.slug': 'some-thing' }).select('menus.sections.items.$').exec(function(error, docs){
console.log(docs.menus[0].sections[0].items[0].slug);
});
Of course "docs.menus[0].sections[0].items[0].slug" only works if there is only one thing in each array. How can I make this work if there is multiple items in each array without having to loop through everything to find it?
If you need more details let me know.
The aggregation framework is good for finding things in deeply nested arrays where the positional operator will fail you:
Model.aggregate(
[
// Match the "documents" that meet your criteria
{ "$match": {
"menus.sections.items.slug": "some-thing"
}},
// Unwind the arrays to de-normalize as documents
{ "$unwind": "$menus" },
{ "$unwind": "$menus.sections" },
{ "$unwind": "$menus.sections.items" }
// Match only the element(s) that meet the criteria
{ "$match": {
"menus.sections.items.slug": "some-thing"
}}
// Optionally group everything back to the nested array
// One step at a time
{ "$group": {
"_id": "$_id",
"items": { "$push": "$menus.sections.items.slug" }
}},
{ "$group": {
"_id": "$_id",
"sections": {
"$push": { "items": "$items" }
}
}},
{ "$group": {
"_id": "$_id",
"menus": {
"$push": { "sections": "$sections" }
}
}},
],
function(err,results) {
}
)
Also see the other aggregation operators such as $first for keeping other fields in your document when using $group.

mongodb get users with most liked posts

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
}
]

Resources