MongoDB - Mongoose : Aggregate generation, $match $group $project - node.js

I'm newbie in the mongoDB world. I'm developping a website on nodeJS with expressJS and mongoose as DB.
I'm studying graphic orientation and I am generated a DB with this modele
:
var userSchema = new Schema({
profile:{
sexe: String, // ('male' or 'female')
age: String, // ('kid', 'adult', 'old')
rank: String, // ('low', 'middle', 'high')
},
design:{
luminosity: String, // ('light', 'dark')
color: String, // ('blue', 'green', 'pink', 'orange')
shape: String, // ('rounded', 'squared')
font: String, // ('serif', 'sans serif')
}
});
On different page, I want to display some statistics, for exemple, on the page luminosity, where user chooses between light or dark. I want to display him what people mostly pick.
For example, with the section sexe : what mostly pick female (68% of them choose light, for example).
I think the best way to get all the numbers to create the statistics is to use aggregate.
As newbie, it destroys my mind to generate the perfect query, I am really lost in this framework !
Would love your advice !

Your aggregation operation will be dependant on the $cond operator in the $group pipeline step to evaluate the counts based on the page luminosity and/or page color. Use the $sum accumulator operator to return the sum on each evaluated group, something like the following:
var pipeline = [
{
"$group": {
"_id": "$profile.sexe",
"light_count": {
"$sum": {
"$cond": [ { "$eq": [ "$design.luminosity", "light" ] }, 1, 0 ]
}
},
"dark_count": {
"$sum": {
"$cond": [ { "$eq": [ "$design.luminosity", "dark" ] }, 1, 0 ]
}
},
"blue_count": {
"$sum": {
"$cond": [ { "$eq": [ "$design.color", "blue" ] }, 1, 0 ]
}
},
"green_count": {
"$sum": {
"$cond": [ { "$eq": [ "$design.color", "green" ] }, 1, 0 ]
}
},
"pink_count": {
"$sum": {
"$cond": [ { "$eq": [ "$design.color", "pink" ] }, 1, 0 ]
}
},
"orange_count": {
"$sum": {
"$cond": [ { "$eq": [ "$design.color", "orange" ] }, 1, 0 ]
}
}
}
},
{
"$project": {
"_id": 0, "gender": "$_id",
"luminosity": {
"light": "$light_count",
"dark": "$dark_count"
},
"color": {
"blue": "$blue_count",
"green": "$green_count",
"pink": "$pink_count",
"orange": "$orange_count"
}
}
}
];
User.aggregate(pipeline)
.exec(function (err, result){
// handle error
if (err) throw Error;
console.log(result);
})

Related

Push items into array of objects with array attribute in mongoose

Hello I am trying to add an element to an array that is inside an object and the object in turn inside an array, below is the structure.
// Schema called "Team" with mongoose
category: [
{
seasson: { type: String, required: true },
categories: [{ type: String, required: true }]
}]
// In code looks like:
[
{
seasson: "The seasson name 1",
categories: ["categoryOne", "categoryTwo"]
}
{
seasson: "The seasson name 2",
categories: ["categoryOne"] // I want to make push in this array the value "categoryTwo"
},
]
// I´m trying something like following code:
const status = await Team.updateOne(
{
_id: mongoose.Types.ObjectId(teamId),
},
{ $addToSet: { "category.$last.categories": "categoryTwo"} }
)
Whenever an array has to be pushed into the object, it will be in the last position of the main array. Honestly, I've been trying to find a way for a while, but I can't think of anything that works.
Thanks in advance.
There is no straight way to update the last element of the array without any identity, you can use update with aggregation pipeline starting from MongoDB 4.2,
$map to iterate loop of category array
$mergeObjects to merge current category object with updated categories field
$last to get the last element value from category.seasson
$cond check condition if above last element's value and current object session matches then do update operation otherwise return existing values
$setUnion to concat new value of categories, if it is present then it will do replace
let category = "categoryTwo";
const status = await Team.updateOne(
{ _id: mongoose.Types.ObjectId(teamId) },
[{
$set: {
category: {
$map: {
input: "$category",
in: {
$mergeObjects: [
"$$this",
{
categories: {
$cond: [
{
$eq: [
{ $last: "$category.seasson" },
"$$this.seasson"
]
},
{ $setUnion: ["$$this.categories", [category]] },
"$$this.categories"
]
}
}
]
}
}
}
}
}]
)
Playground
The bellow query,adds "categoryTwo" in categories,of the last member of array
category.I think this is what you want.
If you can next time give the document in the initial form,describe the query you want,and give the document in the final form,in valid JSON so people can help you easier.
You can try the code here
Its pipeline update,needs MongoDB >= 4.2
Data in(Collection)
[
{
"_id": 1,
"category": [
{
"seasson": "The seasson name 1",
"categories": [
"categoryOne",
"categoryTwo"
]
},
{
"seasson": "The seasson name 2",
"categories": [
"categoryOne"
]
},
]
}
]
Query
db.collection.update({
"_id": {
"$eq": 1
}
},
[
{
"$addFields": {
"category": {
"$let": {
"vars": {
"without_last": {
"$slice": [
"$category",
0,
{
"$subtract": [
{
"$size": "$category"
},
1
]
}
]
},
"last_member": {
"$arrayElemAt": [
{
"$slice": [
"$category",
-1,
1
]
},
0
]
}
},
"in": {
"$concatArrays": [
"$$without_last",
[
{
"$mergeObjects": [
"$$last_member",
{
"categories": {
"$concatArrays": [
"$$last_member.categories",
[
"categoryTwo"
]
]
}
}
]
}
]
]
}
}
}
}
}
])
Results
[
{
"_id": 1,
"category": [
{
"categories": [
"categoryOne",
"categoryTwo"
],
"seasson": "The seasson name 1"
},
{
"categories": [
"categoryOne",
"categoryTwo"
],
"seasson": "The seasson name 2"
}
]
}
]

how to calculate total for each enum of a field in aggregate?

hello I have this function where I want to calculate the number of orders for each status in one array, the code is
let statusEnum = ["pending", "canceled", "completed"];
let userOrders = await Orders.aggregate([
{
$match: {
$or: [
{ senderId: new mongoose.Types.ObjectId(req.user._id) },
{ driverId: new mongoose.Types.ObjectId(req.user._id) },
{ reciverId: new mongoose.Types.ObjectId(req.user._id) },
],
},
},
{
$group: {
_id: null,
totalOrders: { $sum: 1 },
totalPendingOrders: "??", //I want to determine this for each order status
totalCompletedOrders: "??",
totalCanceledOrders: "??",
},
},
]);
so I could add add a $match and use {status : "pending"} but this will filter only the pending orders, I could also map the status enum and replace each element instead of the "pending" above and then push each iteration in another array , but that just seems so messy, is there any other way to calculate total for each order status with using only one aggregate?
thanks
You can use group as you used, but with condition
db.collection.aggregate([
{
$group: {
_id: null,
totalPendingOrders: {
$sum: { $cond: [ { $eq: [ "$status", "pending" ] }, 1, 0 ] }
},
totalCompletedOrders: {
$sum: { $cond: [ { $eq: [ "$status", "completed" ] }, 1, 0 ] }
},
totalCanceledOrders: {
$sum: { $cond: [ { $eq: [ "$status", "canceled" ] }, 1, 0 ] }
}
}
}
])
Working Mongo playground

Use calculated value for comparison in aggregation

var data_form = {
{
_id : "123",
result:{
run:10
},
result_re:{
run:10
},
result_ch:{
run:10
},
result_qm:{
run:10
}
},
{
_id : "345",
result:{
run:20
},
result_re:{
run:20
},
result_ch:{
run:20
},
result_qm:{
run:20
}
},
{
_id : "567",
result:{
run:30
},
result_re:{
run:30
},
result_ch:{
run:30
},
result_qm:{
run:30
}
}
}
var pipeline = [
{ $project: {
total: { $add: [ "$result.run", "$result_re.run", "$result_ch.run", "$result_qm.run"] } ,
discount:{
$cond: [ { $gt: [ total , 50 ] }, 1, 0]
}
}
},
{ $sort: {total: -1}},
{ $limit : 10 }
]
db.getCollection('game_users').aggregate(pipeline)
I need to compare total output with aggregation condition and counter increase if condition match.
My collection is defined in data_form variable.
total field output get from query and if that total is grater than 50 after that counter increase.
You need to specify the expression within the $cond. You cannot reference the value of another calculated field within the same pipeline stage. Either do it twice or put in separate stages. The same stage is the most efficient:
var pipeline = [
{ $project: {
total: {
$add: [
"$result.run",
"$result_re.run",
"$result_ch.run",
"$result_qm.run"
]
} ,
discount:{
$cond: [
{ $gt: [
{ $add: [
"$result.run",
"$result_re.run",
"$result_ch.run",
"$result_qm.run"
]},
50
]},
1,
0
]
}
}},
{ $sort: {total: -1}},
{ $limit : 10 }
]
Or separate the $project in two stages
var pipeline = [
{ $project: {
total: {
$add: [
"$result.run",
"$result_re.run",
"$result_ch.run",
"$result_qm.run"
]
}
}},
{ $project: {
total: 1,
discount:{
$cond: [
{ $gt: [ "$total", 50 ] }
1,
0
]
}
}},
}}
{ $sort: {total: -1}},
{ $limit : 10 }
]
This looks "prettier" but running another stage means another pass through data, so it's probably best to do in one stage.
To get the "totals" across the collection, run a separate aggregation to the paged results.
var pipeline = [
{ $group: {
_id: null,
total: {
$sum: {
$add: [
"$result.run",
"$result_re.run",
"$result_ch.run",
"$result_qm.run"
]
}
} ,
discount:{
$sum: {
$cond: [
{ $gt: [
{ $add: [
"$result.run",
"$result_re.run",
"$result_ch.run",
"$result_qm.run"
]},
50
]},
1,
0
]
}
}
}}
];
Do not try and get both the paged results and the total in the same response since that is not how you do it. These should be run separately as attempting to return in one result will certainly break the BSON limit in real world use cases.

Mongoose SUM + $cond + array field

"payments": [
{
"_id": "57bea755acfbfc4e37c3dfdf",
"user": "57b1c3d2d591a46848c25f45",
"transferred_amount": 10,
"transaction_type": "refund",
"reason": "#1968 shop box refunded",
"__v": 0
},
{
"_id": "57beb883acfbfc4e37c3dfe0",
"user": "57b1c3d2d591a46848c25f45",
"transferred_amount": 10,
"transaction_type": "payout",
"reason": "#1968 shop box refunded",
"__v": 0
}
]
this is my db data.
Model.aggragate().project({
paid_out_amount: {
$sum: {
$cond: [{
$eq: ['$payments.transaction_type', 'payout']
}, 0, '$payments.transferred_amount']
}
}
})
This is my node code to fetch those data. I'm trying sum payout amount alone and store it into a field. Here $cond always returns zero. can anyone help me out.
You can try using $unwind operator.
Like:
Model.aggregate([
{ $unwind: "$payments" },
{
$group:
{
_id: null,
paid_out_amount: { $sum: {$cond: [ { $eq: [ "$payments.transaction_type", 'payout' ] }, '$payments.transferred_amount', 0 ] } }
}
}
]);
I assume that you want to add all transferred_amount of payout type and return total sum that's why use _id:null. if need you can add fieldName for group by

How to match and sort documents based on array elements in common

var UserSchema = Schema (
{
android_id: String,
created: {type: Date, default:Date.now},
interests: [{ type: Schema.Types.ObjectId, ref: 'Interests' }],
});
Users.aggregate([
{ $match: {android_id: {$ne: userID}, interests: {$elemMatch: {$in: ids}} }},
{ $group: { _id: { android_id: '$android_id'},count: {$sum: 1}}},
{ $sort: {count: -1}},
{ $limit: 5 }],
I need the to find the top 5 android_ids of the users with the most interests in common with me (ids array). I can work with the array of only matched elements from the interests array too.
You seemed to be going along the right lines here but you do need to consider that arrays have special considerations for comparisons.
Your basic start here is to find all users that are not the current user, and that you also need at least the "interests" array of the current user as well. You seem to be doing that already, but for here let us consider that you have the whole user object for the current user which will be used in the listing.
This makes your "top 5" basically a product of "Not me, and the most interests in common", which means you basically need to count the "overlap" of interests on each user compared to the current user.
This is basically the $setIntersection of the two arrays or "sets" where the elements in common are returned. In order to count how many are in common, there is also the $size operator. So you apply like this:
Users.aggregate(
[
{ "$match": {
"android_id": { "$ne": user.android_id },
"interests": { "$in": user.interests }
}},
{ "$project": {
"android_id": 1,
"interests": 1,
"common": {
"$size": {
"$setIntersection": [ "$interests", user.interests ]
}
}
}},
{ "$sort": { "common": -1 } },
{ "$limit": 5 }
],
function(err,result) {
}
);
The result returned in "common" is the count of common interests between the current user and the user being examined in the data. This data is then processed by $sort in order to put the largest number of common interests on top, and then $limit returns only the top 5.
If for some reason your MongoDB version is presently lower than MongoDB 2.6 where both the $setIntersection and $size operators are introduced, then you can still do this, but it just takes a longer form of processing the arrays.
Mainly you need to $unwind the arrays and process each match individually:
{ "$match": {
"android_id": { "$ne": user.android_id },
"interests": { "$in": user.interests }
}},
{ "$unwind": "$interests" },
{ "$group": {
"_id": "$_id",
"android_id": { "$first": "$android_id" },
"interests": { "$push": "$interests" },
"common": {
"$sum": {
"$add": [
{ "$cond": [{ "$eq": [ "$interests", user.interests[0] ] },1,0 ] },
{ "$cond": [{ "$eq": [ "$interests", user.interests[1] ] },1,0 ] },
{ "$cond": [{ "$eq": [ "$interests", user.interests[2] ] },1,0 ] }
]
}
}
}},
{ "$sort": { "common": -1 }},
{ "$limit": 5 }
Which is more practically coded to generate the condtional matches in the pipeline:
var pipeline = [
{ "$match": {
"android_id": { "$ne": user.android_id },
"interests": { "$in": user.interests }
}},
{ "$unwind": "$interests" }
];
var group =
{ "$group": {
"_id": "$_id",
"android_id": { "$first": "$android_id" },
"interests": { "$push": "$interests" },
"common": {
"$sum": {
"$add": []
}
}
}};
user.interests.forEach(function(interest) {
group.$group.common.$sum.$add.push(
{ "$cond": [{ "$eq": [ "$interests", interest ] }, 1, 0 ] }
);
});
pipeline.push(group);
pipeline = pipeline.concat([
{ "$sort": { "common": -1 }},
{ "$limit": 5 }
])
User.aggregate(pipeline,function(err,result) {
});
The key elements there being that "both" the current user and the user being inspected have their "interests" separated out for comparison to see if they are "equal". The result from $cond attributes a 1 where this is true or 0 where false.
Any returns ( and only ever expected to be 1 at best, per pair ) are passed to the $sum accumulator which counts the matches in common. You can alternately $match with an $in condition again:
{ "$unwind": "$interests" },
{ "$match": { "interests": { "$in": user.interests } },
{ "$group": {
"_id": "$_id",
"android_id": { "$first": "$android_id" },
"common": { "$sum": 1 }
}}
But this is naturally destructive of the array content as non matches are filtered out. So it depends on what you would rather have in the response.
That is the basic process for getting the "common" counts for use in further processing like $sort and $limit in order to get your "top 5".
Just for fun, here is a basic node.js listing to show the effects of common matches:
var async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.connect('mongodb://localhost/sample');
var interestSchema = new Schema({
name: String
});
var userSchema = new Schema({
name: String,
interests: [{ type: Schema.Types.ObjectId, ref: 'Interest' }]
});
var Interest = mongoose.model( 'Interest', interestSchema );
var User = mongoose.model( 'User', userSchema );
var interestHash = {};
async.series(
[
function(callback) {
async.each([Interest,User],function(model,callback) {
model.remove({},callback);
},callback);
},
function(callback) {
async.each(
[
"Tennis",
"Football",
"Gaming",
"Cooking",
"Yoga"
],
function(interest,callback) {
Interest.create({ name: interest},function(err,obj) {
if (err) callback(err);
interestHash[obj.name] = obj._id;
callback();
});
},
callback
);
},
function(callback) {
async.each(
[
{ name: "Bob", interests: ["Tennis","Football","Gaming"] },
{ name: "Tom", interests: ["Football","Cooking","Yoga"] },
{ name: "Sue", interests: ["Tennis","Gaming","Yoga","Cooking"] }
],
function(data,callback) {
data.interests = data.interests.map(function(interest) {
return interestHash[interest];
});
User.create(data,function(err,user) {
//console.log(user);
callback(err);
})
},
callback
);
},
function(callback) {
async.waterfall(
[
function(callback) {
User.findOne({ name: "Bob" },callback);
},
function(user,callback) {
console.log(user);
User.aggregate(
[
{ "$match": {
"_id": { "$ne": user._id },
"interests": { "$in": user.interests }
}},
{ "$project": {
"name": 1,
"interests": 1,
"common": {
"$size": {
"$setIntersection": [ "$interests", user.interests ]
}
}
}},
{ "$sort": { "common": -1 } }
],
function(err,result) {
if (err) callback(err);
Interest.populate(result,'interests',function(err,result) {
console.log(result);
callback(err);
});
}
);
}
],
callback
);
}
],
function(err) {
if (err) throw err;
//console.dir(interestHash);
mongoose.disconnect();
}
);
Which will output:
{ _id: 55dbd7be0e5516ac16ea62d1,
name: 'Bob',
__v: 0,
interests:
[ 55dbd7be0e5516ac16ea62cc,
55dbd7be0e5516ac16ea62cd,
55dbd7be0e5516ac16ea62ce ] }
[ { _id: 55dbd7be0e5516ac16ea62d3,
name: 'Sue',
interests:
[ { _id: 55dbd7be0e5516ac16ea62cc, name: 'Tennis', __v: 0 },
{ _id: 55dbd7be0e5516ac16ea62ce, name: 'Gaming', __v: 0 },
{ _id: 55dbd7be0e5516ac16ea62d0, name: 'Yoga', __v: 0 },
{ _id: 55dbd7be0e5516ac16ea62cf, name: 'Cooking', __v: 0 } ],
common: 2 },
{ _id: 55dbd7be0e5516ac16ea62d2,
name: 'Tom',
interests:
[ { _id: 55dbd7be0e5516ac16ea62cd, name: 'Football', __v: 0 },
{ _id: 55dbd7be0e5516ac16ea62cf, name: 'Cooking', __v: 0 },
{ _id: 55dbd7be0e5516ac16ea62d0, name: 'Yoga', __v: 0 } ],
common: 1 } ]

Resources