Mongodb :- merging object ifNull then return data if present - mongodb

Extension of this Question, accepted answer is working but there are some things which i am not understanding.
Everything is working but there is one more thing in this is if array1 i.e shiftListData does not have any data of any date for example lets take 25th Jan 2023 but array2 i.e attendances have data of 25th Jan 2023 then it will not merge both object and return nothing i.e it will skip that date even if data is present on array2

From my interpretation, the question is more concerned about "patching" the info from attendances into shiftListData. While your concern is correct and understandable, I would say the case may not be that meaningful since it will not create meaningful, merged shiftListData in this case.
However, the above is just my personal interpretation. We can still solve your question by applying the same idea from the original solution. We just need to revert the merging order to create another merged result. Use $setUnion to join them together to create the final merged list.
db.collection.aggregate([
{
$set: {
shiftListData1: {
$map: {
input: "$shiftListData",
as: "shift",
in: {
$mergeObjects: [
"$$shift",
{
$ifNull: [
{
$first: {
$filter: {
input: "$attendances",
cond: {
$eq: [
"$$this.Date",
"$$shift.date"
]
}
}
}
},
{}
]
}
]
}
}
},
shiftListData2: {
$map: {
input: "$attendances",
as: "a",
in: {
$mergeObjects: [
{
$ifNull: [
{
$first: {
$filter: {
input: "$shiftListData",
cond: {
$eq: [
"$$this.date",
"$$a.Date"
]
}
}
}
},
{}
]
},
"$$a"
]
}
}
},
attendances: "$$REMOVE"
}
},
{
"$project": {
shiftListData: {
$setUnion: [
"$shiftListData1",
"$shiftListData2"
]
}
}
}
])
Mongo Playground

Related

How to remove a field of an array's nested object that has an empty string value using mongodb aggregation?

So far, after i tried, i came up with solution where i am able to remove the whole object inside of the array if that object has field with empty value. That does not work in my case. I only need to remove the field and keep rest of the object. In this case, "Comment" field is the one having empty values occasionally. Thanks in advance!
Structure:
someArray: [
{
field1:"value",
field2:"value",
Comment:"",
Answer:"",
},
{
field1:"value",
field2:"value",
Comment:"",
Answer:"",
}]
Code:
$project: {
someArray: {
$filter: {
input: "$someArray", as: "array",
cond: { $ne: [ "$$array.Comment", ""]}}}}
Use $map to loop over the array elements.For each array element where comment is not an empty string, return whole element, otherwise return the document excluding comment field. Like this:
db.collection.aggregate([
{
"$project": {
someArray: {
$map: {
input: "$someArray",
as: "element",
in: {
$cond: {
if: {
$eq: [
"",
"$$element.Comment"
]
},
then: {
field1: "$$element.field1",
field2: "$$element.field2"
},
else: "$$element"
}
}
}
}
}
},
])
Here, is the working link.
Here is a solution where an array's nested object can have multiple fields and these need not be referred in the aggregation. Removes the nested object's field with value as an empty string (""):
db.collection.aggregate([
{
$set: {
someArray: {
$map: {
input: '$someArray',
as: 'e',
in: {
$let: {
vars: {
temp_var: {
$filter: {
input: { $objectToArray: '$$e' },
cond: { $ne: [ '', '$$this.v' ] },
}
}
},
in: {
$arrayToObject: '$$temp_var'
}
}
}
}
}
}
},
])
Solution from Charchit Kapoor works only if your array has exactly
{
field1: ...
field2: ...
Comment:""
}
But it does not work for arbitrary fields. I was looking for more generic solution, my first idea was this:
db.collection.aggregate([
{
"$project": {
someArray: {
$map: {
input: "$someArray",
in: {
$cond: {
if: { $eq: ["$$this.Comment", ""] },
then: { $mergeObjects: ["$$this", { Comment: "$$REMOVE" }] },
else: "$$this"
}
}
}
}
}
}
])
but it does not work.
I ended on this one:
db.collection.aggregate([
{
"$project": {
someArray: {
$map: {
input: "$someArray",
in: {
$cond: {
if: { $eq: ["", "$$this.Comment"] },
then: {
$arrayToObject: {
$filter: {
input: {
$map: {
input: { $objectToArray: "$$this" },
as: "element",
in: { $cond: [{ $eq: ["$$element.k", "Comment"] }, null, "$$element"] }
}
},
as: "filter",
cond: "$$filter" // removes null's from array
}
}
},
else: "$$this"
}
}
}
}
}
}
])
Mongo Playground

$filter inside $reduce or inside $map from array without unwind

I need some help:
I want to optimize this query to be faster , it need to filter by events.eventType:"log" all docs with server:"strong" , but without separate unwind & filter stages , maybe somehow inside the $reduce stage to add $filter.
example single document:
{
server: "strong",
events: [
{
eventType: "log",
createdAt: "2022-01-23T10:26:11.214Z",
visitorInfo: {
visitorId: "JohnID"
}
}
current aggregation query:
db.collection.aggregate([
{
$match: {
server: "strong"
}
},
{
$project: {
total: {
$reduce: {
input: "$events",
initialValue: {
visitor: [],
uniquevisitor: []
},
in: {
visitor: {
$concatArrays: [
"$$value.visitor",
[
"$$this.visitorInfo.visitorId"
]
]
},
uniquevisitor: {
$cond: [
{
$in: [
"$$this.visitorInfo.visitorId",
"$$value.uniquevisitor"
]
},
"$$value.uniquevisitor",
{
$concatArrays: [
"$$value.uniquevisitor",
[
"$$this.visitorInfo.visitorId"
]
]
}
]
}
}
}
}
}
}
])
expected output , two lists with unique visitorId & list of all visitorId:
[
{
"total": {
"uniquevisitor": [
"JohnID"
],
"visitor": [
"JohnID",
"JohnID"
]
}
}
]
playground
In the example query no filter is added for events.eventType:"log" , how can this be implemented without $unwind?
I am not sure this approach is more optimized than yours but might be this will help,
$filter to iterate loop of events and filter by eventType
$let to declare a variable events and store the above filters result
return array of visitor by using dot notation $$events.visitorInfo.visitorId
return array of unique visitor uniquevisitor by using dot notation $$events.visitorInfo.visitorId and $setUnion operator
db.collection.aggregate([
{ $match: { server: "strong" } },
{
$project: {
total: {
$let: {
vars: {
events: {
$filter: {
input: "$events",
cond: { $eq: ["$$this.eventType", "log"] }
}
}
},
in: {
visitor: "$$events.visitorInfo.visitorId",
uniquevisitor: {
$setUnion: "$$events.visitorInfo.visitorId"
}
}
}
}
}
}
])
Playground
Or similar approach without $let and two $project stages,
db.collection.aggregate([
{ $match: { server: "strong" } },
{
$project: {
events: {
$filter: {
input: "$events",
cond: { $eq: ["$$this.eventType", "log"] }
}
}
}
},
{
$project: {
total: {
visitor: "$events.visitorInfo.visitorId",
uniquevisitor: {
$setUnion: "$events.visitorInfo.visitorId"
}
}
}
}
])
Playground

Sum time series with different time stamps MongoDB

I have a mongoDB database with multiple time series data and each time stamp is a separate document with some additional meta data from sensors. I want to sum the two time series in an aggregation but I am heavily struggling with that and can't find any examples.
Assume we have sensor A and B and the time stamps from the different sensors don't align. See an example of the data below. Next I want to sum the "volume" metric of the two time series. So for the example below sensor A has two time stamps en sensor B 3. So the sum of A and B should have 5 time stamps such that the sum reflects all the changes in the total volume (see also the schematic example below).
Anyone knows how to solve this in a mongoDB aggregation query? I can only use the mongoDB query language and not use NodeJS.
Sensor A
{
"_id":5d67d9ee074e99274eef30d5
"sensor": A
"volume":12.4
"temperatue": 20
"timestamp":2019-08-29 15:58:06.093
"__v":0
}
{
"_id":5d67da66074e99274eef30ea
"sensor": A
"volume":12.3
"temperatue": 21
"timestamp":2019-08-29 16:48:06.078
"__v":0
}
..etc
Sensor B
{
"_id":5d67d9ee074e99274eef30d5
"sensor": B
"volume":32.4
"temperatue": 20
"timestamp":2019-08-29 15:55:06.093
"__v":0
}
{
"_id":5d67da66074e99274eef30ea
"sensor": B
"volume":21.2
"temperatue": 21
"timestamp":2019-08-29 16:49:06.178
"__v":0
}
{
"_id":5d67da66074e99274eef30ea
"sensor": B
"volume":22.3
"temperatue": 22
"timestamp":2019-08-29 17:04:06.078
"__v":0
}
..etc
Here also a sketch of the result I would like to have.
Try this one:
db.collection.aggregate([
// Determine start and end-time
{ $sort: { timestamp: -1 } },
{ $group: { _id: "$sensor", data: { $push: "$$ROOT" } } },
{
$set: {
data: {
$reduce: {
input: "$data",
initialValue: [],
in: {
$concatArrays: [
"$$value",
[
{
$mergeObjects: [
"$$this",
{
timestamp_end: {
$ifNull: [ { $last: "$$value.timestamp" }, "$$NOW" ]
}
}
]
}
]
]
}
}
}
}
},
{ $unwind: "$data" },
// find data per interval
{ $sort: { "data.timestamp": 1 } },
{
$group: {
_id: null,
data: { $push: "$data" },
timestamp: { $addToSet: "$data.timestamp" }
}
},
{
$set: {
sum_data: {
$map: {
input: "$timestamp",
as: "t",
in: {
$filter: {
input: "$data",
cond: {
$and: [
{ $lte: [ "$$this.timestamp", "$$t" ] },
{ $gt: [ "$$this.timestamp_end", "$$t" ] }
]
}
}
}
}
}
}
},
// sum up temperatures
{
$set: {
volume: {
$map: {
input: "$sum_data",
in: { $sum: "$$this.volume" }
}
},
result: { $range: [ 0, { $size: "$timestamp" } ] }
}
},
// Join arrays to final result
{
$project: {
result: {
$map: {
input: "$result",
as: "i",
in: {
timestamp: { $arrayElemAt: [ "$timestamp", "$$i" ] },
volume: { $arrayElemAt: [ "$volume", "$$i" ] }
}
}
}
}
}
])
Mongo Playground

MongoDB aggregate pipelines with linked object

I'm linking two objects in one query and use aggregate function for it. Some data is localized and I'm using a solution from here to get data for specified locale.
I am struggling to do the same with data from the linked object (rooms). Specifically, list data for given locale from the roomDetails.
Please take a look at the Mongo playground
You just need to add filter in your second $addFields stage that you are filtering roomTypes and rest of the stages would be same, just highlighted the new code in below from start comment and end comment,
I am suggesting this solution in your implemented query, i am not sure this is the right approach to do this, might be more will cause the performance of query.
$reduce to iterate loop of roomDetails.description array $cond to match local and return match result to value, same process for roomDetails.title array, and merge this 2 updated fields with current object using $mergeObjects
{
$addFields: {
roomTypes: {
$map: {
input: "$roomTypes",
in: {
$mergeObjects: [
"$$this",
{
Start:
roomDetails: {
$mergeObjects: [
"$$this.roomDetails",
{
description: {
$reduce: {
input: "$$this.roomDetails.description",
initialValue: "",
in: {
$cond: [
{ $eq: ["$$this.locale", "pl"] },
"$$this.value",
"$$value"
]
}
}
},
title: {
$reduce: {
input: "$$this.roomDetails.title",
initialValue: "",
in: {
$cond: [
{ $eq: ["$$this.locale", "pl"] },
"$$this.value",
"$$value"
]
}
}
}
}
]
},
~End~
available: {
$reduce: {
input: "$$this.capacity",
initialValue: 0,
in: {
$cond: [
{ $eq: ["$$this.cruiseID", "$cruiseID"] },
"$$this.available",
"$$value"
]
}
}
}
}
]
}
}
}
}
}
Playground
In generic option i have answered in your reference question you can use same function like,
function languageFilter(inputField, locale) {
return {
$reduce: {
input: inputField,
initialValue: "",
in: {
$cond: [{ $eq: ["$$this.locale", locale] }, "$$this.value", "$$value"]
}
}
};
}
Your final query would be:
let locale = "pl";
db.cs.aggregate([
{ $match: { cID: "00001" } },
{
$lookup: {
from: "rooms",
localField: "roomTypes.roomID",
foreignField: "roomID",
as: "roomTypes"
}
},
{
$addFields: {
title: languageFilter("$title", locale),
description: languageFilter("$description", locale),
roomTypes: {
$map: {
input: "$roomTypes",
in: {
$mergeObjects: [
"$$this",
{
roomDetails: {
$mergeObjects: [
"$$this.roomDetails",
{
description: languageFilter("$$this.roomDetails.description", locale),
title: languageFilter("$$this.roomDetails.title", locale)
}
]
},
available: {
$reduce: {
input: "$$this.capacity",
initialValue: 0,
in: {
$cond: [
{ $eq: ["$$this.cruiseID", "$cruiseID"] },
"$$this.available",
"$$value"
]
}
}
}
}
]
}
}
}
}
},
{
$project: {
_id: 0,
"roomTypes": { _id: 0 },
"roomTypes.capacity": 0
}
}
]);

Get key of the value based on a condition from unnamed nested array of objects in Mongo DB aggregate

I have a document structures like this :
{
"_id": xxxxx,
"Name": "John Doe",
"Grades":[
{
"Physics":89,
},
{
"Math":45
},
{
"Chemistry":57
}
]
}
I would like to project grades as an array of only the subjects that have over 60.
I tried this but this didn't work:
$arrayElemAt: [{ $objectToArray: { $gte: ['$hhEthGrp',60] } }, 0]
You definitely need $objectToArray to access a values for unknown keys but you also need $filter for outer array and $anyElementTrue along with $map to determine where there's any value for unknown key which has value over 60:
db.collection.aggregate([
{
$addFields: {
Grades: {
$filter: {
input: "$Grades",
cond: {
$let: {
vars: { kv: { $objectToArray: "$$this" } }
in: {
$anyElementTrue: {
$map: {
input: "$$kv.v",
in: {
$gt: [ "$$this", 60 ]
}
}
}
}
}
}
}
}
}
}
])
Mongo Playground