Mongodb aggregation convert array of pairs to key and list of values - mongodb

Trying to condense an array with key value pairs into an array of objects with the key and all the unique values for that key.
I have a structure like:
{
fruits: [
{fruit: apple, type: gaja},
{fruit: apple, type: honey-crisp},
{fruit: apple, type: fuji},
{fruit: cherry, type: black},
{fruit: cherry, type: red},
{fruit: cherry, type: red},
]
}
How can I convert it to:
{
fruits: [
{fruit: apple, types: [gaja, honey-crisp, fuji]},
{fruit: cherry, types: [black, red]}
]
}
Using mongo aggregations I managed to get the first structure from my data using $group and $addToSet. Not sure how to map the array to new object with a key and list of values

Here's another way to do it by using "$reduce". Comments are in the aggregation pipeline.
db.collection.aggregate([
{
"$set": {
// rewrite fruits
"fruits": {
"$reduce": {
"input": "$fruits",
"initialValue": [],
"in": {
"$let": {
"vars": {
// get fruit index in $$value : will be -1 if not there
"idx": {"$indexOfArray": ["$$value.fruit", "$$this.fruit"]}
},
"in": {
"$cond": [
// is fruit not in $$value yet
{"$eq": ["$$idx", -1]},
// new fruit so put in $$value and make "type" an array
{
"$concatArrays": [
"$$value",
[{"$mergeObjects": ["$$this", {"type": ["$$this.type"]}]}]
]
},
// fruit already in $$value, so map $$value with "type" update
{
"$map": {
"input": "$$value",
"as": "val",
"in": {
"$cond": [
// is this array element not the right fruit?
{"$ne": ["$$val.fruit", "$$this.fruit"]},
// nope, leave the element as-is
"$$val",
// this element needs to be updated
{
"$mergeObjects": [
"$$val",
{
"type": {
"$cond": [
// is this "type" already in array?
{"$in": ["$$this.type", "$$val.type"]},
// yes, so leave it as-is
"$$val.type",
// this is a new "type", so add it to array
{"$concatArrays": ["$$val.type", ["$$this.type"]]}
]
}
}
]
}
]
}
}
}
]
}
}
}
}
}
}
}
])
Try it on mongoplayground.net.

Maybe something like this:
db.collection.aggregate([
{
$unwind: "$fruits"
},
{
$group: {
_id: "$fruits.fruit",
type: {
$push: "$fruits.type"
}
}
},
{
$project: {
fruit: "$_id",
type: 1,
_id: 0
}
},
{
$group: {
_id: "",
fruits: {
$push: "$$ROOT"
}
}
}
])
Explained:
Unwind the array
Group to form the type array ( you can use $push or $addToSet in case you need only unique )
Project the necessary fields
Group all documents inside single final one
Playground

Here's another, another way using a multiple "$map" and "$setUnion" to get unique array members.
db.collection.aggregate([
{
"$set": {
// rewrite fruits
"fruits": {
"$map": {
// map over unique fruits
"input": {"$setUnion": "$fruits.fruit"},
"as": "theFruit",
"in": {
// set fruit
"fruit": "$$theFruit",
// "type" are unique elements of fruits.type
// where fruits.fruit == theFruit
"type": {
"$setUnion": {
"$map": {
"input": {
"$filter": {
"input": "$fruits",
"as": "obj",
"cond": {"$eq": ["$$obj.fruit", "$$theFruit"]}
}
},
"in": "$$this.type"
}
}
}
}
}
}
}
}
])
Try it on mongoplayground.net.

Related

MongoDB $filter nested array by date does not work

I have a document with a nested array which looks like this:
[
{
"id": 1,
data: [
[
ISODate("2000-01-01T00:00:00Z"),
2,
3
],
[
ISODate("2000-01-03T00:00:00Z"),
2,
3
],
[
ISODate("2000-01-05T00:00:00Z"),
2,
3
]
]
},
{
"id": 2,
data: []
}
]
As you can see, we have an array of arrays. For each element in the data array, the first element is a date.
I wanted to create an aggregation pipeline which filters only the elements of data where the date is larger than a given date.
db.collection.aggregate([
{
"$match": {
"id": 1
}
},
{
"$project": {
"data": {
"$filter": {
"input": "$data",
"as": "entry",
"cond": {
"$gt": [
"$$entry.0",
ISODate("2000-01-04T00:00:00Z")
]
}
}
}
}
}
])
The problem is that with $gt, this just returns an empty array for data. With $lt this returns all elements. So the filtering clearly does not work.
Expected result:
[
{
"id": 1,
"data": [
[
ISODate("2000-01-05T00:00:00Z"),
2,
3
]
]
}
]
Any ideas?
Playground
I believe the issue is that when you write $$entry.0, MongoDB is trying to evaluate entry.0 as a variable name, when in reality the variable is named entry. You could make use of the $first array operator in order to get the first element like so:
db.collection.aggregate([
{
"$match": {
"id": 1
}
},
{
"$project": {
"data": {
"$filter": {
"input": "$data",
"as": "entry",
"cond": {
"$gt": [
{
$first: "$$entry"
},
ISODate("2000-01-04T00:00:00Z")
]
}
}
}
}
}
])
Mongo playground example
Don't think $$entry.0 work to get the first element of the array. Instead, use $arrayElemAt operator.
db.collection.aggregate([
{
"$match": {
"id": 1
}
},
{
"$project": {
"data": {
"$filter": {
"input": "$data",
"as": "entry",
"cond": {
"$gt": [
{
"$arrayElemAt": [
"$$entry",
0
]
},
ISODate("2000-01-04T00:00:00Z")
]
}
}
}
}
}
])
Sample Mongo Playground
to specify which element in the array you are comparing it is better to use $arrayElemAt instead of $$ARRAY.0. you must pass 2 parameters while using $arrayElemAt, the first one is the array which in your case is $$entry, and the second one is the index which in your case is 0
this is the solution I came up with:
db.collection.aggregate([
{
"$match": {
"id": 1
}
},
{
"$project": {
"data": {
"$filter": {
"input": "$data",
"as": "entry",
"cond": {
"$gt": [
{
"$arrayElemAt": [
"$$entry",
0
]
},
ISODate("2000-01-04T00:00:00Z")
]
}
}
}
}
}
])
playground

Update name of a key in nested mongo document

I did check on multiple platforms for the solution, however did not get the answer. Here is my problem:
I have a nested Mongo Document of the format:
{
_id: "xxxx",
"Type": <value>,
"Data" : {
"1": { <<< this key might differ for diff documents
"00": { <<< this key might differ for diff documents
"cont": "India"
},
"05": {
"cont": "India"
},
....
}
"32": {
"41": {
"cont": "India"
},
"44": {
"cont": "India"
},
....
}
}
}
I want to rename the key cont to country in all the documents in the collection, however as mentioned above the key 1, 00, 32 might differ for different documents hence I could not reach to a solution for this.
Is there something like:
db.collection.updateMany({Type: "abc"}, { $rename: { "Data.*.*.cont": "Data.*.*.country" }})
because the key cont resides at the third level after the key Data
Can someone please help me out here?
Thanks!
Using dynamic value as field name is generally considered as an anti-pattern. Nevertheless, you can still use $objectToArray to convert the nested objects into k-v tuples and manipulate them. Then converted it back using $arrayToObject
db.collection.update({},
[
{
"$addFields": {
"arr": {
// first conversion
"$objectToArray": "$Data"
}
}
},
{
"$addFields": {
"arr": {
// second conversion
"$map": {
"input": "$arr",
"as": "a",
"in": {
k: "$$a.k",
v: {
"$objectToArray": "$$a.v"
}
}
}
}
}
},
{
"$addFields": {
"arr": {
"$map": {
"input": "$arr",
"as": "a",
"in": {
k: "$$a.k",
v: {
"$map": {
"input": "$$a.v",
"as": "a2",
"in": {
k: "$$a2.k",
// rename the field `country`
v: {
country: "$$a2.v.cont"
}
}
}
}
}
}
}
}
},
{
"$addFields": {
"arr": {
"$map": {
"input": "$arr",
"as": "a",
"in": {
k: "$$a.k",
v: {
// 1st backward conversion
"$arrayToObject": "$$a.v"
}
}
}
}
}
},
{
"$project": {
_id: 1,
Type: 1,
"Data": {
// 2nd backward conversion
"$arrayToObject": "$arr"
}
}
}
])
Here is the Mongo playground for your reference.

How to generate object ids when $unwinding with aggregate in mongodb

I'm having the following query
db.getCollection('matches').aggregate([{
"$lookup": {
"from": "player",
"localField": "players.account_id",
"foreignField": "account_id",
"as": "players2"
}
}, {
"$addFields": {
"players": {
"$map": {
"input": "$players",
"in": {
"$mergeObjects": [
"$$this", {
"$arrayElemAt": [
"$players2", {
"$indexOfArray": [
"$players.account_id",
"$$this.account_id"
]
}
]
}
]
}
}
}
}
}, {
"$set": {
"players.match_id": "$match_id",
"players.radiant_win": "$radiant_win"
}
}, {
"$unwind": "$players"
}, {
"$replaceRoot": {
"newRoot": "$players"
}
}, {
"$project": {
"_id": 1,
"match_id": 1,
"account_id": 1,
"hero_id": 1,
"radiant_win": 1
}
}
])
which is supposed to match an inner array with another collection, merge the objects in the arrays by the matching and then unwrap ($unwind) the array into a new collection.
Unfortunately, I'm getting duplicate Object ids which is sort of a problem for when I want to export this collection.
How can I ensure unique Object_Ids for the aggregation?
Thanks in advance!

Mongoose Summing Up subdocument array elements having same alias

I have a document like this:
_id:'someId',
sales:
[
{
_id:'111',
alias:'xxx',
amount:500,
name: Apple, //items with same alias always have same name and quantity
quantity:2
},
{
_id:'222',
alias:'abc',
amount:100,
name: Orange,
quantity:14
},
{
_id:'333',
alias:'xxx',
amount:300,
name: Apple, //items with same alias always have same name and quantity
quantity:2
}
]
The alias field is here to 'group' items/documents whenever they appear to have same alias i.e to be 'embeded' as one with the amount summed up.
I need to display some sort of a report in such a way that those elements which have same alias they should be displayed as ONE and the others which doesn't share same alias to remain as they are.
Example, For the sample document above, I need an output like this
[
{
alias:'xxx',
amount:800
},
{
alias:'abc',
amount:100
}
]
WHAT I HAVE TRIED
MyShop.aggregate([
{$group:{
_id: "$_id",
sales:{$last :"$sales"}
},
{$project:{
"sales.amount":1
}}
}
])
This just displays as a 'list' regardless of the alias. How do I achieve summing up amount based on the alias?
You can achieve this using $group
db.collection.aggregate([
{
$unwind: "$sales"
},
{
$group: {
_id: {
_id: "$_id",
alias: "$sales.alias"
},
sales: {
$first: "$sales"
},
_idsInvolved: {
$push: "$sales._id"
},
amount: {
$sum: "$sales.amount"
}
}
},
{
$group: {
_id: "$_id._id",
sales: {
$push: {
$mergeObjects: [
"$sales",
{
alias: "$_id.alias",
amount: "$amount",
_idsInvolved: "$_idsInvolved"
}
]
}
}
}
}
])
Mongo Playground
You can use below aggregation
db.collection.aggregate([
{
"$addFields": {
"sales": {
"$map": {
"input": {
"$setUnion": [
"$sales.alias"
]
},
"as": "m",
"in": {
"$let": {
"vars": {
"a": {
"$filter": {
"input": "$sales",
"as": "d",
"cond": {
"$eq": [
"$$d.alias",
"$$m"
]
}
}
}
},
"in": {
"amount": {
"$sum": "$$a.amount"
},
"alias": "$$m",
"_idsInvolved": "$$a._id"
}
}
}
}
}
}
}
])
MongoPlayground

Return first element if no match found in array

I have the following document:
{
_id: 123,
state: "AZ",
products: [
{
product_id: 1,
desc: "P1"
},
{
product_id: 2,
desc: "P2"
}
]
}
I need to write a query to return a single element from the products array where state is "AZ" and product_id is 2. If the matching product_id is not found, then return the first (or any) element from the products array.
For example: If product_id is 2 (match found), then the result should be:
products: [
{
product_id: 2,
desc: "P2"
}
]
If the product_id is 3 (not found), then the result should be:
products: [
{
product_id: 1,
desc: "P1"
}
]
I was able to meet one condition when the match is found but not sure how to satisfy the second condition in the same query:
db.getCollection('test').find({"state": "AZ"}, {_id: 0, state: 0, products: { "$elemMatch": {"product_id": "2"}}})
I tried using the aggregation pipeline as well but could not find a working solution.
Note: This is different from the following question as I need to return a default element if the match is not found:
Retrieve only the queried element in an object array in MongoDB collection
You can try below aggregation
Basically you need to $filter the products array and check for the $condition if it doesn't contain any element or equal to [] then you have to $slice with the first element of the products array.
db.collection.aggregate([
{ "$addFields": {
"products": {
"$cond": [
{
"$eq": [
{ "$filter": {
"input": "$products",
"cond": { "$eq": ["$$this.product_id", 2] }
}},
[]
]
},
{ "$slice": ["$products", 1] },
{ "$filter": {
"input": "$products",
"cond": { "$eq": ["$$this.product_id", 2] }
}}
]
}
}}
])
or even using $let aggregation
db.collection.aggregate([
{ "$addFields": {
"products": {
"$let": {
"vars": {
"filt": {
"$filter": {
"input": "$products",
"cond": { "$eq": ["$$this.product_id", 2] }
}
}
},
"in": {
"$cond": [
{ "$eq": ["$$filt", []] },
{ "$slice": ["$products", 1] },
"$$filt"
]
}
}
}
}}
])
If you don't care which element you get back then this is the way to go (you'll get the last element in the array in case of no match since $indexOfArray will return -1):
db.getCollection('test').aggregate([{
$addFields: {
"products": {
$arrayElemAt: [ "$products", { $indexOfArray: [ "$products.product_id", 2 ] } ]
},
}
}])
If you want the first then do this instead ($max will take care of transforming -1 into index 0 which is the first element):
db.getCollection('test').aggregate([{
$addFields: {
"products": {
$arrayElemAt: [ "$products", { $max: [ 0, { $indexOfArray: [ "$products.product_id", 2 ] } ] } ]
},
}
}])
Here is a version that should work on v3.2 as well:
db.getCollection('test').aggregate([{
"$project": {
"products": {
$slice: [{
$concatArrays: [{
$filter: {
"input": "$products",
"cond": { "$eq": ["$$this.product_id", 2] }
}},
"$products" // simply append the "products" array
// alternatively, you could append only the first or a specific item like this [ { $arrayElemAt: [ "$products", 0 ] } ]
]
},
1 ] // take first element only
}
}
}])