How to get date from object stored in mongodb? - node.js

I'm making an application with chats and posts, and in the chat, I want to show the time of the most recent post.
The date part of my mongoose schema for posts is below, as well as a picture of what my Mongodb date field looks like.
I'm trying to get the date to the front end, but am unsure about how to format it. I want to format it as yesterday at 12:45pm or 6 days ago, etc.
Any help is very much appreciated.
Mongoose Schema field for date:
date: {
type: Date,
default: Date.now
}
Mongodb data field

Mongo (and almost? no other database in the world) stores constants like "yesterday" or "last week".
The problem with these concepts like "yesterday" is that it's very semantic. if it's 00:01 is yesterday 2min ago? if the answer is yes you will actually have to update your database every minute if you're willing to compromise to look at time difference you will still have to do it every day.
I'm not sure what your actual business needs that make you want to do this. but I recommend you do it whilst fetching documents. otherwise this is not scaleable.
Here is a quick example on how to do this:
db.collection.aggregate([
{
"$addFields": {
currDay: {
"$dayOfMonth": "$$NOW"
},
dateDay: {
"$dayOfMonth": "$date"
},
dayGap: {
"$divide": [
{
"$subtract": [
"$$NOW",
"$date"
]
},
86400000/**miliseconds in a day*/
]
}
}
},
{
$addFields: {
date: {
"$switch": {
"branches": [
{
"case": {
$and: [
{
$lt: [
"$dayGap",
1
]
},
{
$eq: [
"$dateDay",
"$currDay"
]
}
]
},
"then": "today"
},
{
"case": {
$lt: [
"$dayGap",
2
]
},
"then": "yesterday"
},
{
"case": {
$lt: [
"$dayGap",
1
]
},
"then": "today"
}
],
default: {
"$concat": [
{
"$toString": {
"$round": "$dayGap"
}
},
" days ago"
]
}
}
}
}
}
],
{
allowDiskUse: true
})
MongoPlayground
As you can see you'll have to manually construct the "phrase" you want for every single option. You can obviously do the same in code I just choose to show the "Mongoi" way as I feel is more complicated.
If you do end up choosing updating your database ahead of time you can use the same pipeline combined with $out to achieve this.
One final note is that I cheated a little as this aggregation looks at the miliseconds difference only (apart from today field). meaning if it's 1AM then 50 hours ago. even though the date is "three" days ago will still show up as two days ago.
I hope this example shows you why this formatting is not used anywhere and the difficulties it brings. Mind you I haven't even brought up timezones concepts like "yesterday" are even more semantic for different regions.
In my option the only viable "real" solution is to build a custom function that does this in code. mind you this is not so much fun as you have to account for events like gap years, timezones, geographical zone and more, however it is doable.

Related

how to store hh(hour):mm(minutes) time in mongodb

Currently I am making scheduling system and storing information in mongodb
partial of my model looks like this
{
timings: [{from: "00:00", to: "01:00"}],
weekend: [0, 5, 6]
}
I am not sure if this will be good in the long run
can you please help me decide how to better store time in my documents
MongoDB does not have any time/interval data type, so you have to build your own solution. Some general notes:
Use week days according to ISO-8601 standard, i.e. first day (1) of week is Monday. Then it will be easier to create a Date value with $dateFromParts. For hour values use always 24-hour format.
You may consider to store times as
{from: {hour: 0, minute: 0}, to: {hour: 13, minute: 0}}
Otherwise, when you have to create Date value (e.g. for comparison), then it would be:
{
$dateFromParts : {
...,
hour: { $toInt: { $arrayElemAt: [ { $split: [ "$from", ":" ] }, 0 ] } },
minute: { $toInt: { $arrayElemAt: [ { $split: [ "$from", ":" ] }, 1 ] } }
}
}
Compared to
{
$dateFromParts : {
...,
hour: "$from.hour",
minute: "$from.minute"
}
}
Another approach is to store real Date, e.g. 0000-01-01T13:00:00 or any other arbitrary day value. When you use such values, simply ignore the date part.
Or you store number of minutes (0..1'440) or seconds (0..86'400) from midnight. However, such numbers are not user-friendly to read.

How to create graphs or historigrams with mongoDB

I need to make a graph where to show how many total users we have registered in a time interval. (The language is TypeScript but I can get an answer in another language or using the aggregate of mongoDB)
Example:
Day 1: 10 total users registered
Day 2: 139 ...
Day 3: 1230 ...
Day 4: 2838 ...
...
...
Current day: X number of users ... and so it would end.
It should be noted that all users have a field called createAt, which is of type date.
I tried to obtain the users by means of cubes but it is not an optimal solution.
const response = await this.userModel.aggregate([
{
$bucketAuto: {
groupBy: '$createdAt',
buckets: 4,
},
},
]);
console.log(response);
I have also thought about using mapReduce from mongoDB and pass the specified function to it. But in terms of performance, I would like to know if that could create the pipelines simply with aggregate. mapReduce would be a second option (slightly slower) and as a last option to get all the users (only with the CreateAt field) and process them in my backend.
Thank you in advance for your answers
Update
I also mean that with autoBucket it orders them by non-specific time intervals, it basically orders them by the number of users and groups them by the creation dates, also when i passed the dates to mongodb, with $bucket the result is not as expected
$bucketAuto Option
$bucket option
input Example:
const list = [
{
"createdAt": "2021-08-30T23:47:16.663Z",
"_id": "612d6e044007a95446848cef"
},
{
"createdAt": "2021-08-31T04:18:11.820Z",
"_id": "612dad830541fa001bb63671"
},
{
"createdAt": "2021-08-31T04:18:47.794Z",
"_id": "612dada70541fa001bb63674"
},
{
"createdAt": "2021-08-31T04:20:14.415Z",
"_id": "612dadfe0541fa001bb63678"
},
{
"createdAt": "2021-08-31T04:22:45.580Z",
"_id": "612dae950541fa001bb63682"
},
{
"createdAt": "2021-08-31T11:24:28.471Z",
"_id": "612e116c0541fa001bb63688"
},
{
"createdAt": "2021-08-31T18:47:09.452Z",
"_id": "612e792dba2a3e1d081c9f3d"
}
];

MongoDB aggregation $group stage by already created values / variable from outside

Imaging I have an array of objects, available before the aggregate query:
const groupBy = [
{
realm: 1,
latest_timestamp: 1318874398, //Date.now() values, usually different to each other
item_id: 1234, //always the same
},
{
realm: 2,
latest_timestamp: 1312467986, //actually it's $max timestamp field from the collection
item_id: 1234,
},
{
realm: ..., //there are many of them
latest_timestamp: ...,
item_id: 1234,
},
{
realm: 10,
latest_timestamp: 1318874398, //but sometimes then can be the same
item_id: 1234,
},
]
And collection (example set available on MongoPlayground) with the following schema:
{
realm: Number,
timestamp: Number,
item_id: Number,
field: Number, //any other useless fields in this case
}
My problem is, how to $group the values from the collection via the aggregation framework by using the already available set of data (from groupBy) ?
What have been tried already.
Okay, let skip crap ideas, like:
for (const element of groupBy) {
//array of `find` queries
}
My current working aggregation query is something like that:
//first stage
{
$match: {
"item": 1234
"realm" [1,2,3,4...,10]
}
},
{
$group: {
_id: {
realm: '$realm',
},
latest_timestamp: {
$max: '$timestamp',
},
data: {
$push: '$$ROOT',
},
},
},
{
$unwind: '$data',
},
{
$addFields: {
'data.latest_timestamp': {
$cond: {
if: {
$eq: ['$data.timestamp', '$latest_timestamp'],
},
then: '$latest_timestamp',
else: '$$REMOVE',
},
},
},
},
{
$replaceRoot: {
newRoot: '$data',
},
},
//At last, after this stages I can do useful job
but I found it a bit obsolete, and I already heard that using [.mapReduce][1] could solve my problem a bit faster, than this query. (But official docs doesn't sound promising about it) Does it true?
As for now, I am using 4 or 5 stages, before start working with useful (for me) documents.
Recent update:
I have checked the $facet stage and I found it curious for this certain case. Probably it will help me out.
For what it's worth:
After receiving documents after the necessary stages I am building a representative cluster chart, that you may also know as a heatmap
After that I was iterating each document (or array of objects) one-by-one to find their correct x and y coordinated in place which should be:
[
{
x: x (number, actual $price),
y: y (number, actual $realm),
value: price * quantity,
quantity: sum_of_quantity_on_price_level
}
]
As for now, it's old awful code with for...loop inside each other, but in the future, I will be using $facet => $bucket operators for that kind of job.
So, I have found an answer to my question in another, but relevant way.
I was thinking about using $facet operator and to be honest, it's still an option, but using it, as below is a bad practice.
//building $facet query before aggregation
const ObjectQuery = {}
for (const realm of realms) {
Object.assign(ObjectQuery, { `${realm.name}` : [ ... ] }
}
//mongoose query here
aggregation([{
$facet: ObjectQuery
},
...
])
So, I have chosen a $project stage and $switch operator to filter results, such as $groups do.
Also, using MapReduce could also solve this problem, but for some reason, the official Mongo docs recommends to avoid using it, and choose aggregation: $group and $merge operators instead.

Combine Different Grouping Totals in Aggregate Output

Now that I've had a weekend of banging my head on $project, aggregate(), and $group, it's time for another round of throwing myself on your mercy. I'm trying to do a call where I get back the totals for users, grouped by sex (this was the easier part) and grouped by age range (this is defeating me).
I got it to work with one group:
Person.aggregate([
{
$match: {
user_id: id
}
},
{
$group: {
_id: '$gender',
total: { $sum: 1 }
}
}
])
.exec(function(err, result) {
etc...
From that, it'll give me how many men, how many women in a nice json output. But if I add a second group, it seems to skip the first and throw hissy fits about the second:
Person.aggregate([
{
$match: {
user_id: id
}
},
{
$group: {
_id: '$gender',
total: { $sum: 1 }
},
$group: {
_id: '$age',
age: { $gte: 21 },
age: { $lte: 30 },
total: { $sum: 1 }
}
}
])
.exec(function(err, result) {
etc...
It doesn't like the $gte or $lte. If I switch it to $project, then it'll do the gte/lte but throws fits about $sum or $count. On top of that, I can't find any examples anywhere of how to construct a multi-request return. It's all just "here's this one thing," but I don't want to make 12+ calls just to get all the Person age-groups. I was hoping for output that looks something like this:
[
{"_id":"male","total":49},
{"_id":"woman","total":42},
{"_id":"age0_10", "total": 1},
{"_id":"age11_20", "total": 5},
{"_id":"age21_30", "total": 15}
]
(I have no idea how to make the _id for age be something other than the actual age, which doesn't make sense, b/c I don't want an id of 1517191919 or whatever, I want a reliable name so I know where to output it in my template. So I do know that _id: "$age" won't give me what I want, but I don't know how to get what I want, either.)
The only time I've seen more than one thing, it was a $match, a $group, and a $project. But if $project means I can't use $sum or $count, can I do multiple $groups, and if I can, what's the trick to it?
As for the case of producing the results in different age groupings, the $cond operator of the aggregation framework can help here. As a ternary operator, it takes a logical result ( if condition ) and can return a value where true ( then ) or otherwise where false ( else ). In the case of varying age groups you would "nest" the calls in the else condition to meet each range until logically exhausted.
The overall case is not really practical to do in a single pass with both results for "gender" and "age" in groupings. Whilst it "could" be done, the only method is basically accumulating all data in arrays and working that out again for subsuquent groupings. Not a great idea, as it almost always would break the practical BSON limit of 16MB when attempting to keep the data. So a better approach is generally required.
As such, where the API supports ( you are under nodejs, so it does ), then it is usually best to run each query separately and combine the results. The node async library has just such features:
async.concat(
[
// Gender aggregator
[
{ "$group": {
"_id": "$gender",
"total": { "$sum": 1 }
}}
],
// Age aggregator
[
{ "$group": {
"_id": {
"$cond": {
"if": { "$lte": [ "$age", 10 ] },
"then": "age_0_10",
"else": {
"$cond": {
"if": { "$lte": [ "$age", 20 ] },
"then": "age_11_20",
"else": {
"$cond": {
"if": { "$lte": [ "$age", 30 ] },
"then": "age_21_30",
"else": "age_over_30"
}
}
}
}
}
},
"total": { "$sum": 1 }
}}
]
],
function(pipeline,callback) {
Person.aggregate(pipeline,callback);
},
function(err,results) {
if (err) throw err;
console.log(results);
}
);
The default execution of async.concat here will kick off the tasks to run in parallel, so both can be running on the server at the same time. Each pipeline in the input array will be passed to the aggregate method, which is going to then return the results and combine the output arrays in the final result.
The end result is not only do you have the results nicely keyed to age groups, but the two result sets appear to be in the same combined response, with no other work required to merge the content.
This is not only convenient, but the parallel execution makes this much more time efficient and far less taxing ( if not beating the impossible ) on the aggregation method being used to return the results.

Aggregation Framework on Date

I'm trying to aggregate datas by Date in Mongo, but I can't quite achieve what I want.
Right now, I'm using this:
db.aggregData.aggregate( { $group: {_id: "$Date".toString(),
tweets: { $sum: "$CrawledTweets"} } },
{ $match:{ _id: {$gte: ISODate("2013-03-19T12:31:00.247Z") }}},
{ $sort: {Date:-1} }
)
It results with this:
"result" : [
{
"_id" : ISODate("2013-03-19T12:50:00.641Z"),
"tweets" : 114
},
{
"_id" : ISODate("2013-03-19T12:45:00.631Z"),
"tweets" : 114
},
{
"_id" : ISODate("2013-03-19T12:55:00.640Z"),
"tweets" : 123
},
{
"_id" : ISODate("2013-03-19T12:40:00.628Z"),
"tweets" : 91
},
{
"_id" : ISODate("2013-03-19T12:31:00.253Z"),
"tweets" : 43
},
{
"_id" : ISODate("2013-03-19T13:20:00.652Z"),
"tweets" : 125
},
{
"_id" : ISODate("2013-03-19T12:31:00.252Z"),
"tweets" : 30
}
],
"ok" : 1
It seems to do the job, but with further inspection, we see that there is repetition:
ISODate("2013-03-19T12:31:00.253Z") and ISODate("2013-03-19T12:31:00.252Z").
The only thing that changes is the last bit before the Z.
So here is my question. What is this part ? And how can I do to ignore it in the aggregation ?
Thank you in advance.
EDIT: I wanna aggregate by date, so whole year/month/day + hour and minute. I don't care of the rest.
EDIT: My db in on mongolab, so I'm on 2.2
Well, I did it another way: I save all my date with seconds/milliseconds at 0. So I can keep a simple aggregate, with not a little more code server side, thanks to moment.js
You are trying to aggregate by "whole" date, in other words to drop the time from ISODate(), right? There are several ways to do it, I describe them in detail on my blog in the post called
Stupid Date Tricks with Aggregation Framework.
You can see the full step-by-step breakdown there, but to summarize you have two choices:
if you don't care about the aggregated-on value to be an ISODate() then you can use the {$year}, {$month} and {$dayOfMonth} operators in {$project} phase to pull out just Y-M-D to then {$group} on.
if you do care about the grouped-on value staying an ISODate you can {$subtract} the time part in {$project} phase and be left with ISODate() type - the caveat is that this method requires MongoDB 2.4 (just released) which adds support for date arithmetic and for $millisecond operator (see exact code in the blog post).
Here is probably what you want:
db.aggregData.aggregate([
{
$project:{
CrawledTweets: 1,
newDate: {
year:{$year:"$Date"},
month: {$month:"$Date"},
day: {$dayOfMonth:"$Date"},
hour: {$hour: "$Date"},
min: {$minute: "$Date"}
}
}
},
{
$group: {
_id: "$newDate",
tweets: { $sum: "$CrawledTweets"}
}
}
])
Without being a Mongo expert and without knowing your db fields I'd come up with something like this. Perhaps you can build upon this:
db.aggregData.aggregate(
{
$project:{
CrawledTweets: 1,
groupedTime: {
year:{$year:"$_id"},
month: {$month:"$_id"},
day: {$dayOfMonth:"$_id"},
hour: {$hour: "$_id"},
min: {$minute: "$_id"}
}
}
},
{
$group: {
_id: { groupedTime: "$CrawledTweets" },
tweets: { $sum: "$tweets"}
}
}
)
You can now use the MongoDB date aggregation operators, I have a post on my blog that goes over the Schema setup, using it in Node.js, etc:
http://smyl.es/how-to-use-mongodb-date-aggregation-operators-in-node-js-with-mongoose-dayofmonth-dayofyear-dayofweek-etc/

Resources