Redact nested items in two level arrays - mongodb

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

Related

MongoDB: get documents by last element value in nested array

This question is slightly different from others since I need to get the whole documents and not just specific fields.
I need to filter documents(all of the document, not just specific fields), according to the last elements value of a nested array. (doc.array[i].innerArray[innerArray.length - 1].desiredField)
Documents are looking like this:
[
{
"_id": 0,
"matches": [
{
"name": "match 1",
"ids": [
{
"innerName": "1234"
},
{
"innerName": "3"
}
]
}
]
},
{
"_id": 1,
"matches": [
{
"name": "match 5",
"ids": [
{
"innerName": "123"
},
{
"innerName": "1"
}
]
},
{
"name": "match 5",
"ids": [
{
"innerName": "1"
},
{
"innerName": "1234"
},
]
},
]
}
]
So if we filter according to innerName = '1234', this is the result:
{
"_id": 1,
"matches": [
{
"name": "match 5",
"ids": [
{
"innerName": "123"
},
{
"innerName": "1"
}
]
},
{
"name": "match 5",
"ids": [
{
"innerName": "1"
},
{
"innerName": "1234"
},
]
}
One option is:
db.collection.find({
$expr: {
$in: [
"1234",
{$reduce: {
input: "$matches",
initialValue: [],
in: {$concatArrays: ["$$value", [{$last: "$$this.ids.innerName"}]]}
}
}
]
}
})
See how it works on the playground example
Another option:
db.collection.aggregate([
{
$match: {
$expr: {
$gt: [
{
$size: {
$filter: {
input: "$matches",
cond: {
$in: [
{
$last: "$$this.ids.innerName"
},
[
"1234"
]
]
}
}
}
},
0
]
}
}
}
])
Explained:
Match only documents where size of array is >0 for those who has "1234" in last nested array element.
Playground:

Display the conditionnal size of an array with the others fields of a mongodb document

I have a collection of fridges and I would like to have some fields from each fridge matching a condition plus the 'conditionnal size' of the items in this fridge.
This is an example of my DB :
db={
"fridges": [
{
_id: 1,
items: [
{
itemId: 1,
name:"beer"
},
{
itemId: 2,
name: "chicken"
}
],
brand:"Bosch",
size:195,
cooler:true,
color:"grey"
},
{
_id: 2,
items: [
{
itemId: 1,
name:"beer"
},
{
itemId: 2,
name: "chicken"
},
{
itemId: 3,
name: "lettuce"
}
],
brand:"Electrolux",
size:200,
cooler:true,
color:"white"
},
]
}
I want to get fridges with these mutuals conditions ('and' condition):
brand is $in ["Bosch","Samsung"]
color is $in ["grey","white"]
In addition :
The number of items with a name $in ["beer","lettuce"]
And finally :
Removing some fields like the size and items of the result.
In our example, the excepted output would be :
{
_id:1
itemsNumber:1,
brand:"Bosch",
cooler:true,
color:"grey"
}
Explanations :
We removed the field items and size, itemsNumber counts the number of beers and lettuce from items array. And we only keep the first fridge its brand is Bosch and it's grey.
This what I have so far :
db.fridges.aggregate([
{
"$match": {
$and: [
{
"brand": {
$in: [
"Bosch",
"Samsung"
]
}
},
{
"color": {
$in: [
"grey",
"white"
]
}
}
]
}
},
{
"$project": {
"itemsNumber": {
$size: "$items" // This is not good
},
brand: 1,
cooler: 1,
color: 1
}
}
])
Which returns me :
[
{
"_id": 1,
"brand": "Bosch",
"color": "grey",
"cooler": true,
"itemsNumber": 2
}
]
Counting the items matching with either beer or lettuce is my main problem.
This is an executable example.
Thanks in advance !
I found out how to make it work. Thank you #joe for suggesting to use filter this was indeed the solution.
Here is the complete query :
db.fridges.aggregate([
{
$match: {
$and: [
{
"brand": {
$in: [
"Bosch",
"Samsung"
]
}
},
{
"color": {
$in: [
"grey",
"white"
]
}
}
]
}
},
{
$project: {
"itemsNumber": {
"$filter": {
"input": "$items",
"as": "item",
"cond": {
$in: [
"$$item.name",
[
"beer",
"lettuce"
]
]
}
}
},
brand: 1,
cooler: 1,
color: 1
}
}
])
Runnable example.

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

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

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"
]
}
}
}
])