Mongodb aggregation group by inner array - mongodb

I have aggregated my data to give this output.
[
{
"_id": {
"source": "source_1",
"medium": "medium_1",
"campaign": "campaign_1"
},
"visitors": [
{
"_id": "60073f564d6c915237dbe158",
"location": {
"city": "Miami",
"postal": "33177"
}
},
{
"_id": "60073f564d6c915237dbe158",
"location": {
"city": "Miami",
"postal": "33163"
}
}
]
},
{
"_id": {
"source": "source_2",
"medium": "medium_2",
"campaign": "campaign_2"
},
"visitors": [
{
"_id": "60073f564d6c915237dbe158",
"location": {
"city": "Miami",
"postal": "33177"
}
},
{
"_id": "60073f564d6c915237dbe158",
"location": {
"city": "Miami",
"postal": "33162"
}
}
]
}
]
I want to group inner visitors array and get this output.
[
{
"_id": {
"source": "source_1",
"medium": "medium_1",
"campaign": "campaign_1"
},
"visitors": [
{
"city": "Miami",
"postal": "33177",
"count": 2
},
{
"city": "Miami",
"postal": "33163",
"count": 5
}
]
},
{
"_id": {
"source": "source_2",
"medium": "medium_2",
"campaign": "campaign_2"
},
"visitors": [
{
"city": "Miami",
"postal": "33177",
"count": 1
},
{
"city": "Miami",
"postal": "33163",
"count": 3
}
]
}
]
aggregate pipeline executed on campaigns collection:
[{$match: {
website_id: 1,
$or: [
{
source:{
$regex:/goo/,
$options: 'i'
}
},
{
medium:{
$regex:/goo/,
$options: 'i'
}
},
{
campaign:{
$regex:/goo/,
$options: 'i'
}
}
]
}}, {$addFields: {
visitor_id: {
$toObjectId: "$visitor_id"
}
}}, {$lookup: {
from: 'visitors',
localField: 'visitor_id',
foreignField: '_id',
as: 'visitors'
}}, {$unwind: {
path: '$visitors'
}}, {$group: {
_id: {
source: '$source',
medium: '$medium',
campaign: '$campaign',
},
visitors:{
$push: '$visitors'
}
}}, {$unwind: {
path: '$visitors'
}}, {$group: {
_id: {
'city': '$visitors.location.city',
'postal': '$visitors.location.postal'
},
'count': {
'$sum': 1
}
}}, {$project: {
'_id': 0,
'city': '$_id.city',
'postal': '$_id.postal',
'count': '$count',
'total': {
'$sum': '$count'
}
}}, {$project: {
'city': '$city',
'postal': '$postal',
'count': '$count',
'total': {
'$sum': '$total'
}
}}]

So the idea is first group the visitors by their postal number along with the campaign details to get the count and then aggregate it by only campaign details to accumulate the visitors.
Try this query:
db.campaigns.aggregate([
{
$match: {
// Put your condtions here.
}
},
{
$project: {
source: 1,
medium: 1,
campaign: 1,
visitor_id: 1
}
},
{
$addFields: {
visitor_id: { $toObjectId: "$visitor_id" }
}
},
{
$lookup: {
from: "visitors",
let: { "visitor_id": "$visitor_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$_id", "$$visitor_id"] }
}
},
{
$project: {
location: {
city: 1,
postal: 1
}
}
}
],
as: "visitor"
}
},
{ $unwind: "$visitor" },
{
$group: {
_id: {
source: "$source",
medium: "$medium",
campaign: "$campaign",
postal: "$visitor.location.postal"
},
visitors: { $push: "$visitor" },
count: { $sum: 1 }
}
},
{
$group: {
_id: {
source: "$_id.source",
medium: "$_id.medium",
campaign: "$_id.campaign"
},
visitors: {
$push: {
city: { $arrayElemAt: ["$visitors.location.city", 0] },
postal: { $arrayElemAt: ["$visitors.location.postal", 0] },
count: "$count"
}
}
}
}
]);

You need to correct group stage,
$group by source, medium, campaign and postal, get first city and count total sum
$group by source, medium, campaign and construct visitors array with required fields
db.campaigns.aggregate([
{ $match: .. } //skipped
{ $addFields: .. }, //skipped
{ $lookup: .. }, //skipped
{ $unwind: .. }, //skipped
{
$group: {
_id: {
source: "$source",
medium: "$medium",
campaign: "$campaign",
postal: "$visitors.location.postal"
},
city: { $first: "$visitors.location.city" },
count: { $sum: 1 }
}
},
{
$group: {
_id: {
source: "$_id.source",
medium: "$_id.medium",
campaign: "$_id.campaign"
},
visitors: {
$push: {
city: "$city",
postal: "$_id.postal",
count: "$count"
}
}
}
}
])
Playground

Related

MongoDB: Aggregation/Grouping for poll votes

Am trying to create a poll results aggregation
I have two collections
poll - here is one document
{
"_id": {
"$oid": "636027704f7a15587ef74f26"
},
"question": "question 1",
"ended": false,
"options": [
{
"id": "1",
"option": "option 1"
},
{
"id": "2",
"option": "option 2"
},
{
"id": "3",
"option": "option 3"
}
]
}
Vote - here is one document
{
"_id": {
"$oid": "635ed3210acbf9fd14af8fd1"
},
"poll_id": "636027704f7a15587ef74f26",
"poll_option_id": "1",
"user_id": "1"
}
and i want to perform an aggregate query to get poll results
so am doing the following query
db.vote.aggregate(
[
{
$addFields: {
poll_id: { "$toObjectId": "$poll_id" }
},
},
{
$lookup: {
from: "poll",
localField: "poll_id",
foreignField: "_id",
as: "details"
}
},
{
$group:
{
_id: { poll_id: "$poll_id", poll_option_id: "$poll_option_id" },
details: { $first: "$details" },
count: { $sum: 1 }
}
},
{
$addFields: {
question: { $arrayElemAt: ["$details.question", 0] }
}
},
{
$addFields: {
options: { $arrayElemAt: ["$details.options", 0] }
}
},
{
$group: {
_id: "$_id.poll_id",
poll_id: { $first: "$_id.poll_id" },
question: { $first: "$question" },
options: { $first: "$options" },
optionsGrouped: {
$push: {
id: "$_id.poll_option_id",
count: "$count"
}
},
count: { $sum: "$count" }
}
}
]
)
That is giving me this form of results
{ _id: ObjectId("636027704f7a15587ef74f26"),
poll_id: ObjectId("636027704f7a15587ef74f26"),
question: 'question 1',
options:
[ { id: '1', option: 'option 1' },
{ id: '2', option: 'option 2' },
{ id: '3', option: 'option 3' } ],
optionsGrouped:
[ { id: '1', count: 2 },
{ id: '2', count: 1 } ],
count: 3 }
So what am interested in i want to have the results looking like ( like merging both options & options Group)
{ _id: ObjectId("636027704f7a15587ef74f26"),
poll_id: ObjectId("636027704f7a15587ef74f26"),
question: 'question 1',
optionsGrouped:
[ { id: '1', option: 'option 1', count: 2 },
{ id: '2', option: 'option 2', count: 1 },
{ id: '3', option: 'option 3', count: 0 } ],
count: 4 }
Another question is the DB structure acceptable overall or i can represent that in a better way ?
One option is to group first and use the $lookup later, in order to fetch less data from the poll collection. After the $lookup, use $map with $cond to merge the arrays:
db.vote.aggregate([
{$group: {
_id: {poll_id: {$toObjectId: "$poll_id"}, poll_option_id: "$poll_option_id"},
count: {$sum: 1}
}},
{$group: {
_id: "$_id.poll_id",
counts: {
$push: {count: "$count", option: {$concat: ["option ", "$_id.poll_option_id"]}}
},
countAll: {$sum: "$count"}
}},
{$lookup: {
from: "poll",
localField: "_id",
foreignField: "_id",
as: "poll"
}},
{$project: {poll: {$first: "$poll"}, counts: 1, countAll: 1}},
{$project: {
optionsGrouped: {
$map: {
input: "$poll.options",
in: {$mergeObjects: [
"$$this",
{$cond: [
{$gte: [{$indexOfArray: ["$counts.option", "$$this.option"]}, 0]},
{$arrayElemAt: ["$counts", {$indexOfArray: ["$counts.option", "$$this.option"]}]},
{count: 0}
]}
]}
}
},
count: "$countAll",
question: "$poll.question"
}}
])
See how it works on the playground example
I had reworked the query to match my desires
and this query is achieving the question i have asked
db.poll.aggregate([
{
$addFields: {
_id: {
$toString: "$_id"
}
}
},
{
$lookup: {
from: "poll_vote",
localField: "_id",
foreignField: "poll_id",
as: "votes"
}
},
{
$replaceRoot: {
newRoot: {
$let: {
vars: {
count: {
$size: "$votes"
},
options: {
$map: {
input: "$options",
as: "option",
in: {
$mergeObjects: [
"$$option",
{
count: {
$size: {
$slice: [
{
$filter: {
input: "$votes",
as: "v",
cond: {
$and: [
{
$eq: [
"$$v.poll_option_id",
"$$option._id"
]
}
]
}
}
},
0,
100
]
}
}
},
{
checked: {
$toBool: {
$size: {
$slice: [
{
$filter: {
input: "$votes",
as: "v",
cond: {
$and: [
{
$eq: [
"$$v.user_id",
2
]
},
{
$eq: [
"$$v.poll_option_id",
"$$option._id"
]
}
]
}
}
},
0,
100
]
}
}
}
}
]
}
}
}
},
"in": {
_id: "$_id",
question: "$question",
count: "$$count",
ended: "$ended",
options: "$$options"
}
}
}
}
},
{
$addFields: {
answered: {
$reduce: {
input: "$options",
initialValue: false,
in: {
$cond: [
{
$eq: [
"$$this.checked",
true
]
},
true,
"$$value"
]
}
}
}
}
}
])

Mongodb loop through every distinct values and select tags using aggregate (facet)

I have collection like this:
{
"labels": [{
"description": "Dog"
}, {
"description": "Red"
}, {
"description": "XXX"
}]
}
{
"labels": [{
"description": "Cat"
}, {
"description": "XXX"
}, {
"description": "Yellow"
}]
}
{
"labels": [{
"description": "Dog"
}, {
"description": "Red"
}, {
"description": "Yellow"
}]
}
{
"labels": [{
"description": "Bird"
}, {
"description": "XXX"
}, {
"description": "XXX"
}]
}
I want to filter for example only "Red" and "Yellow" colors from ALL elements and output document like this:
// because "Dog" appears 2 times so total = 2
{
description: "Dog",
total: 2,
colors: [
{ "_id": "Red", total: 2 },
{ "_id": "Yellow", total: 1 }
]
}
{
description: "Cat",
total: 1,
colors: [
{ "_id": "Yellow", total: 1 }
]
}
{
description: "Bird",
total: 1,
colors: []
}
{
description: "Red",
total: 2,
colors: [
{ _id: "Yellow", total: 1 }
]
}
{
description: "XXX",
total: 4,
colors: [
{ _id: "Yellow", total: 1 }
]
}
I can do this by using collection.distinct('labels.description') and then iterating through every single element + make a separate collection.count({ 'labels.description': 'Dog' }) like this:
for (...)
db.collection.aggregate([
{
"$match": {
"labels.description": valueFromLoop // (e.g. Dog)
}
},
{ $unwind : "$labels" },
{
"$group": {
"_id": "$labels.description",
"count": { "$sum": 1 }
}
},
{
"$match": {
"$or": [
{ "_id": "Red" },
{ "_id": "Yellow" }
]
}
},
{
"$sort": {
"count": -1
}
}
])
I want to do this in a single aggregation or mapReduce so that I could easily output it to new collection using $out instead of using Bulk operations separately, however I don't know if it's possible.
Try this:
let filter = ["Red", "Yellow"];
db.testcollection.aggregate([
{
$addFields: { bkp: "$labels" }
},
{ $unwind: "$labels" },
{
$addFields: {
bkp: {
$filter: {
input: "$bkp",
as: "item",
cond: {
$and: [
{ $ne: ["$$item.description", "$labels.description"] },
{ $in: ["$$item.description", filter] }
]
}
}
}
}
},
{
$unwind: {
path: "$bkp",
preserveNullAndEmptyArrays: true
}
},
{
$group: {
_id: {
key1: "$labels.description",
key2: { $ifNull: ["$bkp.description", false] }
},
total: { $sum: 1 }
}
},
{
$group: {
_id: "$_id.key1",
description: { $first: "$_id.key1" },
total: {
$sum: {
$cond: {
if: { $first: [["$_id.key2"]] },
then: 1,
else: "$total"
}
}
},
colors: {
$push: {
$cond: {
if: { $first: [["$_id.key2"]] },
then: {
_id: "$_id.key2",
total: "$total"
},
else: "$$REMOVE"
}
}
}
}
},
{ $project: { _id: 0 } }
]);
For some reason with code from both answers it does not count all tags properly.
I'm posting what works:
db.collection.aggregate([
{
$project: {
labels: 1,
result: {
$filter: {
input: "$labels",
as: "label",
cond: {
$or: [
{ $eq: ["$$label.description", "Blue"] },
{ $eq: ["$$label.description", "Red"] },
{ $eq: ["$$label.description", "Black-and-white"] },
{ $eq: ["$$label.description", "Purple"] },
{ $eq: ["$$label.description", "Orange"] },
{ $eq: ["$$label.description", "Yellow"] },
{ $eq: ["$$label.description", "Green"] },
{ $eq: ["$$label.description", "Teal"] }
]
}
}
}
}
},
{
$unwind: "$labels"
},
{
"$group": {
_id: "$labels.description",
x: {
$push: "$result.description"
},
total: { "$sum": 1 }
}
},
{
$project: {
x: {
$reduce: {
input: '$x',
initialValue: [],
in: {$concatArrays: ['$$value', '$$this']}
}
},
total: 1
}
},
{
$project: {
x: 1,
y: { $setUnion: "$x" },
total: 1
}
},
{
$project: {
_id: 0,
description: "$_id",
"colors": {
$map: {
input: "$y",
as: "item",
in: {
_id: "$$item",
count: {
$size: {
$filter: {
input: "$x",
as: "itemx",
cond: {
$eq: ["$$item", "$$itemx"]
}
}
}
}
}
}
},
total: 1
}
},
{
$out: "backgrounds_meta"
}
])
db.test2.aggregate([
{
$project: {
labels:1,
colours: {
$filter: {
input: "$labels",
as: "label",
cond: {
$or: [
{$eq:["Yellow","$$label.description"]},
{$eq:["Red", "$$label.description"]}
]
}
}
}
}
},
{$unwind:"$labels"},
{$group:{
_id: "$labels.description",
total: {$sum:1},
colours: {$addToSet:"$colours.description"}
}},
{
$project:{
_id:0,
description:"$_id",
total:1,
colours: {
$reduce:{
input: "$colours",
initialValue: [],
in: {$concatArrays: ["$$value", "$$this"]}
}
}
}
},
{
$unwind: {
path:"$colours",preserveNullAndEmptyArrays: true
}
},
{
$group:{
_id:{
description:"$description",
total:"$total",
colour:"$colours"
},
count: {
$sum: {$cond:[{$ifNull:["$colours",false]},1,0]}
}
}
},
{
$group:{
_id:{
description:"$_id.description",
total:"$_id.total"
},
colours: {
$push: {
$cond: [{$gt:["$count",0]},
{
"_id":"$_id.colour",
total:"$count"
},
"$$REMOVE"
]
}
}
}
},
{
$project: {
_id:0,
description: "$_id.description",
total: "$_id.total",
colours: 1
}
}
]);
**Edit In your answer, you are missing the Yellows for Red and Dog because you are taking the first item from $result with $arrayElemAt: ["$result.description", 0].
If description is a colour, do you also want to include the counts for itself in colours?
Never mind, you've updated the answer

Mongoose subquery

I have a collection that looks like below:
[
{
"orderNum": "100",
"createdTime": ISODate("2020-12-01T21:00:00.000Z"),
"amount": 100,
"memo": "100memo",
"list": [
1
]
},
{
"orderNum": "200",
"createdTime": ISODate("2020-12-01T21:01:00.000Z"),
"amount": 200,
"memo": "200memo",
"list": [
1,
2
]
},
{
"orderNum": "300",
"createdTime": ISODate("2020-12-01T21:02:00.000Z"),
"amount": 300,
"memo": "300memo"
},
{
"orderNum": "400",
"createdTime": ISODate("2020-12-01T21:03:00.000Z"),
"amount": 400,
"memo": "400memo"
},
]
and I'm trying to get the total amount of orders that were created before order# 300 (so order#100 and #200, total amount is 300).
Does anyone know how to get it via Mongoose?
You can use this one:
db.collection.aggregate([
{ $sort: { orderNum: 1 } }, // by default the order of documents in a collection is undetermined
{ $group: { _id: null, data: { $push: "$$ROOT" } } }, // put all documents into one document
{ $set: { data: { $slice: ["$data", { $indexOfArray: ["$data.orderNum", "300"] }] } } }, // cut desired elementes from array
{ $unwind: "$data" }, // transform back to documents
{ $replaceRoot: { newRoot: "$data" } },
{ $group: { _id: null, total_amount: { $sum: "$amount" } } } // make summary
])
Actually it is not needed to $unwind and $group, so the shortcut would be this:
db.collection.aggregate([
{ $sort: { orderNum: 1 } },
{ $group: { _id: null, data: { $push: "$$ROOT" } } },
{ $set: { data: { $slice: ["$data", { $indexOfArray: ["$data.orderNum", "300"] }] } } },
{ $project: { total_amount: { $sum: "$data.amount" } } }
])
But the answer from #turivishal is even better.
Update for additional field
{
$set: {
data: { $slice: ["$data", { $indexOfArray: ["$data.orderNum", "300"] }] },
memo: { $arrayElemAt: [ "$data.memo", { $indexOfArray: ["$data.orderNum", "300"] } ] }
}
}
or
{ $set: { data: { $slice: ["$data", { $indexOfArray: ["$data.orderNum", "300"] }] } } },
{ $set: { memo: { $last: { "$data.memo" } } },
$match orderNum less than 300
$group by null and get totalAmount using $sum of amount
YourSchemaModel.aggregate([
{ $match: { orderNum: { $lt: "300" } } },
{
$group: {
_id: null,
totalAmount: { $sum: "$amount" }
}
}
])
Playground

Group on field while getting the last document for each field with MongoDB

Problem
I'm trying to group a stock inventory by products. At first, my stock entries was fully filled each time so I made this aggregate:
[
{ $sort: { date: 1 } },
{
$group: {
_id: '$userId',
stocks: { $last: '$stocks' },
},
},
{ $unwind: '$stocks' },
{
$group: {
_id: '$stocks.productId',
totalQuantity: { $sum: '$stocks.quantity' },
stocks: { $push: { userId: '$_id', quantity: '$stocks.quantity' } },
},
},
]
Now, it can be possible that a stock entry doesn't contain all the products filled. So I'm stuck while writing the new aggregate.
Basically I need to group every products by productId and have an array of the last entry for each user.
Output
This is my expected output:
[
{
"_id": ObjectId("5e75eae1359fc8159d5b6073"),
"totalQuantity": 33,
"stocks": [
{
"userId": ObjectId("5e75f498359fc8159d5b6075"),
"lastDate": "2020-03-21T11:45:53.077Z",
"quantity": 33
}
]
},
{
"_id": ObjectId("5e75eaea359fc8159d5b6074"),
"totalQuantity": 2,
"stocks": [
{
"userId": ObjectId("5e75f498359fc8159d5b6075"),
"lastDate": "2020-03-21T11:45:53.077Z",
"quantity": 2
}
]
}
]
Documents
Documents (when fully filled):
{
"_id": ObjectId("5e75fe71e4a3e0323ba47e0a"),
"date": "2020-03-21T11:45:53.077Z",
"userId": ObjectId("5e75f498359fc8159d5b6075"),
"stocks": [
{
"productId": ObjectId("5e75eae1359fc8159d5b6073"),
"quantity": 33
},
{
"productId": ObjectId("5e75eaea359fc8159d5b6074"),
"quantity": 2
}
]
}
Sometimes it won't be filled for the whole inventory (that's why I need the lastDate):
{
"_id": ObjectId("5e75fe71e4a3e0323ba47e0a"),
"date": "2020-03-21T11:45:53.077Z",
"userId": ObjectId("5e75f498359fc8159d5b6075"),
"stocks": [
{
"productId": ObjectId("5e75eae1359fc8159d5b6073"),
"quantity": 33
}
]
}
Try this one:
db.collection.aggregate([
{
$group: {
_id: "$userId",
root: {
$push: "$$ROOT"
}
}
},
{
$addFields: {
root: {
$map: {
input: "$root",
as: "data",
in: {
"stocks": {
$map: {
input: "$$data.stocks",
as: "stock",
in: {
"productId": "$$stock.productId",
"userId": "$$data.userId",
"quantity": "$$stock.quantity",
"lastDate": "$$data.date"
}
}
}
}
}
}
}
},
{
$unwind: "$root"
},
{
$replaceRoot: {
newRoot: "$root"
}
},
{
$unwind: "$stocks"
},
{
$sort: {
"stocks.lastDate": 1
}
},
{
$group: {
_id: "$stocks.productId",
totalQuantity: {
$last: "$stocks.quantity"
},
stocks: {
$last: "$stocks"
}
}
},
{
$addFields: {
stocks: [
{
"lastDate": "$stocks.lastDate",
"quantity": "$stocks.quantity",
"userId": "$stocks.userId"
}
]
}
}
])
MongoPlayground

Reducing an array of values into object with their count using aggregation framework

We are using MongoDB to record statistics. The approach is to record each action for an object in its own document and later aggregate them on hourly basis and store them in different collection. Sample documents are below:
[{
"_id" : ObjectId("5e05de1e86029610dc2c6f9c"),
"object_type" : 1,
"object_id" : 1003,
"browser" : "chrome",
"os" : "osx",
"device" : "android",
"category" : 3,
"country" : "gb",
"action" : "impression",
"date_added" : ISODate("2019-12-26T19:00:00.000Z")
},{
"_id" : ObjectId("5e06226586029610db417b7a"),
"object_type" : 1,
"object_id" : 1003,
"browser" : "firefox",
"os" : "osx",
"device" : "lg_tv",
"category" : 1,
"country" : "pe",
"action" : "impression",
"date_added" : ISODate("2019-12-25T19:00:00.000Z")
},{
"_id" : ObjectId("5e06226586029610db417b7b"),
"object_type" : 1,
"object_id" : 1009,
"browser" : "uc_browser",
"os" : "osx",
"device" : "android",
"category" : 4,
"country" : "ru",
"action" : "view",
"date_added" : ISODate("2019-12-25T19:00:00.000Z")
}]
Output should be:
[{
"object_id": 1003,
"object_type": 1,
"action": "impression",
"total": 2,
"date": "2019-12-26 19:00:00",
"browsers": { "firefox": 1, "chrome": 1 },
"systems": { "osx": 2 },
"countries": { "gb": 1, "pe": 1 },
"devices": { "android": 1, "lg_tv": 1 },
"categories": { "3": 1, "1": 1 }
},
{
"object_id": 1009,
"object_type": 1,
"action": "view",
"total": 1,
"date": "2019-12-26 19:00:00",
"browsers": { "uc_browser": 1 },
"systems": { "osx": 1 },
"countries": { "ru": 1 },
"devices": { "android": 1 },
"categories": { "4": 1 }
}]
Aggregation pipeline:
[
{
"$match": {
"date_added": {
"$gte": {
"$date": {
"$numberLong": "1576820825000"
}
}
}
}
},
{
"$group": {
"_id": {
"object_id": "$object_id",
"object_type": "$object_type",
"action": "$action",
"date": {
"$dateToString": {
"format": "%Y-%m-%d %H-00-00",
"date": "$date_added"
}
}
},
"total": {
"$sum": 1
},
"countries": {
"$push": "$country"
}
}
},
{
"$project": {
"action": "$_id.action",
"object_id": "$_id.object_id",
"object_type": "$_id.object_type",
"date": "$_id.date",
"total": 1,
"countries": 1,
"systems": 1,
"devices": 1,
"categories": 1,
"_id": 0
}
},
{
"$sort": {
"total": -1
}
}
]
This pipeline provides total of an object for a certain action on given hour and push each country into countries array - for readability removed other indexes from $group.
I’m stuck at transforming countries array into desired object. Two question popped in my mind.
Is it possible with single aggregation pipeline?
Should I just return documents using above pipeline and process rest of the indexes with scripting?
It's possible, but a bit tedious...
You need to $group each new field in the next stage and acumulate previous fields.
ASSUMPTION
Your expected result for "object_id": 1003 with total:2, but date_added is 2019-12-26 and 2019-12-25. So, I've changed to 2019-12-26 both documents.
db.collection.aggregate([
{
"$match": {
"date_added": {
"$gte": {
"$date": {
"$numberLong": "1576820825000"
}
}
}
}
},
{
$group: {
_id: {
"object_id": "$object_id",
"object_type": "$object_type",
"action": "$action",
"date": {
"$dateToString": {
"format": "%Y-%m-%d %H-00-00",
"date": "$date_added",
timezone: "GMT"
}
}
},
data: {
"$push": "$$ROOT"
},
total: {
$sum: 1
}
}
},
{
$unwind: "$data"
},
{
$group: {
_id: {
_id: "$_id",
"tmp": "$data.category"
},
data: {
$push: "$data"
},
total: {
$first: "$total"
},
count: {
$sum: 1
}
}
},
{
$group: {
_id: "$_id._id",
data: {
$push: "$data"
},
total: {
$first: "$total"
},
categories: {
$push: {
k: {
$toString: "$_id.tmp"
},
v: "$count"
}
}
}
},
{
$unwind: "$data"
},
{
$unwind: "$data"
},
{
$group: {
_id: {
_id: "$_id",
"tmp": "$data.device"
},
categories: {
$first: "$categories"
},
data: {
$push: "$data"
},
total: {
$first: "$total"
},
count: {
$sum: 1
}
}
},
{
$group: {
_id: "$_id._id",
data: {
$push: "$data"
},
total: {
$first: "$total"
},
categories: {
$first: "$categories"
},
devices: {
$push: {
k: "$_id.tmp",
v: "$count"
}
}
}
},
{
$unwind: "$data"
},
{
$unwind: "$data"
},
{
$group: {
_id: {
_id: "$_id",
"tmp": "$data.country"
},
devices: {
$first: "$devices"
},
categories: {
$first: "$categories"
},
data: {
$push: "$data"
},
total: {
$first: "$total"
},
count: {
$sum: 1
}
}
},
{
$group: {
_id: "$_id._id",
data: {
$push: "$data"
},
total: {
$first: "$total"
},
devices: {
$first: "$devices"
},
categories: {
$first: "$categories"
},
countries: {
$push: {
k: "$_id.tmp",
v: "$count"
}
}
}
},
{
$unwind: "$data"
},
{
$unwind: "$data"
},
{
$group: {
_id: {
_id: "$_id",
"tmp": "$data.os"
},
countries: {
$first: "$countries"
},
devices: {
$first: "$devices"
},
categories: {
$first: "$categories"
},
data: {
$push: "$data"
},
total: {
$first: "$total"
},
count: {
$sum: 1
}
}
},
{
$group: {
_id: "$_id._id",
data: {
$push: "$data"
},
total: {
$first: "$total"
},
countries: {
$first: "$countries"
},
devices: {
$first: "$devices"
},
categories: {
$first: "$categories"
},
systems: {
$push: {
k: "$_id.tmp",
v: "$count"
}
}
}
},
{
$unwind: "$data"
},
{
$unwind: "$data"
},
{
$group: {
_id: {
_id: "$_id",
"tmp": "$data.browser"
},
systems: {
$first: "$systems"
},
countries: {
$first: "$countries"
},
devices: {
$first: "$devices"
},
categories: {
$first: "$categories"
},
data: {
$push: "$data"
},
total: {
$first: "$total"
},
count: {
$sum: 1
}
}
},
{
$group: {
_id: "$_id._id",
data: {
$push: "$data"
},
total: {
$first: "$total"
},
systems: {
$first: "$systems"
},
countries: {
$first: "$countries"
},
devices: {
$first: "$devices"
},
categories: {
$first: "$categories"
},
browsers: {
$push: {
k: "$_id.tmp",
v: "$count"
}
}
}
},
{
$project: {
_id: 0,
action: "$_id.action",
date: "$_id.date",
object_id: "$_id.object_id",
object_type: "$_id.object_type",
total: 1,
categories: {
$arrayToObject: "$categories"
},
countries: {
$arrayToObject: "$countries"
},
devices: {
$arrayToObject: "$devices"
},
systems: {
$arrayToObject: "$systems"
},
browsers: {
$arrayToObject: "$browsers"
}
}
},
{
$sort: {
object_id: 1,
date: 1
}
}
])
MongoPlayground
Note: Other approach was to use $facet and create fields separately and then merge them into final object, but MongoPlayground sometimes worked buggy (click Run button several times and you get different result)