MongoDB Get every Nth element of array - node.js

I've been working on a small project that takes MQTT data from sensors and stores it in a MongoDB database. I'm working with nodeJS and mongoose. These are my schemas.
export const SensorSchema = new mongoose.Schema({
name: { type: String, required: true, unique: true },
location: { type: String, required: true },
type: { type: String, required: true },
unit: { type: String, required: true },
measurements: { type: [MeasurementSchema] }
},
{
toObject: { virtuals: true },
toJSON: { virtuals: true }
});
export const MeasurementSchema = new mongoose.Schema({
value: {type: Number, required: true},
time: {type: Date, required: true}
});
First I wrote a function that retrieves all measurements that were made in between two timestamps.
const values = Sensor.aggregate([
{ $match: Sensor.getValuesFromPath(sensorPath) },
{ $unwind: "$measurements"},
{ $match: { "measurements.time": { $gte: startTime, $lte: endTime} }},
{ $replaceRoot: { newRoot: "$measurements" } },
{ $project: { _id: 0}},
{ $sort: {time: 1}}
]).exec();
In order to draw a graph in the UI, I need to somehow sort and then limit the data that gets sent to the client. I want to send every Nth Value in a certain interval to ensure that the data somewhat resembles the course of the data.
I would prefer a solution that doesn't fetch all the data from the database.
How would I go about doing this on the db? Can I somehow access the positional index of an element after sorting it? Is $arrayElemAt or $elemMatch the solution?

Befure you run $unwind you can use $filter to apply start/end Date filtering. This will allow you to process measurements as an array. In the next step you can get every N-th element by using $range to define a list of indexes and $arrayElemAt to retrieve elements from these indexes:
const values = Sensor.aggregate([
{ $match: Sensor.getValuesFromPath(sensorPath) },
{ $addFields: {
measurements: {
$filter: {
input: "$measurements",
cond: { $and: [
{ $gte: [ "$$this.time", startTime ] },
{ $lte: [ "$$this.time", endTime ] }
]
}
}
}
} },
{ $addFields: {
measurements: {
$map: {
input: input: { $range: [ 0, { $size: "$measurements" }, N ] },
as: "index",
in: { $arrayElemAt: [ "$measurements", "$$index" ] }
}
}
} },
{ $unwind: "$measurements" },
{ $replaceRoot: { newRoot: "$measurements" } },
{ $project: { _id: 0}},
{ $sort: {time: 1}}
]).exec();

The following aggregation (i) retrieves all measurements that were made in between two timestamps, (ii) sorts by timestamp for each sensor, and (iii) gets every Nth value (specified by the variable EVERY_N).
Sample documents (with some arbitrary data for testing):
{
name: "s-1",
location: "123",
type: "456",
measurements: [ { time: 2, value: 12 }, { time: 3, value: 13 },
{ time: 4, value: 15 }, { time: 5, value: 22 },
{ time: 6, value: 34 }, { time: 7, value: 9 },
{ time: 8, value: 5 }, { time: 9, value: 1 },
]
},
{
name: "s-2",
location: "789",
type: "900",
measurements: [ { time: 1, value: 31 }, { time: 3, value: 32 },
{ time: 4, value: 35 }, { time: 6, value: 39 },
{ time: 7, value: 6}, { time: 8, value: 70 },
{ time: 9, value: 74 }, { time: 10, value: 82 }
]
}
The aggregation:
var startTime = 3, endTime = 10
var EVERY_N = 2 // value can be 3, etc.
db.collection.aggregate( [
{
$unwind: "$measurements"
},
{
$match: {
"measurements.time": { $gte: startTime, $lte: endTime }
}
},
{
$sort: { name: 1, "measurements.time": 1 }
},
{
$group: {
_id: "$name",
measurements: { $push: "$measurements" },
doc: { $first: "$$ROOT" }
}
},
{
$addFields: {
"doc.measurements": "$measurements"
}
},
{
$replaceRoot: { newRoot: "$doc" }
},
{
$addFields: {
measurements: {
$reduce: {
input: { $range: [ 0, { $size: "$measurements" } ] },
initialValue: [ ],
in: { $cond: [ { $eq: [ { $mod: [ "$$this", EVERY_N ] }, 0 ] },
{ $concatArrays: [ "$$value", [ { $arrayElemAt: [ "$measurements", "$$this" ] } ] ] },
"$$value"
]
}
}
}
}
}
] )

Related

I want to write a MongoDB query in NodeJS where it return the matching documents as well as the count of documents too

I want to write a MongoDB query in NodeJS where it return the matching documents as well as the count of documents too. For ex consider the below code -
const result = await Student.aggregate(
[
{
$match: {
...filter
}
},
{
$project: {
_id: 1,
payment: 1,
type: 1,
BirthDate: 1
}
},
{
$sort: { StudentData: -1 }
},
{
$count: 'count'
},
{
$skip: skip
},
{
$limit: limit
}
]
);
Here I want to save two things in the result variable - the number of documents and individually all the documents.
let [{ totalItems, result }] = await Student.aggregate(
[
{
$match: {
...filter
}
},
{
$project: {
_id: 1,
payment: 1,
type: 1,
BirthDate: 1
}
},
{
$facet: {
result: [
{
$sort: { BirthDate: -1 },
},
{
$skip: skip
},
{
$limit: limit
}
],
totalItems: [{ $count: 'count' }]
}
},
{
$addFields: {
totalItems: {
$arrayElemAt: ["$totalItems.count", 0]
},
}
}
]
);

MongoDB get SUM of fields with conditions

On my backend I use mongoDB with nodejs and mongoose
I have many records in mongodb with this structure:
{
..fields
type: 'out',
user: 'id1', <--mongodb objectID,
orderPayment: [
{
_id: 'id1',
paid: true,
paymentSum: 40
},
{
_id: 'id2',
paid: true,
paymentSum: 60,
},
{
_id: 'id3',
paid: false,
paymentSum: 50,
}
]
},
{
..fields
type: 'in',
user: 'id1', <--mongodb objectID
orderPayment: [
{
_id: 'id1',
paid: true,
paymentSum: 10
},
{
_id: 'id2',
paid: true,
paymentSum: 10,
},
{
_id: 'id3',
paid: false,
paymentSum: 77,
}
]
}
I need to group this records by 'type' and get sum with conditions.
need to get sum of 'paid' records and sum of noPaid records.
for a better understanding, here is the result Ι need to get
Output is:
{
out { <-- type field
paid: 100, <-- sum of paid
noPaid: 50 <-- sum of noPaid
},
in: { <-- type field
paid: 20, <-- sum of paid
noPaid: 77 <-- sum of noPaid
}
}
Different solution would be this one. It may give better performance than solution of #YuTing:
db.collection.aggregate([
{
$project: {
type: 1,
paid: {
$filter: {
input: "$orderPayment",
cond: "$$this.paid"
}
},
noPaid: {
$filter: {
input: "$orderPayment",
cond: { $not: "$$this.paid" }
}
}
}
},
{
$set: {
paid: { $sum: "$paid.paymentSum" },
noPaid: { $sum: "$noPaid.paymentSum" }
}
},
{
$group: {
_id: "$type",
paid: { $sum: "$paid" },
noPaid: { $sum: "$noPaid" }
}
}
])
Mongo Playground
use $cond in $group
db.collection.aggregate([
{
"$unwind": "$orderPayment"
},
{
"$group": {
"_id": "$type",
"paid": {
"$sum": {
$cond: {
if: { $eq: [ "$orderPayment.paid", true ] },
then: "$orderPayment.paymentSum",
else: 0
}
}
},
"noPaid": {
"$sum": {
$cond: {
if: { $eq: [ "$orderPayment.paid", false ] },
then: "$orderPayment.paymentSum",
else: 0
}
}
}
}
}
])
mongoplayground

Group nested fields with aggregation and return results with others fields mongo db

I need to change the structure of some field in my mongoDB document.
Here the sample:
[
{
_id: "ObjectId('997v2ha1cv9b0036fa648zx3')",
title: "Adidas Predator",
size: "8",
colors: [
{
hex: "005FFF",
name: "Blue"
},
{
hex: "FF003A",
name: "Red"
},
{
hex: "FFFE00",
name: "Yellow"
},
{
hex: "07FF00",
name: "Green"
},
],
extras: [
{
description: "laces",
type: "exterior"
},
{
description: "sole",
type: "interior"
},
{
description: "logo"
},
{
description: "stud",
type: "exterior"
}
],
media: {
images: [
{
url: "http://link.com",
type: "exterior"
},
{
url: "http://link3.com",
type: "interior"
},
{
url: "http://link2.com",
type: "interior"
},
{
url: "http://link4.com",
type: "exterior"
}
]
}
}
];
My goal is to group some fields:
colors need to be and array with just the colors,
extras need to be an array with 3 object each one for a "type" (interior, exterior, null)
the same for images that is inside media
Here what I expected:
{
_id: "ObjectId('997b5aa1cv9b0036fa648ab5')",
title: "Adidas Predator",
size: "8",
colors: ["Blue", "Red", "Yellow", "Green"],
extras: [
{type: exterior, description: ["laces", "stud"]},
{type: interior, description: ["sole"]},
{type: null, description: ["logo"]}
],
images: [
{type: exterior, url: ["http://link.com", "http://link4.com"]},
{type: interior, url: ["http://link2.com", "http://link3.com"]},
]
};
With my code I can achieve my goal but I don't understand how to show all the information together through the pipeline.
Here my code:
db.collection.aggregate([
{
$project: {
title: 1,
size: 1,
colors: "$colors.name",
extras: 1,
media: "$media.images"
},
},
{
$unwind: "$media"
},
{
$group: {
_id: {
type: "$media.type",
url: "$media.url",
},
},
},
{
$group: {
_id: "$_id.type",
url: {
$push: "$_id.url"
},
},
},
]);
The result is:
[
{
_id: "exterior",
url: [
"http://link.com",
"http://link4.com"
]
},
{
_id: "interior",
url: [
"http://link3.com",
"http://link2.com"
]
}
];
If I do the same thing with extras I get the same (correct) structure.
How can I show all the data together like in the expected structure?
Thanks in advice.
The strategy will be to maintain the require parent fields throughout the pipeline using $first to just grab the initial value, It ain't pretty but it works:
db.collection.aggregate([
{
"$addFields": {
colors: {
$map: {
input: "$colors",
as: "color",
in: "$$color.name"
}
}
}
},
{
$unwind: "$extras"
},
{
"$addFields": {
imageUrls: {
$map: {
input: {
$filter: {
input: "$media.images",
as: "image",
cond: {
$eq: [
"$$image.type",
"$extras.type"
]
}
}
},
as: "image",
in: "$$image.url"
}
}
}
},
{
$group: {
_id: {
_id: "$_id",
extraType: "$extras.type"
},
extraDescriptions: {
"$addToSet": "$extras.description"
},
imageUrls: {
"$first": "$imageUrls"
},
colors: {
$first: "$colors"
},
size: {
$first: "$size"
},
title: {
$first: "$title"
}
}
},
{
$group: {
_id: "$_id._id",
colors: {
$first: "$colors"
},
size: {
$first: "$size"
},
title: {
$first: "$title"
},
images: {
$push: {
type: {
"$ifNull": [
"$_id.extraType",
null
]
},
url: "$imageUrls"
}
},
extras: {
$push: {
type: {
"$ifNull": [
"$_id.extraType",
null
]
},
description: "$extraDescriptions"
}
}
}
}
])
Mongo Playground
You can try $function operator, to defines a custom aggregation function or expression in JavaScript.
$project to show required fields and get array of colors name
$function, write your JS logic if you needed you can sort this logic of group, it will return result with 2 fields (extras, images)
$project to show required fields and separate extras and images field from result
db.collection.aggregate([
{
$project: {
title: 1,
size: 1,
colors: "$colors.name",
result: {
$function: {
body: function(extras, images) {
function groupBy(objectArray, k, v) {
var results = [], res = objectArray.reduce((acc, obj) => {
if (!acc[obj[k]]) acc[obj[k]] = [];
acc[obj[k]].push(obj[v]);
return acc;
}, {});
for (var o in res) {
results.push({ [k]: o === 'undefined' ? null : o, [v]: res[o] })
}
return results;
}
return {
extras: groupBy(extras, 'type', 'description'),
images: groupBy(images, 'type', 'url')
}
},
args: ["$extras", "$media.images"],
lang: "js"
}
}
}
},
{
$project: {
title: 1,
size: 1,
colors: 1,
extras: "$result.extras",
images: "$result.images"
}
}
])
Playground
IMPORTANT:
Executing JavaScript inside an aggregation expression may decrease performance. Only use the $function operator if the provided pipeline operators cannot fulfill your application's needs.

I need mongodb group pipeline for multiple condition

Document
[
{
type: 1,//credit
amount: 60
},
{
type: 2,//debit
amount: 35
},
{
type: 3,//credit
amount: 25
},
{
type: 4,//debit
amount: 80
},
{
type: 5,//credit
amount: 70
},
]
Result
[
{
_id: {
Name: "Credition",
Type: [1, 3, 5]
},
Total_Amount: 155
},
{
_id: {
Name: "Debition",
Type: [2, 4]
},
Total_Amount: 115
},
]
In my schema, there are millions of logs records in which few are credited logs, few are debited logs.
I want to use MongoDB aggregate pipe and have to group like above for million records at a time
Yes you can do that first you need to add a new field transaction on the basis of the type of logs, then you can group the logs on the basis of that field.
Working example - https://mongoplayground.net/p/e4kqeKLIuIr
db.collection.aggregate([
{
$addFields: {
transaction: {
$cond: {
if: {
$in: [
"$type",
[
1,
3,
5
]
]
},
then: "Credition",
else: "Debition"
}
}
}
},
{
$group: {
_id: "$transaction",
Type: {
$addToSet: "$type"
},
Total_Amount: {
$sum: "$amount"
}
}
}
])
After this, you can also use $project operator to change the name or structure of the record, if needed
You can use the operator $cond during the grouping stage:
db.collection.aggregate([
{
$group: {
_id: {
$cond: [
{
$in: [ "$type", [1,3,5] ]
},
"Credition",
"Debition"
]
},
type: {
$addToSet: "$type"
},
amount: {
$sum: "$amount"
}
}
},
{
$project: {
_id: {
Name: "$_id",
Type: "$type"
},
Total_Amount: "$amount"
}
}
])
MongoPlayground

Mongo Aggregate - Get count of how many times multiple values appear in query results

I'm attempting to count the number of times two separate fields are true. I have two values "clickedWouldRecommend" and "clickedWouldNotRecommend". These values are defaulted too FALSE. When a button is clicked in the interface, they are set too TRUE. I'm trying to see how many clickedWouldRecommend = true and how many clickedWouldNotRecommend = true for each branch.name.
db.appointments.aggregate([
{
$match: {
$and: [
{
'branch.org_id': '100000'
},
{ "analytics.clickedWouldRecommend": true },
// Add OR statement to include analytics.clickedWouldNotRecommend = true?
]
}
},
{
$group: {
_id: '$branch.name',
wouldRecommend: { $sum: 1 }
}
}
])
This provides results similar to:
{
"_id": [ 'Clinic Name' ],
"wouldRecommend": 115.0
}
I need to modify the query to also look for cases where analytics.clickedWouldNotRecommend is set to true. I'm trying to get output similar to this ( also notice removing the array from _id if possible ):
{
"name": 'Clinic Name'
"wouldRecommend": 115,
"wouldNotRecommend": 10
},
{
"name": 'Second Clinic Name'
"wouldRecommend": 200,
"wouldNotRecommend": 12
}
Here is the truncated model / schema:
{
branch: [
{
name: {
type: String,
required: true
},
clinic_id: {
type: String,
required: true
},
org_id: {
type: String
}
}
],
analytics: {
clickedWouldRecommend: {
type: Boolean,
default: false
},
clickedWouldNotRecommend: {
type: Boolean,
default: false
}
},
date: {
type: Date,
default: Date.now
}
};
You can use below aggregations
db.appointments.aggregate([
{ "$match": { "branch.org_id": "100000" }},
{ "$unwind": "$branch" },
{ "$facet": {
"wouldRecommend": [
{ "$match": { "analytics.clickedWouldRecommend": true }},
{ "$group": { "_id": "$branch.name" }}
],
"wouldNotRecommend": [
{ "$match": { "analytics.clickedWouldNotRecommend": true }},
{ "$group": { "_id": "$branch.name" }}
]
}}
])
Or
db.appointments.aggregate([
{ "$match": { "branch.org_id": "100000" }},
{ "$unwind": "$branch" },
{ "$group": {
"_id": "$branch.name",
"wouldRecommend": {
"$sum": {
"$cond": [{ "$eq": ["$analytics.clickedWouldRecommend", true] }, 1, 0]
}
},
"wouldNotRecommend": {
"$sum": {
"$cond": [{ "$eq": ["$analytics.clickedWouldRecommend", true]}, 1, 0]
}
}
}}
])

Resources