Mongo db Group by, count with a condition - mongodb

Using Mongodb, I want to get the count of sensor values above 100 and sensorvalues below 100 for each particular region(group by region).
I have a sensorValue property and it has 4 sub properties namely.
1)sensorValue (the values will be 100, 200 122, 80 etc) - I want to know the count of above 100 and below 100 per region.
2)Latitude
3)Longitude
4)Region (The name of the region) - I want the count with respect to this region.
With the help of stackoverflow, I wrote the below query.
getProximityIntervalRate = (req, res) => {
console.log("entered1")
this.model = ProximityLocation;
const startDate = req.headers.startdate, endDate = req.headers.enddate;
console.log(req.headers, startDate, endDate);
// TODO: server validatoin
this.model.aggregate([
{ $match: { 'observationTimestamp': { $gte: new Date(startDate), $lte: new Date(endDate) } } },
{
$project: {
regoin: 1,
lessthan: {
$cond: [{ $lt: ["$sensorValue.sensorValue", 5] }, 1, 0]
},
morethan: {
$cond: [{ $gt: ["$sensorValue.sensorValue", 5] }, 1, 0]
}
}
},
{
$group: { _id: { regoin: "$sensorValue.regoin" },
countSmaller: { $sum: "$lessThan" },
countBigger: { $sum: "$moreThan" } uh
}
},
], (err, location) => {
console.log('location', location);
if (!location) { return res.sendStatus(404); }
res.status(200).json(location);
});
}
I am not sure how to address the subproperty "sensorValue.regoin" under the "$project" option.Please let me know if I am missing something.

You can try below aggregation to get the result
db.t66.aggregate([
{$group: {
_id : "$sensorValue.region",
lessThan : {$sum : {$cond: [{$lt : [{$toInt : "$sensorValue.sensorValue"}, 50]}, 1,0]}},
greaterThan : {$sum : {$cond: [{$gte : [{$toInt : "$sensorValue.sensorValue"}, 50]}, 1,0]}},
}}
])
you can remove $toInt if the sensorValue is int datatype

Related

variable undefined after findOne operation mongodb

I am trying to make an API that makes use of 2 databases to generate a fine. Here is the code:
router.get("/generateFine/:bookingID/:currDate", function (req, res, next) {
var currDate,
returnDate,
fine,
totalFine = 0;
Booking.findOne({ _id: req.params.bookingID }).then(function (booking) {
Car.findOne({ _id: booking.carID }).then(function (car) {
currDate = Date.parse(req.params.currDate) / 1000 / 3600 / 24;
returnDate = Date.parse(booking.bookingDates[1]) / 1000 / 3600 / 24;
fine = car.fine;
if (currDate > returnDate) {
totalFine = fine * (currDate - returnDate);
}
console.log(totalFine);
// res.send(totalFine);
});
console.log("totalFine is " + totalFine);
// res.send(totalFine);
});
});
Here are the two Schemas used in the code:
Booking Schema:
{
"_id" : ObjectId("621bf46602edf12942f0d5c9"),
"carID" : "621b87af70c150da70b1dabf",
"bookingDates" : [
"2022-03-05",
"2022-03-06"
],
}
Car Schema:
{
"_id" : ObjectId("621b87af70c150da70b1dabf"),
"name" : "Toyota",
"rate" : 60,
"fine" : 10,
"datesBooked" : [
{
"from" : "2022-03-05",
"to" : "2022-03-06"
},
{
"from" : "2022-03-07",
"to" : "2022-03-08"
},
{
"from" : "2022-03-09",
"to" : "2022-03-10"
}
],
"__v" : 0
}
I want to return the generated fine to the user. When I am trying to send the result, it throwing an error. The first console log prints the correct result, but the second console log prints 0. Also, how can I send the result without getting an error.
Thanks already!
You could use $lookup aggregation pipeline stage to include the car document that matches on the carID field, create additional computed fields that will aid you in getting the total fine whilst using the necessary aggregation operators.
Essentially you would need to run an aggregate pipeline that follows:
const mongoose = require('mongoose');
router.get('/generateFine/:bookingID/:currDate', async function (req, res, next) {
const currDate = new Date(req.params.currDate);
const [{ totalFine }] = await Booking.aggregate([
{ $match: { _id: mongoose.Types.ObjectId(req.params.bookingID) }},
{ $lookup: {
from: 'cars', // or from: Car.collection.name
let: { carId: { $toObjectId: '$carID' } }, // convert the carID string field to ObjectId for the match to work correctly
pipeline: [
{ $match: {
$expr: { $eq: [ '$_id', '$$carId' ] }
} }
],
as: 'car'
} },
{ $addFields: {
car: { $arrayElemAt: ['$car', 0 ] }, // get the car document from the array returned above
returnDate: {
$toDate: { $arrayElemAt: ['$bookingDates', 1 ]}
}
} },
// compute the overdue days
{ $addFields: {
overdueDays: {
$trunc: {
$ceil: {
$abs: {
$sum: {
$divide: [
{ $subtract: [currDate, '$returnDate'] },
60 * 1000 * 60 * 24
]
}
}
}
}
}
} },
{ $project: { // project a new field
totalFine: {
$cond: [
{ $gt: [currDate, '$returnDate'] }, // IF current date is greater than return date
{ $multiply: ['$car.fine', '$overdueDays'] }, // THEN multiply car fine with the overdue days
0 // ELSE total fine is 0
]
}
} }
]).exec();
console.log("totalFine is " + totalFine);
// res.send(totalFine);
});

MongoDB: How to speed up my data reorganisation query/operation?

I'm trying to analyse some data and I thought my queries would be faster ultimately by storing a relationship between my collections instead. So I wrote something to do the data normalisation, which is as follows:
var count = 0;
db.Interest.find({'PersonID':{$exists: false}, 'Data.DateOfBirth': {$ne: null}})
.toArray()
.forEach(function (x) {
if (null != x.Data.DateOfBirth) {
var peep = { 'Name': x.Data.Name, 'BirthMonth' :x.Data.DateOfBirth.Month, 'BirthYear' :x.Data.DateOfBirth.Year};
var person = db.People.findOne(peep);
if (null == person) {
peep._id = db.People.insertOne(peep).insertedId;
//print(peep._id);
}
db.Interest.updateOne({ '_id': x._id }, {$set: { 'PersonID':peep._id }})
++count;
if ((count % 1000) == 0) {
print(count + ' updated');
}
}
})
This script is just passed to mongo.exe.
Basically, I attempt to find an existing person, if they don't exist create them. In either case, link the originating record with the individual person.
However this is very slow! There's about 10 million documents and at the current rate it will take about 5 days to complete.
Can I speed this up simply? I know I can multithread it to cut it down, but have I missed something?
In order to insert new persons into People collection, use this one:
db.Interest.aggregate([
{
$project: {
Name: "$Data.Name",
BirthMonth: "$Data.DateOfBirth.Month",
BirthYear: "$Data.DateOfBirth.Year",
_id: 0
}
},
{
$merge: {
into: "People",
// requires an unique index on {Name: 1, BirthMonth: 1, BirthYear: 1}
on: ["Name", "BirthMonth", "BirthYear"]
}
}
])
For updating PersonID in Interest collection use this pipeline:
db.Interest.aggregate([
{
$lookup: {
from: "People",
let: {
name: "$Data.Name",
month: "$Data.DateOfBirth.Month",
year: "$Data.DateOfBirth.Year"
},
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: ["$Name", "$$name"] },
{ $eq: ["$BirthMonth", "$$month"] },
{ $eq: ["$BirthYear", "$$year"] }
]
}
}
},
{ $project: { _id: 1 } }
],
as: "interests"
}
},
{
$set: {
PersonID: { $first: "$interests._id" },
interests: "$$REMOVE"
}
},
{ $merge: { into: "Interest" } }
])
Mongo Playground

Is it better to iterate over documents to get the minimum value of field or use aggregate function in MongoDB?

Suppose I have a collection of items.
[
{
'item': ... ,
'price' : ...
}
.
.
.
]
I need all the documents that have the 'item' : 'A', along with the minimum price of all such items. So, in general, if the number of such documents of item "A" can be at max 2000, is it better to find the minimum price by iterating over the retrieved documents or use the aggregate function in mongodb (if i use aggregate, i will essentially be going over the documents twice, once by using the .find({..}) and once by using .aggregate(). Or is there a way to combine both retrieving and getting the minimum.
EDIT [Added Explanation]
For example, if I have 3 documents
[
{
'item': 'A' ,
'price' : 30
},
{
'item': 'B' ,
'price' : 40
},
{
'item': 'A' ,
'price' : 20
}
]
I want the output to be similar to:
[
{
'item': 'A' ,
'price' : 30
},
{
'item': 'A' ,
'price' : 20
},
'min_price' : 20
]
Thanks a lot in advance.
Here are two ways to write the query to get minimum price:
(1) Aggregation:
The aggregation query runs on the server and returns the result (minimum price) to the client (the mongo shell from where you run the query). That is the result in one action.
db.collection.aggregate([
$match: { item: 'A' },
$group: { _id: null, min_price: { $min: "$price" } }
)
(2) Not so efficient way:
The find query runs on the server and gets all the documents matching the query filter { item : 'A' } to the client (mongo shell). In the shell you iterate (loop) over the returned documents to figure which document has the minimum price. That is multiple actions - query on the server, a network trip back to the client, and processing on the client.
db.collection.find( { item: 'A' } )
.toArray()
.map(obj => obj.price)
.reduce((acc, curr) => (acc < curr) ? acc : curr)
[ EDIT ADD ]
The aggregation gets all the documents with minimum price:
db.collection.aggregate( [
{
$match: { item: "A" }
},
{
$group: {
_id: null,
min_price: { $min: "$price" },
docs: { $push: "$$ROOT" }
}
},
{
$project: {
docs: {
$filter: { input: "$docs", as: "doc", cond: { $eq: [ "$$doc.price", "$min_price" ] } } }
}
},
{
$unwind: "$docs"
},
{
$replaceRoot: { newRoot: "$docs" }
}
] ).pretty()
aggregationQuery =
[ {
$match : { item: 'A' }
}
{
$group:
{
_id: "$item",
minPrice: { $min: "$price" }
}
}
]
db.collection.aggregate(aggregationQuery)

MongoDB aggregate query to SpringDataMongoDB

I have below MongoDB aggregate query and would like to have it's equivalent SpringData Mongodb query.
MongoDB Aggregate Query :
db.response.aggregate(
// Pipeline
[
// Stage 1 : Group by Emotion & Month
{
$group: {
_id: {
emotion: "$emotion",
category: "$category"
},
count: {
$sum: 1
},
point: {
$first: '$point'
}
}
},
// Stage 2 : Total Points
{
$addFields: {
"totalPoint": {
$multiply: ["$point", "$count"]
}
}
},
// Stage3 : Group By Category - Overall Response Total & totalFeedbacks
{
$group: {
_id: '$_id.category',
totalFeedbacks: {
$sum: "$count"
},
overallResponseTotal: {
$sum: "$totalPoint"
}
}
},
// Stage4 - Overall Response Total & totalFeedbacks
{
$project: {
_id: 1,
overallResponseTotal: '$overallResponseTotal',
maxTotalFrom: {
"$multiply": ["$totalFeedbacks", 3.0]
},
percent: {
"$multiply": [{
"$divide": ["$overallResponseTotal", "$maxTotalFrom"]
}, 100.0]
}
}
},
// Stage4 - Percentage Monthwise
{
$project: {
_id: 1,
overallResponseTotal: 1,
maxTotalFrom: 1,
percent: {
"$multiply": [{
"$divide": ["$overallResponseTotal", "$maxTotalFrom"]
}, 100.0]
}
}
}
]
);
I have tried it's equivalent in Spring Data but got stuck at Stage 2 on how to convert "$addFields" to java code. Though I search about it on multiple sites but couldn't find anything useful. Please see my equivalent java code for Stage 1.
//Stage 1 -Group By Emotion and Category and return it's count
GroupOperation groupEmotionAndCategory = Aggregation.group("emotion","category").count().as("count").first("point")
.as("point");
Aggregation aggregation = Aggregation.newAggregation(groupEmotionAndCategory);
AggregationResults<CategoryWiseEmotion> output = mongoTemplate.aggregate(aggregation, Response.class, CategoryWiseEmotion.class);
Any helps will be highly appreciated.
$addFields is not yet supported by Spring Data Mongodb.
One workaround is to pass the raw aggregation pipeline to Spring.
But since you have a limited number of fields after stage 1, you could also downgrade stage 2 to a projection:
{
$project: {
// _id is included by default
"count" : 1, // include count
"point" : 1, // include point
"totalPoint": {
$multiply: ["$point", "$count"] // compute totalPoint
}
}
}
I haven't tested it myself, but this projection should translate to something like:
ProjectionOperation p = project("count", "point").and("point").multiply(Fields.field("count")).as("totalPoint");
Then you can translate stage 3, 4 and 5 similarly and pass the whole pipeline to Aggregation.aggregate().

Sum unique properties in different collection elements

I am quite new to MongoDB. Hopefully I am using the correct terminology to express my problem.
I have the following collection:
Data collection
{
"name":"ABC",
"resourceId":"i-1234",
"volumeId":"v-1234",
"data":"11/6/2013 12AM",
"cost": 0.5
},
{
"name":"ABC",
"resourceId":"v-1234",
"volumeId":"",
"data":"11/6/2013 2AM",
"cost": 1.5
}
I want to query the collection such that if a volumeId matches with another entries resourceId, then sum up the corresponding resourceId's cost together.
As a result, the cost would be 2.0 in this case.
Basically I want to match the volumeId of one entry to the resourceId of another entry and sum the costs if matched.
I hope I have explained my problem properly. Any help is appreciated. Thanks
Try this aggregation query:
db.col.aggregate([
{
$project: {
resourceId: 1,
volumeId: 1,
cost: 1,
match: {
$cond: [
{$eq: ["$volumeId", ""]},
"$resourceId",
"$volumeId"
]
}
}
},
{
$group: {
_id: '$match',
cost: {$sum: '$cost'},
resId: {
$addToSet: {
$cond: [
{$eq: ['$match', '$resourceId']},
null,
'$resourceId'
]
}
}
}
},
{$unwind: '$resId'},
{$match: {
resId: {
$ne: null
}
}
},
{
$project: {
resourseId: '$resId',
cost: 1,
_id: 0
}
}
])
And you will get the following:
{ "cost" : 2, "resourseId" : "i-1234" }
This is assuming the statement I wrote in the comment is true.