How to query Child Reference Pattern collection with $graphLookup - mongodb

I have a collection with child reference. Each document can have multiple parents.
How can I query it with $graphLookup in order to make a result prepared for a treeview?
Example:
{
"_id" : ObjectId("6143450cc0318c23d8f18424"),
"id" : "3",
"name" : "prod03",
"children" : [
{
"_id" : "6143440ac0318c23d8f1841f",
"qty" : 10
},
{
"_id" : "614344b1c0318c23d8f18422",
"qty" : 100
}
],
"totalQty" : 110
},
{
"_id" : ObjectId("614344b1c0318c23d8f18422"),
"id" : "2",
"name" : "prod02",
"children" : [ ],
"totalQty" : 100
},
{
"_id" : ObjectId("6143440ac0318c23d8f1841f"),
"id" : "1",
"name" : "prod01",
"children" : [ ],
"totalQty" : 10
}
Prod03 is formed from prod01 and prod02
The desired result would be like:
{
id: '3',
name: 'prod03',
totalQty: 110
children: [
{
id: '1',
name: 'prod01',
qty: 10
},
{
id: '2',
name: 'prod02',
qty: 100
},
],
}
The query must go multiple levels down until find no more children.
Final result would be a tree with all history of product manufacture components.

You are actually on the right track to use $graphLookup. You just need to convert children._id back to objectId from String for lookup.
db.collection.aggregate([
{
"$match": {
"id": "3"
}
},
{
"$addFields": {
"children": {
"$map": {
"input": "$children",
"as": "c",
"in": {
"_id": {
"$toObjectId": "$$c._id"
},
"qty": "$$c.qty"
}
}
}
}
},
{
"$graphLookup": {
"from": "collection",
"startWith": "$children._id",
"connectFromField": "children._id",
"connectToField": "_id",
"as": "children"
}
},
{
"$addFields": {
"children": {
"$map": {
"input": "$children",
"as": "c",
"in": {
"id": "$$c.id",
"name": "$$c.name",
"qty": "$$c.totalQty"
}
}
}
}
}
])
Here is the Mongo playground for your reference.

Related

mongodb aggregation multiple lookup with conditions

I have 3 collection.
db.a.insert([
{ "_id" : ObjectId("5b56989172ebcb11105e8f41"), "db_type":b, "number" : 1},
{ "_id" : ObjectId("5b56989172ebcb11105e8f42"), "db_type":c, "number" : 2},
])
db.b.insert([
{ "_id" : ObjectId("5b56989172ebcb11105e8f43"), "number" : 1, "value" : "111"},
])
db.c.insert([
{ "_id" : ObjectId("5b56989172ebcb11105e8f44"), "number" : 2, "value" : "222"},
])
I want to make a lookup query that gets values from each collection according to db_type.
What should I do in this case?
result :
{ "_id" : ObjectId("5b56989172ebcb11105e8f41"), "db_type" : b, "number" : 1, "value" : "111"}
{ "_id" : ObjectId("5b56989172ebcb11105e8f42"), "db_type" : c, "number" : 2, "value" : "222"}
clogged part...
db.getCollection('a').aggregate([
{
"$lookup":{
"from": "b" or "c", // I want to give condition here.
"localField": "number",
"foreignField": "number",
"as": "result"
}
},
])
For your case, as you have only 2 cases b and c to lookup from. You can simply do 2 separate lookups and use $setUnion to group the results together.
db.a.aggregate([
{
"$lookup": {
"from": "b",
"let": {
db_type: "$db_type",
number: "$number"
},
"pipeline": [
{
$match: {
$expr: {
$and: [
{
$eq: [
"$$db_type",
"b"
]
},
{
$eq: [
"$$number",
"$number"
]
}
]
}
}
}
],
"as": "bLookup"
}
},
{
"$lookup": {
"from": "c",
"let": {
db_type: "$db_type",
number: "$number"
},
"pipeline": [
{
$match: {
$expr: {
$and: [
{
$eq: [
"$$db_type",
"c"
]
},
{
$eq: [
"$$number",
"$number"
]
}
]
}
}
}
],
"as": "cLookup"
}
},
{
"$addFields": {
"allLookup": {
"$setUnion": [
"$bLookup",
"$cLookup"
]
}
}
}
])
Here is the Mongo playground for your reference.

Remove duplicates from array without $unwind

Using query aggregation I want to create a new array by a filter of another array, so that the filtered result will be done by a specific field of the preliminary array.
In this case, I want to filter by the field "fieldName".
I will always want to filter out the last occur
example:
I have one document:
{
"fullyQualifiedName" : "MongoDB",
"items" : [
{
"fieldName" : "_id",
"fieldCount" : 7,
"confidence_level" : 1,
"fieldClassifications" : [
"LineageGuid"
],
},
{
"fieldName" : "_id",
"fieldCount" : 7,
"fieldClassifications" : [
{
"classificationName" : "LineageGuid",
}
]
},
{
"fieldName" : "details",
"fieldCount" : 7,
},
{
"fieldName" : "state",
"fieldCount" : 7,
}
]
}
I want to create a new array like:
"items" : [
{
"fieldName" : "_id",
"fieldCount" : 7,
"confidence_level" : 1,
"fieldClassifications" : [
"LineageGuid"
],
},
{
"fieldName" : "details",
"fieldCount" : 7,
},
{
"fieldName" : "state",
"fieldCount" : 7,
}
]
The simple solution is to $unwind and $group again but I can't do it because of performance issue.
I am using MongoDB 3.4
You can use below aggregation
db.collection.aggregate([
{ "$addFields": {
"items": {
"$map": {
"input": {
"$setUnion": [
{ "$map": {
"input": "$items",
"in": { "$indexOfArray": ["$items.fieldName", "$$this.fieldName"] }
}}
]
},
"as": "i",
"in": {
"fieldName": { "$arrayElemAt": ["$items.fieldName", "$$i"] },
"fieldCount": { "$arrayElemAt": ["$items.fieldCount", "$$i"] },
"confidence_level": { "$arrayElemAt": ["$items.confidence_level", "$$i"] },
"fieldClassifications": { "$arrayElemAt": ["$items.fieldClassifications", "$$i"] }
}
}
}
}}
])

Match Items In Multi-Level Embedded Arrays

I use the following collection which represents sports > categories > tournaments.
{
"_id" : ObjectId("597846358bbbc4440895f2e8"),
"Name" : [
{ "k" : "en-US", "v" : "Soccer" },
{ "k" : "fr-FR", "v" : "Football" }
],
"Categories" : [
{
"Name" : [
{ "k" : "en-US", "v" : "France" },
{ "k" : "fr-FR", "v" : "France" }
],
"Tournaments" : [
{
"Name" : [
{ "k" : "en-US", "v" : "Ligue 1" },
{ "k" : "fr-FR", "v" : "Ligue 1" }
],
},
{
"Name" : [
{ "k" : "en-US", "v" : "Ligue 2" },
{ "k" : "fr-FR", "v" : "Ligue 2" }
],
}
]
},
{
"Name" : [
{ "k" : "en-US", "v" : "England" },
{ "k" : "fr-FR", "v" : "Angleterre" }
],
"Tournaments" : [
{
"Name" : [
{ "k" : "en-US", "v" : "Premier League" },
{ "k" : "fr-FR", "v" : "Premier League" }
],
},
{
"Name" : [
{ "k" : "en-US", "v" : "Championship" },
{ "k" : "fr-FR", "v" : "Championnat" }
],
}
]
},
]
}
I want to query the collection using the category’s name and the tournament’s name. I’ve successfully use “$elemMatch” with the following code:
db.getCollection('Sport').find({
Categories: {
$elemMatch: {
Name: {
$elemMatch: { v: "France" }
},
Tournaments: {
$elemMatch: {
Name: {
$elemMatch: { v: "Ligue 1" }
}
}
}
}
} },
{ "Categories.$": 1, Name: 1 })
However, I cannot receive only the matching tournament in the category object.
Using the answer in this question: MongoDB Projection of Nested Arrays, I’ve built an aggregation:
db.getCollection('Sport').aggregate([{
"$match": {
"Categories": {
"$elemMatch": {
"Name": {
"$elemMatch": {
"v": "France"
}
},
"Tournaments": {
"$elemMatch": {
"Name": {
"$elemMatch": {
"v": "Ligue 1"
}
}
}
}
}
}
}
}, {
"$addFields": {
"Categories": {
"$filter": {
"input": {
"$map": {
"input": "$Categories",
"as": "category",
"in": {
"Tournaments": {
"$filter": {
"input": "$$category.Tournaments",
"as": "tournament",
"cond": {
// stuck here
}
}
}
}
}
},
"as": "category",
"cond": {
// stuck here
}
}
}
}
}
])
I tried to use a condition but MongoDB doesn’t recognize (Use of undefined variable:) $$KEEP and $$PRUNE ($redact) when I use $anyElementTrue then $map on the “Name” property.
My question: how can I check that the collection of names contains my string?
I'm more surprised that on the answer you reference I did not not "strongly recommend you do not nest arrays" like this. Nesting in this way is impossible to update atomically until the next release of MongoDB, and they are notoriously difficult to query.
For this particular case you would do:
db.getCollection('Sport').aggregate([
{ "$match": {
"Categories": {
"$elemMatch": {
"Name.v": "France",
"Tournaments.Name.v": "Ligue 1"
}
}
}},
{ "$addFields": {
"Categories": {
"$filter": {
"input": {
"$map": {
"input": "$Categories",
"as": "c",
"in": {
"Name": {
"$filter": {
"input": "$$c.Name",
"as": "n",
"cond": { "$eq": [ "$$n.v", "France" ] }
}
},
"Tournaments": {
"$filter": {
"input": {
"$map": {
"input": "$$c.Tournaments",
"as": "t",
"in": {
"Name": {
"$filter": {
"input": "$$t.Name",
"as": "n",
"cond": {
"$eq": [ "$$n.v", "Ligue 1" ]
}
}
}
}
}
},
"as": "t",
"cond": {
"$ne": [{ "$size": "$$t.Name" }, 0]
}
}
}
}
}
},
"as": "c",
"cond": {
"$and": [
{ "$ne": [{ "$size": "$$c.Name" },0] },
{ "$ne": [{ "$size": "$$c.Tournaments" },0] }
]
}
}
}
}}
])
Which returns the result:
/* 1 */
{
"_id" : ObjectId("597846358bbbc4440895f2e8"),
"Name" : [
{
"k" : "en-US",
"v" : "Soccer"
},
{
"k" : "fr-FR",
"v" : "Football"
}
],
"Categories" : [
{
"Name" : [
{
"k" : "en-US",
"v" : "France"
},
{
"k" : "fr-FR",
"v" : "France"
}
],
"Tournaments" : [
{
"Name" : [
{
"k" : "en-US",
"v" : "Ligue 1"
},
{
"k" : "fr-FR",
"v" : "Ligue 1"
}
]
}
]
}
]
}
The whole point is that each array needs a $filter, and at the outer levels you are looking for $size not being 0 as a result of "inner" $filter operations on contained arrays.
Since the "inner" arrays can change in content as a result, the "outer" arrays need a $map in order to return the "changed" elements.
So in terms of the structure "Categories" needs a $map because it has inner elements. And the "inner" "Tournaments" needs a $map for the same reason. Every array all the way to the final properties need $filter, and each wrapping array with a $map has a $filter with a $size condition.
That's the general logic pattern, and it works by repeating that pattern for each nested level. As stated though, it's pretty horrible. Which is why you really should avoid "nesting" like this at all costs. The increased complexity just about always outweighs any perceived gains.
I should also note you went a little overboard with $elemMatch, You really only need it at the "Categories" array level since that's the only thing that has multiple conditions to be met for it's element.
The sub-elements can use plain "Dot Notation" since they are only "singular" conditions within their respective arrays. So that does cut down on the terse syntax somewhat and still matches exactly the same documents.

Rewind data of two nested array field after $unwind and $lookup and $filter on date range in $project

{
"_id" : ObjectId("590b12b6330e1567acd29e69"),
"name": "Foo",
"sales_history" : [
{
"_id" : ObjectId("593ce8e4cfaa652df543d9e3"),
"sold_at" : ISODate("2017-06-11T06:53:24.881Z"),
"sold_to" : ObjectId("593509e938792e046ba14a02"),
"sold_products" : [
{
"product_dp" : 100,
"quantity" : 1,
"product_id" : ObjectId("591068be1f4c6c79a442a788"),
"_id" : ObjectId("593ce8e4cfaa652df543d9e5")
},
{
"product_dp" : 100,
"quantity" : 1,
"product_id" : ObjectId("593a33dccfaa652df543d924"),
"_id" : ObjectId("593ce8e4cfaa652df543d9e4")
}
]
},
{
"_id" : ObjectId("5944cb7142a04740357020b9"),
"sold_at" : ISODate("2017-06-17T06:25:53.332Z"),
"sold_to" : ObjectId("5927d4a59e58ba0c61066f3b"),
"sold_products" : [
{
"product_dp" : 500,
"quantity" : 1,
"price" : 5650,
"product_id" : ObjectId("593191ed53a2741dd9bffeb5"),
"_id" : ObjectId("5944cb7142a04740357020ba")
}
]
}
]
}
I have User schema like this. I want detail of product_id reference, with a date range search criteria on sold_at date field.
My expected data like following when I searched in sold_at at: 2017-06-11
{
"_id" : ObjectId("590b12b6330e1567acd29e69"),
"name": "Foo",
"sales_history" : [
{
"_id" : ObjectId("593ce8e4cfaa652df543d9e3"),
"sold_at" : ISODate("2017-06-11T06:53:24.881Z"),
"sold_to" : ObjectId("593509e938792e046ba14a02"),
"sold_products" : [
{
"product_dp" : 100,
"quantity" : 1,
"product_id": {
_id:ObjectId("hsfgg123412yh3gy1u2g3"),
name: "Product1",
code: "FG0154"
},
}
]
}
]
}
Product detail need to be populate in product_id, sales_history array need to be filtered in date range.
You can try below aggregation query.
$filter sales history on date range followed by $unwinding sales history & sold_products.
$lookup sold_products to get the product details.
$group back sold_products & sales history
db.collection.aggregate([
{
"$project": {
"name": 1,
"sales_history": {
"$filter": {
"input": "$sales_history",
"as": "history",
"cond": {
"$and": [
{
"$gte": [
"$$history.sold_at",
ISODate("2017-06-11T00:00:00.000Z")
]
},
{
"$lt": [
"$$history.sold_at",
ISODate("2017-06-12T00:00:00.000Z")
]
}
]
}
}
}
}
},
{
"$unwind": "$sales_history"
},
{
"$unwind": "$sales_history.sold_products"
},
{
"$lookup": {
"from": lookupcollection,
"localField": "sales_history.sold_products.product_id",
"foreignField": "_id",
"as": "sales_history.sold_products.product_id"
}
},
{
"$group": {
"_id": {
"_id": "$_id",
"sales_history_id": "$sales_history._id"
},
"name": {
"$first": "$name"
},
"sold_at": {
"$first": "$sales_history.sold_at"
},
"sold_to": {
"$first": "$sales_history.sold_to"
},
"sold_products": {
"$push": "$sales_history.sold_products"
}
}
},
{
"$group": {
"_id": "$_id._id",
"name": {
"$first": "$name"
},
"sales_history": {
"$push": {
"_id": "$_id.sales_history_id",
"sold_at": "$sold_at",
"sold_to": "$sold_to",
"sold_products": "$sold_products"
}
}
}
}
]);

MongoDB join data inside an array of objects

I have document like this in a collection called diagnoses :
{
"_id" : ObjectId("582d43d18ec3f432f3260682"),
"patientid" : ObjectId("582aacff3894c3afd7ad4677"),
"doctorid" : ObjectId("582a80c93894c3afd7ad4675"),
"medicalcondition" : "high fever, cough, runny nose.",
"diagnosis" : "Viral Flu",
"addmissiondate" : "2016-01-12",
"dischargedate" : "2016-01-16",
"bhtno" : "125",
"prescription" : [
{
"drug" : ObjectId("58345e0e996d340bd8126149"),
"instructions" : "Take 2 daily, after meals."
},
{
"drug" : ObjectId("5836bc0b291918eb42966320"),
"instructions" : "Take 1 daily, after meals."
}
]
}
The drug id inside the prescription object array is from a separate collection called drugs, see sample document below :
{
"_id" : ObjectId("58345e0e996d340bd8126149"),
"genericname" : "Paracetamol Tab 500mg",
"type" : "X",
"isbrand" : false
}
I am trying to create a mongodb query using the native node.js driver to get a result like this:
{
"_id" : ObjectId("582d43d18ec3f432f3260682"),
"patientid" : ObjectId("582aacff3894c3afd7ad4677"),
"doctorid" : ObjectId("582a80c93894c3afd7ad4675"),
"medicalcondition" : "high fever, cough, runny nose.",
"diagnosis" : "Viral Flu",
"addmissiondate" : "2016-01-12",
"dischargedate" : "2016-01-16",
"bhtno" : "125",
"prescription" : [
{
"drug" :
{
"_id" : ObjectId("58345e0e996d340bd8126149"),
"genericname" : "Paracetamol Tab 500mg",
"type" : "X",
"isbrand" : false
},
"instructions" : "Take 2 daily, after meals."
},
...
]
}
Any advice on how to approach a similar result like this is much appreciated, thanks.
Using MongoDB 3.4.4 and newer
With the aggregation framework, the $lookup operators supports arrays
db.diagnoses.aggregate([
{ "$addFields": {
"prescription": { "$ifNull" : [ "$prescription", [ ] ] }
} },
{ "$lookup": {
"from": "drugs",
"localField": "prescription.drug",
"foreignField": "_id",
"as": "drugs"
} },
{ "$addFields": {
"prescription": {
"$map": {
"input": "$prescription",
"in": {
"$mergeObjects": [
"$$this",
{ "drug": {
"$arrayElemAt": [
"$drugs",
{
"$indexOfArray": [
"$drugs._id",
"$$this.drug"
]
}
]
} }
]
}
}
}
} },
{ "$project": { "drugs": 0 } }
])
For older MongoDB versions:
You can create a pipeline that first flattens the prescription array using the $unwind operator and a $lookup subsequent pipeline step to do a "left outer join" on the "drugs" collection. Apply another $unwind operation on the created array from the "joined" field. $group the previously flattened documents from the first pipeline where there $unwind operator outputs a document for each element in the prescription array.
Assembling the above pipeline, run the following aggregate operation:
db.diagnoses.aggregate([
{
"$project": {
"patientid": 1,
"doctorid": 1,
"medicalcondition": 1,
"diagnosis": 1,
"addmissiondate": 1,
"dischargedate": 1,
"bhtno": 1,
"prescription": { "$ifNull" : [ "$prescription", [ ] ] }
}
},
{
"$unwind": {
"path": "$prescription",
"preserveNullAndEmptyArrays": true
}
},
{
"$lookup": {
"from": "drugs",
"localField": "prescription.drug",
"foreignField": "_id",
"as": "prescription.drug"
}
},
{ "$unwind": "$prescription.drug" },
{
"$group": {
"_id": "$_id",
"patientid" : { "$first": "$patientid" },
"doctorid" : { "$first": "$doctorid" },
"medicalcondition" : { "$first": "$medicalcondition" },
"diagnosis" : { "$first": "$diagnosis" },
"addmissiondate" : { "$first": "$addmissiondate" },
"dischargedate" : { "$first": "$dischargedate" },
"bhtno" : { "$first": "$bhtno" },
"prescription" : { "$push": "$prescription" }
}
}
])
Sample Output
{
"_id" : ObjectId("582d43d18ec3f432f3260682"),
"patientid" : ObjectId("582aacff3894c3afd7ad4677"),
"doctorid" : ObjectId("582a80c93894c3afd7ad4675"),
"medicalcondition" : "high fever, cough, runny nose.",
"diagnosis" : "Viral Flu",
"addmissiondate" : "2016-01-12",
"dischargedate" : "2016-01-16",
"bhtno" : "125",
"prescription" : [
{
"drug" : {
"_id" : ObjectId("58345e0e996d340bd8126149"),
"genericname" : "Paracetamol Tab 500mg",
"type" : "X",
"isbrand" : false
},
"instructions" : "Take 2 daily, after meals."
},
{
"drug" : {
"_id" : ObjectId("5836bc0b291918eb42966320"),
"genericname" : "Paracetamol Tab 100mg",
"type" : "Y",
"isbrand" : false
},
"instructions" : "Take 1 daily, after meals."
}
]
}
In MongoDB 3.6 or later versions
It seems that
$lookup will overwrite the original array instead of merging it.
A working solution (a workaround, if you prefer) is to create a different field,
and then merge two fields, as shown below:
db.diagnoses.aggregate([
{ "$lookup": {
"from": "drugs",
"localField": "prescription.drug",
"foreignField": "_id",
"as": "prescription_drug_info"
} },
{ "$addFields": {
"merged_drug_info": {
"$map": {
"input": "$prescription",
"in": {
"$mergeObjects": [
"$$this",
{ "$arrayElemAt": [
"$prescription_drug_info._id",
"$$this._id"
] }
]
}
}
}
} }
])
This would add two more fields and the name of the desired field
will be merged_drug_info. We can then add $project stage to filter
out excessive fields and $set stage to rename the field:
...
{ "$set": { "prescription": "$merged_drug_info" } },
{ "$project": { "prescription_drug_info": 0, "merged_drug_info": 0 } }
...