I need limited nested array in mongodb document - mongodb

I have a document like
{
"deviceId" : "1106",
"orgId" : "5ffe9fe1c9e77c0006f0aad3",
"values" : [
{
"paramVal" : 105.0,
"dateTime" : ISODate("2021-05-05T09:18:08.000Z")
},
{
"paramVal" : 110.0,
"dateTime" : ISODate("2021-05-05T09:18:08.000Z")
},
{
"paramVal" : 115.0,
"dateTime" : ISODate("2021-05-05T10:18:08.000Z")
},
{
"paramVal" : 125.0,
"dateTime" : ISODate("2021-05-05T11:18:08.000Z")
},
{
"paramVal" : 135.0,
"dateTime" : ISODate("2021-05-05T12:18:08.000Z")
}
]
}
Now I need to filter a document which I can do easily with match or find but in that document the subarray i.e. values should have latest 2 values because in future the count can be more than 100.
the output should be like
{
"deviceId" : "1106",
"orgId" : "5ffe9fe1c9e77c0006f0aad3",
"values" : [
{
"paramVal" : 125.0,
"dateTime" : ISODate("2021-05-05T11:18:08.000Z")
},
{
"paramVal" : 135.0,
"dateTime" : ISODate("2021-05-05T12:18:08.000Z")
}
]
}

Try $slice operator, to select number of elements, pass negative value to select documents from below/last elements,
db.collection.aggregate([
{ $set: { values: { $slice: ["$values", -2] } } }
])
Playground
I need for the array values in sorted order by date
There is no straight way to do this, check the below aggregation query, but it will cause the performance issues, i would suggest to change you schema structure to manage this data order by date,
$unwind deconstruct values array
$sort by dateTime in descending order
$group by _id and reconstruct values array and return other required fields
$slice to select number of elements, pass negative value to select documents from below/last elements
db.collection.aggregate([
{ $unwind: "$values" },
{ $sort: { "values.dateTime": -1 } },
{
$group: {
_id: "$_id",
deviceId: { $first: "$deviceId" },
orgId: { $first: "$orgId" },
values: { $push: "$values" }
}
},
{ $set: { values: { $slice: ["$values", 2] } } }
])
Playground

Related

How can I find the sum and average of a document array?

Currently, I have the following document structure. The range field holds sub JSON objects as an array.
{
"_id" : ObjectId("62f60ba0ed0f1a1a0v"),
"userId" : "1431",
"range" : [
{
"index" : 0,
"clubType" : "driver",
"swingSize" : "full",
"distance" : 200,
"createdAt" : "2022-08-12T08:13:20.435+00:00"
},
{
"index" : 0,
"clubType" : "driver",
"swingSize" : "full",
"distance" : 150,
"createdAt" : "2022-08-12T08:13:20.435+00:00"
},
{
"index" : 0,
"clubType" : "wood",
"swingSize" : "full",
"distance" : 180,
"createdAt" : "2022-08-12T08:13:20.435+00:00"
}
]
}
In the above document, I want to sum and average the indexes with the same clubType and swingSize. So I used mongoose Aggregate like below.
result = await ClubRangeResultSchema.aggregate([
{
$match : {
userId : "1431",
range : {
$elemMatch : {
$and : [
{
createdAt : { $gte : lastDate }
},
{
createdAt : { $lte : lastDate }
}
]
}
}
}
},
{
$group : {
'_id' : {
'clubName' : '$range.clubName',
'swingSize' : '$range.swingSize'
},
'totalDistance' : { $sum : { $sum : '$range.distance' }}
}
}
]);
The result of the above query is all duplicate field names, and the total is also extracted for all data.
How should I modify the query?
You're close but need to do a couple of changes:
you want to $unwind the range array, $group doesn't flattern the array so when you use $range.clubType you are basically grouping the array itself as the value.
You want an additional match after the $unwind, the $elemMatch you use does not filter the range object, it does matches the initial document.
After the changes the pipeline should look like this:
db.collection.aggregate([
{
$match: {
userId: "1431",
range: {
$elemMatch: {
createdAt: "2022-08-12T08:13:20.435+00:00"
}
}
}
},
{
$unwind: "$range"
},
{
$match: {
"range.createdAt": "2022-08-12T08:13:20.435+00:00"
}
},
{
$group: {
"_id": {
"clubName": "$range.clubType",
"swingSize": "$range.swingSize"
},
"totalDistance": {
$sum: "$range.distance"
},
avgDistance: {
$avg: "$range.distance"
}
}
}
])
Mongo Playground

Query value in sub-document dictionary MongoDB

I have the following document:
"_id" : 19,
"name" : "Elizabeth Moore",
"achronym" : "EM19",
"calc" : {
"20" : {
"role" : 20,
"score" : 15,
"inRole" : false,
"range" : {
"int" : 80,
"min" : 20
}
and I need to retrieve all _ids having "calc.inRole" false.
I tried:
db.coll.find({'calc.$.inRole': false})
db.coll.find({'calc.inRole': false})
but none of these worked.
How can I achieve that?
Since calc has fields with unknown keys you need to run $objectToArray to transofrm it into array of keys and values. Then you can run $in on that array. If you want to have it as single pipeline step you can use $let operator to define temporary variable:
db.collection.aggregate([
{
$match: {
$expr:{
$let: {
vars: {
arr: { $objectToArray: "$calc" }
},
in: {
$in: [ false, "$$arr.v.inRole" ]
}
}
}
}
},
{
$project: {
_id: 1
}
}
])
Mongo Playground

Calculate average of ratings in array, then add field to original document in MongoDB

I have a documents that have a field called ratings. This is an array of objects, each object containing userId and ratingValue
ratings: Array
0: Object
userId: "uidsample1"
ratingValue: 5
1: Object
userId:"uidsample2"
ratingValue:1.5
I want to do an aggregation pipeline to calculate the new average when one of the ratings in the array is updated or added. Then, I want to put that value in the document as a new field called averageRating.
I have tried unwinding, then $ add field of $avg : "ratings.ratingValue" but it adds to the unwinded documents and doesnt get the average. It looks something like this (not exactly since testing on compass)
db.test.aggregate{
[
{
$unwind: {
path: "$ratings"
}
},
{
$addFields {
averageRating: {
$avg: "$ratings.ratingValue"
}
}
}
]
}
What's a good query structure for this ?
you don't actually need to $unwind and $group to calculate the average, these operations are costly
you can simply $addFields with $avg
db.col.aggregate([
{$addFields : {averageRating : {$avg : "$ratings.ratingValue"}}}
])
sample collection and aggregation
> db.t62.drop()
true
> db.t62.insert({data : {ratings : [{val : 1}, {val : 2}]}})
WriteResult({ "nInserted" : 1 })
> db.t62.find()
{ "_id" : ObjectId("5c44d9719d56bf65be5ab2e6"), "data" : { "ratings" : [ { "val" : 1 }, { "val" : 2 } ] } }
> db.t62.aggregate([{$addFields : {avg : {$avg : "$data.ratings.val"}}}])
{ "_id" : ObjectId("5c44d9719d56bf65be5ab2e6"), "data" : { "ratings" : [ { "val" : 1 }, { "val" : 2 } ] }, "avg" : 1.5 }
Use $group after $unwind as below to calculate the averageRating. Aggregate is a read operation. You need to update the doc afterward.
[
{
'$unwind': {
'path': '$ratings'
}
}, {
'$group': {
'_id': '$_id',
'averageRating': {
'$avg': '$ratings.ratingValue'
}
}
}
]

Check if subdocument in range complies with a condition

I'm working on a mongoDB query.
I have several documents which I query with following results:
{
"_id" : 1000.0,
"date" : ISODate("2018-05-25T00:20:00.000Z"),
"value" : true
}
{
"_id" : 1000.0,
"date" : ISODate("2018-05-25T00:26:00.000Z"),
"value" : false
}
{
"_id" : 1000.0,
"date" : ISODate("2018-05-25T00:30:00.000Z"),
"value" : false
}
The original documents are filtered so that I get only document within the last 15 minutes before now and there is no way of knowing how many entries are in that time range.
I need to expand my existing query so that it returns a status based on the "value". If there are no true I need a status 0, if there is at least 1 but not only true I need a status 1, and if there are only true I need a status 2.
For example:
{
"_id" : 1000,
"status" : 1
},
{
"_id" : 1001,
"status" : 2
}
Is there a way of accomplishing this using mongoDB? Or would it be better/easier to do it on java side? Note that there are several _id in the database.
You can gather all values from each group into one array (using $group and $push) and then use $switch to apply your logic. To determine whether array contains any true value or all values are true you can use $anyElementTrue and $allElementsTrue:
db.col.aggregate([
{
$group: {
_id: "$_id",
values: { $push: "$value" }
}
},
{$unwind:"$values"},
{
$project: {
_id: 1,
status: {
$switch: {
branches: [
{ case: { $allElementsTrue: "$values" }, then: 2 },
{ case: { $anyElementTrue: "$values" }, then: 1 },
],
default: 0
}
}
}
}
])

Create new properties based on embedded array elements in MongoDB

I have this kind of document
{
"_id" : ObjectId("573342930348ce88ff1685f3"),
"presences" : [
{
"_id" : ObjectId("573342930348ce88ff1685f2"),
"createdAt" : NumberLong(1458751869000)
},
{
"_id" : ObjectId("573342930348ce88ff1685f5"),
"createdAt" : NumberLong(1458751885000)
},
{
"_id" : ObjectId("573342930348ce88ff1685f7"),
"createdAt" : NumberLong(1458751894000)
}
]
}
How can I extract first and last presences element to new properties firstPresence and lastPresence like this?
{
"_id" : ObjectId("573342930348ce88ff1685f3"),
"firstPresence": {
"_id" : ObjectId("573342930348ce88ff1685f2"),
"createdAt" : NumberLong(1458751869000)
},
"lastPresence": {
"_id" : ObjectId("573342930348ce88ff1685f7"),
"createdAt" : NumberLong(1458751894000)
},
"presences" : [
...
]
}
I want to use a query that can be applied to all documents in one time.
You need to $unwind your presences array to do the aggregation. Before grouping you can sort them by createdAt to utilize $first and $last operators.
db.collection.aggregate(
[
{ $unwind: "$presences" },
{ $sort: { "presences.createdAt": 1 } },
{
$group: {
_id: "$_id",
"presences": { $push: "$presences" },
"lastPresence": { $last: "$presences" },
"firstPresence": { $first: "$presences" },
}
},
{ $out : "collection" }
])
Last aggregation pipeline ($out) will replace existing collection.
According to above mentioned description as a solution to it please try executing following aggregate query into MongoDB shell
db.collection.aggregate(
// Pipeline
[
// Stage 1
{
$project: {
first: {
$arrayElemAt: ["$presences", 0]
},
last: {
$arrayElemAt: ["$presences", -1]
},
presences: 1
}
},
]
);