MongoDB aggregation to change the key based on value in each object - mongodb

Is there an aggregation query available to convert the following:
after unwind I plan to change the key based on the value inside each object and append it too has---(type)
{
"rows": [
{
"type": "aaa",
"values": [
1,
2,
3
]
},
{
"type": "bbb",
"values": [
4,
5,
6
]
}
]
}
to
{
"hasaaa": {
"type": "aaa",
"values": [
1,
2,
3
]
},
"hasbbb": {
"type": "bbb",
"values": [
4,
5,
6
]
}
}

$map to iterate loop of rows array
$concat to prepare the custom key string
return key-value from map
$arrayToObject convert key-value array to object
db.collection.aggregate([
{
$project: {
rows: {
$arrayToObject: {
$map: {
input: "$rows",
in: {
k: { $concat: ["has", "$$this.type"] },
v: "$$this"
}
}
}
}
}
}
])
Playground

Related

How to use `$size` with embedded document?

I'm having difficulty getting the size of arrays within embedded documents.
Given a collection with some document:
{
"_id": { "$oid": "60e26137600b71f2939ebbe7" },
"name": "Outer doc",
"inner_docs": [
{ "name": "one", "vals": [1,2,3,4,5] },
{ "name": "two", "vals": [1,2,3,4] },
{ "name": "three", "vals": [1,2,3] }
]
}
By applying a $project stage in aggregation with $size like:
{
"outer_num": { "$size": "$inner_docs" },
"inner_docs.num_vals": { "$size": "$inner_docs.vals" }
}
I get the resultant doc:
{
"_id": { "$oid": "60e26137600b71f2939ebbe7" },
"inner_docs": [
{ "name": "one", "num_vals": 3 },
{ "name": "two", "num_vals": 3 },
{ "name": "three", "num_vals": 3 }
],
"outer_num": 3,
}
outer_num is correct, but num_vals is not, it should be the values 5, 4 and 3 respectively.
How could I fix this?
Demo - https://mongoplayground.net/p/dpsp907E_fa
Use $map
Applies an expression to each item in an array and returns an array with the applied results.
db.collection.aggregate([
{
$project: {
"outer_num": { $size: "$inner_docs"},
"inner_docs": {
$map: {
input: "$inner_docs",
as: "doc",
in: { name: "$$doc.name", num_vals: { $size: [ "$$doc.vals" ] } }
}
}
}
}
])

How to $sum a specific array inside an array?

Consider the following structure:
{
"groups": [
{
"group_id": "A",
"total": -1
"items": [
{
"item_id": "a",
"val": 1
},
{
"item_id": "b",
"val": 2
}
]
},
{
"group_id": "B",
"total": -1
"items": [
{
"item_id": "c",
"val": 3
},
{
"item_id": "d",
"val": 4
}
]
}
]
}
given a group_id, I'd like to $sum the items values of this group and set the total value with that number (e.g. the total of group A is 3)
How can I do that?
I know about $unwind but I don't fully understand how to do that only to the specific array I'm interested in
$map to iterate loop of groups array
$cond to check if group_id match then do sum operation otherwise nothing
$sum to get total of items.val
$mergeObjects to merge group object and new updated total field
db.collection.aggregate([
{
$set: {
groups: {
$map: {
input: "$groups",
in: {
$mergeObjects: [
"$$this",
{
$cond: [
{ $eq: ["$$this.group_id", "A"] },
{ total: { $sum: "$$this.items.val" } },
{}
]
}
]
}
}
}
}
}
])
Playground

MongoDB aggregation: how to find out if an array contains multiple elements

I have following data.
[
{
"_id": 1,
"array": [
{
"name": "name1",
"nestedArray": [
{
"key": "key1",
"value": "value1"
},
{
"key": "key2",
"value": "value2"
}
]
},
{
"name": "name2",
"nestedArray": [
{
"key": "key1",
"value": "abc"
},
{
"key": "key2",
"value": "value2"
}
]
}
]
}
]
playground
I want to keep the entry whose nestedArray contains 2 matching elements and remove others. The 2 elements are below
{
"key": "key1",
"value": "abc"
},
{
"key": "key2",
"value": "value2"
}
So that the result will be below
[
{
"_id": 1,
"array": [
{
"name": "name2",
"nestedArray": [
{
"key": "key1",
"value": "abc"
},
{
"key": "key2",
"value": "value2"
}
]
}
]
}
]
The one whose name="name1" is removed since it has only one matching element.
Feels like we could use $elemMatch but couldn't figure it out.
First, Unwind the array so that you can easily access the nestedArray .
Second, use $all and $elementMatch on nestedArray
db.collection.aggregate([
{
$unwind: "$array"
},
{
$match: {
"array.nestedArray": {
$all: [
{
"$elemMatch": {
key: "key1",
value: "abc"
}
},
{
"$elemMatch": {
key: "key2",
value: "value2"
}
}
]
}
}
},
{
$group: {
_id: "$_id",
array: {
"$push": "$array"
}
}
}
])
playground
You didn't really specify how you input looks but the strategy will stay the same regardless, We will iterate over the array with $filter and match only documents that their subarray's size is equal to a filtered subarray based on the given input, like so:
// the current input structure.
inputArray = [
{
"key": "key1",
"value": "abc"
},
{
"key": "key2",
"value": "value2"
}
];
db.collection.aggregate([
{
$project: {
array: {
$filter: {
input: "$array",
as: "element",
cond: {
$and: [
{
$eq: [
{
$size: {
$filter: {
input: "$$element.nestedArray",
as: "nested",
cond: {
"$setIsSubset": [
[
"$$nested"
],
inputArray
]
}
}
},
},
{
$size: "$$element.nestedArray"
}
]
},
{ // this is required for an exact match otherwise subsets are possible, if you wan't to allow it delete this equality completly.
$eq: [
{
$size: "$$element.nestedArray"
},
{
$size: inputArray
}
]
}
]
}
}
}
}
}
])
Again if the input is coming in a different structure you'll have to change the $eq conditions

Mongodb Update subdocument values matching subdocument and without altering other fields

I have a schema like this
{
"_id": "5f86da4b5bb9a62742371409",
"status" : true,
"doc": [
{
"_id": "5f86cacbe4d7c423f21faf50",
"a": 4,
"b": null
},
{
"_id": "5f86cb01a1ca5124063299c1",
"a": 6,
"b": null
}
]
}
And I have an update in the form of
[
{
"_id": "5f86cacbe4d7c423f21faf50",
"b": 90
},
{
"_id": "5f86cb01a1ca5124063299c1",
"b": 45
}
]
How can I update the collection to end up like this?
{
"_id": "5f86da4b5bb9a62742371409",
"status" : true,
"doc": [
{
"_id": "5f86cacbe4d7c423f21faf50",
"a": 4,
"b": 90
},
{
"_id": "5f86cb01a1ca5124063299c1",
"a": 6,
"b": 45
}
]
}
Basically I want to update the subdocument with specific keys only (leaving other keys intact)
You can use update with aggregation pipeline from MongoDB 4.2,
$map to iterate loop of array doc and inside it iterate through updateDocs array, it will return matching b, and then $mergeObjects will return updated document,
let updateDocs = [
{ "_id": mongoose.Types.ObjectId("5f86cacbe4d7c423f21faf50"), "b": 90 },
{ "_id": mongoose.Types.ObjectId("5f86cb01a1ca5124063299c1"), "b": 45 }
];
db.collection.updateMany({},
[
{
$set: {
doc: {
$map: {
input: "$doc",
as: "d",
in: {
$mergeObjects: [
"$$d",
{
$reduce: {
input: updateDocs,
initialValue: {},
in: {
$cond: [
{ $eq: ["$$this._id", "$$d._id"] },
{ b: "$$this.b" },
"$$value"
]
}
}
}
]
}
}
}
}
}
]
)
Playground

Add field to $map entry in MongoDB

I have MongoDB collection items with following document:
{
"values": [
{ "number1": 5, "number2": 6, "anotherProp": "...", "anotherProp2": "..." },
{ "number1": 8, "number2": 1, "anotherProp": "...", "anotherProp2": "..." }
]
}
Is there any way to add sum property to each item of values (sum = number1 + number2)? I would like to avoid naming all other properties (number1, number2, anotherProp, anotherProp2, ...), only add new one (sum). My current solution is:
db.items.aggregate([{
$project: {
values: {
$map: {
input: "$values",
as: "v",
in: {
sum: {$add: ["$$v.number1", "$$v.number2"]},
number1: "$$v.number1", // This and next 3 lines I would like to omit.
number2: "$$v.number2",
anotherProp: "$$v.anotherProp",
anotherProp2: "$$v.anotherProp2"
}
}
}
}
}])
Desired result is:
{
"values": [
{ "number1": 5, "number2": 6, "anotherProp": "...", "anotherProp2": "...", "sum": 11 },
{ "number1": 8, "number2": 1, "anotherProp": "...", "anotherProp2": "...", "sum": 9 }
]
}
Is there any way to do this? I tried use $addFields instead of $project, however result is same.
Yes, you can use $mergeObjects
db.collection.aggregate([
{
$project: {
values: {
$map: {
input: "$values",
as: "v",
in: {
"$mergeObjects": [
{
sum: {
$add: [
"$$v.number1",
"$$v.number2"
]
}
},
"$$v"
]
}
}
}
}
}
])
MongoPlayground