How to use $subtract along with conditions to use a default value from an array, if null? - mongodb

I'm trying to subtract values from final array and start array in my aggregation pipeline. But there are certain exceptional cases that needs some additional logic before subtraction.
Expected Output:
I need to subtract nth value of startarray from nth value of final array
And then, get the total sum of subtracted values
Exceptional Cases:
If nth value of start array is NULL, use a start_default value(from the query)
If nth value of final array is NULL, use value from the final_default array
After some of the aggregation stages, my MongoDB document has this format:
Assuming that start_default value = 1
I have commented the way I expect to perform subtractions in each of the group
{
"data": [
{
"key": "TP-1",
"status_map": [
{
"status": "Closed",
"final": [
6,
3
], // Expected Output
"start": [ // sum:6 [(6-2)+(3-1(start_default))=4+2]
2
],
"final_default": [
4
]
},
{
"status": "Done",
"final": [
4
], // Expected Output
"start": [ // sum:2 [(4-3)+(2(final_default)-1)=1+1]
3,
1
],
"final_default": [
2
]
}
]
},
{
"key": "TP-2",
"status_map": [
{
"status": "Closed",
"final": [
1,
5
], // Expected Output
"start": [], //sum:4 [(1-1(start_default))+(5-1(start_default))=0+4]
"final_default": [
3
]
},
{
"status": "Done",
"final": [], // Expected Output
"start": [ //sum:3 [(5(final_default)-3)+(5(final_default)-4)=2+1]
3,
4
],
"final_default": [
5
]
}
]
}
]
}
Here is my expected output assuming that start_default value = 1
{
"data": [
{
"key": "TP-1",
"status_map": [
{
"status": "Closed",
"sum": 6 //[(6-2)+(3-1(start_default))=4+2]
{
"status": "Done",
"sum": 2 //[(4-3)+(2(final_default)-1)=1+1]
}
]
},
{
"key": "TP-2",
"status_map": [
{
"status": "Closed",
"sum": 4 //[(1-1(start_default))+(5-1(start_default))=0+4]
},
{
"status": "Done",
"sum": 3 //[(5(final_default)-3)+(5(final_default)-4)=2+1]
}
]
}
]
}
How to achieve this use case?

You can start with double $map to rewrite your nested array. You'll also need $reduce since you'll be converting an array into scalar value. Since you need to "pair" two arrays, there's a perfect operator called $zip which can be used even if arrays have different lengths. Pairing final and start for the first subdocument will return:
[ [ 6,2 ], [ 3, null ] ]
which is great because you can use $ifNull to provide a default value.
Your aggregation can look like below:
db.collection.aggregate([
{
$project: {
data: {
$map: {
input: "$data",
as: "d",
in: {
key: "$$d.key",
status_map: {
$map: {
input: "$$d.status_map",
as: "sm",
in: {
status: "$$sm.status",
sum: {
$reduce: {
input: {
$zip: {
inputs: [ "$$sm.final", "$$sm.start" ],
useLongestLength: true
}
},
initialValue: 0,
in: {
$add: [
"$$value",
{
$subtract: [
{ $ifNull: [ { $arrayElemAt: [ "$$this", 0 ] }, { $arrayElemAt: [ "$$sm.final_default" , 0] } ] },
{ $ifNull: [ { $arrayElemAt: [ "$$this", 1 ] }, 1 ] }
]
}
]
}
}
}
}
}
}
}
}
}
}
}
])
Mongo Playground

Related

Querying an array of key value pairs using $and with nested $and logical operator

Given the following MongoDB collection:
[
{
"_id": 1,
"basket": [
{
"key": "A",
"value": [
"Bananas"
]
},
{
"key": "B",
"value": [
"Apples"
]
}
]
},
{
"_id": 2,
"basket": [
{
"key": "A",
"value": [
"Oranges"
]
},
{
"key": "B",
"value": [
"Bananas"
]
}
]
},
{
"_id": 3,
"basket": [
{
"key": "A",
"value": [
"Bananas"
]
},
{
"key": "B",
"value": [
"Bananas"
]
}
]
},
{
"_id": 4,
"basket": [
{
"key": "A",
"value": [
"Oranges"
]
},
{
"key": "B",
"value": [
"Apples"
]
}
]
}
]
I want to query this collection to get all documents where "Bananas" appears on basket 'A' and 'B', meaning that the expected result would be:
[
{
"_id": 3,
"basket": [
{
"key": "A",
"value": [
"Bananas"
]
},
{
"key": "B",
"value": [
"Bananas"
]
}
]
}
]
This is the actual outcome:
[
{
"_id": 1,
"basket": [
{
"key": "A",
"value": [
"Bananas"
]
},
{
"key": "B",
"value": [
"Apples"
]
}
]
},
{
"_id": 2,
"basket": [
{
"key": "A",
"value": [
"Oranges"
]
},
{
"key": "B",
"value": [
"Bananas"
]
}
]
},
{
"_id": 3,
"basket": [
{
"key": "A",
"value": [
"Bananas"
]
},
{
"key": "B",
"value": [
"Bananas"
]
}
]
}
]
So, for some reason that I don't quite get why, I'm getting all the documents where "Bananas" appear in any basket when I should be getting only the document with the _id: 3
This is the query I'm using:
{
$and: [
{
$and: [
{ "basket.value": { $in: ["Bananas"] } },
{ "basket.key": { $eq: "A" } }
]
},
{
$and: [
{ "basket.value": { $in: ["Bananas"] } },
{ "basket.key": { $eq: "B" } }
]
}
]
}
Try running a query that uses the $expr operator as it allows you to leverage aggregation pipeline operators which are best suited for the above query. Essentially you need to filter the basket array on the above condition i.e. where the value "Bananas" is in the values array AND where the key value is either 'A' OR 'B'. This condition as expressed in the aggregation pipeline is as follows:
{
$and: [
{ $in: ['Bananas', '$$basket_element.value'] },
{ $in: ['$$basket_element.key', ['A', 'B'] ] }
]
}
The length of this filtered array should be equal to 2 for the overall query to be satisfied thus you would need the operators
$filter - returns the basket array filtered on the above condition
$size - returns the length of the filtered array
$eq - is the conditional operator used by $expr query to compare the size of the filtered array, which should have exactly 2 elements.
Overall, your query should be as follows:
db.collection.find({
$expr: {
$eq: [
{ $size: {
$filter: {
input: '$basket',
as: 'basket_element',
cond: {
$and: [
{ $in: ['Bananas', '$$basket_element.value'] },
{ $in: ['$$basket_element.key', ['A', 'B'] ] }
]
}
}
} },
2
]
}
})
Mongo Playground

MongoDB query to select documents with array with all of its elements matching some conditions

I am trying to come up with a query in MongoDB that lets me select documents in a collection based on the contents of subdocuments in a couple of levels deep arrays.
The collection in the example (simplified) represents situations. The purpose of the query is, given a moment in time, to know the currently active situation. The conditionGroups array represents different conditions in which the situation becomes active, and each of those has an array of conditions all of which have to be true.
In other words, the conditionGroups array operates as an OR condition, and its children array "conditions" operates as an AND. So, given any root document "situation", this situation will be active if at least one of its conditionGroups meets all of its conditions.
[
{
"name": "Weekdays",
"conditionGroups": [
{
"conditions": [
{
"type": "DayOfWeek",
"values": [1, 2, 3, 4, 5]
},
{
"type": "HourIni",
"values": [8]
},
{
"type": "HourEnd",
"values": [19]
}
]
}
]
},
{
"name": "Nights and weekends",
"conditionGroups": [
{
"conditions": [
{
"type": "DayOfWeek",
"values": [1, 2, 3, 4, 5]
},
{
"type": "HourIni",
"values": [20]
},
{
"type": "HourEnd",
"values": [23]
}
]
},
{
"conditions": [
{
"type": "DayOfWeek",
"values": [6, 7]
},
{
"type": "HourIni",
"values": [8]
},
{
"type": "HourEnd",
"values": [19]
}
]
}
]
},
{
"name": "Weekend night",
"conditionGroups": [
{
"conditions": [
{
"type": "DayOfWeek",
"values": [6, 7]
},
{
"type": "HourIni",
"values": [20]
},
{
"type": "HourEnd",
"values": [23]
}
]
}
]
}
]
Another thing to note is that there are other types of conditions, like DayOfMonth, Month, Year, and others that might come, so the query should look for conditions that match the type and value or do not exist at all.
Given this example data, and imagining a december monday at lunchtime (so DayOfWeek is 1, current hour is 12, DayOfMonth is 13, Month is 12, Year is 2021) only the first document should be selected, because it has a "conditionGroup" all of which conditions match the current parameters, even if parameters like DayOfMonth/Year/Month are not specified. The important thing is that all the conditions must be met.
Now, I've tried the following with no luck:
db.situations.find({
'conditionGroups': { $all: [
{
$elemMatch: { $nor: [
{ 'conditions.type': 'HourIni', 'conditions.values.0': { $gt: 12 } },
{ 'conditions.type': 'HourEnd', 'conditions.values.0': { $lte: 12 } },
{ 'conditions.type': 'DayOfWeek', 'conditions.values.0': { $nin: [1] } },
{ 'conditions.type': 'DayOfMonth', 'conditions.values.0': { $nin: [13] } },
{ 'conditions.type': 'Month', 'conditions.values.0': { $nin: [12] } },
{ 'conditions.type': 'Year', 'conditions.values.0': { $nin: [2021] } },
]}
}
] }
})
This query is coming back empty.
Another thing I've tried is to first unwind the conditionGroups with the aggregation pipeline, and then try $elemMatch on conditions, but getting odd results. My guess is that I don't fully understand the $elemMatch and other array operators and I'm confusing them somehow...
It's quite a tricky question...so I've simplified it, but a largely appreciated bonus would be to consider that every condition, apart from "type" and "values" can also have an "inverse" boolean attribute that acts like a "not", so that condition would have to be "reversed".
I've spent many hours trying to get this to work but I'm kind of lost now. I understand the info might not be enough, so if anyone was able to give me a hint I could provide extra info if needed...
Any tip would be appreciated as I'm quite lost! ;)
You can do the following in an aggregation pipeline:
$unwind conditionGroups for future processing/filtering
use a $switch to perform condition checking on the condition level. Set the result to be true if the condition is matched, otherwise set the result to be false. By using $map, you obtained a mapped boolean result for the condition array
$allElementsTrue to check if the result array in step 2 is all true; if true, that means one condition passed all matchings
use the _id to find back the _id of all original documents
db.collection.aggregate([
{
"$addFields": {
"dateInput": ISODate("2021-12-13T12:00:00Z")
}
},
{
"$unwind": "$conditionGroups"
},
{
"$addFields": {
"matchedCondition": {
"$map": {
"input": "$conditionGroups.conditions",
"as": "c",
"in": {
"$switch": {
"branches": [
{
"case": {
$and: [
{
$eq: [
"$$c.type",
"DayOfWeek"
]
},
{
"$in": [
{
"$dayOfWeek": "$dateInput"
},
"$$c.values"
]
}
]
},
"then": true
},
{
"case": {
$and: [
{
$eq: [
"$$c.type",
"HourIni"
]
},
{
"$gt": [
{
"$hour": "$dateInput"
},
{
"$arrayElemAt": [
"$$c.values",
0
]
}
]
}
]
},
"then": true
},
{
"case": {
$and: [
{
$eq: [
"$$c.type",
"HourEnd"
]
},
{
"$lte": [
{
"$hour": "$dateInput"
},
{
"$arrayElemAt": [
"$$c.values",
0
]
}
]
}
]
},
"then": true
}
],
default: false
}
}
}
}
}
},
{
"$match": {
$expr: {
$eq: [
true,
{
"$allElementsTrue": "$matchedCondition"
}
]
}
}
},
{
"$group": {
"_id": "$_id"
}
},
{
"$lookup": {
"from": "collection",
"localField": "_id",
"foreignField": "_id",
"as": "originalDocument"
}
},
{
"$unwind": "$originalDocument"
},
{
"$replaceRoot": {
"newRoot": "$originalDocument"
}
}
])
Here is the Mongo playground for your reference.

Redact nested items in two level arrays

Having some issues understanding the $redact stage and cant seem to understand what i’m doing wrong or just missing. I’ve read the docs but it doesn’t go through my specific need or I'm just not understanding it correctly. I want to redact some nested items based on a id/number in a two level deep array. If i omit the array and use a regular key/value it does work. But not when having the values in an array.
I want to redact/remove all the items (articles) where the ids don’t match the input/query id that i provide. If $redact is not suitable for this i will happy take other solutions, but preferably not with $unwind and $group.
If I have a single key/value as authorId as in this playground it does work.
https://mongoplayground.net/p/V4mOboXp7zR
On the link above is the result i want to achieve but where the authorIds is an array and not key/values.
But if I have multiple Ids in an array it does not work. As this
https://mongoplayground.net/p/pqvJLUfL1f4
Thanks!
Sample data
[
{
"title": "one title",
"articles": [
{
content: "lorem ipsum",
authorIds: [
1
],
},
{
content: "bacon ipsum",
authorIds: [
2,
3,
4
]
},
{
content: "hippsum dippsum",
authorIds: [
3,
5
]
},
{
content: "hippsum dippsum",
authorIds: [
4
]
}
],
}
]
Current non working stage
db.collection.aggregate([
{
"$project": {
title: 1,
articles: 1,
articleCount: {
$size: "$articles"
},
},
},
{
"$redact": {
"$cond": {
"if": {
"$or": [
{
"$eq": [
"$authorIds",
2 // match on this authorId
]
},
{
$gte: [
"$articleCount",
1
]
},
]
},
"then": "$$DESCEND",
"else": "$$PRUNE"
}
}
},
])
Use setIntersection in redact condition.
aggregate
db.collection.aggregate([
{
"$project": {
title: 1,
articles: 1,
articleCount: {
$size: "$articles"
}
}
},
{
"$redact": {
"$cond": {
"if": {
"$or": [
{
$gte: [
"$articleCount",
1
]
},
{
$gt: [
{
$size: {
$setIntersection: [
"$authorIds",
[
2
]
]
}
},
0
]
}
]
},
"then": "$$DESCEND",
"else": "$$PRUNE"
}
}
}
])
data
[
{
"title": "one title",
"articles": [
{
content: "lorem ipsum",
authorIds: [
1
]
},
{
content: "bacon ipsum",
authorIds: [
2,
3,
4
]
},
{
content: "hippsum dippsum",
authorIds: [
3,
5
]
},
{
content: "hippsum dippsum",
authorIds: [
4
]
}
]
}
]
result
[
{
"_id": ObjectId("5a934e000102030405000000"),
"articleCount": 4,
"articles": [
{
"authorIds": [
2,
3,
4
],
"content": "bacon ipsum"
}
],
"title": "one title"
}
]
mongoplayground

How to use Mongo Aggregation to limit results around a given input?

I looked through the pipeline stages docs, but did not see how to do this.
Suppose you have a user, and each user has points.
User Points
A 22
B 11
C 15
D 7
So, we use '$sort': { points: -1 } to order the users by points.
Is it possible to use a Mongo Aggregation Stage to find the users before and after a given user?
So, given user C (by id), it would return [A, C, B].
Very interesting question. Maybe exists any better solution.
Disclaimer: I assume the user points is unique
We can use $facet to get expected result, but at high cost (very large query)
db.collection.aggregate([
{
$facet: {
"givenUser": [
{
$match: {
"user": "C"
}
}
],
"allUser": [
{
$sort: {
"Points": -1
}
}
],
"orderedPoints": [
{
$sort: {
"Points": -1
}
},
{
$group: {
_id: null,
Points: {
$push: "$Points"
}
}
},
{
$unwind: "$Points"
}
]
}
},
{
$project: {
allUser: 1,
currIndex: {
$indexOfArray: [
"$orderedPoints.Points",
{
$arrayElemAt: [
"$givenUser.Points",
0
]
}
]
},
beforeIndex: {
$add: [
{
$indexOfArray: [
"$orderedPoints.Points",
{
$arrayElemAt: [
"$givenUser.Points",
0
]
}
]
},
-1
]
},
afterIndex: {
$add: [
{
$indexOfArray: [
"$orderedPoints.Points",
{
$arrayElemAt: [
"$givenUser.Points",
0
]
}
]
},
1
]
}
}
},
{
$project: {
result: [
{
$arrayElemAt: [
"$allUser",
{
$cond: {
if: {
$lt: [
"$beforeIndex",
0
]
},
then: 999,
else: "$beforeIndex"
}
}
]
},
{
$arrayElemAt: [
"$allUser",
"$currIndex"
]
},
{
$arrayElemAt: [
"$allUser",
"$afterIndex"
]
}
]
}
}
])
[
{
"result": [
{
"Points": 22,
"_id": ObjectId("5a934e000102030405000000"),
"user": "A"
},
{
"Points": 15,
"_id": ObjectId("5a934e000102030405000002"),
"user": "C"
},
{
"Points": 11,
"_id": ObjectId("5a934e000102030405000001"),
"user": "B"
}
]
}
]
MongoPlayground
Steps:
We keep into separate fields:
Given user (C),
Order all users by points
Order all points and store inside array (I wish MongoDB allows find array index by object too)
Now we find given user index, calculate indexes for "before"/"after" players.
Now, we create result with 3 elements (before, current, after).
Note: If given user is first / last, we ensure to return null for before / after items.

Zip two array and create new array of object

hello all i'm working with a MongoDB database where each data row is like:
{
"_id" : ObjectId("5cf12696e81744d2dfc0000c"),
"contributor": "user1",
"title": "Title 1",
"userhasRate" : [
"51",
"52",
],
"ratings" : [
4,
3
],
}
and i need to change it to be like:
{
"_id" : ObjectId("5cf12696e81744d2dfc0000c"),
"contributor": "user1",
"title": "Title 1",
rate : [
{userhasrate: "51", value: 4},
{userhasrate: "52", value: 3},
]
}
I already try using this method,
db.getCollection('contens').aggregate([
{ '$group':{
'rates': {$push:{ value: '$ratings', user: '$userhasRate'}}
}
}
]);
and my result become like this
{
"rates" : [
{
"value" : [
5,
5,
5
],
"user" : [
"51",
"52",
"53"
]
}
]
}
Can someone help me to solve my problem,
Thank you
You can use $arrayToObject and $objectToArray inside $map to achieve the required output.
db.collection.aggregate([
{
"$project": {
"rate": {
"$map": {
"input": {
"$objectToArray": {
"$arrayToObject": {
"$zip": {
"inputs": [
"$userhasRate",
"$ratings"
]
}
}
}
},
"as": "el",
"in": {
"userhasRate": "$$el.k",
"value": "$$el.v"
}
}
}
}
}
])
Alternative Method
If userhasRate contains repeated values then the first solution will not work. You can use arrayElemAt and $map along with $zip if it contains repeated values.
db.collection.aggregate([
{
"$project": {
"rate": {
"$map": {
"input": {
"$zip": {
"inputs": [
"$userhasRate",
"$ratings"
]
}
},
"as": "el",
"in": {
"userhasRate": {
"$arrayElemAt": [
"$$el",
0
]
},
"value": {
"$arrayElemAt": [
"$$el",
1
]
}
}
}
}
}
}
])
Try below aggregate, first of all you used group without _id that grouped all the JSONs in the collection instead set it to "$_id" also you need to create 2 arrays using old data then in next project pipeline concat the arrays to get desired output:
db.getCollection('contens').aggregate([
{
$group: {
_id: "$_id",
rate1: {
$push: {
userhasrate: {
$arrayElemAt: [
"$userhasRate",
0
]
},
value: {
$arrayElemAt: [
"$ratings",
0
]
}
}
},
rate2: {
$push: {
userhasrate: {
$arrayElemAt: [
"$userhasRate",
1
]
},
value: {
$arrayElemAt: [
"$ratings",
1
]
}
}
}
}
},
{
$project: {
_id: 1,
rate: {
$concatArrays: [
"$rate1",
"$rate2"
]
}
}
}
])