MongoDB: Add field from Aggregation Output to Query - mongodb

I would like to perform an aggregation query, then a find query, and apply the output of the aggregation as a new field in the find results, ie:
A have dataset like this:
{id: 1, city: "Paris", comment: "...", status: "Active"},
{id: 2, city: "London", comment: "...", status: "Active"},
{id: 3, city: "Paris", comment: "...", status: "Active"},
{id: 4, city: "New York", comment: "...", status: "Active"},
{id: 5, city: "London", comment: "...", status: "Active"},
{id: 6, city: "London", comment: "...", status: "Active"},
{id: 7, city: "London", comment: "...", status: "Disabled"}
I want to get the counts for each active city:
collection.aggregate([
{$match: {status: "Active"}},
{$group: {_id: "$city", count: {$sum: 1}}}
])
But I would like to apply the count to each entry, matched according to city. It would return something like this:
{id: 1, city: "Paris", comment: "...", status: "Active", count: 2},
{id: 2, city: "London", comment: "...", status: "Active", count: 3},
{id: 3, city: "Paris", comment: "...", status: "Active", count: 2},
{id: 4, city: "New York", comment: "...", status: "Active", count: 1},
{id: 5, city: "London", comment: "...", status: "Active", count: 3},
{id: 6, city: "London", comment: "...", status: "Active", count: 3},
{id: 7, city: "London", comment: "...", status: "Disabled", count: 3}
Ideally I would like to do this in a single query so that it can be sorted and paginated according to count.

$group by city and push root object to a root field, count status that is Active only
$unwind deconstruct root array
$mergeObjects to merge $root object and count field
$replaceRoot to replace merged object to root
db.collection.aggregate([
{
$group: {
_id: "$city",
root: { $push: "$$ROOT" },
count: {
$sum: {
$cond: [{ $eq: ["$status", "Active"] }, 1, 0]
}
}
}
},
{ $unwind: "$root" },
{
$replaceRoot: {
newRoot: { $mergeObjects: ["$root", { count: "$count" }] }
}
}
])
Playground

Related

If a value is available in one collection then I want to take one data otherwise I want to take another value in aggregation

I am working on a project where I have data like below. I want to use MongoDB aggregation. Here some objects have the likes value and some don't. Now I want to use the value of likes if it is available otherwise I want to take the value of dislikes. If no like or dislike is available I don't want to take this.
const data = [
{ _id :0, name:"jane", joined : ISODate("2011-03-02"), dislikes: 9},
{ _id :1, name: "joe", joined : ISODate("2012-07-02")},
{ _id: 2, name: "Ant", joined: ISODate("2012-07-02"), likes: 60, dislikes: 02},
{ _id: 2, name: "Ant", joined: ISODate("2012-07-02"), dislikes: 12},
{ _id: 2, name: "Ant", joined: ISODate("2012-07-02"), dislikes: 12},
{ _id: 2, name: "Ant", joined: ISODate("2012-07-02"), }
],
Output will be like:
[
{ _id :0, name:"jane", joined : ISODate("2011-03-02"), dislikes: 9},
{ _id: 2, name: "Ant", joined: ISODate("2012-07-02"), likes: 60, dislikes: 02},
{ _id: 2, name: "Ant", joined: ISODate("2012-07-02"), dislikes: 12},
{ _id: 2, name: "Ant", joined: ISODate("2012-07-02"), dislikes: 12},
],
Is there any way I can do the aggregation? Thank you.
Simply wrap the 2 $exists criteria in a $or.
db.collection.find({
$or: [
{
likes: {
$exists: true
}
},
{
dislikes: {
$exists: true
}
}
]
})
Mongo Playground

MongoDB sort documents by specified element in array

I have a collection like that:
[
{
student: "a",
package: [
{name: "one", createdAt: "2021-10-12T00:00:00", active: true},
{name: "two", createdAt: "2021-10-13T00:00:00", active: false},
{name: "three", createdAt: "2021-10-14T00:00:00", active: false}
]
},
{
student: "b",
package: [
{name: "one", createdAt: "2021-10-16T00:00:00", active: true},
{name: "two", createdAt: "2021-10-17T00:00:00", active: false},
{name: "three", createdAt: "2021-10-18T00:00:00", active: false}
]
},
{
student: "c",
package: [
{name: "one", createdAt: "2021-10-10T00:00:00", active: true},
{name: "two", createdAt: "2021-10-17T00:00:00", active: false},
{name: "three", createdAt: "2021-10-18T00:00:00", active: false}
]
}
]
I have no idea how can I do a query (Mongodb) to sort this collection based on the createdAt with active: true in the package array?
The expectation looks like this:
[
{
student: "c",
package: [
{name: "one", createdAt: "2021-10-10T00:00:00", active: true},
...
]
},
{
student: "a",
package: [
{name: "one", createdAt: "2021-10-12T00:00:00", active: true},
...
]
},
{
student: "b",
package: [
{name: "one", createdAt: "2021-10-16T00:00:00", active: true},
...
]
},
]
Could anyone help me with this? The idea comes up to my mind just to use the code to sort it, but is it possible to use a query MongoDB?
Query
creates a sort-key for each document, this is the latest date of the active package members (the $reduce does, this keeping the max date)
sort by it
unset to remove this extra key
*for descended or ascedent, you can chage the $gt with $lt and the sort 1, with sort -1. depending on what you need. If you use $lt replace "0" also with a max string like "9". Or if you have real dates, with a min or max date.
PlayMongo
aggregate(
[{"$set":
{"sort-key":
{"$reduce":
{"input": "$package",
"initialValue": "0",
"in":
{"$cond":
[{"$and":
["$$this.active", {"$gt": ["$$this.createdAt", "$$value"]}]},
"$$this.createdAt", "$$value"]}}}}},
{"$sort": {"sort-key": 1}},
{"$unset": ["sort-key"]}])
You can use this aggregation query:
First $unwind to deconstruct the array ang get each value.
Then $sort by active.
$group to get the initial data but sorted.
And last $sort again by createdAt.
db.collection.aggregate([
{
"$unwind": "$package"
},
{
"$set": {
"package.createdAt": {
"$toDate": "$package.createdAt"
}
}
},
{
"$sort": {
"package.active": -1
}
},
{
"$group": {
"_id": "$_id",
"student": {
"$first": "$student"
},
"package": {
"$push": "$package"
}
}
},
{
"$sort": {
"package.createdAt": 1
}
}
])
Example here
Also, to do the sorting, is better if createdAt is a Date field, otherwise you should parse to date. Like this example

Need help aggregating mongoose data

I have a MERN stack application and when I make a GET call I want to return the votes of all of my MongoDB objects.
This is what my objects look like:
{_id: ObjectId("6092d48d96984d233cf77152")
user: "John Doe"
movies: [
0:
_id: ObjectId("6092b19345f48a33447468a7"),
title: "Alpha",
ranking: 3
1:
_id: ObjectId("6092b19345f48a33447468a7"),
title: "Bravo",
ranking: 2
2:
_id: ObjectId("6092b19345f48a33447468a7"),
title: "Charlie",
ranking: 1
]}
{_id: ObjectId("6092d48d96984d233cf77152")
user: "John Doe"
movies: [
0:
_id: ObjectId("6092b19345f48a33447468a7"),
title: "Alpha",
ranking: 3
1:
_id: ObjectId("6092b19345f48a33447468a7"),
title: "Bravo",
ranking: 2
2:
_id: ObjectId("6092b19345f48a33447468a7"),
title: "Charlie",
ranking: 1
]}
Obviously there will be more data but basically I would like it to return:
[{title: "Alpha", ranking: 6},
{title: "Bravo", ranking: 4},
{title: "Charlie", ranking: 2}]
I assume I have to use the $match function but this is my first time using Mongoose/MongoDB like this. Thanks in advance!
$unwind deconstruct movies array
$group by movies.title and sum ranking
let result = await YourModelName.aggregate([ // replace your model name
{ $unwind: "$movies" },
{
$group: {
_id: "$movies.title",
ranking: { $sum: "$movies.ranking" }
}
}
])
Playground

Adding a new field to the N most reset docs by day in MongoDB 4.2

I have a collection of documents with scheme :
{ _id: ObjectId, userId: ObjectId, marker: string, datetime: Date, etc... }
This is a collection of markers (marker) bounded to a user (userId). The date of bound is stored in datetime field.
Each day user can receive an arbitrary number of markers.
When I'm fetching data from this collection, I need to add an extra field called allowed of type boolean and this field have to be true only if this record is in the N most resent records for calendar day for a user.
For example, if initial collection looks like this and N == 2 :
{_id: ..., userId: "a", marker: "m1", datetime: "2020-01-01.10:00"}
{_id: ..., userId: "a", marker: "m2", datetime: "2020-01-02.10:00"}
{_id: ..., userId: "a", marker: "m3", datetime: "2020-01-02.11:00"}
{_id: ..., userId: "a", marker: "m4", datetime: "2020-01-02.12:00"}
{_id: ..., userId: "a", marker: "m5", datetime: "2020-01-02.13:00"}
{_id: ..., userId: "b", marker: "m1", datetime: "2020-01-01.10:00"}
{_id: ..., userId: "b", marker: "m2", datetime: "2020-01-01.11:00"}
{_id: ..., userId: "b", marker: "m3", datetime: "2020-01-01.13:00"}
{_id: ..., userId: "b", marker: "m4", datetime: "2020-01-02.11:00"}
{_id: ..., userId: "b", marker: "m5", datetime: "2020-01-02.12:00"}
{_id: ..., userId: "b", marker: "m6", datetime: "2020-01-03.10:00"}
Then final result should look like this:
{_id: ..., userId: "a", marker: "m1", datetime: "2020-01-01.10:00", allowed: true}
{_id: ..., userId: "a", marker: "m2", datetime: "2020-01-02.10:00", allowed: true}
{_id: ..., userId: "a", marker: "m3", datetime: "2020-01-02.11:00", allowed: true}
{_id: ..., userId: "a", marker: "m4", datetime: "2020-01-02.12:00", allowed: false}
{_id: ..., userId: "a", marker: "m5", datetime: "2020-01-02.13:00", allowed: false}
{_id: ..., userId: "b", marker: "m1", datetime: "2020-01-01.10:00", allowed: true}
{_id: ..., userId: "b", marker: "m2", datetime: "2020-01-01.11:00", allowed: true}
{_id: ..., userId: "b", marker: "m3", datetime: "2020-01-01.13:00", allowed: false}
{_id: ..., userId: "b", marker: "m4", datetime: "2020-01-02.11:00", allowed: true}
{_id: ..., userId: "b", marker: "m5", datetime: "2020-01-02.12:00", allowed: true}
{_id: ..., userId: "b", marker: "m6", datetime: "2020-01-03.10:00", allowed: true}
I'm using MongoDB 4.2.
Please try below queries :
Query 1:
db.markers.aggregate([
/** group docs based on userId & date(2020-01-01), push all matched docs to data */
{ $group: { _id: { userId: '$userId', datetime: { $arrayElemAt: [{ $split: ["$datetime", "."] }, 0] } }, data: { $push: '$$ROOT' } } },
/** Re-forming data field with added new field allowed for only docs where criteria is met */
{
$addFields: {
data: {
$map:
{
input: "$data",
as: "each",
/** conditional check to add new field on only docs which are 0 & 1 position of array */
in: { $cond: [{ $lte: [{ $indexOfArray: ["$data", '$$each'] }, 1] }, { $mergeObjects: ['$$each', { allowed: true }] }, { $mergeObjects: ['$$each', { allowed: false }] }] }
}
}
}
},
/** unwind data */
{ $unwind: '$data' },
/** making data object as root level doc */
{ $replaceRoot: { newRoot: "$data" } }])
Query 2:
db.markers.aggregate([
{ $group: { _id: { userId: '$userId', datetime: { $arrayElemAt: [{ $split: ["$datetime", "."] }, 0] } }, data: { $push: '$$ROOT' } } }, {
$addFields: {
data: {
$map:
{
input: "$data",
as: "each",
in: {
$cond: [{
$or: [{ $eq: [{ $arrayElemAt: ["$data", -1] }, '$$each'] }, { $eq: [{ $arrayElemAt: ["$data", -2] }, '$$each'] }]
},
{ $mergeObjects: ['$$each', { allowed: true }] },
{ $mergeObjects: ['$$each', { allowed: false }] }]
}
}
}
}
}, { $unwind: '$data' }, { $replaceRoot: { newRoot: "$data" } }])
Query1 will work & get you the results, but assuming data given in question is sample data & in real-time when you look at collection userId: "a", marker: "m5" will be first document as if this collection has continuous data writes then latest document would will have latest data time, So Query1's index 0 or 1 will not work, but here Query2 would work. You can use Query1 if markers collection has exactly same ordered data as given in question.
Note : In Query2 - We can use same logic of Query1 (which is to check indexes(0,1)) instead of object comparison but this can be applicable only if we've $sort of dateTime field as first stage, And I haven't gone that route is because sorting on a whole collection's data on a field would not be efficient than this.

Mongodb - $group inside a $group (by 'key')

As proposed by 4J41 (thanks to him/her), I split my previous question into a new one.
I have one last problem with my mongoDB query. And I would be delighted to get some help...
Below my MongoCollection :
Note that KLLS is either a, b or c, and there are 3 types : processus, work, viewing and, for some, a special key(only for work).
{_id: 1, KLLS: "a", action: "A", type: "Processus", date: Date }
{_id: 2, KLLS: "b", action: "B", type: "Processus", date: Date }
{_id: 5, KLLS: "a", action: "E", type: "Viewing" , date: Date }
{_id: 6, KLLS: "b", action: "F", type: "Viewing" , date: Date }
...
{_id: 3, KLLS: "a", action: "AB", type: "Work", date: Date, key:"123" }
{_id: 4, KLLS: "b", action: "XY", type: "Work", date: Date, key: "123" }
{_id: 3, KLLS: "a", action: "AB", type: "Work", date: Date, key:"456" }
{_id: 4, KLLS: "b", action: "XY", type: "Work", date: Date, key: "456" }
...
Currently, the query is (thanks again to 4J41 user) :
db.collection('events').aggregate([
{ $match: { KLLS: {$in: something} } },
{ $sort: { date: 1 } },
{ '$group': {
'_id': '$KLLS',
'Processus': {'$push': {'$cond': [{'$eq': ['$type', 'Processus']}, '$$ROOT', false]}},
'Works': {'$push': {'$cond': [{'$eq': ['$type', 'Works']}, '$$ROOT', false]}},
'Viewings': {'$push': {'$cond': [{'$eq': ['$type', 'Viewings']}, '$$ROOT', false]}},
'Details': {'$push': {'$cond': [{'$eq': ['$action', 'Stuff']}, '$$ROOT', false]}}
}},
{'$project': {
'_id': 0,
'KLLS': '$_id',
'Processus': { '$setDifference': ['$Processus', [false]] },
'Works': { '$setDifference': ['$Works', [false]] },
'Viewings': { '$setDifference': ['$Viewings', [false]] },
'Details': { '$setDifference': ['$Details', [false]] }
}}
]
)
I edited from here changing some order/wording to make it clearer
At this time I got:
[ { _id: { KLLS: 'a'},
Processus: [ everything is ok ]
Viewing: [ everything is ok ]
Details: [ everything is ok ]
Work: [
{_id: 3, KLLS: "a", action: "AB", type: "Work", date: Date, key:"123" }
{_id: 4, KLLS: "b", action: "XY", type: "Work", date: Date, key: "123" }
{_id: 5, KLLS: "a", action: "AB", type: "Work", date: Date, key:"456" }
{_id: 6, KLLS: "b", action: "XY", type: "Work", date: Date, key: "456" }
]
}]
...
The only last problem is into Work. Currently, I have a list composed of 4 records (_id: 3, 4, 5, 6). See above. Whereas I'd like to get subgroups by key like below (caution: I do not know the value of the key).
[ { _id: { KLLS: 'a'},
Processus: [ everything is ok ]
Viewing: [ everything is ok ]
Details: [ everything is ok ]
Work: [
[ // this a "$group" by key
{_id: ..., KLLS: "...", action: "...", type: "Work", date: Date, key:"123" }
{_id: ..., KLLS: "...", action: "...", type: "Work", date: Date, key: "123" }
]
[ // this a "$group" by key
{_id: ..., KLLS: "...", action: "...", type: "Work", date:..., key:"456" }
{_id: ..., KLLS: "...", action: "...", type: "Work", date:..., key: "456" }
]
]
...
I tried doing (1) a kind of nested $group and also (2) to add criteria into first $group to make a composite id, but it doesn't seem to work (empty query result).
Do you have any idea how to resolve this?
Thanks.
You can add nested $condto filter the keys 123 or 456. Then, a final $project stage can be used to construct the array.
db.events.aggregate([
{"$group":
{ "_id":"$KLLS",
"Processus":{"$push":{"$cond":[{"$eq":["$type","Processus"]},'$$ROOT',false]}},
"Work123":
{"$push":
{"$cond":
[
{"$eq":["$type","Work"]},
{"$cond":
[
{"$eq":["$key","123"]},
'$$ROOT',
false
]
},
false
]
}
},
"Work456":
{"$push":
{"$cond":
[
{"$eq":["$type","Work"]},
{"$cond":
[
{"$eq":["$key","456"]},
'$$ROOT',
false
]
},
false
]
}
},
"Viewing":{"$push":{"$cond":[{"$eq":["$type","Viewing"]},'$$ROOT',false]}}
}
},
{"$project": { "_id":0, "KLLS":"$_id", "Processus":{"$setDifference":["$Processus",[false]]},
"123":{"$setDifference":["$Work123",[false]]},
"456":{"$setDifference":["$Work456",[false]]},
"Viewing":{"$setDifference":["$Viewing",[false]]}
}
},
{"$project": { "KLLS":1, "Processus":1, "Work" : [{"123" : "$123"}, {"456" : "$456"}],"Viewing":1}}
])