on my project i have users that complete combinations (called sessions) of courses. the fact of playing a course is called an attempt. During the attempt they can close it and come back later (so we keep a timelog object).
I have a request from the client which needs to return for each session, the users (and their attempts) that have played whole or part of their session during a certain timeframe.
During a certain timeframe means that the client sends a begin and end date and we count a user for a specific session if:
- the first attempt has begun before the end of the timeframe => the started of the first timelog of the first < ending date
- the last attempt has been finished after the begining of the timeframe => the end of the last timelog of the last attempt > starting date
Here is an example of an attempt object (the only one we need to use here):
{
"_id" : ObjectId("5b9148650ab5f43b5e829a4b"),
"index" : 0,
"author" : ObjectId("5acde2646055980a84914b6b"),
"timelog" : [
{
"started" : ISODate("2018-09-06T15:31:49.163Z"),
"ended" : ISODate("2018-09-06T15:32:03.935Z")
},
...
],
"session" : ObjectId("5b911d31e58dc13ab7586f9b")}
My idea was to make an aggregate on the attempts, to group those using author and session as an _id for the $group stage, and to push all the attempts of the user for this particular session into an array userAttempts.
Then to make an $addField stage to retrieve the started field of the first timelog of the first attempt and the last ended of the last attempt.
And finally to $filter or $match using those new fields.
Here is my aggregate:
const newDate = new Date()
_db.attempts.aggregate([
{ $match: {
author: { $in: programSessionsData.users },
$or: [{ programSession: { $in: programSessionIds } }, { oldTryFor: { $in: programSessionIds } }],
globalTime: $ex,
timelog: $ex }
},
{
$group: {
_id: {
user: "$author",
programSession: "$programSession"
},
userAttempts: { $push: { attemptId: "$_id", lastTimelog: { $arrayElemAt: ["$timelog", -1] }, timelog: "$timelog" } }
}
},
{
$addFields: { begin: { $reduce: {
input: "$userAttempts",
initialValue: newDate,
in: {
$cond: {
if: { $lt: ["$$this.timelog.0.started", "$$value"] },
then: "$$this.timelog.0.started",
else: "$$value"
} }
} } }
}
I also tried this for the addFields stage:
{
$addFields: { begin: { $reduce: {
input: "$userAttempts",
initialValue: newDate,
in: { $min: ["$$this.timelog.0.started", "$$value] }
} } }
}
However everytime begin is an empty array.
I do not really know how i can extract those two date, or compare dates between them.
To Note: the end one is more difficult that is why i have to first extract lastTimelog. If you an other method i would gladly take it.
Also this code is on a node server so i cannot use ISODate. and the mongo version used is 3.6.3.
After playing with aggregate a bit i came up with 2 solutions:
Solution 1
_db.attempts.aggregate([
{ $match: {
query
},
{
$group: {
_id: {
user: "$author",
programSession: "$programSession"
},
userAttempts: { $push: { attemptId: "$_id", timelog: "$timelog" } }
}
}, {
$addFields: {
begin: { $reduce: {
input: "$userAttempts",
initialValue: newDate,
in: { $min: [{ $reduce: {
input: "$$this.timelog",
initialValue: newDate,
in: { $min: ["$$this.started", "$$value"] }
} }, "$$value"] }
} },
end: { $reduce: {
input: "$userAttempts",
initialValue: oldDate,
in: { $max: [{ $reduce: {
input: "$$this.timelog",
initialValue: oldDate,
in: { $max: ["$$this.ended", "$$value"] }
} }, "$$value"] }
} }
}
},
{
$match: {
begin: { $lt: req.body.ended },
end: { $gt: req.body.started }
}
}
], { allowDiskUse: true });
newDate is today and oldDate is an arbitrary date in the past.
I had to chain 2 reduce because "$$this.timelog.0.started" would always return nothing. Don't really know why though.
Solution 2
_db.attempts.aggregate([
{ $match: {
query
},
{
$addFields: {
firstTimelog: { $arrayElemAt: ["$timelog", 0] },
lastTimelog: { $arrayElemAt: ["$timelog", -1] }
}
},
{
$group: {
_id: {
user: "$author",
programSession: "$programSession"
},
begin: { $min: "$firstTimelog.started" },
end: { $max: "$lastTimelog.ended" },
userAttempts: { $push: { attemptId: "$_id", timelog: "$timelog"} }
}
},
{
$match: {
begin: { $lt: req.body.ended },
end: { $gt: req.body.started }
}
}
], { allowDiskUse: true });
This one is a lot more straight forward and seems simpler, but oddly enough, from my testing, Solution 1 is always quicker at least in the object distribution for my project.
Related
I'm working on a MongoDB (+mongoose) based scheduler where tasks have the following type
TaskSchema {
running: Boolean
executedAt: Date,
next: Number (millisecond)
}
I wish to fetch Tasks which should be executed meaning the sum of executedAt + next < now
Since the scheduler should lock the Task, the running flag should be flipped to true in the same operation, hence I'm using findOneAndUpdate()
I'm stuck at dealing with the sum of executedAt and next. How would one compare the sum of these to new Date/Date.now() ?
When doing an aggregation one could use $dateAdd from what I understand so in a find query could be something. like the following:
Task.find({
$and: [
{ running: { $ne: null } },
{ running: false },
{ next: { $ne: null } },
{ executedAt: { $ne: null } },
{
$expr: {
$lt: [
{
$dateAdd: {
startDate: '$executedAt',
unit: 'millisecond',
amount: '$next',
},
},
new Date().toUTCString(),
],
},
},
],
})
However this does not work.
Apparently, my initial attempt works and the above query is nearly correct. Since I didn't set the $addDate timezone explicitly I tried making the new Date() a UTC string. Considering #Wernfrieds comment this complies to my requirements:
Task.find({
$and: [
{ running: { $ne: null } },
{ running: false },
{ next: { $ne: null } },
{ executedAt: { $ne: null } },
{
$expr: {
$lt: [
{
$dateAdd: {
startDate: '$executedAt',
unit: 'millisecond',
amount: '$next',
},
},
new Date(),
],
},
},
],
})
im trying to perform this aggregation in a mongo $lookup operation where i want to pass a variable down to a date object.
Wondering if this is possible
deal_cliff: new Date(new Date("$integrated").getTime() + 540 * 86400000),
as you can tell the problem is that "$integrated" is being used as a string and not an actual date since its a mongo dollar sign var.
any kind of help would be appreciated.
full code:
$lookup: {
from: 'deals',
// set variables that can be used in the pipeline below
let: {
group_id: '$parentGroupId',
internal_comm: { $toDecimal: '$internalCommission.amount' },
dealer_id: dealerId,
deal_cliff: new Date(new Date('$integrated').getTime() + 540 * 86400000), <---- Problem here
booking_provider: '$provider',
booking_booked_on: '$bookedOn'
},
pipeline: [
{
$match: {
$expr: {
$eq: ['$groupId', '$$group_id']
}
}
},
{ $unwind: '$dealers' },
...(userType === USER_TYPES.BDM && dealerId
? [
{
$match: {
$expr: {
$eq: ['$dealers.dealerId', '$$dealer_id']
}
}
}
]
: []),
{
$addFields: {
dealerComm: {
$cond: [
// If booking provider not Trivago & booking <= cliff date
{
$and: [
{ $ne: ['$$booking_provider', "Trivago"] },
{ $lte: ['$$booking_booked_on', "$$deal_cliff"] }
]
},
// do
{
$multiply: [
'$dealers.effort',
'$$internal_comm',
'$dealers.repCommission'
]
},
// else return
0
]
}
}
},
{
$group: {
_id: '$_id',
totalDealerComm: {
$sum: {
$cond: {
if: '$dealerComm',
then: { $toDecimal: '$dealerComm' },
else: 0
}
}
}
}
}
],
as: 'deal'
}
I have a collection in which the documents sometimes contain a status field and sometimes don't. There is always a data field. I'm trying to form a query to get the latest value for both, but when using the $last operator, I get the values from the latest document and results in status sometimes being null. How can I get it to retrieve the latest defined status value, while still keeping the data value from the latest document?
Current aggregration:
const project = await collection.aggregate([
{
$match: {
projectId: id
}
},
{
$group: {
_id: '$projectId',
status: {
$last: '$status'
},
data: {
$last: '$data'
}
}
}
]).toArray();
You can use $facet and perform multiple query in the parallel on the same set of documents.
db.collection.aggregate([
{
$facet: {
last_status: [
{
"$match": {
status: {
$ne: null
}
}
},
{
"$sort": {
_id: -1
}
},
{
"$limit": 1
}
],
last_data: [
{
"$match": {
data: {
$ne: null
}
}
},
{
"$sort": {
_id: -1
}
},
{
"$limit": 1
}
]
}
},
{
"$project": {
other_fields: {
$first: "$last_data"
},
status: {
$first: "$last_status.status"
}
}
}
])
Working example
I have a mongo db model with the name DrSlots. One of the fields in the model is slots which is as follows
slots: [
{
slot: {
start: {
type: Date,
},
end: {
type: Date,
},
},
status: {
type: String,
},
},
],
Now I want to find the slots based on certain conditions. Firstly the start time should be greater or equal to the start time provided by the user and the end time should be lesser or equal to the end time provided by the user in the same document. For this reason, I wrote the following query which for some reason is not executing correctly.
const slots = await DrSlots.findOne({
$and: [
{ doctor: req.params.doctorId },
{ dateOfAppointment: params.date },
{
"slots.slot": {
start: { $gte: params.start },
end: { $lte: params.end },
},
},
],
});
I am not getting correct results.
Secondly I also want to implement that if params.start or params.end is not provided by user, the query should not check it. How would i implement this? TIA
In order to find the slots between start and end, you could use $elemMatch and do the following:
$and: [
...,
{
slots: {
$elemMatch: {
start: { $gte: params.start },
end: { $lte: params.end },
}
}
}
]
As also pointed out by #Taplar in the comments.
Reference: https://docs.mongodb.com/manual/reference/operator/query/elemMatch/
// You can make query using some condition basis.
let query = [
{ doctor: req.params.doctorId },
{ dateOfAppointment: params.date }
];
// and after that check params.start and params.end values
if (params.start && params.end) {
query.push({
"slots.slot": {
$elemMatch: {
start: { $gte: params.start },
end: { $lte: params.end },
}
}
})
}
const slots = await DrSlots.findOne({
$and: query
});
I am writing a query to find 'n' no of videos from collection. I have set primary , secondary and tertiary language set of the user(Suppose Tamil(P), Hindi(S), English(t)). I want to first find the videos of primary language, if returns videos are less then 'n' then search from secondary language and last from tertiary language. If at any stage n videos are found then no need to search further. I am from c background, so I am thinking to use recursion, but is there any method that I can find videos in one query.
you could do it like this assuming the number of languages is static.
var langs = ['hindi', 'tamil', 'english']; //arrange the languages in the priority you want
var limit = 5;
db.videos.aggregate(
[
{
$group: {
_id: null,
vids: {
$push: '$$ROOT'
}
}
},
{
$project: {
vids: {
$concatArrays: [
{
$slice: [
{
$filter: {
input: "$vids",
cond: { $eq: ["$$this.language", langs[0]] }
}
}, limit
]
},
{
$slice: [
{
$filter: {
input: "$vids",
cond: { $eq: ["$$this.language", langs[1]] }
}
}, limit
]
},
{
$slice: [
{
$filter: {
input: "$vids",
cond: { $eq: ["$$this.language", langs[2]] }
}
}, limit
]
}
]
}
}
},
{
$unwind: '$vids'
},
{
$replaceWith: '$vids'
},
{
$limit: limit
}
])
make sure to add an index on the language field.