MongoDB aggregate reduce mergeObjects field name from value - mongodb

I have an array in a Mongo document that has a number of properties. I want to produce a document that has a single object that represents the array.
{
scores_array: [
{ name: "One", score: 40, key: "abc" },
{ name: "Two", score: 50, key: "def" },
{ name: "Three", score: 40, key: "abc" },
{ name: "Four", score: 40, key: "ghi" },
}
}
What I want is to be able to query for documents that have a particular value for two of the properties in the array, for example, { name: "One", score: 40 }.
I can use $in to find documents that have the matching key or value, and I could redact them to filter them out but I need to reference them again later in the same pipeline, so I'm trying to find a way to filter without removing anything from the documents.
My plan is to use $reduce to produce a new property that looks like this:
scores_summary: {
One: 40,
Two: 50,
Three: 40,
Four: 40,
}
I'm having trouble getting the $reduce syntax right. Here's what I've got:
db.test.aggregate([
{
$match: {
scores_array: {$exists: true}
}
},
{
$project: {
scores_summary: {
$reduce: {
input: "$scores_array",
initialValue: {},
"in": {
"$mergeObjects": [
"$$value",
{
"$$this.name":"$$this.score"
}
]
}
}
}
}
},
]);
This gives me "Unrecognized expression $$this.name". If I replace that with a constant, I get an output that looks right, except for that property.
Is there a reason I can't use $$this.name as the property name in the mergeObjects in a reduce? Hoping I'm just missing something.
(I'm using Mongo 3.4 currently which means I can't just fall back to $expr).
Thanks

You can use $map and $arrayToObject
db.collection.aggregate([
{
$match: {
scores_array: {
$exists: true
}
}
},
{
$project: {
scores_summary: {
"$arrayToObject": {
$map: {
input: "$scores_array",
"in": {
k: "$$this.name",
v: "$$this.score"
}
}
}
}
}
}
])
Working Mongo playground

Related

Run a loop through an array of objects in the $project pipeline of MongoDB

I have used the $group pipeline and inside it, I have used $addToSet which has created an array of objects which contains two values - {subGenre (string), flag (bool)}. In the next pipeline of $project, I need to run a loop through this array and I need to select only that element of the array where flag is false.
So my code looks like this:
let data = await Books.aggregate(
[
{
$group: {
_id: genre,
price: { $sum: "$price" },
data: {
$addToSet: {
subGenre: "$subGenre",
flag: "$flagSelectGenre"
}
}
}
}
]
);
This would return documents like:
_id: {
genre: "suspense",
},
price: 10210.6,
data: [
{
subGenre: "Thriller",
flag: false,
},
{
subGenre: "jumpScare",
flag: true,
}
{
subGenre: "horror",
flag: false,
}
]
After this, I need to run a $project pipeline where I have to only project that element of the data array where the flag is true. The flag will be true for only one element.
$project: {
price: "$price",
subGenre: {$...... } // some condition on data array??
}
The final output should look like this:
price: 10210.6,
subGenre: "jumpScare",
You can do it like this:
$filter - to filter items from data array, where flag property is equal to true.
$first - to get first item from above array.
$getField - to get value of subGenre property of the above item.
db.collection.aggregate([
{
"$project": {
"_id": 0,
"price": 1,
"data": {
"$getField": {
"field": "subGenre",
"input": {
"$first": {
"$filter": {
"input": "$data",
"cond": "$$this.flag"
}
}
}
}
}
}
}
])
Working example
You can use $filter array operator to loop and filter elements by required conditions,
$filter to iterate loop of data array, if flag is true then return element
$let to define a variable and store the above filter result
$first to return the first element from the filtered result, you can also use $arrayElemAt if you are using lower version of the MongoDB
{
$project: {
price: 1,
subGenre: {
$let: {
vars: {
data: {
$filter: {
input: "$data",
cond: "$$this.flag"
}
}
},
in: { $first: "$$data.subGenre" }
}
}
}
}
Playground
Another approach using $indexOfArray and $arrayElemAt operators,
$indexOfArray will find the matching element index of the array
$arrayElemAt to get specific element by specifying the index of the element
{
$project: {
price: 1,
subGenre: {
$arrayElemAt: [
"$data.subGenre",
{ $indexOfArray: ["$data.flag", true] }
]
}
}
}
Playground

MongoDB - How to write a nested group aggregation query

I have a collection in this format:
{
"place":"land",
"animal":"Tiger",
"name":"xxx"
},
{
"place":"land",
"animal":"Lion",
"name":"yyy"
}
I want to result to be something like this:
{
"place":"land".
"animals":{"Lion":"yyy", "Tiger":"xxx"}
}
I wrote the below query. I think there needs to be another group stage but not able to write it.
db.collection.aggregate({
'$group': {
'_id':{'place':'$place', 'animal':'$animal'},
'animalNames': {'$addToSet':'$name'}
}
})
What changes need to be made to get the required result?
$group - Group by animals. Push objects with { k: "animal", v: "name" } type into animals array.
$project - Decorate output document. Convert animals array to key-value pair via $arrayToObject.
db.collection.aggregate([
{
"$group": {
"_id": "$place",
"animals": {
"$push": {
k: "$animal",
v: "$name"
}
}
}
},
{
$project: {
_id: 0,
place: "$_id",
animals: {
"$arrayToObject": "$animals"
}
}
}
])
Sample Mongo Playground
If you are on version >=4.4, a reasonable alternative is to use the $function operator:
db.foo.aggregate([
{$project: {
'income_statement.annual': {
$function: {
body: function(arr) {
return arr.sort().reverse();
},
args: [ "$income_statement.annual" ],
lang: "js"
}}
}}
]);

mongoDB Find by id and return subdocument matched

I have this Collection:
{
_id:0,
user_id: 12,
list: [{_.id:0, name:"john"},{_.id:1, name:"hanna"}]
},
{
_id:1,
user_id: 22,
list: [{_.id:0, name:"john"},{_.id:1, name:"hanna"}]
}
I want to query the collection like this: find the document by user_id
and return only {_.id:0, name:"john"} inside list
couldnt find any clue how to do that
some example for better explanation this what I want to achive:
const johnDoc = findOne({user_id:0}).list.findOne({name:"john"})
I know its not valid only for explaining what I want to achive.
You can try this $unwind
db.collection.aggregate([
{
$match: {
user_id: 12,
"list.name": "john"
}
},
{
$unwind: "$list"
},
{
$match: {
user_id: 12,
"list.name": "john"
}
},
])
Playground
You can use the aggregation operator in find's projection from MongoDB 4.4,
$filter to iterate loop of list and find matching user by name property
$first to select the first element from filtered result
db.collection.find({
user_id: 12
},
{
list: {
$first: {
$filter: {
input: "$list",
cond: {
$eq: [
"$$this.name",
"john"
]
}
}
}
}
})
Playground

MongoDB : Retrieve Associated Value from Object in an Array of Arrays

In mongo I have a documents that follow the below pattern :
{
name: "test",
codes: [
[
{
code: "abc",
value: 123
},
{
code: "def",
value: 456
},
],
[
{
code: "ghi",
value: 789
},
{
code: "jkl",
value: 012
},
]
]
}
I'm using an aggregate query (because of joins) and in a $project block I need to return the "name" and the value of the object that has a code of "def" if it exists and an empty string if it doesn't.
I can't simply $unwind codes and $match because the "def" code is not guaranteed to be there.
$filter seems like the right approach as $elemMatch doesn't work, but its not obvious to me how to do this on nested array of arrays.
You can try below query, instead of unwinds & filter this can give you required result with less docs to operate on :
db.collection.aggregate([
/** merge all arrays inside codes array into code array */
{
$addFields: {
codes: {
$reduce: {
input: '$codes',
initialValue: [],
in: { $concatArrays: ["$$value", "$$this"] }
}
}
}
},
/** project only needed fields & value will be either def value or '',
* if 'def' exists in any doc then we're check index of it to get value of that particular object using arrayElemAt */
{
$project: {
_id:0, name: 1, value:
{
$cond: [{ $in: ["def", '$codes.code'] }, { $arrayElemAt: ['$codes.value', { $indexOfArray: ["$codes.code", 'def'] }] }, '']
}
}
}])
Test : MongoDB-Playground

MongoDB aggregate - filter by subdocument

I have a mongodb collection with structure like that:
[
{
name: "name1",
instances: [{value:1, score:2}, {value:2, score:5}, {value:2.5, score:9}]
},
{
name: "name2",
instances: [{value:6, score:3}, {value:1, score:6}, {value:3.7, score:5.2}]
}
]
When I want to get all the data from a document, I use aggregate because I want each instance returned as a separate document:
db.myCollection.aggregate([{$match:{name:"name1"}}, {$unwind:"$instances"}, {$project:{name:1, value:"$instances.value", score:"$instances.score"}}])
And everything works like I want it to.
Now for my question: I want to filter the returned data by score or by value. For example, I want an array of all the subdocuments of name1 which have a value greater or equal to 2.
I tried to add to the $match object 'instances.value':{$gte:2}, but it didn't filter anything, and I still get all 3 documents for this query.
Any ideas?
After unwinding instances then again used $match as below
db.collectionName.aggregate({
"$match": {
"name": "name1"
}
}, {
"$unwind": "$instances"
}, {
"$match": {
"instances.value": {
"$gte": 2
}
}
}, {
$project: {
name: 1,
value: "$instances.value",
score: "$instances.score"
}
})
Or if you tried $match after project then used as below
db.collectionName.aggregate([{
$match: {
name: "name1"
}
}, {
$unwind: "$instances"
}, {
$project: {
name: 1,
value: "$instances.value",
score: "$instances.score"
}
}, {
"$match": {
"value": {
"$gte": 2
}
}
}])