MongoDB - Aggregate: How to group by depending on query - mongodb

I want to aggregate over a collection where a type is given. The types come from query string and the can be day, month or year. Depending on what type the users chooses I want to group by.
For example: If the user chooses "month" I want to group by month.
Event.aggregate([
{
$lookup: { from: Product.collection.name, localField: 'product', foreignField: '_id', as: 'product' }
},
{
$group: {
_id: { $month: { date: "$date" } },
price: { $sum: "$price" },
result: { $mergeObjects: { name: "$product.name", _id: "$product._id" } },
count: { $sum: 1 }
},
},
]).then(response => {
console.log(response)
res.send(response)
})
I can not figure it out how to find a clean solution.
So far the only way I found was to use if conditional before Model.aggregate([])...
if (req.query.dateAvailability && req.query.dateAvailability === 'month') {
Event.aggregate([
{
$lookup: { from: Product.collection.name, localField: 'product', foreignField: '_id', as: 'product' }
},
{
$group: {
_id: { $month: { date: "$date" } },
price: { $sum: "$price" },
result: { $mergeObjects: { name: "$product.name", _id: "$product._id" } },
count: { $sum: 1 }
},
},
]).then(response => {
console.log(response)
res.send(response)
})
} else if (req.query.dateAvailability && req.query.dateAvailability === 'day') {
Event.aggregate([
{
$lookup: { from: Product.collection.name, localField: 'product', foreignField: '_id', as: 'product' }
},
{
$group: {
_id: { $dateToString: { format: "%d-%m-%Y", date: "$date" } },
price: { $sum: "$price" },
result: { $mergeObjects: { name: "$product.name", _id: "$product._id" } },
count: { $sum: 1 }
},
},
]).then(response => {
console.log(response)
res.send(response)
})
} else if (req.query.dateAvailability && req.query.dateAvailability === 'year') {
Event.aggregate([
{
$lookup: { from: Product.collection.name, localField: 'product', foreignField: '_id', as: 'product' }
},
{
$group: {
_id: { $year: { date: "$date" } },
price: { $sum: "$price" },
result: { $mergeObjects: { name: "$product.name", _id: "$product._id" } },
count: { $sum: 1 }
},
},
]).then(response => {
console.log(response)
res.send(response)
})
}
Model Event:
const EventSchema = new Schema({
client: {
type: [{
type: Schema.Types.ObjectId,
ref: 'Client'
}]
},
product: {
type: [{
type: Schema.Types.ObjectId,
ref: 'Product'
}]
},
date: {
type: Date,
maxlength: 64,
lowercase: true,
trim: true
},
place: {
type: String,
maxlength: 1200,
minlength: 1,
},
price: {
type: Number
},
comment: {
type: String,
maxlength: 12000,
minlength: 1,
},
status: {
type: Number,
min: 0,
max: 1,
default: 0,
validate: {
validator: Number.isInteger,
message: '{VALUE} is not an integer value'
}
},
},
{
toObject: { virtuals: true },
toJSON: { virtuals: true }
},
{
timestamps: true
},
);

There's no magic solution to remove the use of logic, In cases like this it will always be required.
However we can make the code a little sexier:
let groupCond;
if (req.query.dateAvailability && req.query.dateAvailability === 'month') {
groupCond = { $month: { date: "$date" } };
} else if (req.query.dateAvailability && req.query.dateAvailability === 'day') {
groupCond = { $dateToString: { format: "%d-%m-%Y", date: "$date" } };
} else if (req.query.dateAvailability && req.query.dateAvailability === 'year') {
groupCond = { $year: { date: "$date" } };
}
Event.aggregate([
{
$lookup: { from: Product.collection.name, localField: 'product', foreignField: '_id', as: 'product' }
},
{
$group: {
_id: groupCond,
price: { $sum: "$price" },
result: { $mergeObjects: { name: "$product.name", _id: "$product._id" } },
count: { $sum: 1 }
},
},
]).then(response => {
console.log(response)
res.send(response)
})

There is no magic bullet to your problem the logic has to happen somewhere. Either with an if statement outside the query or a $switch operator inside the query if you are using a version of mongodb 3.4 or greater.
{"$group": {
"_id":{
"$switch": {
"branches": [
{ "case":{ "$eq": [ { "$literal": "day" }, { "$literal": req.query.dateAvailability } ] },
"then": { $dateToString: { format: "%d-%m-%Y", date: "$date" } } },
{ "case":{ "$eq": [ { "$literal": "month" }, { "$literal": req.query.dateAvailability } ] },
"then": { $month: { date: "$date" } } },
{ "case":{ "$eq": [ { "$literal": "year" }, { "$literal": req.query.dateAvailability } ] },
"then": { $year: { date: "$date" } } }
],
"default": { ... default logic for when dateAvailability isn't set ... }
}
}
... rest of the group operation
} }

Related

How to get this pipeline to return exactly one document?

I am running the following aggregation pipeline:
const agg = [
{
'$match': {
'aaa': 'bbb'
}
}, {
'$group': {
'_id': '',
'total': {
'$sum': '$num'
}
}
}
];
My problem is, when $match matches nothing, the pipeline returns 0 documents. How do I get the pipeline to always return 1 document?
In MongoDB version 6.0 you can do it like this one:
db.collection.aggregate([
{ $match: { aaa: 'bbb' } },
{
$group: {
_id: null,
total: { $sum: "$num" }
}
},
{
$densify: {
field: "total",
range: { step: 1, bounds: [0, 0] }
}
},
{ $set: { _id: { $cond: [{ $eq: [{ $type: "$_id" }, "missing"] }, MaxKey, "$_id"] } } },
{ $sort: { _id: 1 } },
{ $limit: 1 }
])
In version < 6.0 you can try this one:
db.collection.aggregate([
{
$facet: {
data: [
{ $match: { aaa: 'bbb' } },
{ $group: { _id: null, total: { $sum: "$num" } } }
],
default: [
{ $limit: 1 },
{ $group: { _id: null, total: { $sum: 0 } } },
{ $set: { _id: MaxKey } }
]
}
},
{ $replaceWith: { $mergeObjects: [{ $first: "$default" }, { $first: "$data" }] } },
])
Or this one:
db.collection.aggregate([
{ $match: { aaa: 'bbb' } },
{ $group: { _id: null, total: { $sum: "$num" } } },
{
$unionWith: {
coll: "collection",
pipeline: [
{ $limit: 1 },
{ $set: { _id: MaxKey, total: 0 } },
{ $project: { _id: 1, total: 1 } }
]
}
},
{ $sort: { _id: 1 } },
{ $limit: 1 }
])

MongoDB match computed value

I've created an aggregate query but for some reason it doesn't seem to work for custom fields created in the aggregation pipeline.
return this.repository.mongo().aggregate([
{
$match: { q1_avg: { $regex: baseQuery['value'], $options: 'i' } }, // NOT WORKING
},
{
$group: {
_id: '$product_sku',
id: { $first: "$_id" },
product_name: { $first: '$product_name' },
product_category: { $first: '$product_category' },
product_sku: { $first: '$product_sku' },
q1_cnt: { $sum: 1 },
q1_votes: { $push: "$final_rating" }
},
},
{
$facet: {
pagination: [ { $count: 'total' } ],
data: [
{
$project: {
_id: 1,
id: 1,
product_name: 1,
product_category: 1,
product_sku: 1,
q1_cnt: 1,
q1_votes: {
$filter: {
input: '$q1_votes',
as: 'item',
cond: { $ne: ['$$item', null] }
}
},
},
},
{
$set: {
q1_avg: { $round: [ { $avg: '$q1_votes' }, 2 ] },
}
},
{ $unset: ['q1_votes'] },
{ $skip: skip },
{ $limit: limit },
{ $sort: sortList }
]
}
},
{ $unwind : "$pagination" },
]).next();
q1_avg value is an integer and as far as I know, regex only works with strings. Could that be the reason

mongoDB Aggregate please.... list Duplicate Total

code currently in use
db.events.aggregate([
{ '$match':
{ thingId: 'node_0002',
eventState: 'test',
deviceId: {'$in': ['ARCT-1', 'ARCT-2', 'ARSS-1', 'ARSS-2', 'ARSW-1', 'ARCO-1']},
collectTimeText: {'$gte': '2022-04-01T00:00:00+09:00', '$lte': '2022-04-19T23:59:59+09:00' }}},
{ '$group': {
_id: {'$dateFromString': { dateString: { '$substr': [ '$collectTimeText', 0, 10 ] }}
},
list: { '$addToSet': {'deviceId':'$deviceId'} }},
},
{'$unwind':'$list'},
{ '$group': {
_id: {'$dateToString': {'format': "%Y-%m", date: "$_id"}
},
list: { '$push': '$list' }},
},
{'$sort': { _id: 1 } }], {})
result of using the code
{ _id: '2022-04',
list:
[ { deviceId: 'ARCT-2' },
{ deviceId: 'ARSS-1' },
{ deviceId: 'ARCT-1' },
{ deviceId: 'ARSW-1' },
{ deviceId: 'ARCO-1' },
{ deviceId: 'ARSS-2' },
{ deviceId: 'ARCT-2' },
{ deviceId: 'ARSS-1' },
{ deviceId: 'ARCT-1' },
{ deviceId: 'ARSW-1' },
{ deviceId: 'ARCO-1' },
{ deviceId: 'ARSS-2' },
{ deviceId: 'ARCT-2' },
{ deviceId: 'ARSS-1' },
{ deviceId: 'ARCT-1' },
{ deviceId: 'ARSW-1' },
{ deviceId: 'ARCO-1' },
{ deviceId: 'ARSS-2' } ] }
I want an output like the code below. Help
{ _id: '2022-04',
list:
{ 'ARCO-1': 3,
'ARCT-1': 3,
'ARSS-1': 3,
'ARCT-2': 3,
'ARSS-2': 3,
'ARSW-1': 3 } }
This is the code for generating statistics.
How do I get the results I want?
Did I write the code wrong in the first place?
When I added $project , I got the following result. How to check the count
db.events.aggregate([
{ '$match':
{ thingId: 'node_0002',
eventState: 'test',
deviceId: {'$in': ['ARCT-1', 'ARCT-2', 'ARSS-1', 'ARSS-2', 'ARSW-1', 'ARCO-1']},
collectTimeText: {'$gte': '2022-04-01T00:00:00+09:00', '$lte': '2022-04-19T23:59:59+09:00' }}},
{ '$group': {
_id: {'$dateFromString': { dateString: { '$substr': [ '$collectTimeText', 0, 10 ] }}
},
list: { '$addToSet': {'deviceId':'$deviceId'} }},
},
{'$unwind':'$list'},
{ '$group': {
_id: {'$dateToString': {'format': "%Y-%m", date: "$_id"}
},
list: { '$push': '$list' }},
},
{'$project':{
'list':{
'$arrayToObject':{
'$map': {
'input': '$list',
'as': 'el',
'in': {
'k': '$$el.deviceId',
'v': {'$sum':1}
}
}
}
}
}},
{'$sort': { _id: 1 } }], {})
Below is the result of adding $project.
{ _id: '2022-04',
list:
{ 'ARCO-1': 1,
'ARCT-1': 1,
'ARSS-1': 1,
'ARCT-2': 1,
'ARSS-2': 1,
'ARSW-1': 1 } }
I don't know what to do.... It seems like I'm almost there, but I don't know...
Maybe not all...?
$group twice and then $arrayToObject
db.collection.aggregate([
{
$group: {
_id: {
date: "$date",
deviceId: "$deviceId"
},
count: {
$sum: 1
}
}
},
{
$group: {
_id: "$_id.date",
list: {
$push: {
k: "$_id.deviceId",
v: "$count"
}
}
}
},
{
$set: {
list: {
$arrayToObject: "$list"
}
}
}
])
mongoplayground

Do an aggregate with a populate

I'm having troubles with the following. I wonder if it's possible to do it with a single query.
So I have the following model :
const Analytics = new Schema({
createdAt: {
type: Date,
default: Moment(new Date()).format('YYYY-MM-DD')
},
loginTrack: [
{
user_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Users',
}
}
]
}, { collection: 'analytics' });
And the user model :
const UserSchema = new mongoose.Schema(
{
nickname: {
type: String,
required: true,
unique: true
},
instance: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Instances',
default: null
}}, {collection: 'users'});
I want to get the connected users for a specific instance at a specific date.
AnalyticsModel.aggregate([
{
$match: {
createdAt: { "$gte": moment(args.startDate).format('YYYY-MM-DD'), "$lt": moment(args.endDate).format('YYYY-MM-DD')}
}
},
{
"$project": {
users: { $size: "$loginTrack" },
"createdAt": 1,
"_id": 0
}
}, {
"$group": {
"_id": "$createdAt",
"count": { "$sum": "$users" }
}
}
This gets me
[ { _id: '2019-02-11', count: 3 },
{ _id: '2019-02-08', count: 6 },
{ _id: '2019-02-07', count: 19 },
{ _id: '2019-02-06', count: 16 } ]
The results expected will be the same but I want to filter on users that belongs to a specific instance
Is it possible to do it with a single query or I need to do a populate first before the aggregation ?
UPDATE
I did some progress on it, I needed to add a lookup and I think it's ok :
AnalyticsModel.aggregate([
{"$unwind": "$loginTrack"},
{
$lookup:
{
from: 'users',
localField:'loginTrack.user_id',
foreignField: '_id',
as: '_users'
}
},
{
$match: {
createdAt: { "$gte": new Date(args.startDate), "$lt": new Date(args.endDate)}
}
},
{
$project: {
_users: {
$filter: {
input: '$_users',
as: 'item',
cond: {
$and: [
{ $eq: ["$$item.instance", new ObjectId(args.instance_id)] }
]
}
}
},
"createdAt": 1,
"_id": 0
}
},
{
"$group": {
"_id": "$createdAt",
"count": { "$sum": { "$size": "$_users" } }
}
}
Also the dates were in string in the model.
The output is now :
[ { _id: 2019-02-11T00:00:00.000Z, count: 2 } ]

Exclude user in match MongoDB

I want to add Not in query. but unfortunately I'm not getting the required result. i want to get result where user is not equal to userid. But I'm confused as to how I can add that. I tried multiple scenarios but failed.
server.get('/myFeedback', (req, res) => {
var userid = req.query.userID;
//console.log(req.query);
db.collection("tweetsWithSentimentFeedback").aggregate( [
{
$group: {
_id: {
topic: "$topic",
group : "$group",
type : "$type",
user : "$userName"
},
count: { $sum: 1 }
}
},{ $group: {
_id: {
topic: "$_id.topic",
group : "$_id.group",
},
typeAndCount: {
$addToSet: {
type: "$_id.type",
count: "$count"
}
},
userName: {
$addToSet: {
user: "$_id.userName"
}
},
totalCount: {
$sum: "$count"
}
}
},
{ $match: { $and: [ { totalCount: { $gt: 0, $lt: 15 } }, {userEqual: { $ne: [ "$userName.user", userid ] }} ] } },
// Then sort
{ "$sort": { "totalCount": -1 } }
], (err, result) => {
if (err) {
console.log(err);
}
res.status(200).send(result);
} );
});
You should add a $match as a first stage to filter the user.
{ $match: { userName: { $ne: userid } } }
Update:
db.collection("tweetsWithSentimentFeedback").aggregate(
[{
$group: {
_id: {
topic: "$topic",
group: "$group",
type: "$type",
user: "$userName"
},
count: {
$sum: 1
}
}
}, {
$group: {
_id: {
topic: "$_id.topic",
group: "$_id.group"
},
typeAndCount: {
$addToSet: {
type: "$_id.type",
count: "$count"
}
},
userName: {
$addToSet: "$_id.userName"
},
totalCount: {
$sum: "$count"
}
}
}, {
$match: {
{
totalCount: {
$gt: 0,
$lt: 15
}
},
{
userName: {
$ne: userid
}
}
}
}, , {
$sort: {
totalCount: -1
}
}])