MongoDB Aggregation - Assign a value of a field in an object to a custom field - mongodb

Given the following data structure:
[
{
"body": {
"Fields": [
{
"Name": "description",
"Value": "Some text"
},
{
"Name": "size",
"Value": "40"
}
]
}
}
]
I need to get the following output containing keys extracted from 'Name' fields and values extracted by "Value" fields:
[
{
"description": "Some text",
"size": "40"
}
]
Could you please provide me with the ideas?
I've ended up filtering required element, but have no idea how to extract values and assign them to the keys. What I have so far:
db.collection.aggregate([
{
"$project": {
"description": {
"$filter": {
"input": "$body.Fields",
"as": "bfields",
"cond": {
"$eq": [
"$$bfields.Name",
"description"
]
}
}
},
"size": {
"$filter": {
"input": "$body.Fields",
"as": "bfields",
"cond": {
"$eq": [
"$$bfields.Name",
"size"
]
}
}
}
}
}
])
It produces:
[
{
"_id": ObjectId("5a934e000102030405000000"),
"description": [
{
"Name": "description",
"Value": "Some text"
}
],
"size": [
{
"Name": "size",
"Value": "40"
}
]
}
]

db.collection.aggregate([
{
$addFields: {
body: {
$map: {
input: "$body.Fields",
as: "fi",
in: {
k: "$$fi.Name",
v: "$$fi.Value"
}
}
}
}
},
{
"$addFields": {
"body": {
"$arrayToObject": "$body"
}
}
},
{
$project: {
description: "$body.description",
size: "$body.size",
_id: 0
}
}
])
explained:
Use $map to rename the keys to k,v suitable for $arrayToObject
Convert the array to object with $arrayToObject ( the magic trick)
$project to the exact desired output
playground

Related

Change object into array in key-value collection

I've got a document in MongoDB like this:
{
"name": "wine",
"foodstuffSelectedPortions": {
"id_1": [
{
"foodstuffId": "f1",
"portion": {
"portionName": "portion name 1",
"portionWeight": {
"value": 1,
"unit": "KG"
}
}
},
{
"foodstuffId": "f2",
"portion": {
"portionName": "portion name 2",
"portionWeight": {
"value": 100,
"unit": "ML"
}
}
}
],
"id_2": [
{
"foodstuffId": "f3",
"portion": {
"portionName": "portion name 3",
"portionWeight": {
"value": 15,
"unit": "ML"
}
}
}
]
}
}
and I want to update foodstuffSelectedPortions.portion object into array that contains this object. So the expected result should look like this:
{
"name": "wine",
"foodstuffSelectedPortions": {
"id_1": [
{
"foodstuffId": "f1",
"portion": [
{
"portionName": "portion name 1",
"portionWeight": {
"value": 1,
"unit": "KG"
}
}
]
},
{
"foodstuffId": "f2",
"portion": [
{
"portionName": "portion name 2",
"portionWeight": {
"value": 100,
"unit": "ML"
}
}
]
}
],
"id_2": [
{
"foodstuffId": "f3",
"portion": [
{
"portionName": "portion name 3",
"portionWeight": {
"value": 15,
"unit": "ML"
}
}
]
}
]
}
}
I've tried this query:
db.foodstuff.update(
{ },
{ $set: { "foodstuffSelectedPortions.$[].portion": ["$foodstuffSelectedPortions.$[].portion"] } }
)
but it gives me an error: Cannot apply array updates to non-array element foodstuffSelectedPortions: which looks fine because the foodstuffSelectedPortions is an object not array.
How to write this query correctly? I use MongoDB 4.4.4 and Mongo Shell.
Query (the one you asked)
(do updateMany to update all to the new schema)
converts to array
map on array
map on nested array, replacing portion object with array
Test code here
db.collection.update({},
[
{
"$set": {
"foodstuffSelectedPortions": {
"$arrayToObject": {
"$map": {
"input": {
"$objectToArray": "$foodstuffSelectedPortions"
},
"in": {
"k": "$$f.k",
"v": {
"$map": {
"input": "$$f.v",
"in": {
"$mergeObjects": [
"$$f1",
{
"portion": [
"$$f1.portion"
]
}
]
},
"as": "f1"
}
}
},
"as": "f"
}
}
}
}
}
])
Query
(alternative schema with 2 levels of nesting instead of 3, and without data on keys)
An example why data on keys is bad(i guess there are exceptions), is why you are stuck, you wanted to update all, but you didn't know their names so you couldn't select them. If you had them in array you could do a map on all or you could use update operator to do update in all members of an array (like the way you tried to do it, and complained that is object)
*But dont use this schema unless it fits your data and your queries better.
Test code here
db.collection.update({},
[
{
"$set": {
"foodstuffSelectedPortions": {
"$reduce": {
"input": {
"$objectToArray": "$foodstuffSelectedPortions"
},
"initialValue": [],
"in": {
"$let": {
"vars": {
"f": "$$this"
},
"in": {
"$concatArrays": [
"$$value",
{
"$map": {
"input": "$$f.v",
"in": {
"$mergeObjects": [
"$$f1",
{
"name": "$$f.k",
"portion": [
"$$f1.portion"
]
}
]
},
"as": "f1"
}
}
]
}
}
}
}
}
}
}
])
Pipeline updates require MongoDB >=4.2

MongoDB Trasnform string array to string concatenated in alphabetical order

Playground
Lets say I have this collection:
[
{ "Topics": [ "a", "b" ] },
{ "Topics": [ "x", "a" ] },
{ "Topics": [ "k", "c", "z" ] }
]
I want to transform this string array to a single string with the itens of it in alphabetical order. The result would be:
[
{ Topic: "a/b"},
{ Topic: "a/x"},
{ Topic: "c/k/z"}
]
How can I project this result? Using Map? Reduce?
I have Mongo 5.0
Playground
cheers
just found the solution after some tries...
Just A Unwind, Sort, Group, Project with Reduce made the job...
Data
[
{
"Topics": [
"a",
"b"
]
},
{
"Topics": [
"x",
"a"
]
},
{
"Topics": [
"k",
"c",
"z"
]
}
]
Query
db.collection.aggregate([
{
"$unwind": "$Topics"
},
{
"$sort": {
"Topics": 1
}
},
{
"$group": {
"_id": "$_id",
Topics: {
"$push": "$Topics"
}
}
},
{
"$project": {
Topic: {
$reduce: {
input: "$Topics",
initialValue: "1T1",
in: {
$concat: [
"$$value",
"/",
"$$this"
]
}
}
}
}
}
])
Result:
[
{
"Topic": "1T1/a/x",
"_id": ObjectId("5a934e000102030405000001")
},
{
"Topic": "1T1/c/k/z",
"_id": ObjectId("5a934e000102030405000002")
},
{
"Topic": "1T1/a/b",
"_id": ObjectId("5a934e000102030405000000")
}
]
The common way to do this is
unwind
sort
group by id
reduce to 1 string
Bellow is a way to not unwind all collection but do a "local unwind".
Query
lookup with a dummy collection of 1 empty document [{}]
(this is "trick" that allows us to use stage operators like sort inside 1 document array) you need that collection in your database
unwind topics, sort them, group in 1 array, reduce them and create 1 string
we will have only 1 joined document (the transformed root document),
we replace the root with that
remove the "/" from start (it could be done on the reduce stage also)
added one extra case where topics are empty array to return ""
Test code here
db.topics.aggregate([
{
"$lookup": {
"from": "dummy",
"let": {
"topics": "$Topics"
},
"pipeline": [
{
"$set": {
"Topics": "$$topics"
}
},
{
"$unwind": {
"path": "$Topics"
}
},
{
"$sort": {
"Topics": 1
}
},
{
"$group": {
"_id": null,
"Topics": {
"$push": "$Topics"
}
}
},
{
"$project": {
"_id": 0
}
},
{
"$set": {
"Topics": {
"$reduce": {
"input": "$Topics",
"initialValue": "",
"in": {
"$let": {
"vars": {
"s": "$$value",
"t": "$$this"
},
"in": {
"$concat": [
"$$s",
"/",
"$$t"
]
}
}
}
}
}
}
}
],
"as": "joined"
}
},
{
"$replaceRoot": {
"newRoot": {
"$cond": [
{
"$eq": [
"$joined",
[]
]
},
{
"Topics": ""
},
{
"$arrayElemAt": [
"$joined",
0
]
}
]
}
}
},
{
"$set": {
"Topics": {
"$cond": [
{
"$gt": [
{
"$strLenCP": "$Topics"
},
0
]
},
{
"$substrCP": [
"$Topics",
1,
{
"$strLenCP": "$Topics"
}
]
},
""
]
}
}
}
])

MongoDB how to filter in nested array

I have below data. I want to find value=v2 (remove others value which not equals to v2) in the inner array which belongs to name=name2. How to write aggregation for this? The hard part for me is filtering the nestedArray which only belongs to name=name2.
{
"_id": 1,
"array": [
{
"name": "name1",
"nestedArray": [
{
"value": "v1"
},
{
"value": "v2"
}
]
},
{
"name": "name2",
"nestedArray": [
{
"value": "v1"
},
{
"value": "v2"
}
]
}
]
}
And the desired output is below. Please note the value=v1 remains under name=name1 while value=v1 under name=name2 is removed.
{
"_id": 1,
"array": [
{
"name": "name1",
"nestedArray": [
{
"value": "v1"
},
{
"value": "v2"
}
]
},
{
"name": "name2",
"nestedArray": [
{
"value": "v2"
}
]
}
]
}
You can try,
$set to update array field, $map to iterate loop of array field, check condition if name is name2 then $filter to get matching value v2 documents from nestedArray field and $mergeObject merge objects with available objects
let name = "name2", value = "v2";
db.collection.aggregate([
{
$set: {
array: {
$map: {
input: "$array",
in: {
$mergeObjects: [
"$$this",
{
$cond: [
{ $eq: ["$$this.name", name] }, //name add here
{
nestedArray: {
$filter: {
input: "$$this.nestedArray",
cond: { $eq: ["$$this.value", value] } //value add here
}
}
},
{}
]
}
]
}
}
}
}
}
])
Playground
You can use the following aggregation query:
db.collection.aggregate([
{
$project: {
"array": {
"$concatArrays": [
{
"$filter": {
"input": "$array",
"as": "array",
"cond": {
"$ne": [
"$$array.name",
"name2"
]
}
}
},
{
"$filter": {
"input": {
"$map": {
"input": "$array",
"as": "array",
"in": {
"name": "$$array.name",
"nestedArray": {
"$filter": {
"input": "$$array.nestedArray",
"as": "nestedArray",
"cond": {
"$eq": [
"$$nestedArray.value",
"v2"
]
}
}
}
}
}
},
"as": "array",
"cond": {
"$eq": [
"$$array.name",
"name2"
]
}
}
}
]
}
}
}
])
MongoDB Playground

How to combine mongodb original output of query with some new fields?

Collection:
[
{
"name": "device1",
"type": "a",
"para": {
"number": 3
}
},
{
"name": "device2",
"type": "b",
"additional": "c",
"para": {
"number": 1
}
}
]
My query:
db.collection.aggregate([
{
"$addFields": {
"arrayofkeyvalue": {
"$objectToArray": "$$ROOT"
}
}
},
{
"$unwind": "$arrayofkeyvalue"
},
{
"$group": {
"_id": null,
"allkeys": {
"$addToSet": "$arrayofkeyvalue.k"
}
}
}
])
The output currently:
[
{
"_id": null,
"allkeys": [
"additional",
"_id",
"para",
"type",
"name"
]
}
]
Detail see Playground
What I want to do is add a new column which includes all of top key of the mongodb query output, exclude "para". And then combine it with the old collection to form a new json.
Is it possible?
The expected result:
{
"column": [{"prop": "name"}, {"prop": "type"}, {"prop": "additional"}],
"columnData": [
{
"name": "device1",
"type": "a",
"para": {
"number": 3
}
},
{
"name": "device2",
"type": "b",
"additional": "c",
"para": {
"number": 1
}
}
]
}
You have the right general idea in mind, here's how I would do it by utilizing operators like $filter, $map and $reduce to manipulate the objects structure.
I separated the aggregation into 3 parts for readability but you can just merge stage 2 and 3 if you wish.
db.collection.aggregate([
{
"$group": {
"_id": null,
columnData: {
$push: "$$ROOT"
},
"keys": {
"$push": {
$map: {
input: {
"$objectToArray": "$$ROOT"
},
as: "field",
in: "$$field.k"
}
}
}
}
},
{
"$addFields": {
unionedKeys: {
$filter: {
input: {
$reduce: {
input: "$keys",
initialValue: [],
in: {
"$setUnion": [
"$$this",
"$$value"
]
}
}
},
as: "item",
cond: {
$not: {
"$setIsSubset": [
[
"$$item"
],
[
"_id",
"para"
]
]
}
}
}
}
}
},
{
$project: {
_id: 0,
columnData: 1,
column: {
$map: {
input: "$unionedKeys",
as: "key",
in: {
prop: "$$key"
}
}
}
}
}
])
Mongo Playground

Is there a way in mongodb to group at multiple levels

I have a document which contains an array of array as given below.
This is the first document.
{
"_id": "5d932a2178fdfc4dc41d75da",
"data": [
{
"nestedData": [
{
"_id": "5d932a2178fdfc4dc41d75e1",
"name": "Special 1"
},
{
"_id": "5d932a2178fdfc4dc41d75e0",
"name": "Special 2"
}
]
}
]
}
I need to lookup(join) to another collection with the _id in the nestedData array in the aggregation framework.
The 2nd document from which I need to lookup is
{
"_id": "5d8b1ac3b15bc72d154408e1",
"status": "COMPLETED",
"rating": 4
}
I know I need to $unwind it twice to convert nestedData array into object.
But how do I group back again to form the same object like given below
{
"_id": "5d932a2178fdfc4dc41d75da",
"data": [
{
"array": [
{
"_id": "5d932a2178fdfc4dc41d75e1",
"name": "Special 1",
"data": {
"_id": "5d8b1ac3b15bc72d154408e1",
"status": "COMPLETED",
"rating": 4
},
{
"_id": "5d932a2178fdfc4dc41d75e0",
"name": "Special 2",
"data": {
"_id": "5d8b1ac3b15bc72d154408e0",
"status": "COMPLETED",
"rating": 4
},
}
]
}
]
}
Try this query
db.testers.aggregate([
{$lookup: {
from: 'demo2',
pipeline: [
{ $sort: {'_id': 1}},
],
as: 'pointValue',
}},
{
$addFields:{
"data":{
$map:{
"input":"$data",
"as":"doc",
"in":{
$mergeObjects:[
"$$doc",
{
"nestedData":{
$map:{
"input":"$$doc.nestedData",
"as":"nestedData",
"in":{
$mergeObjects:[
{ $arrayElemAt: [ {
"$map": {
"input": {
"$filter": {
"input": "$pointValue",
"as": "sn",
"cond": {
"$and": [
{ "$eq": [ "$$sn._id", "$$nestedData._id" ] },
]
}
}
},"as": "data",
"in": {
"name": "$$nestedData.name",
"data":"$$data",
}}
}, 0 ] },'$$nestedData'
],
}
}
}
}
]
}
}
}
}
},
{$project: { pointValue: 0 } }
]).pretty()