How to $lookup entries from array in MongoDB? - mongodb

I have three collections, Posts, Languages and Translations. And the structure looks like this:
Post: {
id: ObjectID
title: String,
translated_versions: ObjectID
}
TranslatedVersion: {
id: ObjectID
translations: Translation[]
}
// Just an object structure in TranslatedVersion.translations, not collection
Translation: {
post: ObjectID
language: ObjectID
}
Language {
id: ObjectID
name: String
}
What I want to do is to get entry from translated versions collections, put it into my post and then for every entry in TranslatedVersion.translation I want to get post and language objects, so it will look like this:
Post {
title: "Test title",
translated_versions: {
id: 'some-id',
tranlations: [
Translation {
language: {
id: 'some-id',
name: 'Turkish'
},
post: {
id: 'some-id'
title: 'Turkish post'
}
},
Translation {
language: {
id: 'some-id',
name: 'English'
},
post: {
id: 'some-id',
title: 'English post'
}
}
]
}
}
I am using Golang and I tried to do it this way (sorry lots of lines of code):
lookup_translations_group := bson.D{
{
Key: "$lookup", Value: bson.D{
{
Key: "from", Value: Models.Translation,
},
{
Key: "localField", Value: "translations_group_id",
},
{
Key: "foreignField", Value: "_id",
},
{
Key: "as", Value: "translations",
},
},
},
}
unwind_translations_group := bson.D{
{
Key: "$unwind", Value: bson.D{
{
Key: "path", Value: "$translations",
},
{
Key: "preserveNullAndEmptyArrays", Value: true,
},
},
},
}
lookup_translations_posts := bson.D{
{
Key: "$lookup", Value: bson.D{
{
Key: "from", Value: Models.Post,
},
{
Key: "localField", Value: "translations.translations.post",
},
{
Key: "foreignField", Value: "_id",
},
{
Key: "as", Value: "translations.translations.post",
},
},
},
}
Then I run mongo.Pipeline and what I get is an object with "post" field which is an array with my translated posts:
So obviously I am doing something wrong here :( I assume I need to add another pipeline in lookup_translations_group stage, but I have no idea how to it correctly.
upd2:
Even more complex examples:
db.posts.insertMany([
{ "_id" : 1, "title": "English post", language: 1, category: 1, "translations": 1, },
{ "_id" : 2, "title": "Turkish post", langauge: 2, category: 1, "translations": 1, },
{ "_id" : 2, "title": "Finnish post", language: 3, category: 1, "translations": 1, },
])
db.languages.insertMany([
{ "_id" : 1, "name": "English", },
{ "_id" : 2, "name": "Turkish", },
{ "_id" : 3, "name": "Finnish", },
])
db.translations.insert({
"_id" : 1,
translations: [
{ language: 1, post: 1 },
{ language: 2, post: 2 },
{ language: 3, post: 3 },
]
})
db.categories.insert({
"_id" : 1,
"name": "Random category"
})
Result that I want:
{
"_id": 1,
"title": "English post",
"language": {
"_id": 1,
"name": "English"
},
"category": {
"_id": 1,
"name": "Random category"
},
"translations": {
"_id": 1,
translations: [
{
"language": {
"_id": 1,
"name": "English"
},
"post": {
"_id": 1,
"title": "English post",
"category": {
"_id": 1,
"name": "Random category"
}
}
},
{
"language": {
"_id": 2,
"name": "Turkish"
},
"post": {
"_id": 2,
"title": "Turkish post",
"category": {
"_id": 1,
"name": "Random category"
}
}
},
{
"language": {
"_id": 3,
"name": "Finnish"
},
"post": {
"_id": 3,
"title": "Finnish post",
"category": {
"_id": 1,
"name": "Random category"
}
}
},
]
}
}

If I understand correctly, you want something like:
db.translations.aggregate([
{$match: {_id: 1}},
{$lookup: {
from: "posts",
localField: "translations.post",
foreignField: "_id",
as: "posts"
}},
{$lookup: {
from: "languages",
localField: "posts.language",
foreignField: "_id",
as: "translations"
}},
{$project: {
data: {$map: {
input: "$posts",
in: {
post: {title: "$$this.title", _id: "$$this._id"},
language: {$arrayElemAt: [
"$translations",
{$indexOfArray: ["$translations._id", "$$this._id"]}
]}
}
}}
}},
{$replaceRoot: {
newRoot: {$mergeObjects: [
{$first: {$filter: {input: "$data", cond: {$eq: ["$$this.post._id", "$_id"]}}}},
{translations: {
_id: "$_id",
translations: {
$filter: {input: "$data", cond: {$ne: ["$$this.post._id", "$_id"]}}
}
}}
]}
}},
{$project: {_id: "$post._id", title: "$post.title", language: 1, translations: 1}}
])
See how it works on the playground example

Related

MongoDB, return selected fields from document with aggregation operations

With given query I also want to return productId.
I have collection comments that contains documents with data about productId and comments for given product
Example document in this collection:
{
"_id": {
"$oid": "635ee64f55460d1796447662"
},
"productId": "63413800d36ed477adc763d0",
"__v": 0,
"comments": [
{
"userId": "",
"userName": "test",
"date": "2022.12.18.21.51.36",
"confirmed": false,
"likes": {
"up": 0,
"down": 0
},
"content": {
"rating": 6,
"description": "testtesttest"
},
"image": {
"added": false,
"images": []
},
"_id": {
"$oid": "639f7d58b6206a863c4a7aba"
},
"usersWhoLiked": []
},
{
"userId": "",
"userName": "test",
"date": "2022.12.18.21.52.19",
"confirmed": false,
"likes": {
"up": 0,
"down": 0
},
"content": {
"rating": 6,
"description": "testtesttest"
},
"image": {
"added": true,
"images": [
"comments/63413800d36ed477adc763d0/639f7d83b6206a863c4a7ad6/dell.jpg"
]
},
"_id": {
"$oid": "639f7d83b6206a863c4a7ad6"
},
"usersWhoLiked": []
}
]
}
My exmaple query:
db.comments.aggregate([{$match: {"comments._id": {$in: [ObjectId('630b7868f51e10876223b4aa'), ObjectId('630bd277f919a9e9c0e7a559')]}}},
{$project: {comment: {$filter: {input: "$comments", as: "comment", cond: {$in: ["$$comment._id", [ObjectId("630b7868f51e10876223b4aa"), ObjectId("630bd277f919a9e9c0e7a559")]]}}}}}])
With this query I get the result :
{ _id: ObjectId("630b7868f51e10876223b4a6"),
comment:
[ { userId: '62f29c2c324f4778dff443f6',
userName: 'User',
date: '2022.08.19',
confirmed: false,
likes: { up: 3, down: 0 },
content: { rating: 4, description: 'Super laptop <3' },
_id: ObjectId("630b7868f51e10876223b4aa"),
usersWhoLiked:
[ { userId: '62f29c2c324f4778dff443f6',
likeUp: true,
_id: ObjectId("630d2b0494370efb37107983") },
{ userId: '6322434f2b5bbac87f0e7aba',
likeUp: true,
_id: ObjectId("632243702b5bbac87f0e7afa") },
{ userId: '62f2991e324f4778dff443d4',
likeUp: true,
_id: ObjectId("63af4d77c8991b74d6986995") } ] } ] }
{ _id: ObjectId("630bd277f919a9e9c0e7a555"),
comment:
[ { userId: '62f29c2c324f4778dff443f6',
userName: 'User',
date: '2022.08.28',
confirmed: false,
likes: { up: 1, down: 1 },
content:
{ rating: 6,
description: 'Laptop posiada przyjemna klawiature, nie grzeje się. Do codziennego grania wystarczy.' },
_id: ObjectId("630bd277f919a9e9c0e7a559"),
usersWhoLiked:
[ { userId: '62f29c2c324f4778dff443f6',
likeUp: true,
_id: ObjectId("630d2dfc94370efb37107991") },
{ userId: '62fa2549f029348f75bc9c81',
likeUp: false,
_id: ObjectId("631241fe755c641525dc9cfa") } ] } ] }
As you see in the result, the productId is missing.
I was trying to rebuild query with #group operator but still with out effect...
So my questsion is:
How shall I rewrite that query to get the same result but with productId in it for each returned comment
This is how $project works, if a field is not specified will not be output.
So just add productId: 1 into the $project stage and it will be shown.
Check this example.

$lookup ObjectId in an array of ObjectId mongoose

So basically I have a Patient schema, in which there are 2 fields:
patientSchema = {
//other_fields
current_facility: {
type: mongoose.Schema.Types.ObjectId,
ref: "Facility",
},
close_contact_list: {
type: [mongoose.Schema.Types.ObjectId],
ref: "Patient",
}
}
If i populate like this
Patient.find().populate({
path: "close_contact_list",
populate: {
path: "current_facility",
model: "Facility",
},
select: "id_number name dob status current_facility",
})
The result of 1 document would be:
{
//other_fields
"close_contact_list": [
{
"_id": "62e7e80bcdde7f149602775e",
"id_number": "201260388",
"name": "Tom",
"dob": "2003-01-01T00:00:00.000Z",
"status": "F1",
"current_facility": {
"location": {
"formattedAddress": "Phuong Phường 2, Quan Quận 2, TP Hồ Chí Minh",
"province": "Hồ Chí Minh",
"district": "Quận 2",
"ward": "Phường 2"
},
"_id": "62e7d60596997310f14e4bdb",
"name": "Thu Duc",
"capacity": 3000,
"current_count": 2,
"__v": 0
}
},
{
//similar_to_above
},
...
]
}
My question is how can i do the same thing but by using aggregate. I appreciate your help.
Try this:
Patient.aggregate([
{
"$lookup": {
"from": "Facility",
"localField": "_id",
"foreignField": "current_facility",
"as": "current_facility"
}
},
{
"$project": {
"id_number": 1,
"name": 1,
"dob": 1,
"status": 1,
"current_facility": 1
}
}
])

Filter on collection and merge results in another collection in MongoDB

I am using MongoDB 4.2.9 and have the following requirements:
Collection 'A' has multiple documents with a string field 'status' that I need to filter on
Collection 'B' has multiple documents
Collection A
{ _id: "1",
status: "Report",
type: "Academy",
rating: "Excellent",
ReportNo: "A1"
},
{ _id: "2",
status: "Open",
type: "Academy",
rating: "",
ReportNo: ""
},
{ _id: "3",
status: "Draft",
type: "Academy",
rating: "",
ReportNo: ""
},
{ _id: "4",
status: "Report",
type: "Academy",
rating: "Great",
ReportNo: "A4"
}
Collection B
{ _id: "98",
status: "Archived",
type: "Academy",
rating: "So So",
ReportNo: "X2"
},
{ _id: "99",
status: "Archived",
type: "Academy",
rating: "Great",
ReportNo: "X1"
}
Resulting View
{ _id: "1",
status: "Report",
type: "Academy",
rating: "Excellent",
ReportNo: "A1"
},
{ _id: "4",
status: "Report",
type: "Academy",
rating: "Great",
ReportNo: "A4"
},
{ _id: "98",
status: "Archived",
type: "Academy",
rating: "So So",
ReportNo: "X2"
},
{ _id: "99",
status: "Archived",
type: "Academy",
rating: "Great",
ReportNo: "X1"
}
My goal is to create an aggregation view so that I can filter on a status value in Collection 'A' and then merge those results with Collection 'B' and show in the view ?
I can filter on Collection 'A' using the match call, just can't see how to merge resulting documents into Collection 'B'
From my understandings, your "merge" behaviour is actually a union view of filtered view of collection A and collection B.
With MongoDB v4.2, you can use $facet to handle collection A and collection B separately.
simply perform filtering on A
perform uncorrelated $lookup on B
wrangle the result and merge them together to get the union view that you are looking for.
db.createCollection(
"unionView",
{
"viewOn" : "A",
"pipeline" : [
{
"$facet": {
"A": [
{
"$match": {
status: "Report"
}
}
],
"B": [
{
$limit: 1
},
{
"$lookup": {
"from": "B",
"pipeline": [],
"as": "B"
}
},
{
$unwind: "$B"
},
{
"$replaceRoot": {
"newRoot": "$B"
}
}
]
}
},
{
$project: {
all: {
"$setUnion": [
"$A",
"$B"
]
}
}
},
{
$unwind: "$all"
},
{
"$replaceRoot": {
"newRoot": "$all"
}
}
]
}
)
Here is the Mongo Playground for your reference.
With MongoDB v4.4+, you can create a view with $unionWith
db.createCollection(
"unionView",
{
"viewOn" : "A",
"pipeline" : [
{
"$match": {
status: "Report"
}
},
{
"$unionWith": {
"coll": "B"
}
}
]
}
)
Here is the Mongo playground for your reference.

How to transform MongoDB document to tree structure by Aggregation? [duplicate]

I'm working in a REST api with ExpressJS and Mongo and I have a collection with N quantity of levels.
So to solve this problem I'm using an recursive table (or collection) in mongo where a field is the id and every register has a parent_id which is at the same level as it's childs.
To explain better this, here is an E-R representation
So as you se, mongo will save the data like this json (accounts level 0 has null parent)
[
{ "id": "45TYYU", "parent_id": null, "name":"account 1", "type": 1, "category": 1 },
{ "id": "45TYYXT", "parent_id": "45TYYU", "name":"account 2", "type": 1, "category": 1 },
{ "id": "45TYYPZ", "parent_id": "45TYYU", "name":"account 3", "type": 1, "category": 1 },
{ "id": "45TYYPZRE", "parent_id": "45TYYPZ", "name":"account 4", "type": 1, "category": 1 },
{ "id": "45TYYPZSX", "parent_id": "45TYYPZ", "name":"account 5", "type": 1, "category": 1 },
{ "id": "45TYYPZGP", "parent_id": "45TYYXT", "name":"account 6", "type": 1, "category": 1 }
]
account 2 and account 3 are children of account 1, while account 4 and account 5 are children of account tree and account 6 is child of account 2 ... but every register is at the same logical level only identifying through parent_id.
so I need to transform this data into a GET method to restructure it like this:
[
{
"id": "45TYYU",
"parent_id": null,
"name":"account 1",
"type": 1,
"category": 1,
"children": [
{
"id": "45TYYXT",
"parent_id": "45TYYU",
"name":"account 2",
"type": 1,
"category": 1,
"children": [
{ "id": "45TYYPZGP", "parent_id": "45TYYXT", "name":"account 6", "type": 1, "category": 1 }
]
},
{
"id": "45TYYPZ",
"parent_id": "45TYYU",
"name":"account 3",
"type": 1,
"category": 1,
"children": [
{ "id": "45TYYPZRE", "parent_id": "45TYYPZ", "name":"account 4", "type": 1, "category": 1 },
{ "id": "45TYYPZSX", "parent_id": "45TYYPZ", "name":"account 5", "type": 1, "category": 1 }
]
}
]
},
{
"id": "45TFJK",
"parent_id": null,
"name":"account 7",
"type": 1,
"category": 1,
"children": [
{
"id": "47HJJT",
"parent_id": "45TFJK",
"name":"account 8",
"type": 1,
"category": 1
},
{
"id": "47YHJU",
"parent_id": "45TFJK",
"name":"account 8",
"type": 1,
"category": 1
}
]
}
]
Yes... the parents level 0 has null parent_id and I want to put it's children inside an array called "children" and then send like this in the GET response to my UI
What is the best way to do this in expressJS?
Is there a library or component out there that allows me to do this?
Thank you
You can use $graphLookup and other useful array operators,
$match filter that records only have parent_id is null
$graphLookup to get child records and depth number in depthField level
$unwind deconstruct children array and allow to not remove empty children
$sort by depth level field level in descending order
$group by id field and reconstruct children array
db.collection.aggregate([
{ $match: { parent_id: null } },
{
$graphLookup: {
from: "collection",
startWith: "$id",
connectFromField: "id",
connectToField: "parent_id",
depthField: "level",
as: "children"
}
},
{
$unwind: {
path: "$children",
preserveNullAndEmptyArrays: true
}
},
{ $sort: { "children.level": -1 } },
{
$group: {
_id: "$id",
parent_id: { $first: "$parent_id" },
name: { $first: "$name" },
type: { $first: "$type" },
category: { $first: 1 },
children: { $push: "$children" }
}
},
$addFields now find the nested level children and allocate to its level,
$reduce to iterate loop of children array.
initialize default field level default value is -1, presentChild is [], prevChild is [] for the conditions purpose
$let to initialize fields:
prev as per condition if both level are equal then return prevChild otherwise return presentChild
current as per condition if both level are equal then return presentChild otherwise []
in to return level field and prevChild field from initialized fields
presentChild $filter children from prev array and return, merge current objects with children array using $mergeObjects and concat with current array of let using $concatArrays
$addFields to return only presentChild array because we only required that processed array
{
$addFields: {
children: {
$reduce: {
input: "$children",
initialValue: { level: -1, presentChild: [], prevChild: [] },
in: {
$let: {
vars: {
prev: {
$cond: [
{ $eq: ["$$value.level", "$$this.level"] },
"$$value.prevChild",
"$$value.presentChild"
]
},
current: {
$cond: [{ $eq: ["$$value.level", "$$this.level"] }, "$$value.presentChild", []]
}
},
in: {
level: "$$this.level",
prevChild: "$$prev",
presentChild: {
$concatArrays: [
"$$current",
[
{
$mergeObjects: [
"$$this",
{
children: {
$filter: {
input: "$$prev",
as: "e",
cond: { $eq: ["$$e.parent_id", "$$this.id"] }
}
}
}
]
}
]
]
}
}
}
}
}
}
}
},
{
$addFields: {
id: "$_id",
children: "$children.presentChild"
}
}
])
Playground
#turivishal Im using same schema in backend nodejs im getting only show the
null object not for a parent child relation using same aggregration
this.tickets.aggregate([
{
$match: {
parent_id: null
}
},
{
$graphLookup: {
from: "collection",
startWith: "$id",
connectFromField: "id",
connectToField: "parent_id",
depthField: "level",
as: "children"
}
},
{
$unwind: {
path: "$children",
preserveNullAndEmptyArrays: true
}
},
{
$sort: {
"children.level": -1
}
},
{
$group: {
_id: "$id",
parent_id: {
$first: "$parent_id"
},
name: {
$first: "$name"
},
type: {
$first: "$type"
},
category: {
$first: 1
},
children: {
$push: "$children"
}
}
},
{
$addFields: {
children: {
$reduce: {
input: "$children",
initialValue: {
level: -1,
presentChild: [],
prevChild: []
},
in: {
$let: {
vars: {
prev: {
$cond: [
{
$eq: [
"$$value.level",
"$$this.level"
]
},
"$$value.prevChild",
"$$value.presentChild"
]
},
current: {
$cond: [
{
$eq: [
"$$value.level",
"$$this.level"
]
},
"$$value.presentChild",
[]
]
}
},
in: {
level: "$$this.level",
prevChild: "$$prev",
presentChild: {
$concatArrays: [
"$$current",
[
{
$mergeObjects: [
"$$this",
{
children: {
$filter: {
input: "$$prev",
as: "e",
cond: {
$eq: [
"$$e.parent_id",
"$$this.id"
]
}
}
}
}
]
}
]
]
}
}
}
}
}
}
}
},
{
$addFields: {
children: "$children.presentChild"
}
}
]).then((result) => {
console.log('test',result);
// callback(result);
}).catch((error) => {
callback(error);
});
output:
[
{
_id: '45TYYU',
parent_id: null,
name: 'account 1',
type: 1,
category: 1,
children: []
},
{
_id: '45TYYUA',
parent_id: null,
name: 'account 1',
type: 1,
category: 1,
children: []
}
]

Search Solution Mongodb lookup and filter fields

I´m looking a simple solution for my MongoDB Lookup-Problem.
I have two collections:
Disp with the following structure:
{"_id" : ObjectId("5f748869487b4d0013ee50b4"),
"productId": ObjectId("5f9a96f85b909923e8530f0c"),
"subproductId" ObjectId("5e8b3684a82c2a00134e507a"),
},
{"_id" : ObjectId("5f74870e487b4d0013ee50b3"),
"productId": ObjectId("5f7b4b17e8ec6a00158bb5d8"),
"subproductId" ObjectId("5f78303a82f45e0013afebc1"),
}
and a Collection "Product with the following structure:
{_id: ObjectId("5f9a96f85b909923e8530f0c"),
subproduct: [ {
_id:ObjectId("5e8b3684a82c2a00134e507a"),
title: "Test"
},
{_id: ObjectId("5f369a94018c040013c76ede"),
title: "Test 1"}
]},
{_id:ObjectId("5f7b4b17e8ec6a00158bb5d8"),
subproduct: [ {
_id: ObjectId("5f7b43efe8ec6a00158bb5cc"),
title: "Test3"
}, {
_id: ObjectId("5f78303a82f45e0013afebc1"),
title: "Test 4"}
]
As you can see I have to do a lookup to an array "subproduct" with its ObjectID.
But if I do a normal lookup I will receice all other array-elements as well.
How do I filter the result so that the result will be only the subproduct-array element it matches.
Same as an inner join.
My expected result will be:
{"_id" : ObjectId("5f748869487b4d0013ee50b4"),
"productId": ObjectId("5f9a96f85b909923e8530f0c"),
"subproductId" ObjectId("5e8b3684a82c2a00134e507a"),
"subproduct": {
_id:ObjectId("5e8b3684a82c2a00134e507a"),
title: "Test"
},
{"_id" : ObjectId("5f74870e487b4d0013ee50b3"),
"productId": ObjectId("5f7b4b17e8ec6a00158bb5d8"),
"subproductId" ObjectId("5f78303a82f45e0013afebc1"),
},
"subproduct": {
_id: ObjectId("5f78303a82f45e0013afebc1"),
title: "Test 4"
},
Thx,
Alex
You can use the below query:
db.disp.aggregate([
{
"$lookup": {
"from": "product",
"let": {
subprodId: "$subproductId"
},
"pipeline": [
{
"$unwind": "$subproduct"
},
{
"$match": {
$expr: {
$eq: [
"$$subprodId",
"$subproduct._id"
]
}
}
}
],
as: "subDetails"
}
},
{
$unwind: "$subDetails"
},
{
$project: {
_id: 1,
productId: 1,
subproductId: 1,
subproduct: {
_id: "$subDetails.subproduct._id",
title: "$subDetails.subproduct.title"
}
}
}
])