MongoDB aggregation: Aggregate array into keyed objects - mongodb

I'd like to flatten document array into a keyed objects using the aggregation functionality. Here’s an example of my documents:
[
{
"_id": 1,
"texts": [
{ "language": "english", "text": "hello" },
{ "language": "german", "text": "hallo" },
{ "language": "french", "text": "bonjour" }
]
}, …
]
Expected result:
[
{
"_id": 1,
"texts": {
"english": "hello",
"german": "hallo",
"french": "bonjour"
}
}, …
]
I’ve looked at different operators, e.g. $map, but this seems to be focued on transforming array to array. I’d probably need a JS equivalent of $reduce, but couldn’t find a way of accumulating my values into an object.
Any ideas?

You need $map and $arrayToObject which converts an array of k-v pairs into single object:
db.col.aggregate([
{
$addFields: {
texts: {
$arrayToObject: {
$map: {
input: "$texts",
as: "text",
in: {
k: "$$text.language",
v: "$$text.text"
}
}
}
}
}
}
])

You can also use $zip with $arrayToObject.
Something like
db.col.aggregate([{
"$project": {
"texts":{
"$arrayToObject":{
"$zip": {
"inputs": [
"$texts.language",
"$texts.text"
]
}
}
}}
}])

You can try this using $map and $arrayToObject aggregation in mongodb 3.4.4 and above
Just convert language field with key(k) and text with value(v) using $map and transform the array to object using $arrayToObject
db.collection.aggregate([
{ "$addFields": {
"texts": {
"$map": {
"input": "$texts",
"in": {
"k": "$$this.language",
"v": "$$this.text"
}
}
}
}},
{ "$addFields": {
"texts": { "$arrayToObject": "$texts" }
}}
])

Related

Regroup all elements of nested array of nested array into a single array

I have this collection :
{
"_id": ObjectId("6045eec8f113547ddd3472d9"),
"execution": {
"_id": ObjectId("6045eec8f113547ddd3472d8"),
"steps": [
{
"_id": ObjectId("6045eec8f113547ddd3472d7"),
"actions": [
{
"_id": ObjectId("6045eec8f113547ddd3472d6"),
"title": "action title"
}
]
}
]
}
}
and i want to achieve this result:
{
"_id": ObjectId("6045eec8f113547ddd3472d9"),
"allActions": [
{
"_id": ObjectId("6045eec8f113547ddd3472d6"),
"title": "action title"
}
]
}
to clarify more, i want to add an array that groups together all "actions" under the "steps" array
db.getCollection("missions").aggregate([
{
$match: {
_id: ObjectId("6045eec8f113547ddd3472d9")
}
},
{
$addFields: {
"allActions": "$execution.steps"
}
}
])
Could someone help me complete the part of the code that will allow me to achieve the desired output?
You may just use $reduce and $concatArrays to group all the entries in the actions array
db.missions.aggregate([
{
$match: {
_id: ObjectId("6045eec8f113547ddd3472d9")
}
},
{
$addFields: {
"allActions": {
"$reduce": {
"input": "$execution.steps.actions",
"initialValue": [],
"in": {
"$concatArrays": [
"$$value",
"$$this"
]
}
}
}
}
}
])
Here is the Mongo playground for your reference.

Mongodb: Get value from $[<identifier>] in update

I want to rename a field inside a object itself inside a nested array.
As example, I want to rename the all tags m2 to m6 in this document:
{
"_id": 1,
"tagsGroup": [
{
"id": "1234",
"tags": {
"m1": 1,
"m2": 2
}
},
{
"id": "456",
"tags": {
"m3": 1,
"m2": 2
}
},
{
"id": "1234",
"tags": {
"m4": 2,
"m5": 2
}
},
]
}
This is my current state of work:
db.collection.update({},
{
"$set": {"tagsGroup.$[tGp].tags.m6": "$tagsGroup.$[tGp].tags.m2"},
"$unset": {"tagsGroup.$[tGp].tags.m2": ""}
},
{
arrayFilters: [{"tGp.tags.m2": {$exists: 1}}],
multi: true}
)
Unfortunately, the $tagsGroup.$[tGp].tags.m6 is not interpreted.
Do you guys, have a way to do this?
Thanks!
Probably similar to this question MongoDB rename database field within array,
There is no straight way to rename fields within arrays with a single command. You can try update with aggregation pipeline starting from MongoDB v4.2,
$map to iterate loop of tagsGroup array
$map to iterate loop of tags object after converting to array using $objectToArray, it will return in k and v format
$replaceOne will replace specific string on find field, this is starting from MongoDB v4.4
$arrayToObject convert tags array returned by second $map back to object format
$mergeObjects to merge current object with updated tags object
db.collection.update(
{ "tagsGroup.tags.m2": { $exists: true } },
[{
$set: {
tagsGroup: {
$map: {
input: "$tagsGroup",
in: {
$mergeObjects: [
"$$this",
{
tags: {
$arrayToObject: {
$map: {
input: { $objectToArray: "$$this.tags" },
in: {
k: {
$replaceOne: {
input: "$$this.k",
find: "m2",
replacement: "m6"
}
},
v: "$$this.v"
}
}
}
}
}
]
}
}
}
}
}],
{ multi: true }
)
Playground

Find in tripple nested array mongodb [duplicate]

I have this Collection in mongodb
{
"_id" : "777",
"someKey" : "someValue",
"someArray" : [
{
"name" : "name1",
"someNestedArray" : [
{
"name" : "value"
},
{
"name" : "delete me"
}
]
}
]
}
I want to find document based on someArray.someNestedArray.name
but i can't find any useful link all search result about update nested array
i am trying this but return nothing
db.mycollection.find({"someArray.$.someNestedArray":{"$elemMatch":{"name":"1"}}})
db.mycollection.find({"someArray.$.someNestedArray.$.name":"1"})
and Some thing else
how can i find by element in double nested array mongodb?
In the simplest sense this just follows the basic form of "dot notation" as used by MongoDB. That will work regardless of which array member the inner array member is in, as long as it matches a value:
db.mycollection.find({
"someArray.someNestedArray.name": "value"
})
That is fine for a "single field" value, for matching multiple-fields you would use $elemMatch:
db.mycollection.find({
"someArray": {
"$elemMatch": {
"name": "name1",
"someNestedArray": {
"$elemMatch": {
"name": "value",
"otherField": 1
}
}
}
}
})
That matches the document which would contain something with a a field at that "path" matching the value. If you intended to "match and filter" the result so only the matched element was returned, this is not possible with the positional operator projection, as quoted:
Nested Arrays
The positional $ operator cannot be used for queries which traverse more than one array, such as queries that traverse arrays nested within other arrays, because the replacement for the $ placeholder is a single value
Modern MongoDB
We can do this by applying $filter and $map here. The $map is really needed because the "inner" array can change as a result of the "filtering", and the "outer" array of course does not match the conditions when the "inner" was stripped of all elements.
Again following the example of actually having multiple properties to match within each array:
db.mycollection.aggregate([
{ "$match": {
"someArray": {
"$elemMatch": {
"name": "name1",
"someNestedArray": {
"$elemMatch": {
"name": "value",
"otherField": 1
}
}
}
}
}},
{ "$addFields": {
"someArray": {
"$filter": {
"input": {
"$map": {
"input": "$someArray",
"as": "sa",
"in": {
"name": "$$sa.name",
"someNestedArray": {
"$filter": {
"input": "$$sa.someNestedArray",
"as": "sn",
"cond": {
"$and": [
{ "$eq": [ "$$sn.name", "value" ] },
{ "$eq": [ "$$sn.otherField", 1 ] }
]
}
}
}
}
},
},
"as": "sa",
"cond": {
"$and": [
{ "$eq": [ "$$sa.name", "name1" ] },
{ "$gt": [ { "$size": "$$sa.someNestedArray" }, 0 ] }
]
}
}
}
}}
])
Therefore on the "outer" array the $filter actually looks at the $size of the "inner" array after it was "filtered" itself, so you can reject those results when the whole inner array does in fact match noting.
Older MongoDB
In order to "project" only the matched element, you need the .aggregate() method:
db.mycollection.aggregate([
// Match possible documents
{ "$match": {
"someArray.someNestedArray.name": "value"
}},
// Unwind each array
{ "$unwind": "$someArray" },
{ "$unwind": "$someArray.someNestedArray" },
// Filter just the matching elements
{ "$match": {
"someArray.someNestedArray.name": "value"
}},
// Group to inner array
{ "$group": {
"_id": {
"_id": "$_id",
"name": "$someArray.name"
},
"someKey": { "$first": "$someKey" },
"someNestedArray": { "$push": "$someArray.someNestedArray" }
}},
// Group to outer array
{ "$group": {
"_id": "$_id._id",
"someKey": { "$first": "$someKey" },
"someArray": { "$push": {
"name": "$_id.name",
"someNestedArray": "$someNestedArray"
}}
}}
])
That allows you to "filter" the matches in nested arrays for one or more results within the document.
You can also try something like below:
db.collection.aggregate(
{ $unwind: '$someArray' },
{
$project: {
'filteredValue': {
$filter: {
input: "$someArray.someNestedArray",
as: "someObj",
cond: { $eq: [ '$$someObj.name', 'delete me' ] }
}
}
}
}
)

How to find match in documents in Mongo and Mongo aggregation?

I have following json structure in mongo collection-
{
"students":[
{
"name":"ABC",
"fee":1233
},
{
"name":"PQR",
"fee":345
}
],
"studentDept":[
{
"name":"ABC",
"dept":"A"
},
{
"name":"XYZ",
"dept":"X"
}
]
},
{
"students":[
{
"name":"XYZ",
"fee":133
},
{
"name":"LMN",
"fee":56
}
],
"studentDept":[
{
"name":"XYZ",
"dept":"X"
},
{
"name":"LMN",
"dept":"Y"
},
{
"name":"ABC",
"dept":"P"
}
]
}
Now I want to calculate following output.
if students.name = studentDept.name
so my result should be as below
{
"name":"ABC",
"fee":1233,
"dept":"A",
},
{
"name":"XYZ",
"fee":133,
"dept":"X"
}
{
"name":"LMN",
"fee":56,
"dept":"Y"
}
Do I need to use mongo aggregation or is it possible to get above given output without using aggregation???
What you are really asking here is how to make MongoDB return something that is actually quite different from the form in which you store it in your collection. The standard query operations do allow a "limitted" form of "projection", but even as the title on the page shared in that link suggests, this is really only about "limiting" the fields to display in results based on what is present in your document already.
So any form of "alteration" requires some form of aggregation, which with both the aggregate and mapReduce operations allow to "re-shape" the document results into a form that is different from the input. Perhaps also the main thing people miss with the aggregation framework in particular, is that it is not just all about "aggregating", and in fact the "re-shaping" concept is core to it's implementation.
So in order to get results how you want, you can take an approach like this, which should be suitable for most cases:
db.collection.aggregate([
{ "$unwind": "$students" },
{ "$unwind": "$studentDept" },
{ "$group": {
"_id": "$students.name",
"tfee": { "$first": "$students.fee" },
"tdept": {
"$min": {
"$cond": [
{ "$eq": [
"$students.name",
"$studentDept.name"
]},
"$studentDept.dept",
false
]
}
}
}},
{ "$match": { "tdept": { "$ne": false } } },
{ "$sort": { "_id": 1 } },
{ "$project": {
"_id": 0,
"name": "$_id",
"fee": "$tfee",
"dept": "$tdept"
}}
])
Or alternately just "filter out" the cases where the two "name" fields do not match and then just project the content with the fields you want, if crossing content between documents is not important to you:
db.collection.aggregate([
{ "$unwind": "$students" },
{ "$unwind": "$studentDept" },
{ "$project": {
"_id": 0,
"name": "$students.name",
"fee": "$students.fee",
"dept": "$studentDept.dept",
"same": { "$eq": [ "$students.name", "$studentDept.name" ] }
}},
{ "$match": { "same": true } },
{ "$project": {
"name": 1,
"fee": 1,
"dept": 1
}}
])
From MongoDB 2.6 and upwards you can even do the same thing "inline" to the document between the two arrays. You still want to reshape that array content in your final output though, but possible done a little faster:
db.collection.aggregate([
// Compares entries in each array within the document
{ "$project": {
"students": {
"$map": {
"input": "$students",
"as": "stu",
"in": {
"$setDifference": [
{ "$map": {
"input": "$studentDept",
"as": "dept",
"in": {
"$cond": [
{ "$eq": [ "$$stu.name", "$$dept.name" ] },
{
"name": "$$stu.name",
"fee": "$$stu.fee",
"dept": "$$dept.dept"
},
false
]
}
}},
[false]
]
}
}
}
}},
// Students is now an array of arrays. So unwind it twice
{ "$unwind": "$students" },
{ "$unwind": "$students" },
// Rename the fields and exclude
{ "$project": {
"_id": 0,
"name": "$students.name",
"fee": "$students.fee",
"dept": "$students.dept"
}},
])
So where you want to essentially "alter" the structure of the output then you need to use one of the aggregation tools to do. And you can, even if you are not really aggregating anything.

Mongodb array concatenation

When querying mongodb, is it possible to process ("project") the result so as to perform array concatenation?
I actually have 2 different scenarios:
(1) Arrays from different fields:, e.g:
Given:
{companyName:'microsoft', managers:['ariel', 'bella'], employees:['charlie', 'don']}
{companyName:'oracle', managers:['elena', 'frank'], employees:['george', 'hugh']}
I'd like my query to return each company with its 'managers' and 'employees' concatenated:
{companyName:'microsoft', allPersonnel:['ariel', 'bella','charlie', 'don']}
{companyName:'oracle', allPersonnel:['elena', 'frank','george', 'hugh']}
(2) Nested arrays:, e.g.:
Given the following docs, where employees are separated into nested arrays (never mind why, it's a long story):
{companyName:'microsoft', personnel:[ ['ariel', 'bella'], ['charlie', 'don']}
{companyName:'oracle', personnel:[ ['elena', 'frank'], ['george', 'hugh']}
I'd like my query to return each company with a flattened 'personal' array:
{companyName:'microsoft', allPersonnel:['ariel', 'bella','charlie', 'don']}
{companyName:'oracle', allPersonnel:['elena', 'frank','george', 'hugh']}
I'd appreciate any ideas, using either 'find' or 'aggregate'
Thanks a lot :)
Of Course in Modern MongoDB releases we can simply use $concatArrays here:
db.collection.aggregate([
{ "$project": {
"companyNanme": 1,
"allPersonnel": { "$concatArrays": [ "$managers", "$employees" ] }
}}
])
Or for the second form with nested arrays, using $reduce in combination:
db.collection.aggregate([
{ "$project": {
"companyName": 1,
"allEmployees": {
"$reduce": {
"input": "$personnel",
"initialValue": [],
"in": { "$concatArrays": [ "$$value", "$$this" ] }
}
}
}}
])
There is the $setUnion operator available to the aggregation framework. The constraint here is that these are "sets" and all the members are actually "unique" as a "set" requires:
db.collection.aggregate([
{ "$project": {
"companyname": 1,
"allPersonnel": { "$setUnion": [ "$managers", "$employees" ] }
}}
])
So that is cool, as long as all are "unique" and you are in singular arrays.
In the alternate case you can always process with $unwind and $group. The personnel nested array is a simple double unwind
db.collection.aggregate([
{ "$unwind": "$personnel" },
{ "$unwind": "$personnel" },
{ "$group": {
"_id": "$_id",
"companyName": { "$first": "$companyName" },
"allPersonnel": { "$push": { "$personnel" } }
}}
])
Or the same thing as the first one for versions earlier than MongoDB 2.6 where the "set operators" did not exist:
db.collection.aggregate([
{ "$project": {
"type": { "$const": [ "M", "E" ] },
"companyName": 1,
"managers": 1,
"employees": 1
}},
{ "$unwind": "$type" },
{ "$unwind": "$managers" },
{ "$unwind": "$employees" },
{ "$group": {
"_id": "$_id",
"companyName": { "$first": "$companyName" },
"allPersonnel": {
"$addToSet": {
"$cond": [
{ "$eq": [ "$type", "M" ] },
"$managers",
"$employees"
]
}
}
}}
])