For example, I have something in my database like in customers collection.
{
Max: {
shoping_list: {
food: { Pizza: 2, Ramen: 1, Sushi: 5 }
}
},
John: {
shoping_list: {
food: { Pizza: 2, Ramen: 1, Burger: 1 }
}
}
}
In my backend, I want to get the sum of food
const request = await customers.aggregate([
{
$group: {
_id: null,
Pizza: {
$sum: '$shoping_list.food.Pizza',
},
Is there a way how to update or get the sum automatically without manually writing every food from the shopping_list?
The design of the document may lead the query looks complex but still achievable.
$replaceRoot - Replace the input document with a new document.
1.1. $reduce - Iterate the array and transform it into a new form (array).
1.2. input - Transform key-value pair of current document $$ROOT to an array of objects such as: [{ k: "", v: "" }]
1.3. initialValue - Initialize the value with an empty array. And this will result in the output in the array.
1.4. in
1.4.1. $concatArrays - Combine aggregate array result ($$value) with 1.4.2.
1.4.2. With the $cond operator to filter out the document with { k: "_id" }, and we transform the current iterate object's v shoping_list.food to the array via $objectToArray.
$unwind - Deconstruct the foods array into multiple documents.
$group - Group by foods.k and perform sum for foods.v.
db.collection.aggregate([
{
$replaceRoot: {
newRoot: {
foods: {
$reduce: {
input: {
$objectToArray: "$$ROOT"
},
initialValue: [],
in: {
$concatArrays: [
"$$value",
{
$cond: {
if: {
$ne: [
"$$this.k",
"_id"
]
},
then: {
$objectToArray: "$$this.v.shoping_list.food"
},
else: []
}
}
]
}
}
}
}
}
},
{
$unwind: "$foods"
},
{
$group: {
_id: "$foods.k",
sum: {
$sum: "$foods.v"
}
}
}
])
Demo # Mongo Playground
Related
I have documents that consist of an array of objects, and each object in this array consists of another array of objects.
For simplicity, irrelevant fields of the documents were omitted.
It looks like this (2 documents):
{
title: 'abc',
parts: [
{
part: "verse",
progressions: [
{
progression: "62a4a87da7fdbdabf787e47f",
key: "Ab",
_id: "62b5aaa0c9e9fe8a7d7240d3"
},
{
progression: "62adf477ed11cbbe156d5769",
key: "C",
_id: "62b5aaa0c9e9fe8a7d7240d3"
},
],
_id: "62b5aaa0c9e9fe8a7d7240d2"
},
{
part: "chorus",
progressions: [
{
progression: "62a4a51b4693c43dce9be09c",
key: "E",
_id: "62b5aaa0c9e9fe8a7d7240d9"
}
],
_id: "62b5aaa0c9e9fe8a7d7240d8"
}
],
}
{
title: 'def',
parts: [
{
part: "verse",
progressions: [
{
progression: "33a4a87da7fopvvbf787erwe",
key: "E",
_id: "62b5aaa0c9e9fe8a7d7240d3"
},
{
progression: "98opf477ewfscbbe156d5442",
key: "Bb",
_id: "62b5aaa0c9e9fe8a7d7240d3"
},
],
_id: "12r3aaa0c4r5me8a7d72oi8u"
},
{
part: "bridge",
progressions: [
{
progression: "62a4a51b4693c43dce9be09c",
key: "C#",
_id: "62b5aaa0c9e9fe8a7d7240d9"
}
],
_id: "62b5aaa0rwfvse8a7d7240d8"
}
],
}
The parameters that the client sends with a request are an array of objects:
[
{ part: 'verse', progressions: ['62a4a87da7fdbdabf787e47f', '62a4a51b4693c43dce9be09c'] },
{ part: 'chorus', progressions: ['62adf477ed11cbbe156d5769'] }
]
I want to retrieve, through mongodb aggregation, the documents that at least one of objects in the input array above is matching them:
In this example, documents that have in their parts array field, an object that has the value 'verse' in the part property and one of the progressions id's ['62a4a87da7fdbdabf787e47f', '62a4a51b4693c43dce9be09c'] in the progression property in one of the objects in the progressions property, or documents that have in their parts array field, an object that has the value 'chorus' in the part property and one of the progressions id's ['62adf477ed11cbbe156d5769'] in the progression property in one of the objects in the progressions property.
In this example, the matching document is the first one (with the title 'abc'), but in actual use, there might be many matching documents.
I tried to create an aggregation pipeline myself (using the mongoose 'aggregate' method):
// parsedProgressions = [
// { part: 'verse', progressions: ['62a4a87da7fdbdabf787e47f', '62a4a51b4693c43dce9be09c'] },
// { part: 'chorus', progressions: ['62adf477ed11cbbe156d5769'] }
// ]
songs.aggregate([
{
$addFields: {
"tempMapResults": {
$map: {
input: parsedProgressions,
as: "parsedProgression",
in: {
$cond: {
if: { parts: { $elemMatch: { part: "$$parsedProgression.part", "progressions.progression": mongoose.Types.ObjectId("$$parsedProgression.progression") } } },
then: true, else: false
}
}
}
}
}
},
{
$addFields: {
"isMatched": { $anyElementTrue: ["$tempMapResults"] }
}
},
{ $match: { isMatched: true } },
{ $project: { title: 1, "parts.part": 1, "parts.progressions.progression": 1 } }
]);
But it didn't work - as I understand it, because the $elemMatch can be used only in the $match stage.
Anyway, I guess I overcomplicated the aggregation pipeline, so I will be glad if you can fix my aggregation pipeline/offer a better working one.
This is not a simple case as these are both nested arrays and we need to match both the part and the progressions, which are not on the same level
One option looks complicated a bit, but keeps your data small:
In order to make things easier, $set a new array field called matchCond which includes an array called progs containing the parts.progressions. To each sub-object inside it insert the matching progressions input array. We do need to be careful here and handle the case where there is no matching progressions input arrayprogressions input array, as this is the case for the "bridge" part on the second document.
Now we just need to check if for any of these progs items, the progression field is matching one option in input array. This is done using $filter, and $rediceing the number of results.
Just match document which have results and format the answer
db.collection.aggregate([
{
$set: {
matchCond: {
$map: {
input: "$parts",
as: "parts",
in: {progs: {
$map: {
input: "$$parts.progressions",
in: {$mergeObjects: [
"$$this",
{input: {progressions: []}},
{input: {$first: {
$filter: {
input: inputData,
as: "inputPart",
cond: {$eq: ["$$inputPart.part", "$$parts.part"]}
}
}}}
]}
}
}}
}
}
}
},
{$set: {
matchCond: {
$reduce: {
input: "$matchCond",
initialValue: 0,
in: {$add: [
"$$value",
{$size: {
$filter: {
input: "$$this.progs",
as: "part",
cond: {$in: ["$$part.progression", "$$part.input.progressions"]}
}
}
}
]
}
}
}
}
},
{$match: {matchCond: {$gt: 0}}},
{$project: {title: 1, parts: 1}}
])
See how it works on the playground example
Another option is to use $unwind, which looks simple, but will duplicate your data, thus, likely to be slower:
db.collection.aggregate([
{$addFields: {inputData: inputData, cond: "$parts"}},
{$unwind: "$cond"},
{$unwind: "$cond.progressions"},
{$unwind: "$inputData"},
{$match: {
$expr: {
$and: [
{$eq: ["$cond.part", "$inputData.part"]},
{$in: ["$cond.progressions.progression", "$inputData.progressions"]}
]
}
}
},
{$project: {title: 1, parts: 1}}
])
See how it works on the playground example - unwind
There are several options between these two...
I have this one schema
{
_id: "123456",
id: "123",
inventory: [
{
id: "foo",
count: 0
},
{
id: "bar",
count: 3
}
]
}
I wanted every "count" keys in the inventory array to be "price" which will look like this at the end:
{
_id: "123456",
id: "123",
inventory: [
{
id: "foo",
price: 0
},
{
id: "bar",
price: 3
}
]
}
And I've tried this
Model.updateOne({ id: "123" }, { $unset: { inventory: [{ count: 1 }] } } )
But it seems to be deleting the "inventory" field itself
The first thing here is to try to use $rename but how the docs explain:
$rename does not work if these fields are in array elements.
So is necessary to look for another method. So you can use this update with aggregation query:
This query uses mainly $map, $arrayToObject and $objectToArray. The trick here is:
Create a new field called inventory (overwrite existing one)
Iterate over every value of the array with $map, and then for each object in the array use $objectToArray to create an array and also iterate over that second array using again $map.
Into this second iteration create fields k and v. Field v will be the same (you don't want to change the value, only the key). And for field k you have to change only the one whose match with your condition, i.e. only change from count to price. If this condition is not matched then the key remain.
db.collection.update({},
[
{
$set: {
inventory: {
$map: {
input: "$inventory",
in: {
$arrayToObject: {
$map: {
input: {$objectToArray: "$$this"},
in: {
k: {
$cond: [
{
$eq: ["$$this.k","count"]
},
"price",
"$$this.k"
]
},
v: "$$this.v"
}
}
}
}
}
}
}
}
])
Example here
Suppose I have the following document:
{
_id: "609bcd3160653e022a6d0fd8",
companies:{
apple: [
{
product_id: "609bcd3160653e022a6d0fd7"
},
{
product_id: "609bcd3160653e022a6d0fd6"
}
],
microsoft: [
{
product_id: "609bcd3160653e022a6d0fd5"
},
{
product_id: "609bcd3160653e022a6d0fd4"
}
]
}
}
How can I find and delete the object with the product_id: "609bcd3160653e022a6d0fd4".
P.S: I use MongoDB native driver for Nodejs.
Thanks a lot!
If I don't know the name of the array? I want to find the object by product_id
It is not possible with regular update query, you can try update with aggregation pipeline starting from MongoDB 4.2,
$objectToArray convert companies object to array in key-value format
$map to iterate loop of above converted array
$filter to filter dynamic array's by product_id
$arrayToObject convert key-value array to regular object format
let product_id = "609bcd3160653e022a6d0fd4";
YourSchema.findOneAndUpdate(
{ _id: "609bcd3160653e022a6d0fd8" }, // put your query
[{
$set: {
companies: {
$arrayToObject: {
$map: {
input: { $objectToArray: "$companies" },
in: {
k: "$$this.k",
v: {
$filter: {
input: "$$this.v",
cond: { $ne: ["$$this.product_id", product_id] }
}
}
}
}
}
}
}
}],
{
new: true // return updated document
}
);
Playground
My question is pretty similar to: MongoDB Aggregation join array of strings to single string, but instead of pure Array, like: ['Batman', 'Robin'] I have Array of objects:
_id: 1,
field_value: [
{
_id: 2,
name: "Batman"
},
{
_id: 3,
name: "Robin"
}
]
I am trying to use $reduce but got error instead.
I want to receive the following result:
_id: 1,
field_value: "Batman, Robin" /** <= String value */
or at least array of property values:
_id: 1,
field_value: ["Batman", "Robin"] /** <= Array of strings (name property) */
My MongoPlayground data example
You need the same approach with $reduce, $$this represents a single field_value entity so you need $$this.name:
db.collection.aggregate([
{
$project: {
field_value: {
$reduce: {
input: "$field_value",
initialValue: "",
in: {
$concat: [
"$$value",
{ $cond: [ { $eq: [ "$$value", "" ] }, "", "," ] },
{ $toString: "$$this.name" }
]
}
}
}
}
}
])
Mongo Playground
An item in collection contains an array of strings. I would like to find and sort items with most matching elements in array.
Consider a collection:
[
{
"item_name":"Item_1",
"tags":["A","B","C","D","E"]
},
{
"item_name":"Item_2",
"tags":["A","B","D","E","G"]
},
{
"item_name":"Item_3",
"tags":["B","C","E","H"]
}
]
I want to sort the collection based on an array like ["B","D","G","F"]
which will return
[
{
"item_name":"Item_2",
"tags":["A","B","D","E","G"]
},
{
"item_name":"Item_1",
"tags":["A","B","C","D","E"]
},
{
"item_name":"Item_3",
"tags":["B","C","E","H"]
}
]
The expected order will be Item_2,Item_1 and then Item_3 since,
Item_2 matches 3 items ("B","D" and "G")
then Item_1 with 2 matches ("B" and "D")
finally Item_3 with 1 match ("B")
If not in mongodb, JavaScript methods will also be appreciated
If the values in tags are unique, you can use the size of the intersections of tags and the query array using $size and $setIntersection
db.collection.aggregate([
{
$set: {
matchedCount: {
$size: {
$setIntersection: ["$tags", ["B","D","G","F"]]
}
}
}
},
{
$sort: {
matchedCount: -1
}
}
])
You can use $setIntersection to get the number of intersecting items and then $sort based on the computed score.
db.collection.aggregate([
{
$project: {
_id: 0,
item_name: "$item_name",
tags: "$tags",
score: {
$let: {
vars: {
intersection: {
$setIntersection: [
"$tags",
[
"B",
"D",
"G",
"F"
]
]
}
},
in: {
$size: "$$intersection"
}
}
}
}
},
{
$sort: {
score: -1
}
}
])
https://mongoplayground.net/p/tpDTtVKetFT