MongoDB remove objects from multiple nested arrays - mongodb

I have a very complex json document in mongo like below:
{
"Category": [
{
"Name" : "",
"Description": "",
"SubCategory" : [
{
"Name": "",
"Description": "",
"Services": [
{"ServiceA": [
{
"Name": "",
"Description": "",
"CodeA": "1234"
}
]},
{"ServiceB" : [
{
"Name": "",
"Description": "",
"CodeBC": "ABCD",
"Key": ""
}
]},
{"ServiceC": [
{
"Name": "",
"Description": "",
"CodeBC": "ABCD",
"Section": [
{
"Name": "",
"Description": ""
}
]
}
]}
]
}
]
}
]
}
Now I want to retrieve the same document from Mongo but I want it to match only those objects inside ServiceA having CodeA = "1234" and those inside ServiceB and ServiceC having CodeBC = "ABCD".
I want it to remove all other objects inside ServiceA, ServiceB and ServiceC not matching the above condition and retrieve the document while maintaining the structure as it is.
Please Note : In the above example I just showed arrays containing single objects and fields that I want to retrieve but in real case it's very complex.

Try nested $map to iterate loop in your nested arrays, $mergeObjects to merge current object and new that we want to filter,
At the end in Services array filter documents on the base of condition in $filter, if filtered is null then that field will be removed using $ifNull
db.collection.aggregate([
{
$set: {
Category: {
$map: {
input: "$Category",
as: "c",
in: {
$mergeObjects: [
"$$c",
{
SubCategory: {
$map: {
input: "$$c.SubCategory",
as: "sc",
in: {
$mergeObjects: [
"$$sc",
{
Services: {
$map: {
input: "$$sc.Services",
as: "s",
in: {
$mergeObjects: [
"$$s",
{
ServiceA: {
$ifNull: [
{
$filter: {
input: "$$s.ServiceA",
cond: { $eq: ["$$this.CodeA", "1234"] }
}
},
"$$REMOVE"
]
},
ServiceB: {
$ifNull: [
{
$filter: {
input: "$$s.ServiceB",
cond: { $eq: ["$$this.CodeBC", "ABCD"] }
}
},
"$$REMOVE"
]
},
ServiceC: {
$ifNull: [
{
$filter: {
input: "$$s.ServiceC",
cond: { $eq: ["$$this.CodeBC", "ABCD"] }
}
},
"$$REMOVE"
]
}
}
]
}
}
}
}
]
}
}
}
}
]
}
}
}
}
}
])
Playground

Related

Update the existing array values in a Document in Mongodb

I want to update the subject field that starts with http://test and consists of /base in the value with the value without base
for example "http://test/timespan/base/1", as it starts with https://test and has base in it. Then i want to update it to http://test/timespan/1
so at last the subject array becomes :
"subject": [
"http://test/concept/114",
"http://test/timespan/1",
"http://wikidata/1233"
]
Document sample
{
"_id": {
"$oid": "1233"
},
"type": "Entity",
"subject": [
"http://test/concept/114",
"http://test/timespan/base/1",
"http://wikidata/1233"
]
}
You can use a conditional update, like so:
db.collection.updateMany({},
[
{
$set: {
subject: {
$map: {
input: {
$ifNull: [
"$subject",
[]
]
},
in: {
$cond: [
{
$regexMatch: {
input: "$$this",
regex: "http:\\/\\/test.*base"
}
},
{
$replaceAll: {
input: "$$this",
find: "base/",
replacement: ""
}
},
"$$this"
]
}
}
}
}
}
])

Filter deeply nested array in MongoDB

I have a requirement to filter a mongo collection with deeply nested array data. The document has 3 levels of nesting. Below is the sample document. The requirement is to filter the data with "status" as "verified" and also filter "array1" and "array2" based on condition and only return record which has matching data.
To summarise the filter params,
"status":"verified",
"name": "john",
"city": "mexico"
[
{
"_id": "111",
"array1": [
{
"name": "john",
"array2": [
{
"city": "mexico",
"array3": [
{
"address": "address1",
"status": "verified"
},
{
"address": "address2",
"status": "unverified"
}
]
}
]
}
]
},
{
"_id": "112",
"array1": [
{
"name": "john",
"array2": [
{
"city": "mexico",
"array3": [
{
"address": "address1",
"status": "unverified"
},
{
"address": "address2",
"status": "unverified"
}
]
}
]
}
]
}
]
The expected output is as below,
{
"_id": "111",
"array1": [
{
"name": "john",
"array2": [
{
"city": "mexico",
"array3": [
{
"address": "address1",
"status": "verified"
}
]
}
]
}
]
}
Here's how to do it using nested $filter and $map, as you'll see the syntax is not very clean due to the schema being complex to work with.
Without knowing your product I recommend you revisit it, it might be worth to restructure depending on your common access patterns.
db.collection.aggregate([
{
$match: {
"array1.array2.array3.status": "verified"
}
},
{
$addFields: {
array1: {
$filter: {
input: {
$map: {
input: "$array1",
as: "mapone",
in: {
"$mergeObjects": [
"$$mapone",
{
array2: {
$filter: {
input: {
$map: {
input: "$$mapone.array2",
as: "maptwo",
in: {
"$mergeObjects": [
"$$maptwo",
{
array3: {
$filter: {
input: "$$maptwo.array3",
as: "three",
cond: {
$eq: [
"$$three.status",
"verified"
]
}
}
}
}
]
}
}
},
as: "filtertwo",
cond: {
$and: [
{
$gt: [
{
$size: [
"$$filtertwo.array3"
]
},
0
]
},
{
$eq: [
"$$filtertwo.city",
"mexico"
]
}
]
}
}
}
}
]
}
}
},
as: "filterone",
cond: {
$and: [
{
$gt: [
{
$size: [
"$$filterone.array2"
]
},
0
]
},
{
$eq: [
"$$filterone.name",
"john"
]
}
]
}
}
}
}
}
])
Mongo Playground

MongoDB addFields based on condition (array of array)

Let's say I have a sample object as below
[
{
"status": "ACTIVE",
"person": [
{
"name": "Jim",
"age": "2",
"qualification": [
{
"type": "education",
"degree": {
"name": "bachelor",
"year": "2022"
}
},
{
"type": "certification",
"degree": {
"name": "aws",
"year": "2021"
}
}
]
}
]
}
]
Now, I need to add a field "score" only when the qualification.type == bachelor
This is the query I tried but could not get the proper result. Not sure what mistake I am doing. Any help is highly appreciated. Thank you in advance.
db.collection.aggregate([
{
$addFields: {
"person.qualification.score": {
$reduce: {
input: "$person.qualification",
initialValue: "",
in: {
$cond: [
{
"$eq": [
"$$this.type",
"bachelor"
]
},
"80",
"$$REMOVE"
]
}
}
}
}
}
])
$set - Set person array field.
1.1. $map - Iterate person array and return a new array.
1.1.1. $mergeObjects - Merge current iterate (person) document and the document with qualification.
1.1.1.1. $map - Iterate the qualification array and return a new array.
1.1.1.1.1. $cond - Match type of current iterate qualification document. If true, merge current iterate qualification document and the document with score field via $mergeObjects. Else remain the existing document.
db.collection.aggregate([
{
$set: {
person: {
$map: {
input: "$person",
as: "p",
in: {
$mergeObjects: [
"$$p",
{
qualification: {
$map: {
input: "$$p.qualification",
in: {
$cond: {
if: {
$eq: [
"$$this.type",
"education"
]
},
then: {
$mergeObjects: [
"$$this",
{
score: "80"
}
]
},
else: "$$this"
}
}
}
}
}
]
}
}
}
}
}
])
Sample Mongo Playground

MongoDB - How to find and update elements in a nested array

Here is the collection:
db.employees.insertMany([
{
"data": {
"category": [
{
"name": "HELLO",
"subcategory": [
"EDUCATION",
"ART",
]
},
{
"name": "HELLO",
"subcategory": [
"GG",
"ART",
]
},
{
"name": "HELLO",
"subcategory": [
"EDUCATION",
"SHORE",
]
}
]
}
},
{
"data": {
"category": [
{
"name": "HELLO",
"subcategory": [
"EDUCATION",
"HELLO",
]
}
]
}
},
{
"data": {
"category": [
{
"name": "HELLO",
"subcategory": [
"GG",
"ART",
]
}
]
}
}
]);
What I want is to locate the elements in 'category' with a 'subcategory' that contains 'EDUCATION' and replace 'EDUCATION' with another string, let's say 'SPORTS'.
I tried a couple of commands but nothing really did the job:
db.employees.updateMany({
"data.category.subcategory": "EDUCATION"
},
{
"$set": {
"data.category.$": {
"subcategory": "SPORTS"
}
}
})
What I saw is that it doesn't update the element by replacing it and it doesn't replace every element that meets the criteria.
Think that MongoDB Update with Aggregation Pipeline fulfills your scenario.
$set - Set data.category value.
1.1. $map - Iterate each element in data.category and return an array.
1.1.1. $mergeObjects - Merge the current document with the document with subcategory field from 1.1.1.1.
1.1.1.1 $map - Iterate each value from the subcategory array. With $cond to replace the word EDUCATION with SPORTS if fulfilled, else use existing value ($$this).
db.employees.updateMany({
"data.category.subcategory": "EDUCATION"
},
[
{
"$set": {
"data.category": {
$map: {
input: "$data.category",
in: {
$mergeObjects: [
"$$this",
{
subcategory: {
$map: {
input: "$$this.subcategory",
in: {
$cond: {
if: {
$eq: [
"$$this",
"EDUCATION"
]
},
then: "SPORTS",
else: "$$this"
}
}
}
}
}
]
}
}
}
}
}
]
Sample Mongo Playground
Here's another way to do it using "arrayFilters".
db.collection.update({
"data.category.subcategory": "EDUCATION"
},
{
"$set": {
"data.category.$[].subcategory.$[elem]": "SPORTS"
}
},
{
"arrayFilters": [
{ "elem": "EDUCATION" }
],
"multi": true
})
Try it on mongoplayground.net.

How can I do a lookup based on conditional selection?

I have 2 collection based on collection1 I need to fetch from collection2
collection1
[
{
"_id": ObjectId("5ce7454f77af2d1143f84c38"),
"menu_name": "mainmenu1",
"sub_menus": [
{
"name": "submenu1",
"project": [
"All"
]
},
{
"name": "submenu2",
"project": [
"p2"
]
}
]
}
]
based on project field I need to fetch the record. If the project field is "All", I need to fetch all the projects under that submenu. if it is specific project only those project I need to fetch.
Here is my collection2
collection2
"project": [
{
"project_name": "p1",
"sub_menus": "submenu1",
},
{
"project_name": "p2",
"sub_menus": "submenu2",
}
{
"project_name": "p2",
"sub_menus": "submenu1",
},
{
"project_name": "p3",
"sub_menus": "submenu2",
}
{
"project_name": "p3",
"sub_menus": "submenu1",
},
{
"project_name": "p4",
"sub_menus": "submenu2",
}
]
https://mongoplayground.net/p/qH9fuJorq6z.
Can I do a conditional lookup?
Expected Result is
[
{
"_id": ObjectId("5ce7454f77af2d1143f84c38"),
"menu_name": "mainmenu1",
"sub_menus": [
{
"projectData": [
{
"project_name": "p1"
},
{
"project_name": "p2"
},
{
"project_name": "p3"
}
],
"sub_menu_name": "submenu1"
},
{
"projectData": [
{
"project_name": "p2"
}
],
"sub_menu_name": "submenu2"
}
]
}
]
Yes, you can define your own matching condition for $lookup pipeline but since your structure is deeply nested you need to flatten your sub_menus using $reduce before you run your $lookup. Once you bring all projects that match to any submenu you can use $map with $filter to put them into releval sub_menu:
db.collection1.aggregate([
{
$addFields: {
sub_menus_flat: {
$reduce: {
input: "$sub_menus",
initialValue: [],
in: {
$concatArrays: [
"$$value",
{ $map: { input: "$$this.project", as: "p", in: { name: "$$this.name", project: "$$p" } } }
]
}
}
}
}
},
{
$lookup: {
from: "collection2",
let: { sub_menus_flat: "$sub_menus_flat" },
pipeline: [
{
$match: {
$expr: {
$anyElementTrue: {
$map: {
input: "$$sub_menus_flat",
in: {
$and: [
{ $eq: [ "$$this.name", "$sub_menus" ] },
{ $in: [ "$$this.project", [ "All", "$project_name" ] ] }
]
}
}
}
}
}
}
],
as: "projects"
}
},
{
$project: {
_id: 1,
menu_name: 1,
sub_menus: {
$map: {
input: "$sub_menus",
in: {
sub_menu_name: "$$this.name",
projectData: {
$filter: {
input: "$projects",
as: "p",
cond: {
$and: [
{ $eq: [ "$$p.sub_menus", "$$this.name" ] }
]
}
}
}
}
}
}
}
},
{
$project: {
"sub_menus.projectData._id": 0,
"sub_menus.projectData.sub_menus": 0
}
}
])
MongoDB Playground