MongoDB - How to aggregate with deeply nested arrays - mongodb

I have the following MongoDB structure:
Division Collection:
{
"_id": ObjectId("5b28cab902f28e18b863bd36"),
"name": "Premier League",
...
"teams": [
ObjectId("5b28cab902f28e18b863bd01"),
ObjectId("5b28cab902f28e18b863bd02"),
ObjectId("5b28cab902f28e18b863bd03"),
...
]
...
},
...
Teams Collection:
{
"_id": ObjectId("5b28cab902f28e18b863bd01"),
"name": "Liverpool",
...
"players": [
ObjectId('5b23tmb902f28e18b863bd01'),
ObjectId('5b23tmb902f28e18b863bd02'),
ObjectId('5b23tmb902f28e18b863bd03'),
...
]
...
},
...
Players Collection:
{
"_id": ObjectId("5b2b9a8bbda339352cc39ec1"),
"name": "Mohamed Salah",
"nationality": [
ObjectId("5b23cn1902f28e18b863bd01"),
ObjectId("5b23cn2902f28e18b863bd02"),
],
...
},
...
Countries Collection:
{
"_id": ObjectId("5b23cn1902f28e18b863bd01"),
"name": "England",
...
},
{
"_id": ObjectId("5b23cn2902f28e18b863bd02"),
"name": "Egypt",
...
},
...
How to get a result, which is below, using MongoDB aggregation ($lookup, $pipeline, etc):
{
"divisions": [
{
"_id": ObjectId("5b28cab902f28e18b863bd36"),
"name": "Premier League",
...
"teams": [
{
"_id": ObjectId("5b28cab902f28e18b863bd01"),
"name": "Liverpool",
...
"players": [
{
"_id": ObjectId("5b23tmb902f28e18b863bd01"),
"name": "Mohamed Salah",
"nationality": [
{
"_id": ObjectId("5b23cn2902f28e18b863bd02"),
"name": "Egypt",
...
},
{
"_id": ObjectId("5b23cn1902f28e18b863bd01"),
"name": "England",
...
}
]
...
},
...
]
},
...
]
},
{
"_id": ObjectId("5b28cab902f28e18b863bd37"),
"name": "Championship",
...
},
...
]
}
I manage to make a first-level merge:
db.divisions.aggregate([
{
$lookup: {
from: 'teams',
localField: 'teams',
foreignField: '_id',
as: 'teams'
}
},
])
and then I ran into difficulties, so I would be very grateful if someone could help me with this issue.

You need multi-level nested $lookup with pipeline.
db.division.aggregate([
{
$lookup: {
from: "teams",
let: {
teams: "$teams"
},
pipeline: [
{
$match: {
$expr: {
$in: [
"$_id",
"$$teams"
]
}
}
},
{
$lookup: {
from: "players",
let: {
players: "$players"
},
pipeline: [
{
$match: {
$expr: {
$in: [
"$_id",
"$$players"
]
}
}
},
{
$lookup: {
from: "countries",
localField: "nationality",
foreignField: "_id",
as: "nationality"
}
}
],
as: "players"
}
}
],
as: "teams"
}
}
])
Sample Mongo Playground

Maybe someone will be useful. Data also can be merged using the populate method:
db.divisions.find(_id: division_id).populate(
{
path: 'teams',
populate: {
path: 'players',
populate: {
path: 'nationality'
}
}
}
)

Related

mongodb $lookup - suppress non-matching documents

I only want to see matching documents, i.e. only T3 in the example below. I can find the matching documents between lotterytickets (many documents) and lotterydrawing (only a few documents).
How can I filter out the non-matching documents? Basically, I'd not like to see documents with the condition drawnticket==[], but I haven't found the conditional code to apply.
Any help would be appreciated. Thank you in advance
Configuration:
db={
"lotteryticket": [
{
"_id": ObjectId("6021ce0cb4d2c2b4f24c3a2e"),
"ticket": "T1",
"player": "Alice"
},
{
"_id": ObjectId("6021ce0cb4d2c2b4f24c3a2f"),
"ticket": "T2",
"player": "Bob"
},
{
"_id": ObjectId("6021ce0cb4d2c2b4f24c3a33"),
"ticket": "T3",
"player": "Charles"
}
],
"lotterydrawing": [
{
"_id": ObjectId("63309480b749b733c087b758"),
"ticket": "T3"
},
{
"_id": ObjectId("63309480b749b733c087b759"),
"ticket": "T9"
},
{
"_id": ObjectId("63309480b749b733c087b75a"),
"ticket": "T77"
}
]
}
Query:
db.lotteryticket.aggregate([
{
$lookup: {
from: "lotterydrawing",
localField: "ticket",
foreignField: "ticket",
as: "drawnticket",
}
}
])
Result:
[
{
"_id": ObjectId("6021ce0cb4d2c2b4f24c3a2e"),
"drawnticket": [],
"player": "Alice",
"ticket": "T1"
},
{
"_id": ObjectId("6021ce0cb4d2c2b4f24c3a2f"),
"drawnticket": [],
"player": "Bob",
"ticket": "T2"
},
{
"_id": ObjectId("6021ce0cb4d2c2b4f24c3a33"),
"drawnticket": [
{
"_id": ObjectId("63309480b749b733c087b758"),
"ticket": "T3"
}
],
"player": "Charles",
"ticket": "T3"
}
]
https://mongoplayground.net/p/bYcLEzrF5QT
Add a match stage, to filter stages with the empty drawn tickets. Like this:
db.lotteryticket.aggregate([
{
$lookup: {
from: "lotterydrawing",
localField: "ticket",
foreignField: "ticket",
as: "drawnticket",
}
},
{
"$match": {
$expr: {
"$gt": [
{
$size: "$drawnticket"
},
0
]
}
}
}
])
Playground.
try this query
db.lotteryticket.aggregate([
{
$lookup: {
from: "lotterydrawing",
localField: "ticket",
foreignField: "ticket",
as: "drawnticket"
}
},
{
"$match": {
drawnticket: {
$exists: true,
$ne: []
}
}
}
])
Playground.

$lookup in nested array

I need a MongoDB query to return the aggregation result from a collection of events, users and confirmations.
db.events.aggregate([
{
"$match": {
"_id": "1"
}
},
{
"$lookup": {
"from": "confirmations",
"as": "confirmations",
"let": {
"eventId": "$_id"
},
"pipeline": [
{
"$match": {
"$expr": {
"$eq": [
"$eventId",
"$$eventId"
]
}
}
},
{
"$lookup": {
"from": "users",
"as": "user",
"let": {
"userId": "$confirmations.userId"
},
"pipeline": [
{
"$match": {
"$expr": {
"$eq": [
"$_id",
"$$userId"
]
}
}
},
]
},
},
]
}
}
])
Desired
[
{
"_id": "1",
"confirmations": [
{
"_id": "1",
"eventId": "1",
"user": {
"_id": "1",
"name": "X"
},
"userId": "1"
},
{
"_id": "2",
"eventId": "1",
"user": {
"_id": "2",
"name": "Y"
},
"userId": "2"
}
],
"title": "foo"
}
]
Everything works except the embedded user in confirmations array. I need the output to show the confirmations.user, not an empty array.
Playgound: https://mongoplayground.net/p/jp49FW59WCv
You made mistake in variable declaration of inner $lookup. Try this Solution:
db.events.aggregate([
{
"$match": {
"_id": "1"
}
},
{
$lookup: {
from: "confirmations",
let: { "eventId": "$_id" },
pipeline: [
{
$match: {
"$expr": {
$eq: ["$eventId", "$$eventId"]
}
}
},
{
$lookup: {
from: "users",
let: { "userId": "$userId" },
pipeline: [
{
$match: {
$expr: {
$eq: ["$_id", "$$userId"]
}
}
}
],
as: "user"
}
},
{ $unwind: "$user" }
],
as: "confirmations"
}
}
])
Also instead of $unwind of user inside inner $lookup you can use:
{
$addFields: {
user: { $arrayElemAt: ["$user", 0] }
}
}
since $unwind will not preserve empty results from previous stage by default.

How to aggregate nested lookup array in mongoose?

I have a problem with how to lookup nested array, for example i have 4 collections.
User Collection
"user": [
{
"_id": "1234",
"name": "Tony",
"language": [
{
"_id": "111",
"language_id": "919",
"level": "Expert"
},
{
"_id": "111",
"language_id": "920",
"level": "Basic"
}
]
}
]
Language Collection
"language": [
{
"_id": "919",
"name": "English"
},
{
"_id": "920",
"name": "Chinese"
}
]
Job
"job": [
{
"_id": "10",
"title": "Programmer",
"location": "New York"
}
],
CvSubmit Collection
"cvsubmit": [
{
"_id": "11",
"id_user": "1234",
"id_job": "11"
}
]
And my query aggregation is:
db.cvsubmit.aggregate([
{
$lookup: {
from: "user",
localField: "id_user",
foreignField: "_id",
as: "id_user"
}
},
{
$lookup: {
from: "language",
localField: "id_user.language.language_id",
foreignField: "_id",
as: "id_user.language.language_id"
}
},
])
But the result is:
[
{
"_id": "11",
"id_job": "11",
"id_user": {
"language": {
"language_id": [
{
"_id": "919",
"name": "English"
},
{
"_id": "920",
"name": "Chinese"
}
]
}
}
}
]
I want the result like this, also showing all user data detail like name:
[
{
"_id": "11",
"id_job": "11",
"id_user": {
"_id": "1234",
"name": "Tony"
"language": [
{
"_id": "919",
"name": "English",
"Level": "Expert"
},
{
"_id": "920",
"name": "Chinese",
"level": "Basic"
}
]
}
}
]
Mongo Playground link https://mongoplayground.net/p/i0yCucjruey
Thanks before.
$lookup with user collection
$unwind deconstruct id_user array
$lookup with language collection and return in languages field
$map to iterate look of id_user.language array
$reduce to iterate loop of languages array returned from collection, check condition if language_id match then return name
db.cvsubmit.aggregate([
{
$lookup: {
from: "user",
localField: "id_user",
foreignField: "_id",
as: "id_user"
}
},
{ $unwind: "$id_user" },
{
$lookup: {
from: "language",
localField: "id_user.language.language_id",
foreignField: "_id",
as: "languages"
}
},
{
$addFields: {
languages: "$$REMOVE",
"id_user.language": {
$map: {
input: "$id_user.language",
as: "l",
in: {
_id: "$$l._id",
level: "$$l.level",
name: {
$reduce: {
input: "$languages",
initialValue: "",
in: {
$cond: [
{ $eq: ["$$this._id", "$$l.language_id"] },
"$$this.name",
"$$value"
]
}
}
}
}
}
}
}
}
])
Playground
You database structure is not accurate as per NoSQL, there should be max 2 collections, loot of join using $lookup and $unwind will cause performance issues.

MongoDB Aggregation - Lookup pipeline not returning any documents

I'm having hard time getting $lookup with a pipeline to work in MongoDB Compass.
I have the following collections:
Toys
Data
[
{
"_id": {
"$oid": "5d233c3bb173a546386c59bb"
},
"type": "multiple",
"tags": [
""
],
"searchFields": [
"Jungle Stampers - Two",
""
],
"items": [
{
"$oid": "5d233c3cb173a546386c59bd"
},
{
"$oid": "5d233c3cb173a546386c59be"
},
{
"$oid": "5d233c3cb173a546386c59bf"
},
{
"$oid": "5d233c3cb173a546386c59c0"
},
{
"$oid": "5d233c3cb173a546386c59c1"
},
{
"$oid": "5d233c3cb173a546386c59c2"
},
{
"$oid": "5d233c3cb173a546386c59c3"
},
{
"$oid": "5d233c3cb173a546386c59c4"
}
],
"name": "Jungle Stampers - Two",
"description": "",
"status": "active",
"category": {
"$oid": "5cfe727cac920000086b880e"
},
"subCategory": "Stamp Sets",
"make": "",
"defaultCharge": null,
"defaultOverdue": null,
"sizeCategory": {
"$oid": "5d0cfde57561e107c88fbde3"
},
"ageFrom": {
"$numberInt": "24"
},
"ageTo": {
"$numberInt": "120"
},
"images": [
{
"_id": {
"$oid": "5d233c3bb173a546386c59bc"
},
"id": {
"$oid": "5d233c39b173a546386c59ba"
},
"url": "/toyimages/5d233c39b173a546386c59ba.jpg",
"thumbUrl": "/toyimages/thumbs/tn_5d233c39b173a546386c59ba.jpg"
}
],
"__v": {
"$numberInt": "2"
}
}
]
Loans
Data
[
{
"_id": {
"$oid": "5e1f1661b712215978c746d9"
},
"tags": [],
"member": {
"$oid": "5e17495e4f81ab3f900dbb63"
},
"source": "admin portal - potter1#gmail.com",
"items": [
{
"id": {
"$oid": "5e1f160eb712215978c746d5"
},
"status": "new",
"_id": {
"$oid": "5e1f1661b712215978c746db"
},
"toy": {
"$oid": "5d233c3bb173a546386c59bb"
},
"cost": {
"$numberInt": "0"
}
},
{
"id": {
"$oid": "5e1f160eb712215978c746d5"
},
"status": "new",
"_id": {
"$oid": "5e1f1661b712215978c746da"
},
"toy": {
"$oid": "5d233b1ab173a546386c59b5"
},
"cost": {
"$numberInt": "0"
}
}
],
"dateEntered": {
"$date": {
"$numberLong": "1579095632870"
}
},
"dateDue": {
"$date": {
"$numberLong": "1579651200000"
}
},
"__v": {
"$numberInt": "0"
}
}
]
I am trying to return a list of toys and their associated loans that have a status of 'new' or 'out'.
I can use the following $lookup aggregate to fetch all loans:
{
from: 'loans',
localField: '_id',
foreignField: 'items.toy',
as: 'loansSimple'
}
However I am trying to use a pipeline to load loans that have the two statuses I am interested in, but it always only returns zero documents:
{
from: 'loans',
let: {
'toyid': '$_id'
},
pipeline: [
{
$match: {
$expr: {
$and: [
{$eq: ['$items.toy', '$$toyid']},
{$eq: ['$items.status', 'new']} // changed from $in to $eq for simplicity
]
}
}
}
],
as: 'loans'
}
This always seems to return 0 documents, however I arrange it:
Have I made a mistake somewhere?
I'm using MongoDB Atlas, v4.2.2, MongoDB Compass v 1.20.4
You are trying to search $$toyid inside inner array, but Operator Expression $eq cannot resolve it.
Best solution: $let (returns filtered loans by criteria) + $filter (applies filter for inner array) operator helps us to get desired result.
db.toys.aggregate([
{
$lookup: {
from: "loans",
let: {
"toyid": "$_id",
"toystatus": "new"
},
pipeline: [
{
$match: {
$expr: {
$gt: [
{
$size: {
$let: {
vars: {
item: {
$filter: {
input: "$items",
as: "tmp",
cond: {
$and: [
{
$eq: [
"$$tmp.toy",
"$$toyid"
]
},
{
$eq: [
"$$tmp.status",
"$$toystatus"
]
}
]
}
}
}
},
in: "$$item"
}
}
},
0
]
}
}
}
],
as: "loans"
}
}
])
MongoPlayground
Alternative solution 1. Use $unwind to flatten items attribute. (We create extra field named tmp which stores items value, flatten it with $unwind operator, match as you were doing and then exclude from result)
db.toys.aggregate([
{
$lookup: {
from: "loans",
let: {
"toyid": "$_id"
},
pipeline: [
{
$addFields: {
tmp: "$items"
}
},
{
$unwind: "$tmp"
},
{
$match: {
$expr: {
$and: [
{
$eq: [
"$tmp.toy",
"$$toyid"
]
},
{
$eq: [
"$tmp.status",
"new"
]
}
]
}
}
},
{
$project: {
tmp: 0
}
}
],
as: "loans"
}
}
])
MongoPlayground
Alternative solution 2. We use $reduce to create toy's array and with $in operator we check if toyid exists inside this array.
db.toys.aggregate([
{
$lookup: {
from: "loans",
let: {
"toyid": "$_id"
},
pipeline: [
{
$addFields: {
toys: {
$reduce: {
input: "$items",
initialValue: [],
in: {
$concatArrays: [
"$$value",
[
"$$this.toy"
]
]
}
}
}
}
},
{
$match: {
$expr: {
$in: [
"$$toyid",
"$toys"
]
}
}
},
{
$project: {
toys: 0
}
}
],
as: "loans"
}
}
])
$expr receives aggregation expressions, At that point $$items.toy is parsed for each element in an array as you would expect (however if it would it will still give you "bad" results as you'll get loans that have the required toy id and any other item with status new in their items array).
So you have two options to work around this:
If you don't care about the other items in the lookup'd document you can add an $unwind stage at the start of the lookup pipeline like so:
{
from: 'loans',
let: {
'toyid': '$_id'
},
pipeline: [
{
$unwind: "$items"
},
{
$match: {
$expr: {
$and: [
{$eq: ['$items.toy', '$$toyid']},
{$eq: ['$items.status', 'new']} // changed from $in to $eq for simplicity
]
}
}
}
],
as: 'loans'
}
If you do care about them just iterate the array in one of the possible ways to get a 'correct' match, here is an example using $filter
{
from: 'loads',
let: {
'toyid': '$_id'
},
pipeline: [
{
$addFields: {
temp: {
$filter: {
input: "$items",
as: "item",
cond: {
$and: [
{$eq: ["$$item.toy", "$$toyid"]},
{$eq: ["$$item.status", "new"]}
]
}
}
}
}
}, {$match: {"temp.0": {exists: true}}}
],
as: 'loans'
}

Aggregation on multiple collections with key in localfield

I have three collections which are inter-related
Partner
{
"_id": "5d68b91f0ef87f0c36ad1f7b",
"name": "1111 Vijay Dev",
}
Event {
"oneTime": true,
"_id": "5d6cfc09967a6f45c51d4375",
"title": "WelcomeOneEvent1",
}
User which contains data for both Partner and Event with Keys partnerId and eventId
{
"_id": ObjectId("5d68f06e3058326af09d6c53"),
"firstName": "Demo",
"partners": [
{
"_id": ObjectId("5d6cafd3e109107b83068657"),
"partnerId": ObjectId("5d4d60d6f45f1c6e723bc074")
}
],
"events": [
{
"_id": ObjectId("5d6cfc18967a6f45c51d437b"),
"eventId": ObjectId("5d6a4b882d267958eb82071c")
},
{
"_id": ObjectId("5d6cfc18967a6f45c51d437a"),
"eventId": ObjectId("5d6cfc09967a6f45c51d4375")
}
]
}
I need to get the list of all partners with users and events of those users.
I tried fetching the partners array along with users and it was successful but I am unable to fetch events detail. Below is my code for same
partnerSchema.statics.chking = function() {
return new Promise((resolve, reject) => {
this.aggregate(
[
{
$lookup: {
from: 'users',
localField: '_id',
foreignField: 'partners.partnerId',
as: 'users'
}
},
{
$unwind:{
'path': '$user.events',
'preserveNullAndEmptyArrays': true
}
},
{
$lookup: {
from: 'events',
localField: 'users.events.eventId',
foreignField: '_id',
as: 'events'
}
},
{
$project: {
name: 1,
'users._id': 1,
'users.firstName': 1,
'users.events': '$events'
}
}
],
function(err, result) {
console.log('result', result)
if (err) return reject(err)
resolve(result)
}
)
})
}
Actual result is something like this. I need to add the missing events detail like id and title:
{
"data": [
{
"_id": "5d68b91f0ef87f0c36ad1f7b",
"name": "1111 Vijay Dev",
"users": [
{
"_id": "5d6a144f498ea95bf51298ea",
"firstName": "Vijay one",
"events": []
},
{
"_id": "5d6a1459498ea95bf51298ec",
"firstName": "vijay 2",
"events": []
},
{
"_id": "5d6d046c0b6700548397e262",
"firstName": "check11",
"events": []
}
]
}
]
}
Expected:
{
"data": [
{
"_id": "5d68b91f0ef87f0c36ad1f7b",
"name": "1111 Vijay Dev",
"users": [
{
"_id": "5d6a144f498ea95bf51298ea",
"firstName": "Vijay one",
"events": [
{ "_id": "5d6cfc09967a6f45c51d4374",
"title": "WelcomeOneEvent1"},
{ "_id": "5d6cfc09967a6f45c51d4375",
"title": "WelcomeTwoEvent2"},
]
},
{
"_id": "5d6a1459498ea95bf51298ec",
"firstName": "vijay 2",
"events": []
},
{
"_id": "5d6d046c0b6700548397e262",
"firstName": "check11",
"events": []
}
]
}
]
}
Please try this :
Partner.aggregate([
{
$lookup: {
from: 'users',
localField: '_id',
foreignField: 'partners.partnerId',
as: 'users'
}
},
{
$unwind: {
'path': '$users',
'preserveNullAndEmptyArrays': true
}
},
{
$lookup: {
from: 'events',
localField: 'users.events.eventId',
foreignField: '_id',
as: 'events'
}
}, { $addFields: { 'users.events': '$events' } }, { $group: { _id: '$_id', users: { $push: '$users' }, data: { $first: '$$ROOT' } } }, { $addFields: { 'data.users': '$users' } },
{ $replaceRoot: { 'newRoot': '$data' } }, {
$project: {
name: 1,
'users._id': 1,
'users.firstName': 1,
'users.events': 1
}
}
])