clone and rename a field of an array of subdocuments in Mongo - mongodb

I've got a collection like this:
{
name: "A Name",
answers: [
{order: 1},
{order: 2},
{order: 3}
]
}
What I want to do is to add a new filed id to each element of the answers array based on the value of the order property - I want just to clone it, so the output is
{
name: "A Name",
answers: [
{order: 1, id: 1},
{order: 2, id: 2},
{order: 3, id: 3}
]
}
I looked at this post and this one too, but I don't know how to combine them to work properly for subdocuments.
In MongoDB documentation for the aggregate method, I found a simple example of how to update embedded documents here, but I have no idea how to use the order property instead of a fixed term. The following tries seem not to work as I need:
db.collection.aggregate([
{
$addFields: {
"answers.id": "answers.$.order"
}
}
])
db.collection.aggregate([
{
$addFields: {
"answers.id": "$answers.order"
}
}
])
Is it possible to achieve the expected result with the `aggregate method?

Demo - https://mongoplayground.net/p/M79MV6-Zp4C
db.collection.aggregate([
{
$set: {
answers: {
$map: {
input: "$answers",
as: "answer",
in: { $mergeObjects: [ "$$answer", { id: "$$answer.order" } ]
}
}
}
}
}
])
Updated Demo - https://mongoplayground.net/p/iyqIPGQ5-ld
db.collection.aggregate([
{ $unwind: "$answers" },
{
$group: {
_id: "$_id",
answers: { $push: { order: "$answers.order", id: "$answers.order" } },
name: { $first: "$name" } // preserve properties add them to the group pick 1st value
}
}
])
Demo - https://mongoplayground.net/p/Ln5CcmT-Kkm
db.collection.aggregate([
{ $unwind: "$answers" }, // break into individuals documents
{ $addFields: { "answers.id": "$answers.order" } }, // copy order value to id
{ $group: { _id: "$_id", answers: { $push: "$answers" } } } // join and group it back
])
If you want to sort n get id from index
Demo - https://mongoplayground.net/p/6U_sRYWHtDR
db.collection.aggregate([
{ $sort: { "answers.order": 1 } },
{ $unwind: { path: "$answers", includeArrayIndex: "index" } },
{ $group: { _id: "$_id", answers: { "$push": { order: "$answers.order", id: { $add: [ "$index", 1 ] } } } } }
])

Related

Mongodb aggregate grouping elements of an array type field

I have below data in my collection:
[
{
"_id":{
"month":"Jan",
"year":"2022"
},
"products":[
{
"product":"ProdA",
"status":"failed",
"count":15
},
{
"product":"ProdA",
"status":"success",
"count":5
},
{
"product":"ProdB",
"status":"failed",
"count":20
},
{
"product":"ProdB",
"status":"success",
"count":10
}
]
},
...//more such data
]
I want to group the elements of products array on the name of the product, so that we have record of how what was the count of failure of success of each product in each month. Every record is guaranteed to have both success and failure count each month. The output should look like below:
[
{
"_id":{
"month":"Jan",
"year":"2022"
},
"products":[
{
"product":"ProdA","status":[{"name":"success","count":5},{"name":"failed","count":15}]
},
{
"product":"ProdB","status":[{"name":"success","count":10},{"name":"failed","count":20}]
}
]
},
...//data for succeeding months
]
I have tried to do something like this:
db.collection.aggregate([{ $unwind: "$products" },
{
$group: {
"_id": {
month: "$_id.month",
year: "$_id.year"
},
products: { $push: { "product": "$product", status: { $push: { name: "$status", count: "$count" } } } }
}
}]);
But above query doesn't work.
On which level I need to group fields so as to obtain above output.
Please help me to find out what I am doing wrong.
Thank You!
Your first group stage needs to group by both the _id and the product name, aggregate a list of status counts and then another group stage which then forms the products list:
db.collection.aggregate([
{$unwind: "$products"},
{$group: {
_id: {
id: "$_id",
product: "$products.product",
},
status: {
$push: {
name: "$products.status",
count: "$products.count"
}
}
}
},
{$group: {
_id: "$_id.id",
products: {
$push: {
product: "$_id.product",
status: "$status"
}
}
}
}
])
Mongo Playground

mongodb: sort nested array using dynamic parmeter of field name

Assume in an aggregation pipeline one of the steps produces the following results:
{
customer: "WN",
sort_category: "category_a",
locations: [
{
city: "Elkana",
category_a: 11904.0,
category_b: 74.0,
category_c: 657.0,
},
{
city: "Haifa",
category_a: 20.0,
category_b: 841.0,
category_c: 0,
},
{
city" : "Jerusalem",
category_a: 451.0,
category_b: 45.0,
category_c: 712.0,
}
]
}
{
...
}
The next step is to sort the list of the nested objects of each document in the collection.
The list of the nested objects should be sorted by dynamic parameter containing the field name.
For example - the list of locations should be sorted by the value of category_a.
category_a is parmeter given in sort_category field.
Here is a solution without use $function:
db.tests.aggregate([
{$unwind:"$locations"},
{$project: {
_id: 1,
customer: 1,
sort_category: 1,
locations: 1,
locationsKV:{$objectToArray:"$locations"}
}
},
{$unwind:"$locationsKV"},
{$project:{
_id: 1,
customer: 1,
sort_category: 1,
locations: 1,
locationsKV: 1,
category: {
$cond:[{$eq: ["$sort_category","$locationsKV.k"]},
"$locationsKV.v", 0]},
agg: {
$cond: [{$eq: ["$sort_category","$locationsKV.k"]}, true, false]
}
}
},
{$match: {agg: true}},
{$sort: {category: 1}},
{$group: {
_id: "$_id",
customer: {$first: "$customer"},
sort_category: {$first: "$sort_category"},
locations: {$push: "$locations"}
}
},
{$project:{ _id: 0}}
])
You can try custom way,
$addFields will add one copy field named category of key sort_category inside every object in locations array using $map and $reduce
$unwind deconstruct locations array
$sort by category field that we have added in locations array
$project to remove category field
$group by _id and reconstruct locations array
$project to remove _id field
db.collection.aggregate([
{
$addFields: {
locations: {
$map: {
input: "$locations",
as: "l",
in: {
$mergeObjects: [
"$$l",
{
category: {
$reduce: {
input: { $objectToArray: "$$l" },
initialValue: null,
in: {
$cond: [{ $eq: ["$$this.k", "$sort_category"] }, "$$this.v", "$$value"]
}
}
}
}
]
}
}
}
}
},
{ $unwind: "$locations" },
{ $sort: { "locations.category": 1 } },
{ $project: { "locations.category": 0 } },
{
$group: {
_id: "$_id",
customer: { $first: "$customer" },
sort_category: { $first: "$sort_category" },
locations: { $push: "$locations" }
}
},
{ $project: { _id: 0 } }
])
Playground
If you are planing to upgrade your MongoDB to v4.4 or also this will helpful for others, The $function is a option for custom operation and user defined operation.
There are 3 properties:
body The function definition. You can specify the function definition as either BSON type Code or String, define our own function using function(){, we have passed locations array and sort_category that is dynamic field, the function logic is sort by descending order
args Arguments passed to the function body
lang The language used in the body. You must specify lang: "js"
db.collection.aggregate([
{
$addFields: {
locations: {
$function: {
body: function(locations, sort_category){
return locations.sort(function(a, b){
// DESCENDING ORDER
return b[sort_category] - a[sort_category]
// ASCENDING ORDER
// return a[sort_category] - b[sort_category]
})
},
args: ["$locations", "$sort_category"],
lang: "js"
}
}
}
}
])
For more guidelines about restrictions and considerations Refer.

$match in aggregate don't return data in mongodb

I have three tables below is the structure like below
I'm looking to get a result like below
"type1": [ -- type from Accounts collection
{
"_id": "5e97e9a224f62f93d5x3zz46", -- _id from Accounts collection
"locs": "sampleLocks 1", -- field from Accounts collection
"solutions": "sample solutions 1", -- field from Accounts collection
"Clause": "clause 1" -- field from AccountsDesc collection
},
{
"_id": "5e97e9a884f62f93d5x3zz46",
"locs": "sampleLocks2",
"solutions": "sample solutions2",
"Clause": "clause2"
}
],
"type2": [
// same data construction as of type1 above
]
_id, locks, solution to be coming from Accounts collection
Clause field to be coming from AccountsDesc collection
accounts_id is kind of a foreign key in AccountsDesc coming from Account
competitor_id is kind of a foreign key in AccountsDesc coming from Competitor
Below is what my query looks like
db.accountDesc.aggregate([
{
$match : {accounts_Id : "123456"}, active: true}
},
{
$lookup: {
from: 'accounts',
pipeline: [{ $match: { type: { $in: ["type1, type2, type3"] } } }],
as: 'accountsData'
}
},
{
$group: {
_id: "$accountsData.type",
data: {
$push: {_id: "$accountsData._id", clause: "$clause", locs: "$type.locs", solutions: "$type.solutions"}
}
}
},
{
$group: {
_id: null,
data: {
$push: {
k: {
$toString: '$_id'
},
v: '$data'
}
}
}
},
{
$replaceRoot: {
newRoot: {
$arrayToObject: '$data'
}
}
}
])
Issues related with the query -
$match : {accountId : "123456"}, active: true} -- No data is returned if i use match on AccountsDesc collection
cant set localField, foriegnField if im using pipeline, then how the mapping will happen like a LEFT join.
clause: "$clause" don't get the value of this field in the response
As we discussed in chat, you want RIGHT OUTER JOIN for your aggregation.
Try the query below:
db.User_Promo_Map.aggregate([
{
$match: {
user_Id: ObjectId("5e8c1180d59de1704ce68112")
}
},
{
$lookup: {
from: "promo",
pipeline: [
{
$match: {
active: true,
platform: {
$in: [
"twitch",
"youtube",
"facebook"
]
}
}
}
],
as: "accountsData"
}
},
{
$unwind: "$accountsData"
},
{
$group: {
_id: "$accountsData.platform",
data2: {
$addToSet: {
amount: "$amount",
promo_Id: "$promo_Id"
}
},
data: {
$addToSet: {
_id: "$accountsData._id",
format: "$accountsData.format",
description: "$accountsData.description"
}
}
}
},
{
$addFields: {
data: {
$map: {
input: "$data",
as: "data",
in: {
"_id": "$$data._id",
"description": "$$data.description",
"format": "$$data.format",
amount: {
$reduce: {
input: "$data2",
initialValue: "$$REMOVE",
in: {
$cond: [
{
$eq: [
"$$this.promo_Id",
"$$data._id"
]
},
"$$this.amount",
"$$value"
]
}
}
}
}
}
}
}
},
{
$group: {
_id: null,
data: {
$push: {
k: {
$toString: "$_id"
},
v: "$data"
}
}
}
},
{
$replaceRoot: {
newRoot: {
$arrayToObject: "$data"
}
}
}
])
MongoPlayground

Mongodb while aggregate group value as key

I was trying to aggregate and group values but want one of the field as key.
[
{id:1, value: "x"},
{id:2, value: "y"},
{id:1, value: "a"},
{id:2, value: "b"},
]
used this query but no luck
db.getCollection('Test').aggregate([
{
$group: {
_id: "$id",
"value": {$push: "$$ROOT" }
}
}
])
Was trying to achieve this
[
{ 1:[x,a] },
{ 2:[y,b] }
]
Can anyone help me with this query?
You need to run $group twice to get single document which contains an array of k,v pairs. Then you can run $arrayToObject on that document along with $replaceRoot to promote new object into root level:
db.collection.aggregate([
{
$group: {
_id: "$id",
values: { $push: "$value" }
}
},
{
$group: {
_id: null,
root: { $push: { k: { $toString: "$_id" }, v: "$values" } }
}
},
{
$replaceRoot: {
newRoot: {
$arrayToObject: "$root"
}
}
}
])
Mongo Playground

Project first item in an array to new field (MongoDB aggregation)

I am using Mongoose aggregation (MongoDB version 3.2).
I have a field users which is an array. I want to $project first item in this array to a new field user.
I tried
{ $project: {
user: '$users[0]',
otherField: 1
}},
{ $project: {
user: '$users.0',
otherField: 1
}},
{ $project: {
user: { $first: '$users'},
otherField: 1
}},
But neither works.
How can I do it correctly? Thanks
Update:
Starting from v4.4 there is a dedicated operator $first:
{ $project: {
user: { $first: "$users" },
otherField: 1
}},
It's a syntax sugar to the
Original answer:
You can use arrayElemAt:
{ $project: {
user: { $arrayElemAt: [ "$users", 0 ] },
otherField: 1
}},
If it is an array of objects and you want to use just single object field, ie:
{
"users": [
{name: "John", surname: "Smith"},
{name: "Elon", surname: "Gates"}
]
}
you can use:
{ $project: { user: { $first: "$users.name" } }
Edit (exclude case - after comment from #haytham)
In order to exclude a single field from a nested document in array you have to do 2 projections:
{ $project: { user: { $first: "$users" } }
Which return whole first object, and then exclude field you do not want, ie:
{ $project: { "user.name" : 0 }
Starting Mongo 4.4, the aggregation operator $first can be used to access the first element of an array:
// { "users": ["Jean", "Paul", "Jack"] }
// { "users": ["Claude"] }
db.collection.aggregate([
{ $project: { user: { $first: "$users" } } }
])
// { "user" : "Jean" }
// { "user" : "Claude" }