Filter deeply nested array in MongoDB - 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

Related

Update more than one inner document in MongoDb

In my example project, I have employees under manager. Db schema is like this;
{
"employees": [
{
"name": "Adam",
"_id": "5ea36b27d7ae560845afb88e",
"bananas": "allowed"
},
{
"name": "Smith",
"_id": "5ea36b27d7ae560845afb88f",
"bananas": "not-allowed"
},
{
"name": "John",
"_id": "5ea36b27d7ae560845afb88g",
"bananas": "not-allowed"
},
{
"name": "Patrick",
"_id": "5ea36b27d7ae560845afb88h",
"bananas": "allowed"
}
]
}
In this case Adam is allowed to eat bananas and Smith is not. If I have to give the permission of eating bananas from Adam to Smith I need to perform update operation twice like this:
db.managers.update(
{ 'employees.name': 'Adam' },
{ $set: { 'employees.$.bananas': 'not-allowed' } }
);
and
db.managers.update(
{ 'employees.name': 'Smith' },
{ $set: { 'employees.$.bananas': 'allowed' } }
);
Is it possible to handle this in a single query?
You can use $map and $cond to perform conditional update to the array entries depending on the name of the employee. A $switch is used for potential extension of cases.
db.collection.update({},
[
{
"$set": {
"employees": {
"$map": {
"input": "$employees",
"as": "e",
"in": {
"$switch": {
"branches": [
{
"case": {
$eq: [
"$$e.name",
"Adam"
]
},
"then": {
"$mergeObjects": [
"$$e",
{
"bananas": "not-allowed"
}
]
}
},
{
"case": {
$eq: [
"$$e.name",
"Smith"
]
},
"then": {
"$mergeObjects": [
"$$e",
{
"bananas": "allowed"
}
]
}
}
],
default: "$$e"
}
}
}
}
}
}
])
Mongo Playground
db.managers.update(
{
$or: [
{"employees.name": "Adam"},
{"employees.name": "Smith"}
]
},
{
$set: {
"employees.$[e].bananas": {
$cond: [{ $eq: ["$e.name", "Adam"] }, "not-allowed", "allowed"]
}
}
},
{
arrayFilters: [{ "e.name": { $in: ["Adam", "Smith"] } }]
}
)

MongoDB: get documents by last element value in nested array

This question is slightly different from others since I need to get the whole documents and not just specific fields.
I need to filter documents(all of the document, not just specific fields), according to the last elements value of a nested array. (doc.array[i].innerArray[innerArray.length - 1].desiredField)
Documents are looking like this:
[
{
"_id": 0,
"matches": [
{
"name": "match 1",
"ids": [
{
"innerName": "1234"
},
{
"innerName": "3"
}
]
}
]
},
{
"_id": 1,
"matches": [
{
"name": "match 5",
"ids": [
{
"innerName": "123"
},
{
"innerName": "1"
}
]
},
{
"name": "match 5",
"ids": [
{
"innerName": "1"
},
{
"innerName": "1234"
},
]
},
]
}
]
So if we filter according to innerName = '1234', this is the result:
{
"_id": 1,
"matches": [
{
"name": "match 5",
"ids": [
{
"innerName": "123"
},
{
"innerName": "1"
}
]
},
{
"name": "match 5",
"ids": [
{
"innerName": "1"
},
{
"innerName": "1234"
},
]
}
One option is:
db.collection.find({
$expr: {
$in: [
"1234",
{$reduce: {
input: "$matches",
initialValue: [],
in: {$concatArrays: ["$$value", [{$last: "$$this.ids.innerName"}]]}
}
}
]
}
})
See how it works on the playground example
Another option:
db.collection.aggregate([
{
$match: {
$expr: {
$gt: [
{
$size: {
$filter: {
input: "$matches",
cond: {
$in: [
{
$last: "$$this.ids.innerName"
},
[
"1234"
]
]
}
}
}
},
0
]
}
}
}
])
Explained:
Match only documents where size of array is >0 for those who has "1234" in last nested array element.
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.

MongoDB remove objects from multiple nested arrays

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

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