mongodb foreach doc find min of subset and update it - node.js

Product and variants price
Find Minimum of all products.Variants.Price where size is small and update it by 15%
{
"_id" : 23,
"name" : "Polo Shirt",
"Variants" : [
{
"size" : "Large",
"Price" : 82.42
},
{
"size" : "Medium",
"Price" : 20.82 // this should get increased by 15%
},
{
"size" : "Small",
"Price" : 42.29
}
]
},
{
"_id" : 24,
"name" : "Polo Shirt 2",
"Variants" : [
{
"size" : "Large",
"Price" : 182.42
},
{
"size" : "Medium",
"Price" : 120.82 // this should get increased by 15%
},
{
"size" : "Small",
"Price" : 142.29
}
]
}
I started something like this. Not sure if this is the right start
db.products.find().forEach(function(product){
var myArr = product.Variants;
print(myArr.min());
});

There is a problem here in that you cannot in a single update statement identify the "minimum" value in an array to use with a positional update, so you are right in a way with your current approach.
It is arguable that a better approach would be to pre-determine which element is the minimal element and this pass that to the update. You can do this using .aggregate():
var result = db.products.aggregate([
{ "$unwind": "$Variants" },
{ "$sort": { "_id": 1, "Variants.price" } }
{ "$group": {
"_id": "$_id",
"size": { "$first": "$Variants.size" },
"price": { "$first": "$Variants.price" }
}},
{ "$project": {
"size": 1,
"price": 1,
"adjusted": { "$multiply": [ "$price", 1.15 ] }
}}
])
So of course that is yet only a result with simply the lowest Variant item details for each product but then you could use the results like this:
result.result.forEach(function(doc) {
db.products.update(
{
"_id": doc._id,
"Variants": { "$elemMatch": {
"size": doc.size,
"price": doc.price
}}
},
{
"$set": {
"Variants.$.price": doc.adjusted
}
}
}
})
That is not the best form but it does at least remove some of the overhead with iterating an array and allows a way to do the calculations on the server hardware, which is possibly of a higher spec from the client.
It still doesn't really look like too much though until you take in some features available for MongoDB 2.6 and upwards. Notably that aggregate gets a cursor for a response and that you can now also do "bulk updates". So the form can be changed like so:
var cursor = db.products.aggregate([
{ "$unwind": "$Variants" },
{ "$sort": { "_id": 1, "Variants.price" } }
{ "$group": {
"_id": "$_id",
"size": { "$first": "$Variants.size" },
"price": { "$first": "$Variants.price" }
}},
{ "$project": {
"size": 1,
"price": 1,
"adjusted": { "$multiply": [ "$price", 1.15 ] }
}}
]);
var batch = [];
while ( var doc = cursor.next() ) {
batch.push({
"q": {
"_id": doc._id,
"Variants": { "$elemMatch": {
"size": doc.size,
"price": doc.price
}}
},
"u": {
"$set": {
"Variants.$.price": doc.adjusted
}
}
});
if ( batch.length % 500 == 0 ) {
db.command({ "update": "products", "updates": batch });
}
}
db.command({ "update": "products", "updates": batch });
So that is really nice in that while you are still iterating over a list the traffic and waiting for responses over the wire has really been minimized. The best part is the batch updates which are occurring ( by the math usage ) only once per 500 items. The maximum size of the batch items is actually the BSON limit of 16MB so you can tune that as appropriate.
That gives a few good reasons if you are currently developing a product to move to the 2.6 version.
The only final footnote I would add considering you are dealing with "prices" is try not to use floating point math for this and look for a form using whole integers instead as it avoids a lot of problems.

This is how I pulled it off.
var result = db.Products.aggregate(
[ { "$unwind":"$Variants" },{"$match":{"Variants.size":"small"}},
{ "$group":
{"_id":"$_id","minprice":
{"$min":"$Variants.price" }}},
{$sort:{ _id: 1}} ] )
result.result.forEach(function(doc) {
db.Products.update( { "_id": doc._id },
{ "$pull": { "Variants" : {
"price":doc.minprice,
"size":"small"
} } } ,
{ $addToSet: { "Variants":{
"price":doc.minprice*1.15,
"size":"small"
} }
);
});

Related

How to compare two columns whether they are equal in mongoose?

I need to compare two columns and return the count value by grouping.
I've tried the below aggregation:
Activity.aggregate([
{
$group: {
_id: '$phasename',
data: {
'$push': {
Complete:
{
$cond: [
{$eq: ["$plannedEndDate", "$actualEndDate"]},
0,
1
]
}
}
},
}
},
{
$project: {
Complete1: "$data.Complete"
}
}
]);
But its not taking the column values, if I hardcode then this works fine.
please suggest how to fix this.thanks
my sample collection is ,
{
"_id": "5e0dd8628003b63cf48eb2b9",
"text": "act1",
"incumbentUser": "Sam",
"vendorUser": "Sam",
"plannedEndDate": "2020-01-01T00:00:00.000+00:00",
"phasename": "Knowledge Transfer",
"actualEndDate": "2010-01-01T00:00:00.000+00:00"
},
{
"_id": "5e0dd8628003b63cf48eb2b8",
"text": "act1",
"incumbentUser": "Sam",
"vendorUser": "Sam",
"plannedEndDate": "2020-01-01T00:00:00.000+00:00",
"phasename": "Analysis",
"actualEndDate": "2010-01-03T00:00:00.000+00:00"
}

Better Way to Aggregate and Assign Random Winner

I'm trying to aggregate a set of transactions using the data set below and choose a winner in every grade. The winner is randomly chosen from within the grade.
{ "_id" : ObjectId("5ce6fb4b3d1be918e574500a"),
"eventId" : ObjectId("5ce2f540bf126322a6be559b"),
"donationAmt" : 32,
"ccTranId" : "HzP4B",
"firstName" : "Jason",
"lastName" : "Jones",
"grade" : "1",
"teacher" : "Smith, Bob",
"studentId" : 100 },
{ "_id" : ObjectId("5ce6fb4b3d1be918e574500b"),
"eventId" : ObjectId("5ce2f540bf126322a6be559b"),
"donationAmt" : 15,
"ccTranId" : "HzP4A",
"firstName" : "Joey",
"lastName" : "Jones",
"grade" : "1",
"teacher" : "Smith, Jane",
"studentId" : 200 },
{ "_id" : ObjectId("5ce6fb4b3d1be918e574500c"),
"eventId" : ObjectId("5ce2f540bf126322a6be559b"),
"donationAmt" : 25,
"ccTranId" : "HzP4D",
"firstName" : "Carrie",
"lastName" : "Jones",
"grade" : "2",
"teacher" : "Smith, Sally",
"studentId" : 300 }
I'm using this script to aggregate.
Donation.aggregate([
{
$match: {
eventId: mongoose.Types.ObjectId(eventId)
}
},
{
"$group": {
"_id": "$studentId",
"first": { "$first": "$firstName" },
"last": { "$first": "$lastName" },
"grade": { "$first": "$grade" },
"teacher": { "$first": "$teacher" }
}
},
{
"$group": {
"_id": "$grade",
"students": {
$push: '$$ROOT'
}
}
}
, { $sort: { _id: 1 } }
])
The output gives me this to work with. Then, I iterate through the each element and assign one of the students in the subdocument as winner.
The double group seems sloppy and it would be nice to execute an expression within a $project clause to randomly assign the winner.
Is there a cleaner way?
{
"_id":"1",
"students":[
{
"_id":100,
"first":"Jason",
"last":"Jones",
"grade":"1",
"teacher":"Smith, Bob"
},
{
"_id":200,
"first":"Joey",
"last":"Jones",
"grade":"1",
"teacher":"Smith, Jae"
}
]
},
{
"_id":"2",
students":[ .... ]
},
Random means that you need to get unpredictable results. The only operator that can help you in MongoDB is $sample. Unfortunately you can't sample arrays. All you can do is to apply filtering condition and then run { $sample: { size: 1 } } on that filtered data set:
db.col.aggregate([
{
$match: {
eventId: ObjectId("5ce2f540bf126322a6be559b"),
grade: "2"
}
},
{ $sample: { size: 1 } }
])
To make it a little bit more useful you can take advantage of $facet and run multiple samples for every grade in one query:
db.col.aggregate([
{
$match: {
eventId: ObjectId("5ce2f540bf126322a6be559b")
}
},
{
$facet: {
winner1: [
{ $match: { grade: "1" } },
{ $sample: { size: 1 } }
],
winner2: [
{ $match: { grade: "2" } },
{ $sample: { size: 1 } }
]
// other grades ...
}
}
])

Single aggregation query for multiple groups

My Collection JSON
[
{
"_id" : 0,
"finalAmount":40,
"payment":[
{
"_id":0,
"cash":20
},
{
"_id":1,
"card":20
}
]
},
{
"_id" : 1,
"finalAmount":80,
"payment":[
{
"_id":0,
"cash":60
},
{
"_id":1,
"card":20
}
]
},
{
"_id" : 2,
"finalAmount":80,
"payment":[
{
"_id":0,
"cash":80
}
]
}
]
I want to have the amount, cash and card group wise using aggregation framework. Can anyone help?
Please consider my _id as an ObjectId for demo purpose as I have given 0 and 1. I am using Node Js and MongoDB and I want the expected output in just one query as follows:
Expected Output:
{
"cash":160,
"card":40,
"total":200,
"count":3
}
You could try running the following aggregation pipeline, although there might be some performance penalty or potential aggregation pipeline limits with huge datasets since your initial pipeline tries to group all the documents in the collection to get the total document count and the amount as well as pushing all the documents to a temporary list, which may affect performance down the pipeline.
Nonetheless, the following solution will yield the given desired output from the given sample:
collection.aggregate([
{
"$group": {
"_id": null,
"count": { "$sum": 1 },
"doc": { "$push": "$$ROOT" },
"total": { "$sum": "$finalAmount" }
}
},
{ "$unwind": "$doc" },
{ "$unwind": "$doc.payment" },
{
"$group": {
"_id": null,
"count": { "$first": "$count" },
"total": { "$first": "$total" },
"cash": { "$sum": "$doc.payment.cash" },
"card": { "$sum": "$doc.payment.card" }
}
}
], function(err, result) {
console.log(result);
});
When running on big datasets, this problem might be more suitable, more fast to solve with a map reduce operation, since the result is one singel aggregated result.
var map = function map(){
var cash = 0;
var card = 0;
for (i in this.payment){
if(this.payment[i].hasOwnProperty('cash')){
cash += this.payment[i]['cash']
}
if(this.payment[i].hasOwnProperty('card')){
card += this.payment[i]['card']
}
}
var doc = {
'cash': cash,
'card': card,
};
emit(null, doc);
};
var reduce = function(key, values){
var total_cash = 0;
var total_card = 0;
var total = 0;
for (i in values){
total_cash += values[i]['cash']
total_card += values[i]['card']
}
var result = {
'cash': total_cash,
'card': total_card,
'total': total_cash+ total_card,
'count': values.length
};
return result
};
db.runCommand({"mapReduce":"test", map:map, reduce:reduce, out:{replace:"test2"}})
result:
db.test2.find().pretty()
{
"_id" : null,
"value" : {
"cash" : 160,
"card" : 40,
"total" : 200,
"count" : 3
}
}

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

Create Price Range in mongo with aggregation pipeline with Nodejs

Wan't create Price range Using mongodb aggregation pipeline..
while using elastic search or solr we can directly get price filter range value... How can i create price range according to my products price, if there is no product in that range then don't create that range...
{
"_id" : ObjectId("5657412ddb70397479575d1d"),"price" : 1200
},
{
"_id" : ObjectId("5657412ddb70397479575d1d"),"price" : 200
},
{
"_id" : ObjectId("5657412ddb70397479575d1d"),"price" : 2000
},
{
"_id" : ObjectId("5657412ddb70397479575d1d"),"price" : 2020
},
{
"_id" : ObjectId("5657412ddb70397479575d1d"),"price" : 100
},
{
"_id" : ObjectId("5657412ddb70397479575d1d"),"price" : 3500
},
{
"_id" : ObjectId("5657412ddb70397479575d1d"),"price" : 3900
}
From above i have to create price range as par product price like filter available in flipkart OR myntra using Mongo aggregation...
[
{
range : '100-200',
count : 2
},
{
range : '1200-2020',
count : 3
},
{
range : '3500-3900',
count : 2
}
]
Within the aggregation framework pipeline, you can take advantage of the $cond operator in the $project stage to create an extra field that denotes the range the price falls in, and then use the $group step to get the counts:
var pipeline = [
{
"$project": {
"price": 1,
"range": {
"$cond": [
{
"$and": [
{ "$gte": ["$price", 100] },
{ "$lte": ["$price", 200] }
]
},
"100-200",
{
"$cond": [
{
"$and": [
{ "$gte": ["$price", 1200] },
{ "$lte": ["$price", 2020] }
]
},
"1200-2020", "2021-above"
]
}
]
}
}
},
{
"$group": {
"_id": "$range",
"count": { "$sum": 1 }
}
},
{
"$project": {
"_id": 0,
"range": "$_id",
"count": 1
}
}
];
collection.aggregate(pipeline, function (err, result){
if (err) {/* Handle err */};
console.log(JSON.stringify(result, null, 4));
});

Resources