Join multiple collections after parallel aggregation in Mongodb - mongodb

I have a collection called "Reel" which has embedded Objects.
{
"_id":"reel_1",
"category":[
{
"_id" : "cat_1",
"videos": [ {"_id":"vid_1"},{"_id":"vid_2"} ] //_id is reference of Video collection
},
{
"_id" : "cat_2",
"videos": [ {"_id":"vid_3"},{"_id":"vid_4"} ]
}
]
}
Video is another collection whose _id is referred inside reel-> category -> videos -> _id
{
"_id":"vid_1",
"title":"title 1",
"groups":[{"_id":"group_1"},{"_id":"group_2"}]
},
{
"_id":"vid_2",
"title":"title 2",
"groups":[{"_id":"group_1"},{"_id":"group_4"}]
},
{
"_id":"vid_3",
"title":"title 3",
"groups":[{"_id":"group_1"},{"_id":"group_2"}]
},
{
"_id":"vid_4",
"title":"title 4",
"groups":[{"_id":"group_3"},{"_id":"group_4"}]
}
The Document collection which holds _id of Reel and _id of Category
{
"_id":"doc_1",
"title":"document title",
"assessments":[
{
"reel":"reel_1", // reel reference _id
"category":"cat_1", // category reference _id
"groups":[{"_id":"group_1"},{"_id":"group_2"}
]
}
]
}
I need to join and find all related embedded Objects which has group_1.
I have done joining between Reel collection and Video collection and working fine,
{ $unwind: { path: '$category', preserveNullAndEmptyArrays: true }},
{ $unwind: { path: '$category.videos', preserveNullAndEmptyArrays: true }},
{
$lookup: {
from: 'video',
localField: 'category.videos._id',
foreignField: '_id',
as: 'joinVideo'
}
},
{ $unwind: { path: "$joinVideo", preserveNullAndEmptyArrays: true }},
{ $unwind: { path: "$joinVideo.groups", preserveNullAndEmptyArrays: true }},
{ $match: { "joinVideo.groups._id": "group_1" }},
{ $addFields: { "category.videos": "$joinVideo" }},
{
$group: {
_id: {
_id: "$_id",
category: "$category._id"
},
videos: {
$addToSet: "$category.videos"
}
}
}, {
$group: {
_id: "$_id._id",
category: {
$addToSet: {
"_id": "$_id.category",
"videos": "$videos"
}
}
}
}
The document collection should be embedded inside the category object based on reel _id and and category _id filtered by group_1. My expected result is
{
"_id":"reel_1",
"category":[
{
"_id" : "cat_1",
"videos": [
{
"_id":"vid_1",
"title":"title 1",
"groups":[ {"_id":"group_1"},{"_id":"group_2"}]
},
{
"_id":"vid_2",
"title":"title 2",
"groups":[{"_id":"group_1"},{"_id":"group_4"}]
}
],
"documents":[
{ // this document comes by reel="reel_1", category="cat_1", filtered by "group_1"
"_id":"doc_1",
"title":"document title",
}
]
},
{
"_id" : "cat_2",
"videos": [
{
"_id":"vid_3",
"title":"title 3",
"groups":[{"_id":"group_1"},{"_id":"group_2"}]
}
]
}
]
}
I tried in many ways. Since I'm new to Mongodb, I couldn't sort this out.

Since MongoDB v3.6, $lookup allows perform uncorrelated sub-queries. This allows us perform non-standard queries to join two or more collections.
Note: Explanation why we need to use $expr inside $lookup pipeline
Explanation
We apply $unwind to flatten $category
We perform $lookup with 2 conditions:
video.groups._id == 'group_1' and video._id in reel.category.videos._id
Since $reel.category.videos._id returns an array, we need to use $in operator
Again we perform $lookup with 2 conditions. It creates documents field for every document
To remove fields dynamically, we need to use Aggregation expressions called $$REMOVE which allows us exclude conditionally a field from document
We perform $group stage to transform into desired result
db.reel.aggregate([
{
$unwind: {
path: "$category",
preserveNullAndEmptyArrays: true
}
},
{
$lookup: {
from: "video",
let: {
videos: "$category.videos._id"
},
pipeline: [
{
$match: {
"groups._id": "group_1",
$expr: {
$in: [
"$_id",
"$$videos"
]
}
}
}
],
as: "category.videos"
}
},
{
$lookup: {
from: "document",
let: {
reel_id: "$_id",
category_id: "$category._id"
},
pipeline: [
{
$match: {
$expr: {
$and: [
{
$in: [
"$$reel_id",
"$assessments.reel"
]
},
{
$in: [
"$$category_id",
"$assessments.category"
]
}
]
}
}
},
{
$project: {
_id: 1,
title: 1
}
}
],
as: "category.documents"
}
},
{
$addFields: {
"category.documents": {
$cond: [
{
$eq: [
{
$size: "$category.documents"
},
0
]
},
"$$REMOVE",
"$category.documents"
]
}
}
},
{
$group: {
_id: "$_id",
category: {
$push: "$category"
}
}
}
])
MongoPlayground

Related

Mongo- Query two different collections on a field for the same value

I have two collections,
Users-
{
_id:,
name:"",
company_id:"",
}
Companies-
{
_id:,
name:"",
}
Now i want to query all the companies for which either the company name matches the name in companies collection or companies for which name matches in users collection.
You can use $group
db.Users.aggregate([
{
$lookup: {
from: "Companies",
let: {companyId: "$company_id"},
pipeline: [
{
$match: {
$expr: {
$or: [
{ $eq: [ "$_id", "$$companyId" ] },
{ $eq: [ "$name", "Rockefeller" ] }
]
}
}
}
],
as: "Company"
}
},
{
$project: {
_id: 0,
Company: { "$arrayElemAt": [ "$Company", 0 ] }
}
},
{
"$group": {
"_id": null,
"companies": { "$push": "$Company" }
}
}
])
Working Mongo playground

How to aggregate with value matched in an array then sort

I have these collections:
lists
{_id: 1, item: "a", owner: 1}
users
{_id: 1, subs: [{_id: 1, active: "Y"},{_id: 2, active: "N"}]}
subs
{_id: 1, text: "A"}
{_id: 2, text: "B"}
I want to have a result of lists with user info and with subs info that is active.
{_id: 1, item: "a", owner: {_id: 1, subs: [{_id: {_id: 1, text: "A"}, active: "Y"}]}}
I want also to sort it based on "text" field.
I tried aggregation but failed,
db.getCollection("lists").aggregate(
[
{
"$lookup" : {
"from" : "users",
"localField" : "owner",
"foreignField" : "_id",
"as" : "owner"
}
},
{
"$match" : {
"owner.0.subs" : {
"$elemMatch" : {
"active" : "Y"
}
}
}
}
],
{
"allowDiskUse" : false
}
);
I am also using Mongoose and failed using populate.
Any way to get my result?
Here, I updated my aggregation pipeline,
[
{
$lookup: {
from: "users",
as: "owner",
let: { owner: "$owner" },
pipeline: [
{ $match: { $expr: { $eq: ["$$owner", "$_id"] } } },
{ $unwind: { path:"$sub", preserveNullAndEmptyArrays: false} },
{ $match: { "subs.active": "Y" } },
{
$lookup: {
from: "plans",
localField: "subs._id",
foreignField: "_id",
as: "subs.plans"
}
},
{ $unwind: { path:"$subs.plans", preserveNullAndEmptyArrays: false} },
]
}
},
{ $unwind: { path: "$owner", preserveNullAndEmptyArrays: true} },
{ '$sort': { item: 1 } },
{ '$skip': 0 },
{ '$limit': 20 } ]
You can use lookup with pipeline and nested lookup,
inside lookup pipelines are:
$match your owner id in users collection
$unwind deconstruct subs array because we need to lookup with subs collection
$match subs is active or not
$lookup with subs collection
$unwind deconstruct subs._id that we joined from subs collection
$group reconstruct subs array
$unwind deconstruct owner array
$sort by item and pagination by $skip and $limit
db.getCollection("lists").aggregate([
{
$lookup: {
from: "users",
as: "owner",
let: { owner: "$owner" },
pipeline: [
{ $match: { $expr: { $eq: ["$$owner", "$_id"] } } },
{ $unwind: "$subs" },
{ $match: { "subs.active": "Y" } },
{
$lookup: {
from: "subs",
localField: "subs._id",
foreignField: "_id",
as: "subs._id"
}
},
{ $unwind: "$subs._id" },
{
$group: {
_id: "$_id",
subs: {
$push: {
_id: "$subs._id._id",
text: "$subs._id.text",
active: "$subs.active"
}
}
}
}
]
}
},
{ $unwind: "$owner" },
{ $sort: { item: 1 } },
{ $skip: 0 },
{ $limit: 20 }
], { allowDiskUse: false })
Playground
Your Second Edit: there is wrong key name sub in first lookup inside first $unwind, correct this,
{ $unwind: { path:"$sub", preserveNullAndEmptyArrays: false} }
to
{ $unwind: { path:"$subs", preserveNullAndEmptyArrays: false} }
Your Working Query

How to convert to ObjectId and match dates on MongoDB lookup?

I am grabbing an id from a nested value in a schema, then using that to lookup the id from another table and also matching on a couple dates from that table. I've tried a regular match/lookup/unwind/match and also a lookup/let/pipeline technique. In both cases, it ignores matching on the date for some reason. What am I missing?
Here is one method for reference. I'm not sure where to put the sort either since it doesn't seem to pull $meeting out to sort on.
EXAMPLE RECORDS
PRODUCT
{
"_id" : ObjectId("5f36c0df6d5553e6af208cac"),
"items" : [
{
"paramType" : "Meeting",
"paramValue" : "5f36c0df6d5553e6af208cab"
}
],
"ownerId" : ObjectId("12345678901234567")
}
MEETING
{
"_id" : ObjectId("5f36c0df6d5553e6af208cab"),
"startDate" : ISODate("2020-08-18T10:00:00.000+0000"),
"endDate" : ISODate("2020-08-18T11:00:00.000+0000")
}
AGGREGATE
db.getCollection("products").aggregate(
[
{
$match: {
"ownerId": ObjectId("12345678901234567")
}
},
{
$unwind: "$items"
},
{
$lookup: {
from: "meetings",
let: { "meetingId": '$items.paramValue' },
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: ["$_id", "$$meetingId"] },
{
$eq: ["meeting.startDate", {
"$gte": ["$meeting.startDate", ISODate("2020-08-01T00:00:00.000Z")]
}]
},
{
$eq: ["meeting.endDate", {
"$lte": ["$meeting.endDate", ISODate("2020-08-31T23:59:59.999Z")]
}]
}
],
},
},
},
],
as: "meeting"
}
},
{
$unwind: "$meeting"
},
{
$project: {
"_id": 1,
"items": 1,
"meeting": "$meeting"
}
},
{
$sort: {
'meeting.startDate': 1
}
},
]
);
It might be because item.paramValue is not converted to an ObjectId before the lookup. But can't figure out how to convert it inside an aggregate. I tried this, but no go
{
$addFields: {
"convertedMeetingId": { $toObjectId: "$items.paramValue" }
}}
let: { "meetingId": "$convertedMeetingId" }
There are quick fixes in $lookup other looks good,
let you can convert meetingId string to ObjectId here using $toObjectId
$gte and $lte, you used $meeting.startDate and $meeting.endDate it should be $startDate and $endDate because you are already inside meeting lookup.
i am not sure why you have used $eq and with $gte and $lte, if i am not wrong i have corrected and removed $eq it will work directly.
{
$lookup: {
from: "meetings",
let: { meetingId: { $toObjectId: "$items.paramValue" } },
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: ["$_id", "$$meetingId"] },
{ $gte: ["$startDate", ISODate("2020-08-01T00:00:00.000Z")] },
{ $lte: ["$endDate", ISODate("2020-08-31T23:59:59.999Z")] }
]
}
}
}
],
as: "meeting"
}
},
Playground

$match in aggregate don't return data in mongodb

I have three tables below is the structure like below
I'm looking to get a result like below
"type1": [ -- type from Accounts collection
{
"_id": "5e97e9a224f62f93d5x3zz46", -- _id from Accounts collection
"locs": "sampleLocks 1", -- field from Accounts collection
"solutions": "sample solutions 1", -- field from Accounts collection
"Clause": "clause 1" -- field from AccountsDesc collection
},
{
"_id": "5e97e9a884f62f93d5x3zz46",
"locs": "sampleLocks2",
"solutions": "sample solutions2",
"Clause": "clause2"
}
],
"type2": [
// same data construction as of type1 above
]
_id, locks, solution to be coming from Accounts collection
Clause field to be coming from AccountsDesc collection
accounts_id is kind of a foreign key in AccountsDesc coming from Account
competitor_id is kind of a foreign key in AccountsDesc coming from Competitor
Below is what my query looks like
db.accountDesc.aggregate([
{
$match : {accounts_Id : "123456"}, active: true}
},
{
$lookup: {
from: 'accounts',
pipeline: [{ $match: { type: { $in: ["type1, type2, type3"] } } }],
as: 'accountsData'
}
},
{
$group: {
_id: "$accountsData.type",
data: {
$push: {_id: "$accountsData._id", clause: "$clause", locs: "$type.locs", solutions: "$type.solutions"}
}
}
},
{
$group: {
_id: null,
data: {
$push: {
k: {
$toString: '$_id'
},
v: '$data'
}
}
}
},
{
$replaceRoot: {
newRoot: {
$arrayToObject: '$data'
}
}
}
])
Issues related with the query -
$match : {accountId : "123456"}, active: true} -- No data is returned if i use match on AccountsDesc collection
cant set localField, foriegnField if im using pipeline, then how the mapping will happen like a LEFT join.
clause: "$clause" don't get the value of this field in the response
As we discussed in chat, you want RIGHT OUTER JOIN for your aggregation.
Try the query below:
db.User_Promo_Map.aggregate([
{
$match: {
user_Id: ObjectId("5e8c1180d59de1704ce68112")
}
},
{
$lookup: {
from: "promo",
pipeline: [
{
$match: {
active: true,
platform: {
$in: [
"twitch",
"youtube",
"facebook"
]
}
}
}
],
as: "accountsData"
}
},
{
$unwind: "$accountsData"
},
{
$group: {
_id: "$accountsData.platform",
data2: {
$addToSet: {
amount: "$amount",
promo_Id: "$promo_Id"
}
},
data: {
$addToSet: {
_id: "$accountsData._id",
format: "$accountsData.format",
description: "$accountsData.description"
}
}
}
},
{
$addFields: {
data: {
$map: {
input: "$data",
as: "data",
in: {
"_id": "$$data._id",
"description": "$$data.description",
"format": "$$data.format",
amount: {
$reduce: {
input: "$data2",
initialValue: "$$REMOVE",
in: {
$cond: [
{
$eq: [
"$$this.promo_Id",
"$$data._id"
]
},
"$$this.amount",
"$$value"
]
}
}
}
}
}
}
}
},
{
$group: {
_id: null,
data: {
$push: {
k: {
$toString: "$_id"
},
v: "$data"
}
}
}
},
{
$replaceRoot: {
newRoot: {
$arrayToObject: "$data"
}
}
}
])
MongoPlayground

MongoDB Multi level $lookup sort not working

Multi level $lookup sort not working in aggregation.
Sorting works only for country, state name. Tried applying sorting for cities, but country sort overwrites the city sort.
Query2 is working but i don't want to sort collections inside lookup pipeline.
Is there any way to achieve all level of sorting(country,state,city) in Query1
Query1(Not Working):
Country.aggregate([
{
$lookup:{
from: 'states',
localField:'_id',
foreignField:'countryId',
as:'states'
}
},
{
$unwind: {
path: "$states",
preserveNullAndEmptyArrays: true
}
},
{
$sort: {
'states.name': 1
}
},
{
$lookup:{
from: 'cities',
localField:'states._id',
foreignField:'stateId',
as:'states.cities'
}
},
{
$sort: {
'states.cities.name': 1
}
},
{
$group: {
_id: {
_id: '$_id',
name: '$name'
},
states: {
$push: '$states'
}
}
},
{
$project: {
_id: '$_id._id',
name: '$_id.name',
states: 1
}
},
{
$sort: {
name: 1
}
}
])
Query2(Working):
Execution time is 8 times higher than Query1.
[
{
$lookup : {
from : 'states',
let: { 'countryId': '$_id' },
pipeline: [
{
$match: {
$expr:
{
$eq: ['$countryId', '$$countryId']
}
}
},
{
$sort : {
name : -1
}
}
],
as : 'states'
}
},
{
$unwind: {
path: '$states',
preserveNullAndEmptyArrays: true
}
},
{
$lookup : {
from : 'cities',
let: { 'stateId': '$states._id' },
pipeline: [
{
$match: {
$expr:
{
$eq: ['$stateId', '$$stateId']
}
}
},
{
$sort : {
name : -1
}
}
],
as : 'states.cities'
}
},
{
$group: {
_id: {
_id: '$_id',
name: '$name'
},
states: {
$push: '$states'
}
}
},
{
$project: {
_id: '$_id._id',
name: '$_id.name',
states: 1
}
}
]
In the newer $lookup syntax you do not need to use $unwind to join nested fields. You can easily use $lookup inside the pipeline to join multiple level.
[
{ "$lookup": {
"from": "states",
"let": { "countryId": "$_id" },
"pipeline": [
{ "$match": { "$expr": { "$eq": ["$countryId", "$$countryId"] }}},
{ "$lookup": {
"from": "cities",
"let": { "stateId": "$_id" },
"pipeline": [
{ "$match": { "$expr": { "$eq": ["$stateId", "$$stateId"] }}},
{ "$sort": { "name": -1 }}
],
"as": "cities"
}},
{ "$sort": { "name": -1 }}
],
"as": "states"
}}
]