I have this kind of 'comment' model:
{ _id: <comment-id>,
user: {
id: { type: Schema.ObjectId, ref: 'User', required: true },
name: String
},
sharedToUsers: [{ type: Schema.ObjectId, ref: 'User' }],
repliedToUsers: [{ type: Schema.ObjectId, ref: 'User' }],
}
And I want to query for all comments which pass the following conditions:
sharedToUsers array is empty
repliedToUsers array is empty
But also, I want the result to contain only 1 comment (the latest comment) per user by the user id.
I've tried to create this aggregate (Node.js, mongoose):
Comment.aggregate(
{ $match: { "sharedToUsers": [], "repliedToUsers": [] } },
{
$group: {
_id: "$user.id",
user: { $first: "$user" },
}
},
function (err, result) {
console.log(result);
if (!err) {
res.send(result);
} else {
res.status(500).send({err: err});
}
});
It is actually working, but the serious problem is that the results comments _id field is been overwritten by the nested user _id.
How can I keep the aggregate working but not to overwrite the original comment _id field?
Thanks
Ok, I have a solution.
All I wanted is to group by _id but to return the result documents with their _id field (which is been overwritten when using $group operator).
What I did is like wdberkley said, I've added comment_id : { "$first" : "$_id" } but then I wanted not to return the comment_id field (because it doesn't fit my model) so I've created a $project which put the comment_id in the regular _id field.
This is basically how it looks:
Comment.aggregate(
{
$match: {
"sharedToUsers": [], "repliedToUsers": []
}
},
{
$group: {
comment_id: { $last: "$_id" },
_id: "$user.id",
content: { $last: "$content" },
urlId: { $last: "$urlId" },
user: { $last: "$user" }
}
},
{
$project: {
_id: "$comment_id",
content: "$content",
urlId: "$urlId",
user: "$user"
}
},
{ $skip: parsedFromIndex },
{ $limit: (parsedNumOfComments - parsedFromIndex) },
function (err, result) {
console.log(result);
if (!err) {
Comment.populate(result, { path: "urlId"}, function(err, comments) {
if (!err) {
res.send(comments);
} else {
res.status(500).send({err: err});
}
});
} else {
res.status(500).send({err: err});
}
});
thanks wdbkerkley!
Related
I have a collection in mongodb, called recipe, where I have a document, called comments, which is an array, and in each recipe is saving the comments. Inside the comments array I have a ratings, which type is Number. So I want to calculate the average the ratings, but don't know, how can I use the db.collection().aggregate code to work in the recipe collection, in the comments document with the ratings variable.
Here is the recipe collection in mongodb:
const { Double } = require('bson');
const { timeStamp } = require('console');
const mongoose = require('mongoose');
const recipeSchema = new mongoose.Schema({
name: {
type: String,
required: 'This field is required.'
},
description: {
type: String,
required: 'This field is required.'
},
quantity: {
type: Array,
required: 'This field is required.'
},
ingredients: {
type: Array,
required: 'This field is required.'
},
categoryByServing: {
type: String,
enum: ['Reggeli', 'Ebéd', 'Vacsora', 'Desszert', 'Levesek', 'Egyéb'],
required: 'This field is required.'
},
categoryByNationality: {
type: String,
enum: ['Thai', 'Kínai', 'Indiai', 'Olasz', 'Angol', 'Magyar', 'Egyéb'],
required: 'This field is required.'
},
image: {
type: Array,
required: 'This field is required.'
},
comments: [
{
username: String,
comment: String,
date: {
type: Date,
default: Date.now
},
rating: Number
},{
timestamps: true
}
],
count: {
type: Number
},
likes: {
type: Number
},
recipe_id: {
type: String
}
});
recipeSchema.index({ name: 'text', description: 'text' });
const Recipe = module.exports = mongoose.model('Recipe', recipeSchema);
Here is the code, where I implemented the rating avg calculation, which is inside the commenting post method:
/**
* POST /comment-recipe
* Comment Recipe
*/
module.exports.CommentRecipeOnPost = async(req, res) => {
let recipeId = req.params.id
const comment = new Comment({
username: req.body.username,
comment: req.body.comment,
date: req.body.date,
rating: req.body.rating
});
comment.save((err, result) => {
if (err){
console.log(err)
}else {
Recipe.findById(req.params.id, (err, post) =>{
if(err){
console.log(err);
}else{
post.comments.push(result);
post.save();
db.collection('recipes').aggregate([
{
$unwind: "$comments"
},
{
$group: {
_id: null,
avgrating: {
$avg: "$rating"
}
}
}
]).toArray()
.then(results => {
console.log({ rating: results[0].avgrating })
})
.catch(error => console.error(error))
console.log('====comments=====')
console.log(post.comments);
res.redirect('/recipe/' + recipeId);
}
})
}
})
}
UPDATE
There is a simpler way pointed out by chridam in the comments using only a $project which I didn't figure out at first demo
db.collection.aggregate([
{
$project: {
_id: 0,
name: 1,
avgRating: {
$avg: "$comments.rating"
}
}
}
])
..or using $addFields which will give the average of ratings as a new field avgRating for each record demo . you can use a project step after if need to get only certain fields
db.collection.aggregate([
{
$addFields: {
avgRating: {
$avg: "$comments.rating"
}
}
}
])
You have done the $unwind step correctly and now you will get a record for each comment.
{
"_id": "1",
"comments": {
"comment": "commment1-1",
"rating": 4
},
"name": "recipe 1"
},
{
"_id": "1",
"comments": {
"comment": "comment1-2",
"rating": 3
},
"name": "recipe 1"
},
...
In the $group stage group by something unique like the _id or the name and the $avg should be of $comments.rating instead of $rating.
In the end the pipeline should look something like this. demo
db.collection.aggregate([
{
$unwind: "$comments"
},
{
$group: {
_id: "$name", //group by something unique for that document containing comments
avgRating: {
$avg: "$comments.rating"
}
}
}
])
Combining $in and $elemMatch
I'm trying to find a partial match for an employee's first or last name. I tried using the $or operator, but Mongo doesn't seem to recognize it.
Where am I going wrong?
router.get('/employees', async (req, res, next) => {
if(req.query.name) {
const response = await Companies.find({ employees:
{
$elemMatch: {
$in: [
{ first_name:new RegExp(req.query.name, 'i') },
{ last_name:new RegExp(req.query.name, 'i') }
]
}
}
})
res.json({ response })
}
else { next() }
})
My schema looks like this:
const companies = new mongoose.Schema({
company: [{
name: { type:String, required:true },
contact_email: { type:String, required:true },
employees: [{
first_name:String,
last_name:String,
preferred_name:String,
position: { type:String },
birthday: { type:Date },
email: { type:String },
}],
}]
}, {
timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' }
})
I have some names in there that should match (including just "nna"), but sending a get request to /employees?name=nna just returns an empty response array.
the most convenient way to get employees back would be to unwind and then match like so:
db.Companies.aggregate({
"$unwind": "$employees"
}, {
"$project": {
"employees": "$employees",
"_id": 0
}
}, {
"$match": {
"$or": [
{
"employees.first_name": /anna/i
},
{
"employees.last_name": /anna/i
}
]
}
})
on the other hand, if the goal is to get the companies back, then this would work:
db.Companies.find({
"employees": {
"$elemMatch": {
"$or": [
{
"first_name": /anna/i
},
{
"last_name": /anna/i
}
]
}
}
})
So the issue that I'm having is that I want to like user comment only one time, currently im using addToSet operator, since by definition it doesn't add value if that value is already present.
But in my case it adds, probably because I am adding object instead of value and when I add mongo generates _id?
This is my event model:
creator: {
type: Schema.Types.ObjectId,
ref: 'User'
},
comments: [
{
user: {
type: Schema.Types.ObjectId,
ref: 'User'
},
text: {
type: String,
required: true
},
likes: [
{
user: {
type: Schema.Types.ObjectId,
ref: 'User'
}
}
]
}
]
}
And this is my addLike function:
commentLike: async (req, res) => {
console.log('working', req.params.id, req.params.idas, req.params.commentID);
Events.findOneAndUpdate(
{ _id: req.params.idas, comments: { $elemMatch: { _id: req.params.commentID } } },
{ $addToSet: { 'comments.$.likes': { user: req.params.id } } },
(result) => {
res.json(result);
}
);
}
My likes array looks like this when I add like:
"likes" : [
{
"user" : ObjectId("5b53442c1f09f525441dac31"),
"_id" : ObjectId("5b54a5263e65324504035eac")
},
{
"user" : ObjectId("5b4b4725a3a5583ba8f8d513"),
"_id" : ObjectId("5b54a5bb3e65324504035eb0")
},
]
You can follow this
db.collection.update(
{ "_id": req.params.idas, "comments": { "$elemMatch": { "_id": req.params.commentID, "likes.user": { "$ne": req.params.id } } } },
{ "$push": { "comments.$.likes": { "user": req.params.id } } }
})
And if you just started with your project then you should follow JohnnyHK opinion and make your array some thing like this to make $addToSet workable
likes: [{ type: Schema.Types.ObjectId, ref: 'User' }]
Another option is to change your schema definition to add "_id: false" to prevent the _id field from being generated for subdocs in the array.
In this case, $addToSet will work as you expect, even with nested subdocs as long as your doc exactly matches.
comments: [
{
user: {
type: Schema.Types.ObjectId,
ref: 'User'
},
text: {
type: String,
required: true
},
likes: [
{
_id: false, //add this
user: {
type: Schema.Types.ObjectId,
ref: 'User'
}
}
]
}
]
Suppose I have the two following schemas:
var SchemaOne = new mongoose.Schema({
created_at: { type: Date },
schemaTwo: { type: mongoose.Schema.Types.ObjectId, ref: 'SchemaTwo' },
ancestor: { type: mongoose.Schema.Types.ObjectId, ref: 'SchemaOne' }
});
var SchemaTwo = new mongoose.Schema({
headline: { type: String, required: true }
});
What I would like to do is: for every SchemaOne document with the same ancestor as one provided, return the SchemaTwo's headline to which they are associated (if any), taking into account that the results from the query should not return any duplicates and should be limited to 15 results sorted by descending order of SchemaOne's created_at field.
I started doing the following:
SchemaOne
.find({ 'ancestor': ancestor })
.sort({ 'created_at': -1 })
.populate({ path: 'schemaTwo', select: 'headline', options: { limit: 15 } })
.exec(function(err, docs) {
// do something with the results
});
But by doing so, I will still get duplicated results, i.e., I will have multiple SchemaOne documents associated with the same SchemaTwo document.
Can you give me a helping hand on how to solve this?
Using mongoose aggregate method and the async library, I managed to get it to work the way I want and need:
SchemaOne.aggregate([
{ $match: { $and: [{'ancestor': ancestor }, { 'schemaTwo': { $exists: true } }] } },
{ $group: { _id: '$schemaTwo' } },
{ $limit: 15 },
{ $sort : { 'created_at': -1 } }
],
function(err, results) {
// do something with err
async.map(results, function(doc, callback) {
SchemaTwo.findById(doc, function(err, result) {
if(err) { return callback(err, null); }
return callback(null, result);
});
}, function(err, docs) {
// do something with err
// here I have my results the way I want
});
});
I have these Mongoose schemes:
// User schema
exports.User = new Schema({
name: {
type: String,
required: true
},
home: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Post'
}]
});
// Post schema
exports.Post = new Schema({
likes: [{
type: Schema.Types.ObjectId,
ref: 'User'
}],
author: {
id: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
name: {
type: String,
required: true
},
shortId: String, // this is User id
},
created: {
type: Date,
default: Date.now,
required: true
}
});
// THE DATA IN THE DATABASE
// User
{"name" : "Mio Nome",
"home" : [
ObjectId("533af14b994c3647c5c97338")
]}
// Post
{ "author" : {
"id" : ObjectId("533af14b994c3647c5c97338"),
"name" : "AutoreDelPost"
},
"likes" : [
ObjectId("533af14b994c3647c5c97337"),
ObjectId("533af14b994c3647c5c97339"),
ObjectId("533af14b994c3647c5c97340")
]
}
And i want to get from users the posts in home field and count how many likehave one user
With this code i can show all posts in home whit populate, but i can't count likes.
req.db.User.find({
_id: req.user._id //req.user is my test user
}, {
home: 1
})
.limit(200)
.populate('home')
.exec(function (err) {
if (!err) {
return res.json(200)
}
return res.json(500, err)
});
// output
[
{
"_id": "533af0ae994c3647c5c97337",
"name" : "Mio Nome"
"home": [
{
"_id": "533b004e6bcb9105d535597e",
"author": {
"id": "533af14b994c3647c5c97338",
"name": "AutoreDelPost"
},
"likes": [] // i can't see like and i can't count they
}
]
I tryed to use aggregate, to count etc but i can't see the posts getting populated but their _id
req.db.User.aggregate({
$match: {
_id: req.user._id
}
}, {
$project: {
home: 1
}
}, {
$unwind: "$home"
}).exec(function (err, home) {
if (!err) {
return res.json(200, home)
}
return res.json(500, err)
});
// output
[
{
"_id": "533af0ae994c3647c5c97337",
"home": "533b004e6bcb9105d535597e"
},
{
"_id": "533af0ae994c3647c5c97337",
"home": "533b004e6bcb9105d5355980"
},
{
"_id": "533af0ae994c3647c5c97337",
"home": "533b004f6bcb9105d5355982"
},
{
"_id": "533af0ae994c3647c5c97337",
"home": "533b004f6bcb9105d5355984"
},
{
"_id": "533af0ae994c3647c5c97337",
"home": "533b00506bcb9105d5355986"
}
]
QUESTION: I want to get from users the posts in home field and count how many like a user has
Perhaps you can store your data more denormalized and add a counter field which is incremented on each new "like". See http://cookbook.mongodb.org/patterns/votes/. Something like:
update = {'$push': {'voters': user_id}, '$inc': {vote_count: 1}}