Mongodb aggregation pass argument to element size of $sample - mongodb

Hello every body here any one can help me with query below
I want to get quiz list with random amount
the amount of rendom will
base on each lesson
The problem is
mongodb not allow to pass argument to element size of $sample
Any one can give me the solution
lessonModel.aggregate([
{ $match : {'quiz.status':1 } },
{
$lookup : {
from : 'quiz',
let : { 'lesson_id' : '$_id','limit' : '$quiz.amount' },
pipeline : [
{
$match: {
$expr: {
$eq: [ "$lesson_id", "$$lesson_id" ]
}
}
},
{
$project: {
title:1,
check_list:1,
duration:1
}
},
{ $sample: { size: '$$limit' } }
],
as: 'quiz'
}
},
{$unwind: '$quiz'},
{ $replaceRoot: { newRoot: "$quiz" } }
]).exec();
The error said size argument to $sample must be a number
Here is my sample data

UPDATE
I think your main problem is to randomly pick amount number of quizs under each lesson. Since $sample is not helpful use $function (New in version MongoDB 4.4).
Solution
Inside $function operator write some logic to
Shuffle the questions (You can change it to your requirement).
Slice it to return the number(amount) of questions required.
db.lessons.aggregate([
{ $match: { "quiz.status": 1 } },
{
$lookup: {
from: "quiz",
let: { "lesson_id": "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$lesson_id", "$$lesson_id"] }
}
},
{
$project: {
title: 1,
check_list: 1,
duration: 1
}
}
],
as: "questions"
}
},
{
$project: {
quiz: {
$function: {
body: function(questions, amount) {
if (amount == 0) return [];
for (let i = questions.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[questions[i], questions[j]] = [questions[j], questions[i]];
}
return questions.slice(0, amount);
},
args: ["$questions", { $ifNull: ["$quiz.amount", 0] }],
lang: "js"
}
}
}
},
{ $unwind: "$quiz" },
{ $replaceRoot: { newRoot: "$quiz" } }
]);
$sample does not support variables. A number must be specified explicitly like:
{
$sample: { size: 1 }
}
Also replace your let as shown below because last lesson document has no amount filed in the quiz object
let: {
'lesson_id': '$_id',
'limit': { $ifNull: ['$quiz.amount', 0] } // or any other number.
},
Wrong:
{
$sample: { size: "$$limit" } // Wont work!
}

Related

MongoDB search by first attr with value

Is it possible do same filtering as in js
const list = [
{
a: 1,
"mostImportant": "qwer",
"lessImportant": "rty"
},
{
a: 2,
"lessImportant": "weRt",
"notImportant": "asd",
},
{
a: 3,
"mostImportant": "qwe2",
"notImportant": "asd",
}
];
list.filter((data) => {
data.attrToSearch = data.mostImportant || data.lessImportant || data.notImportant;
return data.attrToSearch.match(/wer/i);
});
in MongoDB?
Loot at example:
https://mongoplayground.net/p/VQdfoQ-HQV4
So I want to attrToSearch contain value of first not blank attr with next order mostImportant, lessImportant, notImportant
and then match by regex.
Expected result is receive first two documents
Appreciate your help
Approach 1: With $ifNull
Updated
$ifNull only checks whether the value is null but does not cover checking for the empty string.
Hence, according to the attached JS function which skips for null, undefined, empty string value and takes the following value, you need to set the field value as null if it is found out with an empty string via $cond.
db.collection.aggregate([
{
$addFields: {
mostImportant: {
$cond: {
if: {
$eq: [
"$mostImportant",
""
]
},
then: null,
else: "$mostImportant"
}
},
lessImportant: {
$cond: {
if: {
$eq: [
"$lessImportant",
""
]
},
then: null,
else: "$lessImportant"
}
},
notImportant: {
$cond: {
if: {
$eq: [
"$notImportant",
""
]
},
then: null,
else: "$notImportant"
}
}
}
},
{
"$addFields": {
"attrToSearch": {
$ifNull: [
"$mostImportant",
"$lessImportant",
"$notImportant"
]
}
}
},
{
"$match": {
attrToSearch: {
$regex: "wer",
$options: "i"
}
}
}
])
Demo Approach 1 # Mongo Playground
Approach 2: With $function
Via $function, it allows you to write a user-defined function (UDF) with JavaScript support.
db.collection.aggregate([
{
"$addFields": {
"attrToSearch": {
$function: {
body: "function(mostImportant, lessImportant, notImportant) { return mostImportant || lessImportant || notImportant; }",
args: [
"$mostImportant",
"$lessImportant",
"$notImportant"
],
lang: "js"
}
}
}
},
{
"$match": {
attrToSearch: {
$regex: "wer",
$options: "i"
}
}
}
])
Demo Approach 2 # Mongo Playground

variable undefined after findOne operation mongodb

I am trying to make an API that makes use of 2 databases to generate a fine. Here is the code:
router.get("/generateFine/:bookingID/:currDate", function (req, res, next) {
var currDate,
returnDate,
fine,
totalFine = 0;
Booking.findOne({ _id: req.params.bookingID }).then(function (booking) {
Car.findOne({ _id: booking.carID }).then(function (car) {
currDate = Date.parse(req.params.currDate) / 1000 / 3600 / 24;
returnDate = Date.parse(booking.bookingDates[1]) / 1000 / 3600 / 24;
fine = car.fine;
if (currDate > returnDate) {
totalFine = fine * (currDate - returnDate);
}
console.log(totalFine);
// res.send(totalFine);
});
console.log("totalFine is " + totalFine);
// res.send(totalFine);
});
});
Here are the two Schemas used in the code:
Booking Schema:
{
"_id" : ObjectId("621bf46602edf12942f0d5c9"),
"carID" : "621b87af70c150da70b1dabf",
"bookingDates" : [
"2022-03-05",
"2022-03-06"
],
}
Car Schema:
{
"_id" : ObjectId("621b87af70c150da70b1dabf"),
"name" : "Toyota",
"rate" : 60,
"fine" : 10,
"datesBooked" : [
{
"from" : "2022-03-05",
"to" : "2022-03-06"
},
{
"from" : "2022-03-07",
"to" : "2022-03-08"
},
{
"from" : "2022-03-09",
"to" : "2022-03-10"
}
],
"__v" : 0
}
I want to return the generated fine to the user. When I am trying to send the result, it throwing an error. The first console log prints the correct result, but the second console log prints 0. Also, how can I send the result without getting an error.
Thanks already!
You could use $lookup aggregation pipeline stage to include the car document that matches on the carID field, create additional computed fields that will aid you in getting the total fine whilst using the necessary aggregation operators.
Essentially you would need to run an aggregate pipeline that follows:
const mongoose = require('mongoose');
router.get('/generateFine/:bookingID/:currDate', async function (req, res, next) {
const currDate = new Date(req.params.currDate);
const [{ totalFine }] = await Booking.aggregate([
{ $match: { _id: mongoose.Types.ObjectId(req.params.bookingID) }},
{ $lookup: {
from: 'cars', // or from: Car.collection.name
let: { carId: { $toObjectId: '$carID' } }, // convert the carID string field to ObjectId for the match to work correctly
pipeline: [
{ $match: {
$expr: { $eq: [ '$_id', '$$carId' ] }
} }
],
as: 'car'
} },
{ $addFields: {
car: { $arrayElemAt: ['$car', 0 ] }, // get the car document from the array returned above
returnDate: {
$toDate: { $arrayElemAt: ['$bookingDates', 1 ]}
}
} },
// compute the overdue days
{ $addFields: {
overdueDays: {
$trunc: {
$ceil: {
$abs: {
$sum: {
$divide: [
{ $subtract: [currDate, '$returnDate'] },
60 * 1000 * 60 * 24
]
}
}
}
}
}
} },
{ $project: { // project a new field
totalFine: {
$cond: [
{ $gt: [currDate, '$returnDate'] }, // IF current date is greater than return date
{ $multiply: ['$car.fine', '$overdueDays'] }, // THEN multiply car fine with the overdue days
0 // ELSE total fine is 0
]
}
} }
]).exec();
console.log("totalFine is " + totalFine);
// res.send(totalFine);
});

MongoDB: How to speed up my data reorganisation query/operation?

I'm trying to analyse some data and I thought my queries would be faster ultimately by storing a relationship between my collections instead. So I wrote something to do the data normalisation, which is as follows:
var count = 0;
db.Interest.find({'PersonID':{$exists: false}, 'Data.DateOfBirth': {$ne: null}})
.toArray()
.forEach(function (x) {
if (null != x.Data.DateOfBirth) {
var peep = { 'Name': x.Data.Name, 'BirthMonth' :x.Data.DateOfBirth.Month, 'BirthYear' :x.Data.DateOfBirth.Year};
var person = db.People.findOne(peep);
if (null == person) {
peep._id = db.People.insertOne(peep).insertedId;
//print(peep._id);
}
db.Interest.updateOne({ '_id': x._id }, {$set: { 'PersonID':peep._id }})
++count;
if ((count % 1000) == 0) {
print(count + ' updated');
}
}
})
This script is just passed to mongo.exe.
Basically, I attempt to find an existing person, if they don't exist create them. In either case, link the originating record with the individual person.
However this is very slow! There's about 10 million documents and at the current rate it will take about 5 days to complete.
Can I speed this up simply? I know I can multithread it to cut it down, but have I missed something?
In order to insert new persons into People collection, use this one:
db.Interest.aggregate([
{
$project: {
Name: "$Data.Name",
BirthMonth: "$Data.DateOfBirth.Month",
BirthYear: "$Data.DateOfBirth.Year",
_id: 0
}
},
{
$merge: {
into: "People",
// requires an unique index on {Name: 1, BirthMonth: 1, BirthYear: 1}
on: ["Name", "BirthMonth", "BirthYear"]
}
}
])
For updating PersonID in Interest collection use this pipeline:
db.Interest.aggregate([
{
$lookup: {
from: "People",
let: {
name: "$Data.Name",
month: "$Data.DateOfBirth.Month",
year: "$Data.DateOfBirth.Year"
},
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: ["$Name", "$$name"] },
{ $eq: ["$BirthMonth", "$$month"] },
{ $eq: ["$BirthYear", "$$year"] }
]
}
}
},
{ $project: { _id: 1 } }
],
as: "interests"
}
},
{
$set: {
PersonID: { $first: "$interests._id" },
interests: "$$REMOVE"
}
},
{ $merge: { into: "Interest" } }
])
Mongo Playground

Delete document that has size greater than a specific value

I have a collection which contains a multiple documents whose size has increased from 16MBs or is about to reach 16MBs.
I want query that finds documents which have size greater than 10MBs and delete all of them.
I am using following to find the size of document.
Object.bsonsize(db.test.findOne({type:"auto"}))
Is there a way to embed this query inside db.test.deleteMany() query?
This following query deletes the documents with size greater than the specified size (the size is specified in bytes). This query is valid with MongoDB v4.4 or higher.
db.collection.deleteMany( {
$expr: { $gt: [ { $bsonSize: "$$ROOT" }, SIZE_LIMIT ] },
type: "auto"
} )
The following script runs for MongoDB v4.2 or earlier:
const SIZE_LIMIT = 75 // substitute your value here in bytes
let idsToDelete = [ ]
let crsr = db.collection.find()
while(crsr.hasNext()) {
let doc= crsr.next()
if (Object.bsonsize(doc) > SIZE_LIMIT) {
idsToDelete.push(doc._id)
}
}
db.collection.deleteMany( { _id: { $in: idsToDelete } } )
I think you have to do it like this:
db.test.aggregate([
{ $match: { type: "auto" } },
{ $project: { bsonSize: { $bsonSize: "$$ROOT" } } },
{ $match: { bsonSize: { $gt: 16e6 } } },
]).forEach(function (doc) {
db.test.deleteOne({ _id: doc._id });
})
Or if you prefer deleteMany:
var ids = db.test.aggregate([
{ $match: { type: "auto" } },
{ $project: { bsonSize: { $bsonSize: "$$ROOT" } } },
{ $match: { bsonSize: { $lt: 16e6 } } }
]).toArray().map(x => x._id);
db.test.deleteMany({ _id: { $in: ids } });

mongodb aggregation if condition - if true then execute filter

I'm trying to do a query that get arrays of profession and subProfession, and return all Therapist that has a matching profession/subProfession
But if both arrays of profession and subProfession are empty, then return all therapists.
here is a pseudo code of the logic I'm trying to get:
professionsFlag = professions.length != 0;
subProfessionsFlag = subProfessions.length != 0;
Therapist.aggregate([
{
$match: {
$cond: {if: professionsFlag || subProfessionsFlag,
then:
{$or: [
{ profession: { $in: professions } },
{
subProfession: {
$elemMatch: { $in: subProfessions }
}
}
]}
},
}
},
this code fails with unknown top level operator: $cond
what is the correct why to do so?
NOTE: The $or part of the query works as expected.
I ended up with this code:
let filter = {
$match: {
image: { $exists: true, $ne: null },
'clinic.city': params.place.value
}
};
if ( professions.length != 0 || subProfessions.length != 0) {
filter.$match.$or = [
{ profession: { $in: professions } },
{
subProfession: {
$elemMatch: { $in: subProfessions }
}
}
];
}
Therapist.aggregate([
filter,
], callback);