mongoose aggregation $group with nested object - mongodb

I have the following resolver:
const result = await UserPassage.aggregate([
{ $sort: { createdAt: -1 } },
{
$group: {
_id: '$level',
level: { $first: '$level' },
passageId: { $first: '$passageId' },
userId: { $first: '$userId' },
type: { $first: '$type' },
category: { $first: '$category' },
score: { $first: '$score' },
completedStage: { $first: '$completedStage' },
userPassageStatsId: {
_id: { $first: '$_id' },
stats: {
readingTime: { $first: '$readingTime' },
qtdVocab: { $first: '$qtdVocab' },
qtdTestDone: { $first: '$qtdTestDone' },
totalQuiz: { $first: '$totalQuiz' },
progress: { $first: '$progress' },
},
},
},
},
{ $sort: { level: 1 } },
]);
await UserPassageStats.populate(result, { path: 'userPassageStatsId' });
The problem is that I need to populate 'userPassageStatsId' and return it but it's not working well returning the following error:
MongoError: The field 'userPassageStatsId' must be an accumulator object
does anyone knows what I am doing wrong?

$group can only contain _id or accumulator objects like $first, $last, $sum etc. In your case your building nested object and that syntax is not allowed - accumulator has to be on a top level. You can try two approaches, either return flat structure from $group and then reshape using $project:
{
$group: {
_id: '$level',
level: { $first: '$level' },
passageId: { $first: '$passageId' },
userId: { $first: '$userId' },
type: { $first: '$type' },
category: { $first: '$category' },
score: { $first: '$score' },
completedStage: { $first: '$completedStage' },
userPassageStatsId_id: { $first: '$_id' },
readingTime: { $first: '$readingTime' },
qtdVocab: { $first: '$qtdVocab' },
qtdTestDone: { $first: '$qtdTestDone' },
totalQuiz: { $first: '$totalQuiz' },
progress: { $first: '$progress' }
}
},
{
$project: {
_id: 1,
level: 1,
...,
userPassageStatsId: {
_id: "$userPassageStatsId_id",
stats: {
readingTime: "$readingTime",
...
}
}
}
}
or use $$ROOT to capture first object for every group and reshape it using $project:
{
$group: {
_id: '$level',
d: { $first: "$$ROOT" }
}
},
{
$project: {
_id: 1,
level: "$d.level",
...,
userPassageStatsId: {
_id: "$d._id",
stats: {
readingTime: "$d.readingTime",
...
}
}
}
}

Related

total count from multiple group aggregations

I'm new to mongodb and I've been tasked with building an API capable of returning a total count of objects returned by a previously made aggregation pipeline. The pipeline contains multiple groupings and counts, and I'm wondering how I can tweak the current pipeline to return a total count of objects returned.
function projectionPipeline(islifecycle, pipeline) {
if (islifecycle) {
pipeline.push(
{
$project: {
state: '$state',
_id: 0,
lifecycle: {
$filter: {
input: '$classification',
as: 'lifecycle',
cond: {
$or: [
{
$eq: [
'$$lifecycle.name',
'Creation Lifecycle Status',
],
},
],
},
},
},
},
},
{
$project: {
state: '$state',
lifecycle: '$lifecycle.value',
},
},
{
$group: {
_id: {
state: '$state',
lifecycle: '$lifecycle',
},
counts: { $sum: 1 },
},
},
{
$group: {
_id: '$_id.state',
lifecycles: {
$push: {
lifecycle: '$_id.lifecycle',
lifecycleCount: '$counts',
},
},
count: { $sum: '$counts' },
},
},
{
$project: {
count: 1,
state: '$_id',
lifecycles: '$lifecycles',
_id: 0,
},
},
);
} else {
pipeline.push(
{
$group: {
_id: '$state',
counts: {
$sum: 1,
},
},
},
{
$project: {
counts: 1,
value: '$_id',
_id: 0,
},
},
);
}
}

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

How to paginate in MongoDB with aggregate

I have a collection of documents, each having a groupID. I am able to retrieve only one document from each group, but I'm not sure how to do the effective pagination.
Example:
{
unitquantity: "2"
itemcode: "842852100008"
name: "Atlas Black"
price: "39990"
size: "s"
groupid: "40bf6073-a1d3-4ffa-9ced-dd2f5fcd1b5e",
},
{
unitquantity: "2"
itemcode: "842852100382"
name: "Atlas Black"
price: "39990"
size: "m"
groupid: "40bf6073-a1d3-4ffa-9ced-dd2f5fcd1b5e",
},
{
unitquantity: "2"
itemcode: "842852100746"
name: "Atlas Black"
price: "39990"
size: "xl"
groupid: "40bf6073-a1d3-4ffa-9ced-dd2f5fcd1b5e",
},
These 3 items have a same groupid but they differ on size. I only need to retrieve one from the group, doesn't matter the difference (in this instance it would be size).
I am able to do that with:
var query = {[`category.${type}`]: `${type}`}
var query = {[`category.${type}`]: `${type}`}
db.collection('products').aggregate([
{ $match: query },
{
$group: {
_id: "$groupid",
images: { $last: "$images" },
description: { $last: "$description" },
barcode: { $last: "$barcode" },
category: { $last: "$category" },
subcategory: { $last: "$subcategory" },
id: { $last: "$_id" }
}
},
{ $sort: {'_id': -1} },
{ $limit: 30 }
]).toArray().then(result=>{
lastKey = result[result.length - 1].id
groupId = result[result.length - 1].groupid
})
I store the last groupId into the variable. When I press the Load button I want next 30 to appear. Here is my load code which is basically exactly the same except the query for $match uses the document's _id:
var query = {[`category.${type}`]: `${type}`, 'groupid': { $ne: `${groupId}`}, '_id': {$gt: lastKey}}
db.collection('products').aggregate([
{ $match: query },
{
$group: {
_id: "$groupid",
groupid: { $last: "$groupid" },
images: { $last: "$images" },
description: { $last: "$description" },
barcode: { $last: "$barcode" },
category: { $last: "$category" },
subcategory: { $last: "$subcategory" },
id: { $last: "$_id" }
}
},
{ $sort: {'_id': -1} },
{ $limit: 30 },
]).toArray().then(result => {
lastKey = result[result.length - 1].id
groupId = result[result.length - 1]._id
})
However I get many that repeat themselves.
I saw similar issues other had with $group and $sort and some needed to use $sort before $group and after as well, but it doesn't work for me. I don't really know how to sort them based on document _id correctly.
You can use $skip with the page count. you have to manage page count state in
client
db.collection('products').aggregate([
{ $match: query },
{
$group: {
_id: "$groupid",
groupid: { $last: "$groupid" },
images: { $last: "$images" },
description: { $last: "$description" },
barcode: { $last: "$barcode" },
category: { $last: "$category" },
subcategory: { $last: "$subcategory" },
id: { $last: "$_id" }
}
},
{ $sort: {'_id': -1} },
{ $skip: 30 * page },
{ $limit: 30 },
]).toArray().then(result => {
lastKey = result[result.length - 1].id
groupId = result[result.length - 1]._id
})

Select top 3 per field after group MongoDB

I have a collection with fields like "servicereqesttype", "zipcode", "date"
I want to fing the 3 most common "servicerequesttype" per zipcode for a specific day.
db.event.aggregate([
{
$match: {
creationdate: "2011-01-01"
}
},
{
$project: {
zipcode: "$zipcode",
servicerequesttype: "$servicerequesttype"
}
},
{
$group: {
_id: {
zipcode: "$zipcode",
servicerequesttype: "$servicerequesttype"
},
zipcode: {
$first: "$zipcode"
},
servicerequesttype: {
$first: "$servicerequesttype"
},
count: {$sum: 1}
}
},
{
$sort: {
"zipcode": -1,
"count": -1
}
},
{
$project: {
_id: 0,
zipcode: "$zipcode",
servicerequesttype: "$servicerequesttype",
count: "$count"
}
}
])
now all I have to is to select only 3 per zipcode and I need some help, maybe I have to use $bucket or $map...
db.event.aggregate([
{
$match: {
creationdate: "2011-01-01"
}
},
{
$project: {
zipcode: "$zipcode",
servicerequesttype: "$servicerequesttype"
}
},
{
$group: {
_id: {
zipcode: "$zipcode",
servicerequesttype: "$servicerequesttype"
},
zipcode: {
$first: "$zipcode"
},
servicerequesttype: {
$first: "$servicerequesttype"
},
count: {$sum: 1}
}
},
{
$sort: {
"zipcode": -1,
"count": -1
}
},
{
$project: {
_id: 0,
zipcode: "$zipcode",
servicerequesttype: "$servicerequesttype",
count: "$count",
arrayOfTypes: "$array1",
arrayOfIncidents: "$array2"
}
},
{
$group: {
_id: "$zipcode",
arrayOfTypes: {
$push: {type: "$servicerequesttype", count: "$count"}
}
}
},
{
$project: {
_id: "$_id",
array: {
$slice: ["$arrayOfTypes", 3]
}
}
},
{
$sort: {
"_id": -1
}
}
])

MongoDB - Help needed to make some aggregation

I am having a bad time trying to do an aggregation in MongoDB.
I need to cross some infos from each user and as a final result I want a list of users (where there is only one object for each user) and for each object there is some lists with distinct information.
1 - The createdAtList array must be ordered from the oldest to the newest date. The sumOfTotal means the current position total summed up with the previous sumOfTotal (Exemplified in the code below), not just the sum of the total's
2 - The categotyList must be ordered like: category1, category2, category3 ...
3 - The desired final result must be ordered like: user1, user2, user3 ...
Basically I need some help to do the following:
//List of docs from my collection:
[
{
_id: "doc1",
user: "user1",
category: "category1",
createdAt: "2018-01-01T00:00:00.000Z"
},
{
_id: "doc2",
user: "user1",
category: "category2",
createdAt: "2017-12-12T00:00:00.000Z",
},
{
_id: "doc3",
user: "user1",
category: "category1",
createdAt: "2017-12-12T00:00:00.000Z",
},
{
_id: "doc4",
user: "user1",
category: "category2",
createdAt: "2018-01-01T00:00:00.000Z"
},
{
_id: "doc5",
user: "user1",
category: "category3",
createdAt: "2017-11-11T00:00:00.000Z"
}
]
//Desired result:
{
user: "user1",
createdAtList: [ //list ordered by createdAt
{
createdAt: "2017-11-11T00:00:00.000Z",
total: 1,
sumOfTotal: 0
}
{
createdAt: "2017-12-12T00:00:00.000Z",
total: 2,
sumOfTotal: 3 //summed up with the previous
}
{
createdAt: "2018-01-01T00:00:00.000Z",
total: 2,
sumOfTotal: 5 //summed up with the previous
}
],
categotyList: [ //list ordered by category
{
category: "category1",
total: 2
},
{
category: "category2",
total: 2
},
{
category: "category3",
total: 1
}
]
},
...
Is possible to do this in the same aggregate?
I do not think it really makes sense to have the createdAtList.sumOfTotal field. I do not think the fields in an array should be dependent upon a particular order of the elements. If you want some field to contain the sum of the createdAtList.total field, I think there should only be one field (outside of the array). That being said, here is the query I came up with to give you the desired results (using "users" as the name of the collection):
db.users.aggregate([
{
$group: {
_id: {
user: "$user",
createdAt: "$createdAt"
},
total: { $sum: 1 },
category: { $push: "$category" }
}
},
{
$project: {
_id: 0,
user: "$_id.user",
createdAt: "$_id.createdAt",
total: "$total",
category: 1
}
},
{ $unwind: "$category" },
{
$group: {
_id: {
user: "$user",
category: "$category"
},
catTotal: { $sum: 1 },
createdAtList: {
$push: {
createdAt: "$createdAt",
total: "$total"
}
}
}
},
{
$project: {
_id: 0,
user: "$_id.user",
createdAtList: 1,
category: "$_id.category",
catTotal: 1
}
},
{ $unwind: "$createdAtList" },
{
$group: {
_id: "$user",
createdAtList: {
$addToSet: "$createdAtList"
},
categoryList: {
$addToSet: {
category: "$category",
total: "$catTotal"
}
}
}
},
{ $unwind: "$createdAtList" },
{ $sort: { "createdAtList.createdAt": 1 } },
{
$group: {
_id: "$_id",
createdAtList: {
$push: "$createdAtList"
},
categoryList: {
$first: "$categoryList"
}
}
},
{ $unwind: "$categoryList" },
{ $sort: { "categoryList.category": 1 } },
{
$group: {
_id: "$_id",
createdAtList: {
$first: "$createdAtList"
},
categoryList: {
$push: "$categoryList"
}
}
},
{
$project: {
_id: 0,
user: "$_id",
createdAtList: 1,
sumOfTotal: { $sum: "$createdAtList.total" },
categoryList: 1
}
},
{ $sort: { user: 1 } },
]).pretty()