How to find documents with child object that has matching value? - javascript

Suppose that I have a collection with documents like below
{
"location" : "Tokyo",
"region" : "Asia",
"attraction": {
"transportation" : "Subway",
"food" : {
"food_0" : {
"name" : "Sushi",
"price" : 100,
"restaurant" : "Ookinza"
},
"food_1" : {
"name" : "Sashimi",
"price" : 200,
"restaurant" : "Hibiki"
},
"food_2" : {
"name" : "N/A",
"price" : "N/A",
"restaurant" : "N/A"
}
}
}
},
{
"location" : "Taipei",
"region" : "Asia",
"attraction": {
"transportation" : "Subway",
"food" : {
"food_0" : {
"name" : "Bubble tea",
"price" : 50,
"restaurant" : "The Alley"
},
"food_1" : {
"name" : "Oyster cake",
"price" : 100,
"restaurant" : "Night market"
},
"food_2" : {
"name" : "N/A",
"price" : "N/A",
"restaurant" : "N/A"
}
}
}
},
{
"location" : "Toronto",
"region" : "North America",
"attraction": {
"transportation" : "Uber",
"food" : {
"food_0" : {
"name" : "Raman",
"price" : 300,
"restaurant" : "Kinto"
},
"food_1" : {
"name" : "Bubble tea",
"price" : 200,
"restaurant" : "Fresh Fruit"
},
"food_2" : {
"name" : "N/A",
"price" : "N/A",
"restaurant" : "N/A"
}
}
}
},
How do I find documents that have matching field in the child object of Food?
i.e. If I want to find document that has restaurant:"Fresh Tea"?
Currently what I have:
app.get(route, (req, res) => {
var detail = {};
if(req.query.location){
detail['location'] = req.query.location.toUpperCase();
}
if(req.query.region){
detail['region'] = req.query.region.toUpperCase();
}
if(req.query.transportation){
detail['attraction.transportation'] = new RegExp(req.query.transportation.split(","), "i"),
}
if(req.query.restaurant){
detail['attraction.food.food_0'] = req.query.restaurant;
}
db.collection(config.dbCollections.foodDB)
.aggregate([
$match: detail,
},
{
$lookup: {
... // code continues
Right now detail['attraction.food.food_0'] = req.query.restaurant is only able to find document that has matching food_0.restaurant, but I still can't find a way to make it check all child objects within "food".
Updated with more info:
User has the option to enter multiple search categories, and I want to combine all the search requests into "detail" and find all matching results. I.e. If user looks for transportation="Subway" and food="Bubble tea", then both Taipei and Toronto should come up as result.

Using dynamic value as field name is generally considered as anti-pattern and should be avoided. Nevertheless, you can convert the object attraction.food to an array of k-v tuple and perform the search with your criteria. For your case, $anyElementTrue with $map will help with processing the array.
db.collection.aggregate([
{
"$addFields": {
"test": {
"$anyElementTrue": {
"$map": {
"input": {
"$objectToArray": "$attraction.food"
},
"as": "t",
"in": {
$or: [
{
$eq: [
"$$t.v.transportation",
"Subway"
]
},
{
$eq: [
"$$t.v.name",
"Bubble tea"
]
}
]
}
}
}
}
}
},
{
$match: {
test: true
}
},
{
"$unset": "test"
}
])
Here is the Mongo Playground for your reference.

A possible aggregation pipeline
Add a temporary field using $addFields and $objectToArray which does something similar to javascript Object.entries()
Do the matching
Remove the added temporary field using $project 0
playground
db.collection.aggregate([
{
"$addFields": {
"foodArray": {
"$objectToArray": "$attraction.food"
},
},
},
{
"$match": {
"foodArray.v.restaurant": "Fresh Fruit"
}
},
{
"$project": {
"foodArray": 0
},
},
])

Related

List of array with different keys in a unique array Javascript

I am iterating data from a JSON file that I previousy fetch with the following code:
getData().then(data => {
for (const key in data) {
if (data.hasOwnProperty(key)) {
// do something with the data
}
}
}
The json file is very long, but it look something like this:
{
"01": {
"id" : "01",
"title" : "example1",
"size" : "100",
"pictures" : []
},
"02": {
"id" : "02",
"title" : "example2",
"size" : "0",
"pictures" : []
},
"03": {
"id" : "03",
"title" : "example3",
"size" : "300",
"pictures" : [
{ "pic_name1" : "example_pic1", "source" : "http://example.pic/1234" },
{ "pic_name2" : "example_pic2", "source" : "http://example.pic/4321" },
]
},
}
Now, to create a function that will filter through my data I need to put all of the size in a separate array (that I will later work with) and I tried this (inside the IF condition)
let sizes = new Array(data[key].size);
What I need to return is an array, but I get instead a list of array for each size:
["100"]["0"]["300"]...
How do I return a single array with all sizes as a list?
Simply map over each value and pluck the size. E.g.
const data = {
"01": {
"id" : "01",
"title" : "example1",
"size" : "100",
"pictures" : []
},
"02": {
"id" : "02",
"title" : "example2",
"size" : "0",
"pictures" : []
},
"03": {
"id" : "03",
"title" : "example3",
"size" : "300",
"pictures" : [
{ "pic_name1" : "example_pic1", "source" : "http://example.pic/1234" },
{ "pic_name2" : "example_pic2", "source" : "http://example.pic/4321" },
]
},
};
const sizes = Object.values(data).map(({size}) => size);
console.log(sizes);
Using ES8 new features inside the if condition,
let sizes = Object.values(data).map(({size}) => size);

Add New key at the end of each object inside an array

I have data from mongodb like this from one collection.
/* 1 */
{
"_id" : ObjectId("5be94355f220b62c7449dc0f"),
"districts" : [
{
"name" : "NORTH AND MIDDLE",
"code" : 632.0
},
{
"name" : "EAST",
"code" : 603.0
},
{
"name" : "SOUTH",
"code" : 602.0
}
],
"state" : "ISLANDS"
}
/* 2 */
{
"_id" : ObjectId("5be94355f220b62c7441dc04"),
"districts" : [
{
"name" : "Apple",
"code" : 512.0
},
{
"name" : "Ball",
"code" : 522.0
}
],
"state" : "GOLD"
}
/* 3 */
{
"_id" : ObjectId("5eee07816a011d391a45178"),
"districts" : [
{
"name" : "DAM",
"code" : 478.0
},
{
"name" : "DEN",
"code" : 481.0
},
{
"name" : "DOG AND CAT",
"code" : 461.0
}
],
"state" : "THE NAGAR AND HAVELI"
}
I was given an excel sheet like below as shown with no other information only 2 columns
My work is to add "Short Name of District" for all districts.
I tried below method
var tc = [
"NORTH AND MIDDLE",
"EAST",
"SOUTH",
"Apple",
"Ball ",
"DAM ",
"DEN ",
"DOG AND CAT"
]
db.dummy.find({"districts.name":{$in:tc}}).forEach(x => {
x["districts"].forEach( y => {
if (
y.name == "NORTH AND MIDDLE" ){
y.short_name = "NAM"
}
if (
y.name == "EAST" ){
y.short_name = "ET"
}
if (
y.name == "SOUTH" ){
y.short_name = "ST"
}
})
})
I got the result
/* 1 */
{
"_id" : ObjectId("5be94355f220b62c7449dc0f"),
"districts" : [
{
"name" : "NORTH AND MIDDLE",
"code" : 632.0,
"short_name" : "NAM"
},
{
"name" : "EAST",
"code" : 603.0,
"short_name" : "ET"
},
{
"name" : "SOUTH",
"code" : 602.0,
"short_name" : "ST"
}
],
"state" : "ISLANDS"
}
/* 2 */
{
"_id" : ObjectId("5be94355f220b62c7441dc04"),
"districts" : [
{
"name" : "Apple",
"code" : 512.0,
"short_name" : "Al"
},
{
"name" : "Ball",
"code" : 522.0
"short_name" : "BA"
}
],
"state" : "GOLD"
}
/* 3 */
{
"_id" : ObjectId("5eee07816a011d391a45178"),
"districts" : [
{
"name" : "DAM",
"code" : 478.0,
"short_name" : "DA"
},
{
"name" : "DEN",
"code" : 481.0,
"short_name" : "DN"
},
{
"name" : "DOG AND CAT",
"code" : 461.0
"short_name" : "DAC"
}
],
"state" : "THE NAGAR AND HAVELI"
}
Is this is the only method ??
like using if loop for all districts or any other methods are there like using mongodb aggregate or any other javascript methods. It will be helpful if other methods are there as it will be problem to use if loop when there is 730 districts are there. I dont have experience in working with aggregate frameworks so i thought anyone might know other method.
You may write a mapping:
const districtNameToShort = {
'NORTH AND MIDDLE': 'NAM',
'EAST': 'ET',
...
}
Then in your forEach
const districtNameToShort = {
'NORTH AND MIDDLE': 'NAM',
'EAST': 'ET'
}
db.dummy.find().forEach(x => {
db.dummy.update(
{_id : x._id},
{$set: {
districts: x.districts.map(district => {
district.short_name = districtNameToShort[district.name] || district.name
return district
})
}}
)
})

Mongodb $graphLookup build hierarchy [duplicate]

This question already has answers here:
MongoDB $graphLookup get children all levels deep - nested result
(2 answers)
Closed 3 years ago.
I have an output from mongodb $graphLookup aggregation:
db.getCollection('projects').aggregate([
{
$lookup: {
from: "projects",
localField: "_id",
foreignField: "parent",
as: "childrens"
}
}
])
{
"_id" : "1",
"name" : "Project1",
"parent" : null,
"childrens" : [
{
"_id" : "3",
"name" : "ProjectForId1",
"parent" : "1"
}
]
},
{
"_id" : "3",
"name" : "ProjectForId1",
"parent" : "1",
"childrens" : [
{
"_id" : "6",
"name" : "ProjectForId3",
"parent" : "3"
},
{
"_id" : "7",
"name" : "ProjectForId3",
"parent" : "3"
}
]
}
I need to build hierarchy from this output in javascript or if is possible directly from query so the final output should look like:
{
"_id" : "1",
"name" : "Project1",
"parent" : null,
"childrens" : [
{
"_id" : "3",
"name" : "ProjectForId1",
"parent" : "1",
"childrens" : [
{
"_id" : "6",
"name" : "ProjectForId3",
"parent" : "3"
},
{
"_id" : "7",
"name" : "ProjectForId3",
"parent" : "3"
}
]
}
]
}
Also if someone have a brave heart to help in one more case where the hierarchy will be created by filtering _id:
ex: For _id = "1" the output will be same as above but if _id is 3 the final output should look like:
{
"_id" : "3",
"name" : "ProjectForId1",
"parent" : "1",
"childrens" : [
{
"_id" : "6",
"name" : "ProjectForId3",
"parent" : "3"
},
{
"_id" : "7",
"name" : "ProjectForId3",
"parent" : "3"
}
]
}
Below solution is more or less the same as one of my past answers so you can get thorough explanation here
db.projects.aggregate([
{
$graphLookup: {
from: "projects",
startWith: "$_id",
connectFromField: "_id",
connectToField: "parent",
as: "children",
maxDepth: 4,
depthField: "level"
}
},
{
$unwind: "$children"
},
{
$sort: { "children.level": -1 }
},
{
$group: {
_id: "$_id",
children: { $push: "$children" }
}
},
{
$addFields: {
children: {
$reduce: {
input: "$children",
initialValue: {
currentLevel: -1,
currentLevelProjects: [],
previousLevelProjects: []
},
in: {
$let: {
vars: {
prev: {
$cond: [
{ $eq: [ "$$value.currentLevel", "$$this.level" ] },
"$$value.previousLevelProjects",
"$$value.currentLevelProjects"
]
},
current: {
$cond: [
{ $eq: [ "$$value.currentLevel", "$$this.level" ] },
"$$value.currentLevelProjects",
[]
]
}
},
in: {
currentLevel: "$$this.level",
previousLevelProjects: "$$prev",
currentLevelProjects: {
$concatArrays: [
"$$current",
[
{ $mergeObjects: [
"$$this",
{ children: { $filter: { input: "$$prev", as: "e", cond: { $eq: [ "$$e.parent", "$$this._id" ] } } } }
] }
]
]
}
}
}
}
}
}
}
},
{
$addFields: { children: "$children.currentLevelProjects" }
},
{
$match: {
_id: "1"
}
}
])
Last stage is supposed to be the filtering so you can get the data for any level of depth here.

Dynamically add Keys From Array Content

How can I add fields dynamically in MongoDB? I have an array, with which I want to create columns. For example, from this,
/* 1 */
{
"date" : "2017-07-30",
"brand" : [
{
"name" : "Apple",
"quantity" : 31
}
],
"total" : 31
}
/* 2 */
{
"date" : "2017-08-02",
"brand" : [
{
"name" : "Apple",
"quantity" : 1
},
{
"name" : "Samsung",
"quantity" : 6
}
],
"total" : 7
}
I want to make this,
/* 1 */
{
"date" : "2017-07-30",
"Apple": 31,
"Samsung": 0,
"total" : 31
}
/* 2 */
{
"date" : "2017-08-02",
"Apple": 1,
"Samsung": 6,
"total" : 7
}
There $addFields operator can add a field, but how can I use it for multiple fields?
You can do it, but honestly it's kind of a waste. Use $replaceRoot and $arrayToObject where you have it:
db.collection.aggregate([
{ "$replaceRoot": {
"newRoot": {
"$arrayToObject": {
"$concatArrays": [
[{ "k": "date", "v": "$date" }],
{ "$map": {
"input": "$brand",
"as": "b",
"in": { "k": "$$b.name", "v": "$$b.quantity" }
}},
[{ "k": "total", "v": "$total" }]
]
}
}
}}
])
It's actually a lot better to simply transform from the cursor. As in the shell:
db.collection.find().map( doc =>
Object.assign(
{ date: doc.date },
doc.brand.map(d => ({ [d.name]: d.quantity }) )
.reduce((acc,curr) => Object.assign(acc,curr),{}),
{ total: doc.total }
)
)
Which does the same thing:
[
{
"date" : "2017-07-30",
"Apple" : 31.0,
"total" : 31.0
},
{
"date" : "2017-08-02",
"Apple" : 1.0,
"Samsung" : 6.0,
"total" : 7.0
}
]

Aggregation filter after $lookup

How can I add a filter after an $lookup or is there any other method to do this?
My data collection test is:
{ "_id" : ObjectId("570557d4094a4514fc1291d6"), "id" : 100, "value" : "0", "contain" : [ ] }
{ "_id" : ObjectId("570557d4094a4514fc1291d7"), "id" : 110, "value" : "1", "contain" : [ 100 ] }
{ "_id" : ObjectId("570557d4094a4514fc1291d8"), "id" : 120, "value" : "1", "contain" : [ 100 ] }
{ "_id" : ObjectId("570557d4094a4514fc1291d9"), "id" : 121, "value" : "2", "contain" : [ 100, 120 ] }
I select id 100 and aggregate the childs:
db.test.aggregate([ {
$match : {
id: 100
}
}, {
$lookup : {
from : "test",
localField : "id",
foreignField : "contain",
as : "childs"
}
}]);
I get back:
{
"_id":ObjectId("570557d4094a4514fc1291d6"),
"id":100,
"value":"0",
"contain":[ ],
"childs":[ {
"_id":ObjectId("570557d4094a4514fc1291d7"),
"id":110,
"value":"1",
"contain":[ 100 ]
},
{
"_id":ObjectId("570557d4094a4514fc1291d8"),
"id":120,
"value":"1",
"contain":[ 100 ]
},
{
"_id":ObjectId("570557d4094a4514fc1291d9"),
"id":121,
"value":"2",
"contain":[ 100, 120 ]
}
]
}
But I want only childs that match with "value: 1"
At the end I expect this result:
{
"_id":ObjectId("570557d4094a4514fc1291d6"),
"id":100,
"value":"0",
"contain":[ ],
"childs":[ {
"_id":ObjectId("570557d4094a4514fc1291d7"),
"id":110,
"value":"1",
"contain":[ 100 ]
},
{
"_id":ObjectId("570557d4094a4514fc1291d8"),
"id":120,
"value":"1",
"contain":[ 100 ]
}
]
}
The question here is actually about something different and does not need $lookup at all. But for anyone arriving here purely from the title of "filtering after $lookup" then these are the techniques for you:
MongoDB 3.6 - Sub-pipeline
db.test.aggregate([
{ "$match": { "id": 100 } },
{ "$lookup": {
"from": "test",
"let": { "id": "$id" },
"pipeline": [
{ "$match": {
"value": "1",
"$expr": { "$in": [ "$$id", "$contain" ] }
}}
],
"as": "childs"
}}
])
Earlier - $lookup + $unwind + $match coalescence
db.test.aggregate([
{ "$match": { "id": 100 } },
{ "$lookup": {
"from": "test",
"localField": "id",
"foreignField": "contain",
"as": "childs"
}},
{ "$unwind": "$childs" },
{ "$match": { "childs.value": "1" } },
{ "$group": {
"_id": "$_id",
"id": { "$first": "$id" },
"value": { "$first": "$value" },
"contain": { "$first": "$contain" },
"childs": { "$push": "$childs" }
}}
])
If you question why would you $unwind as opposed to using $filter on the array, then read Aggregate $lookup Total size of documents in matching pipeline exceeds maximum document size for all the detail on why this is generally necessary and far more optimal.
For releases of MongoDB 3.6 and onwards, then the more expressive "sub-pipeline" is generally what you want to "filter" the results of the foreign collection before anything gets returned into the array at all.
Back to the answer though which actually describes why the question asked needs "no join" at all....
Original
Using $lookup like this is not the most "efficient" way to do what you want here. But more on this later.
As a basic concept, just use $filter on the resulting array:
db.test.aggregate([
{ "$match": { "id": 100 } },
{ "$lookup": {
"from": "test",
"localField": "id",
"foreignField": "contain",
"as": "childs"
}},
{ "$project": {
"id": 1,
"value": 1,
"contain": 1,
"childs": {
"$filter": {
"input": "$childs",
"as": "child",
"cond": { "$eq": [ "$$child.value", "1" ] }
}
}
}}
]);
Or use $redact instead:
db.test.aggregate([
{ "$match": { "id": 100 } },
{ "$lookup": {
"from": "test",
"localField": "id",
"foreignField": "contain",
"as": "childs"
}},
{ "$redact": {
"$cond": {
"if": {
"$or": [
{ "$eq": [ "$value", "0" ] },
{ "$eq": [ "$value", "1" ] }
]
},
"then": "$$DESCEND",
"else": "$$PRUNE"
}
}}
]);
Both get the same result:
{
"_id":ObjectId("570557d4094a4514fc1291d6"),
"id":100,
"value":"0",
"contain":[ ],
"childs":[ {
"_id":ObjectId("570557d4094a4514fc1291d7"),
"id":110,
"value":"1",
"contain":[ 100 ]
},
{
"_id":ObjectId("570557d4094a4514fc1291d8"),
"id":120,
"value":"1",
"contain":[ 100 ]
}
]
}
Bottom line is that $lookup itself cannot "yet" query to only select certain data. So all "filtering" needs to happen after the $lookup
But really for this type of "self join" you are better off not using $lookup at all and avoiding the overhead of an additional read and "hash-merge" entirely. Just fetch the related items and $group instead:
db.test.aggregate([
{ "$match": {
"$or": [
{ "id": 100 },
{ "contain.0": 100, "value": "1" }
]
}},
{ "$group": {
"_id": {
"$cond": {
"if": { "$eq": [ "$value", "0" ] },
"then": "$id",
"else": { "$arrayElemAt": [ "$contain", 0 ] }
}
},
"value": { "$first": { "$literal": "0"} },
"childs": {
"$push": {
"$cond": {
"if": { "$ne": [ "$value", "0" ] },
"then": "$$ROOT",
"else": null
}
}
}
}},
{ "$project": {
"value": 1,
"childs": {
"$filter": {
"input": "$childs",
"as": "child",
"cond": { "$ne": [ "$$child", null ] }
}
}
}}
])
Which only comes out a little different because I deliberately removed the extraneous fields. Add them in yourself if you really want to:
{
"_id" : 100,
"value" : "0",
"childs" : [
{
"_id" : ObjectId("570557d4094a4514fc1291d7"),
"id" : 110,
"value" : "1",
"contain" : [ 100 ]
},
{
"_id" : ObjectId("570557d4094a4514fc1291d8"),
"id" : 120,
"value" : "1",
"contain" : [ 100 ]
}
]
}
So the only real issue here is "filtering" any null result from the array, created when the current document was the parent in processing items to $push.
What you also seem to be missing here is that the result you are looking for does not need aggregation or "sub-queries" at all. The structure that you have concluded or possibly found elsewhere is "designed" so that you can get a "node" and all of it's "children" in a single query request.
That means just the "query" is all that is really needed, and the data collection ( which is all that is happening since no content is really being "reduced" ) is just a function of iterating the cursor result:
var result = {};
db.test.find({
"$or": [
{ "id": 100 },
{ "contain.0": 100, "value": "1" }
]
}).sort({ "contain.0": 1 }).forEach(function(doc) {
if ( doc.id == 100 ) {
result = doc;
result.childs = []
} else {
result.childs.push(doc)
}
})
printjson(result);
This does exactly the same thing:
{
"_id" : ObjectId("570557d4094a4514fc1291d6"),
"id" : 100,
"value" : "0",
"contain" : [ ],
"childs" : [
{
"_id" : ObjectId("570557d4094a4514fc1291d7"),
"id" : 110,
"value" : "1",
"contain" : [
100
]
},
{
"_id" : ObjectId("570557d4094a4514fc1291d8"),
"id" : 120,
"value" : "1",
"contain" : [
100
]
}
]
}
And serves as proof that all you really need to do here is issue the "single" query to select both the parent and children. The returned data is just the same, and all you are doing on either server or client is "massaging" into another collected format.
This is one of those cases where you can get "caught up" in thinking of how you did things in a "relational" database, and not realize that since the way the data is stored has "changed", you no longer need to use the same approach.
That is exactly what the point of the documentation example "Model Tree Structures with Child References" in it's structure, where it makes it easy to select parents and children within one query.

Categories