get first and last values for each condition - mongodb

I have a collection like this:
{
_id: ObjectId('534343df3232'),
date: ISODate('2016-01-08T00:00:00Z'),
item_type: "book",
book_id: ObjectId('534343df3232fdf'),
user_id: ObjectId('534343df3232fdf23'),
rating: 6
},
{
_id: ObjectId('534343df3232'),
date: ISODate('2016-01-05T00:00:00Z'),
item_type: "movie",
movie_id: ObjectId('534343df3232fdf'),
user_id: ObjectId('534343df3232fdfa'),
rating: 5
},
{
_id: ObjectId('534343df3232'),
date: ISODate('2016-01-010T00:00:00Z'),
item_type: "song",
song_id: ObjectId('534343df3232fdf'),
user_id: ObjectId('534343df3232fdf13'),
rating: 9
}
There can be only one rating per item per user per day.
I would like to check how the ratings evolve between a period of time for a selection of users and items. I need only the first and the last rating for each book/movie/song.
I have no idea on how I could do this the most efficient way.
As for now, I'm retrieving all the ratings for all the users, and then parsing them with PHP.
db.ratings.find({user_id:{$in:[...]}, $or:[book_id:{$in:[...]}, song_id:{$in:[...]}, movie_id:{$in:[...]}, ], date:{$gte:.., $lte..} });
This is obviously unefficient but I don't know how to handle this case.

You can do it with mongodb mapReduce. So at first you need to filter your data on date range, selection of users and selection of items(query part). Then group by item(map part) and for each item select first and last days with corresponding ratings(reduce part).
Try the following query:
var query = {
user_id: {$in:[...]}
date: { $gte: dateFrom, $lt:dateTo},
$or: [
{book_id: {$in:[...]}},
{song_id:{$in:[...]}},
{movie_id:{$in:[...]}}
]
}
var map = function () {
emit(this.item_type, {
first : {rating: this.rating, date: this.date},
last: {rating: this.rating, date: this.date}
})
}
var reduce = function (key, values) {
var res = values[0];
for (var i=1; i<values.length; i++ ) {
if (values[i].first.date < res.first.date)
res.first = values[i].first;
if (values[i].last.date > res.last.date)
res.last = values[i].last;
}
return res;
}
db.collection.mapReduce( map , reduce , { out: { inline : true }, query: query } )

Related

mongoDB Not Returning Query When Trying to Filter By Date

I am trying to find all documents that are created greater than or equal to a month ago.
But when I query the DB it returns nothing when doing the following code:
console.log(moment().add(-31, "days").toDate()) // this logs 2022-09-30T07:27:26.373Z
let filter = {
companyId: companyId,
userId: userId,
_created_at: {$gte: moment().add(-31, "days").toDate()}
};
db.collection("Users")
.find(filter)
.count()
.then(count => {
if(!count){
return resolve({result: [], count: 0});
} else {
db.collection("Users")
.find(filter)
.sort({_created_at: -1})
.limit(parseInt(limit))
.skip(parseInt(page) * parseInt(limit))
.toArray()
.then(result => {
resolve({result: result, count: count});
})
.catch(error => {
console.log(error);
reject(error);
})
}
});
However when I do the following filter it works and returns the documents:
let filter = {
companyId: companyId,
userId: userId,
_created_at: {$gte: moment().add(-31, "days").format("YYYY-MM-DD[T]HH:mm:ss.SSS[Z]") }
};
The only thing i changed is the format of the date and i am specifiying exactly how it should be formatted to equal the DB but i do not want to do something hard coded. Although the moment().add(-31, "days").toDate() matches the same format in my DB.
Why am i not getting any results from the query?
toDate() returns a JS Date object, try with format() to return an ISO formatted date:
let filter = {
companyId: companyId,
userId: userId,
_created_at: { $gte: moment().add(-31, 'days').format() },
};

How to find data by funcion in mongoose?

I have a collection in mongodb using mongoose packages like below
[
{
_id: new ObjectId("62ae97b6be08b688f93f2c07"),
reportId: '1',
method: 'A1',
category: 'B2',
date: '2022-06-19',
time: '22:55',
emergency: 'normal',
__v: 0
},
{
_id: new ObjectId("62ae97b6be08b688f93f2c08"),
reportId: '2',
method: 'A3',
category: 'B5',
date: '2022-06-18',
time: '23:05',
emergency: 'normal',
__v: 0
},
{
_id: new ObjectId("62ae97b6be08b688f93f2c09"),
reportId: '3',
method: 'A5',
category: 'B1',
date: '2022-06-19',
time: '23:55',
emergency: 'urgent',
__v: 0
}
]
and I want to filter this data, and here is my find function()
const options = [
{ method: { $in: ['A1','A2'] } },
{ emergency: { $in: data.emergency } },
{ category: { $in: data.category } }
];
const response = await Report.find({ $or: options,});
Until now, it works perfectly, but I still got one more filter: the date and time (They are all int type String).
I want to search for the range date and time between last night after 23 o'clock to 23 o'clock tonight.
But I have no idea how to write the query, please help me figure it out, thanks!!!
Here is my testing query:
date: {
$where: function () {
const yesterday = moment().subtract(1, 'days').format('YYYYMMDD') + '2300';
const date = moment(this.date).format('YYYYMMDD') + this.time.replace(':', '');
const today = moment().format('YYYYMMDD') + '2300';
return yesterday < date && date <= today;
},
},
I would recommend you to first save dates in a format that supports comparators (lt, gt, etc.) such as a Timestamp or even a plain number. That way you can use MongoDB filters like this:
let filter = {
date: { $gt: new Date(..), $lt: new Date(..) }
}
// This can be executed directly in the Mongo shell.
{
created_on: {
$gt: ISODate("1972-12-12"),
$lt: ISODate('2022-06-019')
}
}
But if you insist on using strings, then you would need to filter all your documents, and then parse each one to do your comparison.

UpdateOne is not working with array of objects in mongoDB

I try to update an object of an array in mongoDB. When the variable listID is not defined, it'll just update the first object of the array (but that's not what I want).
My goal is to add the word IDs, update the last_practiced field and increment the number of the word count field by the number of wordIDs.
I tried it with aggregation as well, but I couldn't get it to work.
My current query
Words.prototype.updatePersonalWordLists = async function(userID, listData) {
const listID = listData.list_id
const wordIDs = listData.word_ids
const last_practiced = listData.last_practiced
const numberOfNewWords = listData.word_ids.length
const lists = await personalWordLists.updateOne(
{"user_id": userID, "lists.$.list_id": listID },
{
$addToSet: { "lists.$.practiced_words": { $each: wordIDs}},
$set: {
"lists.$.last_practiced": last_practiced,
$inc: {"lists.$.wordCount": numberOfNewWords}
}
}
)
return lists
}
let userID = "609b974f6bd8dc6019d2f304"
let listData = {
list_id: "609d22188ea8aebac46f9dc3",
last_practiced: "03-04-2021 13:25:10",
word_ids: ["1", "2", "3", "4", "5"],
}
word.updatePersonalWordLists(userID, listData).then(res => console.log(res))
My scheme
const mongoose = require('mongoose');
const personalWordListsSchema = new mongoose.Schema({
user_id: {
type: String
},
lists: Array
});
module.exports = personalWordListsSchema
I hope someone can help me to figure this out. Thanks in advance.
Thanks to #joe's comment I was able to make it work.
I removed the $ from the filter portion and used the ´´´$inc´´´ outside of the ´´´$set´´´ portion.
Word IDs are pushed only if they are unique.
Date of last practice can be updated.
Word count increments too.
const list = await personalWordLists.updateOne(
{"user_id": userID, "lists.list_id": listID },
{
$addToSet: { "lists.$.practiced_words": { $each: wordIDs}},
$set: {
"lists.$.last_practiced": last_practiced
},
$inc: {"lists.$.wordCount": numberOfNewWords}
}
)
return list

Getting an avarage value from MongoDB

I have this mongoose model:
var juomaSchema = new mongoose.Schema({
...
valikoima: String,
img: String,
views: {default: 0, type: Number},
rating: [],
...
});
juomaSchema.methods.getAvarageRating = function() {
if (this.rating.length !== 0) {
var totalScore = 0;
this.rating.forEach(function(drinkRating) {
totalScore += drinkRating
});
return totalScore/this.rating.length;
} else {
return 0;
}
};
My problem is, that I need to sort a query by the avarage rating of the rating field.
I already have the method in the model, but I can't seem to use a function inside a sort query.
Here's my sort:
router.get("/oluet", function(req, res) {
var perPage = 20,
page = req.query.page,
sortBy = req.query.sort,
asc = req.query.asc;
var sort = {[sortBy] : asc};
console.log(sort);
if(req.xhr) {
//Sanitazes input
const regex = new RegExp(escapeRegExp(req.query.search), "gi");
Juoma.find({nimi: regex, tyyppi: "oluet"})
.sort(sort)
.limit(perPage)
.skip(perPage * page)
.exec(function(err, drinks) {
How would I do this? Making two fields: totalRating and timesRates, and doing some aggregation magic?
Felix solved this in the comments with
{
$project: {
avg: {$avg: "$rating"}
}
}

Mongoose: Populate field in collection from other collection

I have 2 Mongoose collections: ExpenseCategory and Expense
var ExpenseCategorySchema = new Schema({
name: String,
totalSpentInThisMonth: Number
});
mongoose.model('ExpenseCategory', ExpenseCategorySchema);
var ExpenseSchema = new Schema({
expenseCategoryId: {type: Schema.Types.ObjectId, ref: 'ExpenseCategory'},
amount: Number,
date: Date
});
mongoose.model('Expense', ExpenseSchema);
There is a GET api call written in Node.js to return all ExpenseCategory items.
appRouter.route('/expensecatgories')
.get(function(req, res){
ExpenseCategory.find({}, function (expenseCategories) {
res.json(expenseCategories);
});
});
In the above GET method I want to populate field totalSpentInThisMonth in each expenseCategories item before returning. This field needs to be calculated as a sum of all expense.amount where expense.expenseCategoryId matched the expenseCategory.id and expense.date is in current month.
How can I populate the field totalSpentInThisMonth before returning expenseCategories?
Use the .aggregate() method from the aggregation framework for this. You would need to first construct dates to use as your date range query for documents whose date falls within the current month, thus you need to calculate
the first and last days of the month date objects. These dates would be used in the $match pipeline to filter out the documents that are not in the current month.
The next pipeline stream would be the $group stage which groups the incoming documents by the expenseCategoryId key so that you may calculate the total spent in the current month using the
accumulator operator $sum.
The following code implements the above:
appRouter.route('/expensecatgories').get(function(req, res){
var today = new Date(), y = today.getFullYear(), m = today.getMonth();
var firstDay = new Date(y, m, 1);
var lastDay = new Date(y, m + 1, 0);
var pipeline = [
{
"$match": {
"date": { "$gte": firstDay, "$lt": lastDay }
}
},
{
"$group": {
"_id": "$expenseCategoryId",
"totalSpentInThisMonth": { "$sum": "$amount" }
}
}
];
Expense.aggregate(pipeline, function (err, result){
if (err) throw err;
var categories = result.map(function(doc) { return new ExpenseCategory(doc) });
Expense.populate(categories, { "path": "expenseCategoryId" }, function(err, results) {
if (err) throw err;
console.log(JSON.stringify(results, undefined, 4 ));
res.json(results);
});
});
});