Tricky Mongoose Aggregation Query - node.js

The following schema is given:
LogSchema {
...
likes: Number,
author: User,
created_at: Date
...
}
The following query seems to be very tricky:
Get the top X users who have the most likes for all today created logs.
The result should look like this:
User x: 1000 likes
User y: 558 likes
etc.
I have no clue how I can attack that. I know, that I could use some Aggregation, but how should it be done here, since likes are bound to users somehow.

Here is one example with commands .aggregate() used under Mongo Shell with test data
{ "_id" : ObjectId("56c912a1ebf94ca549e4ab8f"), "likes" : 123, "author" : "x", "created_at" : ISODate("2016-02-21T01:28:01.549Z") }
{ "_id" : ObjectId("56c912aaebf94ca549e4ab90"), "likes" : 120, "author" : "x", "created_at" : ISODate("2016-02-21T01:28:10.116Z") }
{ "_id" : ObjectId("56c912b4ebf94ca549e4ab91"), "likes" : 12, "author" : "y", "created_at" : ISODate("2016-02-21T01:28:20.996Z") }
{ "_id" : ObjectId("56c912bbebf94ca549e4ab92"), "likes" : 22, "author" : "y", "created_at" : ISODate("2016-02-21T01:28:27.644Z") }
Commands
> var d = new Date();
> d.setHours(0, 0, 0); // set to the first second of today, used for query today logs
> db.log.aggregate([
// query today logs by `create_at`
{$match: {created_at: {$gt: d}}},
// group by author and sum the `likes`
{$group: {_id: '$author', likes: {$sum: '$likes'}}}
]).map(function(val) {
// map the field per request
return {[val._id]: val.likes}
});
Result
[ { "y" : 34 }, { "x" : 243 } ]
With mongoose aggregate, please try it
var p = Log.aggregate([
// query today logs by `create_at`
{$match: {created_at: {$gt: d}}},
// group by author and sum the `likes`
{$group: {_id: '$author', likes: {$sum: '$likes'}}}
]).exec();
p.then(function(vals){
return vals.map(function(val) {
return {[val._id]: val.likes};
});
});

Related

How to query two mongoose collections for a given graphql union type? [duplicate]

users
{
"_id":"12345",
"admin":1
},
{
"_id":"123456789",
"admin":0
}
posts
{
"content":"Some content",
"owner_id":"12345",
"via":"facebook"
},
{
"content":"Some other content",
"owner_id":"123456789",
"via":"facebook"
}
Here is a sample from my mongodb. I want to get all the posts which has "via" attribute equal to "facebook" and posted by an admin ("admin":1). I couldn't figure out how to acquire this query. Since mongodb is not a relational database, I couldn't do a join operation. What could be the solution ?
You can use $lookup ( multiple ) to get the records from multiple collections:
Example:
If you have more collections ( I have 3 collections for demo here, you can have more than 3 ). and I want to get the data from 3 collections in single object:
The collection are as:
db.doc1.find().pretty();
{
"_id" : ObjectId("5901a4c63541b7d5d3293766"),
"firstName" : "shubham",
"lastName" : "verma"
}
db.doc2.find().pretty();
{
"_id" : ObjectId("5901a5f83541b7d5d3293768"),
"userId" : ObjectId("5901a4c63541b7d5d3293766"),
"address" : "Gurgaon",
"mob" : "9876543211"
}
db.doc3.find().pretty();
{
"_id" : ObjectId("5901b0f6d318b072ceea44fb"),
"userId" : ObjectId("5901a4c63541b7d5d3293766"),
"fbURLs" : "http://www.facebook.com",
"twitterURLs" : "http://www.twitter.com"
}
Now your query will be as below:
db.doc1.aggregate([
{ $match: { _id: ObjectId("5901a4c63541b7d5d3293766") } },
{
$lookup:
{
from: "doc2",
localField: "_id",
foreignField: "userId",
as: "address"
}
},
{
$unwind: "$address"
},
{
$project: {
__v: 0,
"address.__v": 0,
"address._id": 0,
"address.userId": 0,
"address.mob": 0
}
},
{
$lookup:
{
from: "doc3",
localField: "_id",
foreignField: "userId",
as: "social"
}
},
{
$unwind: "$social"
},
{
$project: {
__v: 0,
"social.__v": 0,
"social._id": 0,
"social.userId": 0
}
}
]).pretty();
Then Your result will be:
{
"_id" : ObjectId("5901a4c63541b7d5d3293766"),
"firstName" : "shubham",
"lastName" : "verma",
"address" : {
"address" : "Gurgaon"
},
"social" : {
"fbURLs" : "http://www.facebook.com",
"twitterURLs" : "http://www.twitter.com"
}
}
If you want all records from each collections then you should remove below line from query:
{
$project: {
__v: 0,
"address.__v": 0,
"address._id": 0,
"address.userId": 0,
"address.mob": 0
}
}
{
$project: {
"social.__v": 0,
"social._id": 0,
"social.userId": 0
}
}
After removing above code you will get total record as:
{
"_id" : ObjectId("5901a4c63541b7d5d3293766"),
"firstName" : "shubham",
"lastName" : "verma",
"address" : {
"_id" : ObjectId("5901a5f83541b7d5d3293768"),
"userId" : ObjectId("5901a4c63541b7d5d3293766"),
"address" : "Gurgaon",
"mob" : "9876543211"
},
"social" : {
"_id" : ObjectId("5901b0f6d318b072ceea44fb"),
"userId" : ObjectId("5901a4c63541b7d5d3293766"),
"fbURLs" : "http://www.facebook.com",
"twitterURLs" : "http://www.twitter.com"
}
}
Trying to JOIN in MongoDB would defeat the purpose of using MongoDB. You could, however, use a DBref and write your application-level code (or library) so that it automatically fetches these references for you.
Or you could alter your schema and use embedded documents.
Your final choice is to leave things exactly the way they are now and do two queries.
Here is answer for your question.
db.getCollection('users').aggregate([
{$match : {admin : 1}},
{$lookup: {from: "posts",localField: "_id",foreignField: "owner_id",as: "posts"}},
{$project : {
posts : { $filter : {input : "$posts" , as : "post", cond : { $eq : ['$$post.via' , 'facebook'] } } },
admin : 1
}}
])
Or either you can go with mongodb group option.
db.getCollection('users').aggregate([
{$match : {admin : 1}},
{$lookup: {from: "posts",localField: "_id",foreignField: "owner_id",as: "posts"}},
{$unwind : "$posts"},
{$match : {"posts.via":"facebook"}},
{ $group : {
_id : "$_id",
posts : {$push : "$posts"}
}}
])
As mentioned before in MongoDB you can't JOIN between collections.
For your example a solution could be:
var myCursor = db.users.find({admin:1});
var user_id = myCursor.hasNext() ? myCursor.next() : null;
db.posts.find({owner_id : user_id._id});
See the reference manual - cursors section: http://es.docs.mongodb.org/manual/core/cursors/
Other solution would be to embed users in posts collection, but I think for most web applications users collection need to be independent for security reasons. Users collection might have Roles, permissons, etc.
posts
{
"content":"Some content",
"user":{"_id":"12345", "admin":1},
"via":"facebook"
},
{
"content":"Some other content",
"user":{"_id":"123456789", "admin":0},
"via":"facebook"
}
and then:
db.posts.find({user.admin: 1 });
Perform multiple queries or use embedded documents or look at "database references".
One solution: add isAdmin: 0/1 flag to your post collection document.
Other solution: use DBrefs
Posting since I wanted to flatten the merged documents, vs a tiered document that the other answers produce.
To merge multiple collections into a flat single document, look at Mongo docs for $lookup with $mergeObjects: https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/#use--lookup-with--mergeobjects

Return whole document in MongoDB with distinct fields and 1 select query

I'm having trouble writing a query that returns the whole document while still having 1 distinct field.
I already tried to mess with Aggregates but to no avail. I already found this document after googling some time:
How to efficiently perform "distinct" with multiple keys?
It describes most of the solution i need, but i need to filter on another field as well.
This is the function I use right now to query a sorted list of the documents in the TempCollection.
function getLatestUserData(UserID, callback) {
console.log("in func " + UserID);
Temp.find({ UId: UserID }).sort({ "created_at" : -1 }).exec( function (err, data) {
console.log(data + " " + err);
callback(data);
});
};
however, I cannot figure out how I can filter, sort and distinct all at the same time.
This is an attempt at using Aggregates, however, read articles for quite some time and I just can't figure out the syntax for the purpose I need:
function getLatestUserData(UserID, callback) {
Temp.aggregate([
{"$group": { "_id": { value: "$value", SerialNumber: "$SerialNumber" } }},
{$filter : {input: "$UId", as: "UId", cond: {$U: [ "$UId", UserID ]} }}
]).exec( function (err, data) {
callback(data);
});
};
Here is a part of the TempCollection:
/* 1 */
{
"updatedAt" : ISODate("2017-05-23T13:01:45.000Z"),
"created_at" : ISODate("2017-05-23T13:01:45.000Z"),
"UId" : "590b10221da091b2618a4913",
"value" : 36,
"SerialNumber" : "TEST2",
"_id" : ObjectId("592432b9372464833d038b80"),
"__v" : 0
}
/* 2 */
{
"updatedAt" : ISODate("2017-05-23T14:23:39.000Z"),
"created_at" : ISODate("2017-05-23T14:23:39.000Z"),
"UId" : "58f8954c3602b80552b6f1fb",
"value" : 39,
"SerialNumber" : "IIOJOIMJ",
"_id" : ObjectId("592445eb372464833d038bf4"),
"__v" : 0
}
Any help is much appreciated!
You'll need to add $match on UID instead of $filter ( only used with array fields
) followed by desc $sort on created_at and $group on distinct keys while using $$ROOT variable inside of $first accumulation operator to pick the latest whole document.
Something like
Temp.aggregate([
{"$match":{ "UId": UserID }},
{"$sort":{ "created_at" : -1 }},
{"$group": { "_id": { value: "$value", SerialNumber: "$SerialNumber" }, "data":{"$first":"$$ROOT"}}}
])
The data field will have the latest doc.

Finding average from referenced Mongoose Schema

Basically I am trying to find the average rating out of 5 based on comments for specific movie titles in a small app using Node.js and Mongoose.
Movie Schema:
var mongoose = require("mongoose");
//SCHEMA SET UP
var movieSchema = new mongoose.Schema({
title: String,
director: String,
year: Number,
comments:[
{
type: mongoose.Schema.Types.ObjectId,
ref: "Comment"
}
]
});
module.exports = mongoose.model("Movie", movieSchema);
Comment Schema:
var mongoose = require("mongoose");
var commentSchema = mongoose.Schema({
author: String,
text: String,
rating: Number,
movie: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Movie"
}
]
});
module.exports = mongoose.model("Comment", commentSchema);
Trying to find the average movie rating:
Comment.aggregate([
{
$group: {
_id: '$movie',
ratingAvg: {$avg: '$rating'}
}
}
], function(err, results){
if(err){
console.log(err);
}else{
console.log(results);
}
}
);
Based on 3 sample ratings(5, 5, 4) on two separate movies this will return:
[ { _id: 'movie', ratingAvg: 4.666666666666667 } ]
So it is getting me the average of ALL the comments, but I want the average depending on the title of the movie. So for example I have a movie with the title of "Big Trouble in Little China" and it has two comments with the ratings of 5 and 4. I want it to return an average of 4.5. Sorry if this question is confusing, I'm super new to MongoDB and Mongoose and I think its a little over my head I'm afraid :(.
Edit: Here are some sample documents from the movie collection:
{ "_id" : ObjectId("57b135aefaa8fcff66b94e3e"), "title" : "Star Trek", "director" : "Idu No", "year" : 2008, "comments" : [ ObjectId("57b
135b9faa8fcff66b94e3f"), ObjectId("57b135c5faa8fcff66b94e40") ], "__v" : 2 }
{ "_id" : ObjectId("57b137b0a64ba6296d0df2d0"), "title" : "Buntley", "director" : "Lucy Mayfield", "year" : 2016, "comments" : [ ObjectId
("57b137bca64ba6296d0df2d1"), ObjectId("57b137caa64ba6296d0df2d2") ], "__v" : 2 }
and the comment collection:
{ "_id" : ObjectId("57b135b9faa8fcff66b94e3f"), "author" : "Tyler", "text" : "Ok", "rating" : 5, "movie" : [ ], "__v" : 0 }
{ "_id" : ObjectId("57b135c5faa8fcff66b94e40"), "author" : "Jennicaa", "text" : "asdlfjljsf", "rating" : 1, "movie" : [ ], "__v" : 0 }
{ "_id" : ObjectId("57b137bca64ba6296d0df2d1"), "author" : "Bentley", "text" : "Its amazing", "rating" : 5, "movie" : [ ], "__v" : 0 }
{ "_id" : ObjectId("57b137caa64ba6296d0df2d2"), "author" : "Lucy", "text" : "Its pretty good", "rating" : 4, "movie" : [ ], "__v" : 0 }
I'm noticing that the comment collection is not associating the movie id as it is just an empty array. That must be my issue?
I just want the average of the reviews for each movie by title. So if a movie titled Star Trek had four reviews with scores of 3, 3, 4, 5, the average would be 3.75. Normally I would just sum all the numbers in an array and then divide by the length of the array, but I just don't know enough with mongoDB/mongoose to do that :(
Sorry for not providing the proper amount of information straight away.
Since the comment model has an array which holds the movies, you need to unwind the array field first before calculating aggregates based on that group. Your aggregation pipeline would have an $unwind step before the $group
to flatten the array (denormalize it). You can then group the flattened documents by the movie _id and calculate the average.
The following example shows this:
Comment.aggregate([
{ "$unwind": "$movie" },
{
"$group": {
"_id": "$movie",
"ratingAvg": { "$avg": "$rating" }
}
}
], function(err, results) {
if(err) handleError(err);
Movie.populate(results, { "path": "_id" }, function(err, result) {
if(err) handleError(err);
console.log(result);
});
})
Here is the solution I used to actually render the average to a template.
app.get("/movies/:id", function(req, res){
Movie.findById(req.params.id).populate("comments").exec(function(err, showMovie){
if(err){
console.log(err);
} else{
var total = 0;
for(var i = 0; i < showMovie.comments.length; i++) {
total += showMovie.comments[i].rating;
}
var avg = total / showMovie.comments.length;
res.render("show", {movie: showMovie, ratingAverage: avg});
}
});
});

Mongoose aggregate query $max in $match

I am new to mongoose, I am facing a problem while trying to fetch some data using aggregate query.
One part of my auction schema is:
"_id" : ObjectId("56c58be1faaa402c0d4ae66f"),
"auction_name" : "Auction2",
"auction_start_datetime" : ISODate("2016-02-18T09:30:00.000Z"),
"auction_end_datetime" : ISODate("2016-02-22T09:00:00.000Z"),
"auction_status" : "active",
"auction_series" : "GA-06-C",
"auction_reserve_price" : 1000,
"auction_increment_amount" : 200,
"fancy_numbers" : [
{
"number_end_datetime" : ISODate("2016-02-22T09:00:00.000Z"),
"number_start_datetime" : ISODate("2016-02-18T09:30:00.000Z"),
"increment_amount" : 200,
"reserve_price" : 1000,
"number" : 5000,
"_id" : ObjectId("56c58da3faaa402c0d4ae739"),
"bid_users" : [
{
"user_id" : "56c416a599ad7c9c1611b90b",
"bid_amount" : 7200,
"bid_time" : ISODate("2016-02-18T11:58:53.025Z"),
"user_name" : "amit#mailinator.com",
"_id" : ObjectId("56c5aec4acebf3b4061a645e")
},
{
"user_id" : "56c172dc302a2c90179c7fd1",
"bid_amount" : 15400,
"bid_time" : ISODate("2016-02-19T10:38:43.506Z"),
"user_name" : "rbidder#mailinator.com",
"_id" : ObjectId("56c5afe0d2baef7020ede1b6")
},
{
"user_id" : "56c477afb27a7ed824c54427",
"bid_amount" : 2800,
"bid_time" : ISODate("2016-02-18T11:56:58.830Z"),
"user_name" : "bidder2#mailinator.com",
"_id" : ObjectId("56c5b18a78c3fb340a8c6d75")
},
{
"user_id" : "56c5b17378c3fb340a8c6d73",
"bid_amount" : 5600,
"bid_time" : ISODate("2016-02-18T11:58:34.616Z"),
"user_name" : "bidder3#mailinator.com",
"_id" : ObjectId("56c5b1d778c3fb340a8c6d78")
}
]
}
]
Here, fancy_number is an array under auction collection and bid_users is an array under each fancy_number.
I have the user_id, I want to query and get only the bid_user records in which he is the highest bidder.
For example:
There are 3 users bidded 200,300,400 respectively, I want to get the
record (i.e number and amount) only if this particular user bid is 400
(highest). where ill be passing the user_id
The aggregate query which I wrote is:
var ObjectID = require('mongodb').ObjectID;
tempId = new ObjectID(req.body.aId);
auctionModel.aggregate({$match: {'_id': tempId}},
{$unwind: '$fancy_numbers'},
{$unwind:"$fancy_numbers.bid_users"},
{$group: {'_id':"$fancy_numbers.number" , maxBid: { $max: '$fancy_numbers.bid_users.bid_amount'}}},
function(err, bidData){
if(err){
console.log('Error :' + err);
}
else if(bidData) {
console.log(bidData);
}
});
Somehow this query is not working, its only giving records of max bid and number. I want records only if he is the highest bidder.
If I catch you correctly, please try to do it through $sort, and $limit to retrieve the highest bidder as below
auctionModel.aggregate(.aggregate([
{$match: {'_id': '123'}},
{$unwind: '$fancy_numbers'},
{$unwind: '$fancy_numbers.bid_users'},
{$sort: {bid_amount: 1}},
{$limit: 1}]);

Query sub documents in mongoose

I am new to node.js.
I am having JSON object of the form
{ "_id" : ObjectId("540b03ddf1768fe562fbb715"),
"productid" : "men1",
"comments" : [ { "name" : "shiva", "text" : "Haidddsdcccccc", "_id" : ObjectId("540b03dd0570a6261e20a59e"), "date_entered" : ISODate("2014-09-06T12:53:49.658Z") },
{ "name" : "shiva", "text" : "Haidddsdcccccc", "_id" : ObjectId("540cb2be35f8145a2d296ea0"), "date_entered" : ISODate("2014-09-07T19:32:14.827Z") },
{ "name" : "shiva", "text" : "Haidddsdcccccc", "_id" : ObjectId("540cb2c335f8145a2d296ea1"), "date_entered" : ISODate("2014-09-07T19:32:19.456Z") } ] }
I want to query comments of a product after a specific time
I am using query as
var query=Product.find({"productid":obj.productid,
'comments.date_entered': {$gt: obj.timed}}, {'comments.$': 1})
I am getting only one object after the specific time i.e
{
_id: "540b03ddf1768fe562fbb715"
comments: [1]
0: {
name: "shiva"
text: "Haidddsdcccccc"
_id: "540cb2be35f8145a2d296ea0"
date_entered: "2014-09-07T19:32:14.827Z"
}
}
How to get all the comments of the product after the specified time?
Doesn't seem to be a good way to do this from what I can find out. A work-around could be doing something like this:
Product.findOne({productid: PRODUCT_ID}, {comments: 1}, function(err, product) {
var comments = product.comments.filter(function(comment) {
return comment.date_entered > DATE;
});
...
})

Resources