MongoDb get all match data from deeply nested array document - mongodb

How to get education data (include the parent) for each family member in families that has year 2006? Supose I have data in mongodb:
{
"name": "Families",
"size": 3,
"families": [
{
"name": "Johny's Family",
"size": 2,
"family_member": [
{
"name": "Ruben",
"age": 22,
"education": [
{
"school": "Edward Academy",
"year": 2003
}
]
},
{
"name": "Hana",
"age": 20,
"education": [
{
"school": "Edward Academy",
"year": 2006
},
{
"school": "Nanyang University",
"year": 2012
}
]
}
]
},
{
"name": "Boy's Family",
"size": 1,
"family_member": [
{
"name": "Boy",
"age": 23,
"education": [
{
"school": "Broklyn Academy",
"year": 2003
},
{
"school": "Home School",
"year": 2006
}
]
}
]
}
]
}
I have try to get it using plain find function in mongodb but the result is not realy I wanted. this is my mongo script:
db.getCollection('tester').find(
{
"name":"Families",
"families.family_member.education.year":2006
},
{
"families.$.family_member.education.year":1
}
)
Can anyone suggest the best method to get the data become something like this:
{
"name": "Families",
"size": 3,
"families": [
{
"name": "Johny's Family",
"size": 2,
"family_member": [
{
"name": "Hana",
"age": 20,
"education": [
{
"school": "Edward Academy",
"year": 2006
}
]
}
]
},
{
"name": "Boy's Family",
"size": 1,
"family_member": [
{
"name": "Boy",
"age": 23,
"education": [
{
"school": "Home School",
"year": 2006
}
]
}
]
}
]
}

You may use JavaScript code to process your data because mongodb return data in unit of Document.
process "families" field like this:
doc.families.map((family)=>{
family.family_member = family.family_member.map((member)=>{
member.education = member.education.filter((edu)=>edu.year==2006);
return member;
});
return family;
})

You can try this one. You can get expected result using aggregation and you have to use $unwind then $match and $group
db.CollectionName.aggregate([
{$unwind: "$families"},
{$unwind: "$families.family_member"},
{$unwind: "$families.family_member.education"},
{$match: {"families.family_member.education.year": 2006}},
{
$group: {
_id: "$_id",
name: {$first: "$name"},
size: {$first: "$size"},
families: {$push: "$families"}
}
}
])

Using aggregation pipeline we can get this result. To achieve this use $unwind and $match. $unwind need to be used twice since we have deeply nested arrays
db.tester.aggregate([
{$unwind:"$families"},
{$unwind:"$families.family_member"},
{$unwind:"$families.family_member.education"},
{$match:{"families.family_member.education.year":2006}}
])

Change your query like below, It should work:-
db.getCollection('tester').find({"families.family_member.education.year": 2006},{"families.family_member.education.year":1})
OR
If you want to print whole things, use only this:-
db.getCollection('tester').find({"families.family_member.education.year": 2006}
Thanks.

Related

Mongodb: Push element in multiple nested array with condition

How can I push value into multiple nested array with specific conditions? I have a document like this
[
{
"_id": "class_a",
"students": [
{
"_id": "1a",
"name": "John",
"grades": []
},
{
"_id": "1b",
"name": "Katie",
"grades": []
},
{
"_id": "1c",
"name": "Andy",
"grades": []
},
]
}
]
Query to insert into nested array. (Not sure what is missing here)
db.collection.update({
"_id": "class_a",
"students": {
$elemMatch: {
"_id": {
"$in": [
"1a",
"1b"
]
}
}
}
},
{
$push: {
"students.$.grades": "A+"
}
})
Got the following result. But I was expecting both John and Katie have A+ in grades
[
{
"_id": "class_a",
"students": [
{
"_id": "1a",
"grades": ["A+"],
"name": "John"
},
{
"_id": "1b",
"grades": [],
"name": "Katie"
},
{
"_id": "1c",
"grades": [],
"name": "Andy"
}
]
}
]
Expected result
[
{
"_id": "class_a",
"students": [
{
"_id": "1a",
"grades": ["A+"],
"name": "John"
},
{
"_id": "1b",
"grades": ["A+"],
"name": "Katie"
},
{
"_id": "1c",
"grades": [],
"name": "Andy"
}
]
}
]
Mongo playground to test the code
You can use $[<identifier>] to update only the items that match a condition. Your first {} is to find the relevant documents, while the arrayFilters is to find the relevant items inside the document nested array:
db.collection.update(
{_id: "class_a", students: {$elemMatch: {_id: {$in: ["1a", "1b"]}}}},
{$push: {"students.$[item].grades": "A+"}},
{arrayFilters: [{"item._id": {$in: ["1a", "1b"]}}], upsert: true}
)
See how it works on the playground example
You should really use arrayFilters for these otherwse it'll only match the first entity. You don't need to use $elemMatch at all.
Playground - https://mongoplayground.net/p/_7y89KB83Ho
db.collection.update({
"_id": "class_a"
},
{
$push: {
"students.$[students].grades": "A+"
}
},
{
"arrayFilters": [
{
"students._id": {
"$in": [
"1a",
"1b"
]
}
}
]
})

MongoDB aggregate merging fields

I have a mongo Database I'll like to "join" two of them and then merge some other fields:
Let's see the schemas:
Students Schema (and data):
{
"_id": ObjectId("5fbd564981b1313de790b580"),
"name": "John Doe",
"age": "21",
"image": "https://XXXX/481.png",
"subjects": [
{
"_id": ObjectId("5fbd4e6881b1313de790b56b"),
"passed": true,
},
{
"_id": ObjectId("5fcb63fa8814d96876c687bf"),
}
],
"__v": NumberInt("1"),
}
and Subject schema:
{
"_id": ObjectId("5fbd4e6881b1313de790b56b"),
"course": 3,
"teacher": "John Smith",
"name": "Math",
},
{
"_id": ObjectId("5fcb63fa8814d96876c687bf"),
"name": "IT",
"course": 8,
"teacher": "John Peter",
}
What I'll like to make a query with the subjects (all info) of a student, also if the student have additional fields in subject like passed add it to the subject subdocument.
Here is my query till now:
db.students.aggregate([
{
$match:
{
_id : ObjectId('5fbd564981b1313de790b580')
}
},
{
$lookup :
{
from : "subjects",
localField : "subjects._id",
foreignField : "_id",
as : "FoundSubject"
}
}
]);
which correctly make the "join" but the merge is still missing, I got as result:
{
"_id": ObjectId("5fbd564981b1313de790b580"),
"name": "John Doe",
"age": "21",
"image": "https://XXXX/481.png",
"subjects": [
{
"_id": ObjectId("5fbd4e6881b1313de790b56b"),
"passed": true,
},
{
"_id": ObjectId("5fcb63fa8814d96876c687bf"),
}
],
"__v": NumberInt("1"),
"FoundSubject": [
{
"_id": ObjectId("5fbd4e6881b1313de790b56b"),
"course": 3,
"teacher": "John Smith",
"name": "Math"
},
{
"_id": ObjectId("5fcb63fa8814d96876c687bf"),
"name": "IT",
"course": 8,
"teacher": "John Peter"
}
]
}
but I'll like to have:
{
"_id": ObjectId("5fbd564981b1313de790b580"),
"name": "John Doe",
"age": "21",
"image": "https://XXXX/481.png",
"subjects": [
{
"_id": ObjectId("5fbd4e6881b1313de790b56b"),
"course": 3,
"teacher": "John Smith",
"name": "Math",
"passed": true,
},
{
"_id": ObjectId("5fcb63fa8814d96876c687bf"),
"name": "IT",
"course": 8,
"teacher": "John Peter"
}
],
"__v": NumberInt("1"),
}
with merged data and field "passed" added. How can accomplish that?
I'm new to MongoDB coming from MySQL.
Thanks
You need to merge both objects, add below stage after $lookup,
MongoDB Version From 3.4
$map to iterate loop of students array
$reduce to iterate loop of FoundSubject array, check condition if condition match then return required fields otherwise return initial value
$project to remove FoundSubject from result
{
$addFields: {
subjects: {
$map: {
input: "$subjects",
as: "s",
in: {
$reduce: {
input: "$FoundSubject",
initialValue: {},
in: {
$cond: [
{ $eq: ["$$s._id", "$$this._id"] },
{
_id: "$$this._id",
course: "$$this.course",
name: "$$this.name",
teacher: "$$this.teacher",
passed: "$$s.passed"
},
"$$value"
]
}
}
}
}
}
}
},
{ $project: { FoundSubject: 0 } }
Playground
MongoDB Version From 4.4
$map to iterate loop of students array,
$filter to get matching document from FoundSubject array and $first to get first object from array returned by filter
$mergeObjects to merge current objects with found result object from filter
remove FoundSubject using $$REMOVE
// skipping your stages
{
$addFields: {
FoundSubject: "$$REMOVE",
subjects: {
$map: {
input: "$subjects",
as: "s",
in: {
$mergeObjects: [
"$$s",
{
$first: {
$filter: {
input: "$FoundSubject",
cond: { $eq: ["$$s._id", "$$this._id"] }
}
}
}
]
}
}
}
}
}
Playground

Filtering a mongodb query result based on the position of a field in an array

Apologies for the confusing title, I am not sure how to summarize this.
Suppose I have the following list of documents in a collection:
{ "name": "Lorem", "source": "A" }
{ "name": "Lorem", "source": "B" }
{ "name": "Ipsum", "source": "A" }
{ "name": "Ipsum", "source": "B" }
{ "name": "Ipsum", "source": "C" }
{ "name": "Foo", "source": "B" }
as well an ordered list of accepted sources, where lower indexes signify higher priority
sources = ["A", "B"]
My query should:
Take a list of available sources and a list of wanted names
Return a maximum of one document per name.
In case of multiple matches, the document with the most prioritized source should be chosen.
Example:
wanted_names = ['Lorem', 'Ipsum', 'Foo', 'NotThere']
Result:
{ "name": "Lorem", "source": "A" }
{ "name": "Ipsum", "source": "A" }
{ "name": "Foo", "source": "B" }
The results don't necessarily have to be ordered.
Is it possible to do this with a Mongo query alone? If so could someone point me towards a resource detailing how to accomplish it?
My current solution doesn't support a list of names, and instead relies on a Python script to execute multiple queries:
db.collection.aggregate([
{$match: {
"name": "Lorem",
"source": {
$in: sources
}}},
{$addFields: {
"order": {
$indexOfArray: [sources, "$source"]
}}},
{$sort: {
"order": 1
}},
{$limit: 1}
]);
Note: _id fields are omitted in this question for the sake of brevity
How about this: With $group we have $min operator which takes lower source
Note: If you prioritize as ['B', 'A'], use $max then
db.collection.aggregate([
{
$match: {
"name": {
$in: [
"Lorem",
"Ipsum",
"Foo",
"NotThere"
]
},
"source": {
$in: [
"A",
"B"
]
}
}
},
{
$group: {
_id: "$name",
source: {
$min: "$source"
}
}
},
{
$project: {
_id: 0,
name: "$_id",
source: 1
}
}
])
MongoPlayground

How to match each array field to other field in Mongodb

I have an array as below:
const test = [{
"_id": 1,
"name": "apple",
"car": "ford"
},{
"_id": 2,
"name": "melon",
"car": "ferrari"
},{
"_id": 3,
"name": "perl",
"car": "Renaut"
}]
And there is are documents of Mongodb as below:
[{
"name": "perl", "company": "A"
},{
"name": "melon", "company": "B"
},{
"name": "apple", "company": "C"
},{
"name": "apple", "company": "D"
},{
"name": "perl", "company": "E"
},{
"name": "apple", "company": "F"
}]
And I want to get this result using mongodb aggregate:
[{
"name": "perl", "company": "A", testInform: { "_id": 3, "name": "perl", "car": "Renaut"}
},{
"name": "melon", "company": "B", testInform: { "_id": 2, "name": "melon", "car": "ferrari"}
},{
"name": "apple", "company": "C", testInform: { "_id": 1, "name": "apple", "car": "ford"}
},{
"name": "apple", "company": "D", testInform: { "_id": 1, "name": "apple", "car": "ford"}
},{
"name": "perl", "company": "E", testInform: { "_id": 3, "name": "perl", "car": "Renaut"}
},{
"name": "apple", "company": "F", testInform: { "_id": 1, "name": "apple", "car": "ford"}
}]
I think to use aggregate with $match and $facet, etc., but I don't know exactly how to do this. Could you recommend a solution for this?
Thank you so much for reading this.
$lookup with pipeline keyword
db.demo2.aggregate(
{
$lookup:
{
from: "demo1",
let: { recordName: "$name"},
pipeline: [
{ $match:
{ $expr:
{ $and:
[
{ $eq: [ "$$recordName", "$name" ] },
]
}
}
},
],
as: "testInform"
}
}
)
If the test array data is stored in a collection then acheiving O/P is pretty straightforward $lookup with $project aggregation
$arrayElemAt Why? because the lookup would fetch the joined documents in an array as testInform
db.maindocs.aggregate([
{
$lookup: {
from: "testdocs",
localField: "name",
foreignField: "name",
as: "testInform"
}
},
{
$project: {
_id: 0,
name: 1,
company: 1,
testInform: { $arrayElemAt: ["$testInform", 0] }
}
}
]);
Update based on comments:
The idea is to iterate the cursor from the documents stored in mongodb Array.prototype.find() the object from test which matches the name field, add it to result.
const test = [
{
_id: 1,
name: "apple",
car: "ford"
},
{
_id: 2,
name: "melon",
car: "ferrari"
},
{
_id: 3,
name: "perl",
car: "Renaut"
}
];
const cursor = db.collection("maindocs").find();
const result = [];
while (await cursor.hasNext()) {
const doc = await cursor.next();
const found = test.find(e => e.name === doc.name);
if (found) {
doc["testInform"] = found;
}
result.push(doc);
}
console.info("RESULT::", result);
The aggregation has one stage: Iterate over the test array and get the array element as an object which matches the name field in both the document and the array (using the $reduce operator).
const test = [ { ... }, ... ]
db.test_coll.aggregate( [
{
$addFields: {
testInform: {
$reduce: {
input: test,
initialValue: { },
in: {
$cond: [ { $eq: [ "$$this.name", "$name" ] },
{ $mergeObjects: [ "$$this", "$$value" ] },
"$$value"
]
}
}
}
}
}
] )

Mongodb aggregate and return multiple document value

Assuming I have the following JSON structure I want to group by gender and want to return multiple document values on in the same field:
[
{
"id": 0,
"age": 40,
"name": "Tony Bond",
"gender": "male"
},
{
"id": 1,
"age": 30,
"name": "Nikki Douglas",
"gender": "female"
},
{
"id": 2,
"age": 23,
"name": "Kasey Cardenas",
"gender": "female"
},
{
"id": 3,
"age": 25,
"name": "Latasha Burt",
"gender": "female"
}
]
Now I know I can do something like this but I need to join both age and name into one field
.aggregate().group({ _id:'$gender', age: { $addToSet: "$age" }, name: { $addToSet: "$name"}})
Yes you can, just have a sub-document as the argument:
db.collection.aggregate([
{ "$group": {
"_id": "$gender",
"person": { "$addToSet": { "name": "$name", "age": "$age" } }
}}
])
Of course if you actually expect no duplicates here the $push does the same thing:
db.collection.aggregate([
{ "$group": {
"_id": "$gender",
"person": { "$push": { "name": "$name", "age": "$age" } }
}}
])
In addition to what Neil mentioned for the aggregation query, you have to consider the fact that the document that contains the grouped individual record cannot be beyond 16 MB (in v2.5+) or the entire aggregate result cannot be more than 16MB (on v2.4 and below).
So in case you have huge data set, you have to be aware of limitations that may impose on the model you use to aggregate data.