I'm trying to query a document and return a single object that lists three counts of 3 different scenarios in a single key. Here's how the collection is organized:
{
response_main:{
type: Schema.ObjectId,
ref: "topics"
},
response_in: {
type: Number
}
}
The response_in key needs to be sorted as to whether it is a 0,1 or 2. The way I am currently solving this problem is:
Collection.aggregate([
{$match: {response_main: mainTopic._id}},
{$group: {
_id: {
$cond: {if: {$eq: ["$response_in", 0]}, then: "agree", else:{
$cond: {if: {$eq: ["$response_in", 1]}, then: "neutral", else:{
$cond: {if: {$eq: ["$response_in", 2]}, then: "disagree", else:false}
}
}
}, count: {$sum: 1}
}}
], callback);
The format the data is returned in is an array of objects that looks like this:
[
{
"_id": "agree",
"count": 14
},
{
"_id": "neutral",
"count": 12
},
{
"_id": "disagree",
"count": 16
}
]
However, I'd prefer the returned object to look like this:
{
"agree": 14,
"neutral": 12,
"disagree": 16,
}
Is there another way I could structure my query to achieve this more succinct result?
Reformatted the data prior to sending the JSON response, all is well now.
Related
{
"_id": "6339f99ee18b2481a04b4fe8",
"userId": "60a8a51cf2229813a45d2238",
"array1": [
{
"someId1": "6339f99ee18b2481a04b4fe9",
"customIndex": 2,
"array2": [
{
"someId2": "6339f99ee18b2481a04b4fea",
"startDate": 2022-10-10T19:56:26.000+00:00,
"endDate": 2022-10-12T19:56:26.000+00:00,
}
]
},
{
"someId1": "6345ca40112b743fd8172be0",
"customIndex": 4,
"array2": [
{
"someId2": "6345ca40112b743fd8172be1",
"startDate": 2022-10-10T19:56:26.000+00:00,
"endDate": 2022-10-27T19:56:26.000+00:00,
}
]
}
]
}
I have above structure in mongoDB and want to get only that object from array1 which matches the conditions of endDate > 2022-10-17
Here's what I try to do:
result= await Collection.find({
userId: { '$in': userIdList},
'array1.array2.endDate': { "$gte": 2022-10-17}
})
But above return the both objects from array1 even though the endDate for one object is less than 2022-10-17
How can I get the the response like below? Also, Am I using the right Mongoose calls to achieve what I am trying to achieve.
Expected response that I am trying to achieve:
{
"_id": "6339f99ee18b2481a04b4fe8",
"userId": "60a8a51cf2229813a45d2238",
"array1": [
{
"someId1": "6345ca40112b743fd8172be0",
"customIndex": 4,
"array2": [
{
"someId2": "6345ca40112b743fd8172be1",
"startDate": 2022-10-10T19:56:26.000+00:00,
"endDate": 2022-10-27T19:56:26.000+00:00,
}
]
}
]
}
If array1 can contain several such items, and array2 contain several such items, one option is using $reduce with $filter and $mergeObjects for this:
db.collection.aggregate([
{$match: {userId: {'$in': userIdList}}}
{$project: {
userId: 1,
array1: {
$reduce: {
input: "$array1",
initialValue: [],
in: {$concatArrays: [
"$$value",
[{$mergeObjects: [
"$$this",
{array2: {
$filter: {
input: "$$this.array2",
as: "innerItem",
cond: {$gte: [
"$$innerItem.endDate",
{$dateFromParts: {year: 2022, month: 10, day: 17}}
]}
}
}}
]}]
]}
}
}
}},
{$project: {
userId: 1,
array1: {$filter: {
input: "$array1",
cond: {$gt: [{$size: "$$this.array2"}, 0]}
}}
}}
])
See how it works on the playground example
The two fields named name_id and age_id respectively. Now I would like to find a document that does not have both two fields and count the total numbers.
Below is the code I tried, but it did not work.
db.user.aggregate([{ "$group": {
"_id" : { user_id: "$key_id" },
"requestA_count": { "$sum": {
"$cond": [ { "$ifNull": [{"$name_id", false},{"$age_id",false}] }, 1, 0 ]
} },
{ "$project": {
"_id": 0,
"requestA_count": 1,
} }
])
I think this is what your looking for. If you want to count docs that have either name_id or age_id simply change $and to $or.
https://mongoplayground.net/p/cuAVkYnLUTq
db.collection.aggregate([
{$group: {
_id: {
// Group by bool, has both name_id and age_id
hasIdAndAge: {
$and: [
{$toBool: "$name_id"},
{$toBool: "$age_id"}
]
}
},
// Count sum
count: {$sum: 1}
}},
// Rework to only output one object with both counts
{$group: {
_id: null,
has: {
$sum: {$cond: [
"$_id.hasIdAndAge", "$count", 0
]}
},
hasNot: {
$sum: {$cond: [
"$_id.hasIdAndAge", 0, "$count"
]}
}
}}
])
// Outputs
[
{
"_id": null,
"has": 1,
"hasNot": 4
}
]
Using the $match operator seems more fitting. You could do something like this:
db.user.aggregate([
{ $match: {$and: [{name_id: null},{age_id: null}]}},
{ $count: "null_name&age"}
])
I haven't tested it but that should point you in the right direction.
Okay, so I've been searching for a while but couldn't find an answer to this, and I am desperate :P
I have some documents with this syntax
{
"period": ISODate("2018-05-29T22:00:00.000+0000"),
"totalHits": 13982
"hits": [
{
// some fields...
users: [
{
// some fields...
userId: 1,
products: [
{ productId: 1, price: 30 },
{ productId: 2, price: 30 },
{ productId: 3, price: 30 },
{ productId: 4, price: 30 },
]
},
]
}
]
}
And I want to retrieve a count of how many products (Independently of which user has them) we have on a period, an example output would be like this:
[
{
"period": ISODate("2018-05-27T22:00:00.000+0000"),
"count": 432
},
{
"period": ISODate("2018-05-28T22:00:00.000+0000"),
"count": 442
},
{
"period": ISODate("2018-05-29T22:00:00.000+0000"),
"count": 519
}
]
What is driving me crazy is the "object inside an array inside an array" I've done many aggregations but I think they were simpler than this one, so I am a bit lost.
I am thinking about changing our document structure to a better one, but we have ~6M documents which we would need to transform to the new one and that's just a mess... but Maybe it's the only solution.
We are using MongoDB 3.2, we can't update our systems atm (I wish, but not possible).
You can use $unwind to expand your array, then use $group to sum:
db.test.aggregate([
{$match: {}},
{$unwind: "$hits"},
{$project: {_id: "$_id", period: "$period", users: "$hits.users"}},
{$unwind: "$users"},
{$project: {_id: "$_id", period: "$period", subCout: {$size: "$users.products"}}},
{$group: {"_id": "$period", "count": {$sum: "$count"}}}
])
I am trying to aggregate some data and group it by Time Intervals as well as maintaining a sub-category, if you will. I want to be able to chart this data out so that I will have multiple different Lines corresponding to each Office that was called. The X axis will be the Time Intervals and the Y axis would be the Average Ring Time.
My data looks like this:
Calls: [{
created: ISODate(xyxyx),
officeCalled: 'ABC Office',
answeredAt: ISODate(xyxyx)
},
{
created: ISODate(xyxyx),
officeCalled: 'Office 2',
answeredAt: ISODate(xyxyx)
},
{
created: ISODate(xyxyx),
officeCalled: 'Office 3',
answeredAt: ISODate(xyxyx)
}];
My goal is to get my calls grouped by Time Intervals (30 Minutes/1 Hour/1 Day) AND by the Office Called. So when my aggregate completes, I'm looking for data like this:
[{"_id":TimeInterval1,"calls":[{"office":"ABC Office","ringTime":30720},
{"office":"Office2","ringTime":3070}]},
{"_id":TimeInterval2,"calls":[{"office":"Office1","ringTime":1125},
{"office":"ABC Office","ringTime":15856}]}]
I have been poking around for the past few hours and I was able to aggregate my data, but I haven't figured out how to group it properly so that I have each time interval along with the office data. Here is my latest code:
Call.aggregate([
{$match: {
$and: [
{created: {$exists: 1}},
{answeredAt: {$exists: 1}}]}},
{$project: { created: 1,
officeCalled: 1,
answeredAt: 1,
timeToAns: {$subtract: ["$answeredAt", "$created"]}}},
{$group: {_id: {"day": {"$dayOfYear": "$created"},
"hour": {
"$subtract": [
{"$hour" : "$created"},
{"$mod": [ {"$hour": "$created"}, 2]}
]
},
"officeCalled": "$officeCalled"
},
avgRingTime: {$avg: '$timeToAns'},
total: {$sum: 1}}},
{"$group": {
"_id": "$_id.day",
"calls": {
"$push": {
"office": "$_id.officeCalled",
"ringTime": "$avgRingTime"
},
}
}},
{$sort: {_id: 1}}
]).exec(function(err, results) {
//My results look like this
[{"_id":118,"calls":[{"office":"ABC Office","ringTime":30720},
{"office":"Office 2","ringTime":31384.5},
{"office":"Office 3","ringTime":7686.066666666667},...];
});
This just doesn't quite get it...I get my data but it's broken down by Day only. Not my 2 hour time interval that I was shooting for. Let me know if I'm doing this all wrong, please --- I am VERY NEW to aggregation so your help is very much appreciated.
Thank you!!
All you really need to do is include the both parts of the _id value your want in the final group. No idea why you thought to only reference a single field.
Also "loose the $project" as it is just wasted cycles and processing, when you can just use directly in $group on the first try:
Call.aggregate(
[
{ "$match": {
"created": { "$exists": 1 },
"answeredAt": { "$exists": 1 }
}},
{ "$group": {
"_id": {
"day": {"$dayOfYear": "$created"},
"hour": {
"$subtract": [
{"$hour" : "$created"},
{"$mod": [ {"$hour": "$created"}, 2]}
]
},
"officeCalled": "$officeCalled"
},
"avgRingTime": {
"$avg": { "$subtract": [ "$answeredAt", "$created" ] }
},
"total": { "$sum": 1 }
}},
{ "$group": {
"_id": {
"day": "$_id.day",
"hour": "$_id.hour"
},
"calls": {
"$push": {
"office": "$_id.officeCalled",
"ringTime": "$avgRingTime"
},
},
"total": { "$sum": "$total" }
}},
{ "$sort": { "_id": 1 } }
]
).exec(function(err, results) {
});
Also note the complete omission of $and. This is not needed as all MongoDB query arguments are already "AND" conditions anyway, unless specifically stated otherwise. Just stick to what is simple. It's meant to be simple.
I'm trying to dynamically sticky sort a collection of records with the value that is sticky being different with each query. Let me give an example. Here are some example docs:
{first_name: 'Joe', last_name: 'Blow', offices: ['GA', 'FL']}
{first_name: 'Joe', last_name: 'Johnson', offices: ['FL']}
{first_name: 'Daniel', last_name: 'Aiken', offices: ['TN', 'SC']}
{first_name: 'Daniel', last_name: 'Madison', offices: ['SC', 'GA']}
... a bunch more names ...
Now suppose I want to display the names in alphabetical order by last name but I want to peg all the records with the first name "Joe" at the top.
In SQL this is fairly straight forward:
SELECT * FROM people ORDER first_name == 'Joe' DESC, last_name
The ability to put expressions in the sort criteria makes this trivial. Using the aggregation framework I can do this:
[
{$project: {
first_name: 1,
last_name: 1
offices: 1,
sticky: {$cond: [{$eq: ['$first_name', 'Joe']}, 1, 0]}
}},
{$sort: [
'sticky': -1,
'last_name': 1
]}
]
Basically I create a dynamic field with the aggregation framework that is 1 if the name if Joe and 0 if the name is not Joe then sort in reverse order. Of course when building my aggregation pipeline I can easily change 'Joe' to be 'Daniel' and now 'Daniel' will be pegged to the top. This is partially what I mean by dynamic sticky sorting. The value I am sticky sorting by will change query-by-query
Now this works great for a basic value like a string. The problem comes when I try to the same thing for a value that hold an array. Say I want to peg all users in 'FL' offices. With Mongo's native understanding of arrays I would think I can do the same thing. So:
[
{$project: {
first_name: 1,
last_name: 1
offices: 1,
sticky: {$cond: [{$eq: ['$offices', 'FL']}, 1, 0]}
}},
{$sort: [
'sticky': -1,
'last_name': 1
]}
]
But this doesn't work at all. I did figure out that if I changed it to the following it would put Joe Johnson (who is only in the FL office) at the top:
[
{$project: {
first_name: 1,
last_name: 1
offices: 1,
sticky: {$cond: [{$eq: ['$offices', ['FL']]}, 1, 0]}
}},
{$sort: [
'sticky': -1,
'last_name': 1
]}
]
But it didn't put Joe Blow at the top (who is in FL and GA). I believe it is doing simple match. So my first attempt doesn't work at all since $eq returns false since we are comparing an array to a string. The second attempt works for Joe Johnson because we are comparing the exact same arrays. But Joe Blow doesn't work since ['GA', 'FL'] != ['FL']. Also if I want to peg both FL and SC at the top I can't give it the value ['FL', 'SC'] to compare against.
Next I try using a combination of $setUnion and $size.
[
{$project: {
first_name: 1,
last_name: 1
offices: 1,
sticky: {$size: {$setUnion: ['$offices', ['FL', 'SC']]}}
}},
{$sort: [
'sticky': -1,
'last_name': 1
]}
]
I've tried using various combinations of $let and $literal but it always complains about me trying to pass a literal array into $setUnion's arguments. Specifically it says:
disallowed field type Array in object expression
Is there any way to do this?
Cannot reproduce your error but you have a few "typos" in your question so I cannot be sure what you actually have.
But presuming you actually are working with MongoDB 2.6 or above then you probably want the $setIntersection or $setIsSubset operators rather than $setUnion. Those operators imply "matching" contents of the array they are compared to, where $setUnion just combines the supplied array with the existing one:
db.people.aggregate([
{ "$project": {
"first_name": 1,
"last_name": 1,
"sticky": {
"$size": {
"$setIntersection": [ "$offices", [ "FL", "SC" ]]
}
},
"offices": 1
}},
{ "$sort": {
"sticky": -1,
"last_name": 1
}}
])
In prior versions where you do not have those set operators you are just using $unwind to work with the array, and the same sort of $cond operation as before within a $group to bring it all back together:
db.people.aggregate([
{ "$unwind": "$offices" },
{ "$group": {
"_id": "$_id",
"first_name": { "$first": "$first_name" },
"last_name": { "$first": "$last_name",
"sticky": { "$sum": { "$cond": [
{ "$or": [
{ "$eq": [ "$offices": "FL" ] },
{ "$eq": [ "$offices": "SC" ] },
]},
1,
0
]}},
"offices": { "$push": "$offices" }
}},
{ "$sort": {
"sticky": -1,
"last_name": 1
}}
])
But you were certainly on the right track. Just choose the right set operation or other method in order to get your precise need.
Or since you have posted your way of getting what you want, a better way to write that kind of "ordered matching" is this:
db.people.aggregate([
{ "$project": {
"first_name": 1,
"last_name": 1,
"sticky": { "$cond": [
{ "$anyElementTrue": {
"$map": {
"input": "$offices",
"as": "o",
"in": { "$eq": [ "$$o", "FL" ] }
}
}},
2,
{ "$cond": [
{ "$anyElementTrue": {
"$map": {
"input": "$offices",
"as": "o",
"in": { "$eq": [ "$$o", "SC" ] }
}
}},
1,
0
]}
]},
"offices": 1
}},
{ "$sort": {
"sticky": -1,
"last_name": 1
}}
])
And that would give priority it documents with "offices" containing "FL" over "SC" and hence then over all others, and doing the operation within a single field. That should also be very easy for people to see how to abstract that into the form using $unwind in earlier versions without the set operators. Where you simply provide the higher "weight" value to the items you want at the top by nesting the $cond statements.
I think I figured out the best way to do it.
[
{$project: {
first_name: 1,
last_name: 1
offices: 1,
sticky_0: {
$cond: [{
$anyElementTrue: {
$map: {
input: "$offices",
as: "j",
in: {$eq: ['$$j', 'FL']}
}
}
}, 0, 1]
},
sticky_1": {
$cond: [{
$anyElementTrue: {
$map: {
input: '$offices',
as: 'j',
in: {$eq: ['$$j', 'SC']}
}
}
}, 0, 1]
}
}},
{$sort: [
'sticky_0': 1,
'sticky_1': 1,
'last_name': 1
]}
]
Basically when building my pipeline I iterate through each item I want to make sticky and from that item create it's own virtual field that checks just the one value. To check just the one value I use a combination of $cond, $anyElementTrue and $map. It's a little convoluted but it works. Would love to hear if there is something simpler.