How to group same record into multiple groups using mongodb aggregate pipeline - mongodb

I have a two collections.
OrgStructure (visualise this as a tree structure)
Example Document:
{
"id": "org1",
"nodes": [
{
"nodeId": "root",
"childNodes": ["child1"]
},
{
"nodeId": "child1",
"childNodes": ["child2"]
},
{
"nodeId": "child2",
"childNodes": []
}
]
}
Activity
Example Document:
[
{
"id":"A1",
"orgUnit": "root"
},
{
"id":"A2",
"orgUnit": "child1"
},
{
"id":"A3",
"orgUnit": "child2"
}
]
Now my expectation is to group activities by orgUnit such a way that by considering the child nodes as well.
Here i don't want to do a lookup and i need to consider one OrgStructure document as an input, so that i can construct some condition using the document such a way that the query will return the below result.
Expected result
[
{
"_id": "root",
"activities": ["A1","A2","A3"]
},
{
"_id": "child1",
"activities": ["A2","A3"]
},
{
"_id": "child2",
"activities": ["A3"]
}
]
So im ecpecting an aggregate query something like this
{
"$group": {
"_id": {
"$switch": {
"branches": [
{
"case": {"$in": ["$orgUnit",["root","child1","child2"]]},
"then": "root"
},
{
"case": {"$in": ["$orgUnit",["child1","child2"]]},
"then": "child1"
},
{
"case": {"$in": ["$orgUnit",["child2"]]},
"then": "child2"
}
],
"default": null
}
}
}
}
Thanks in advance!

You will need 2 steps:
create another collection nodes for recursive lookup. The original OrgStructure is hard to perform $graphLookup
db.OrgStructure.aggregate([
{
"$unwind": "$nodes"
},
{
"$replaceRoot": {
"newRoot": "$nodes"
}
},
{
$out: "nodes"
}
])
Perform $graphLookup on nodes collection to get all child nodes. Perform $lookup to Activity and do some wrangling.
db.nodes.aggregate([
{
"$graphLookup": {
"from": "nodes",
"startWith": "$nodeId",
"connectFromField": "childNodes",
"connectToField": "nodeId",
"as": "nodesLookup"
}
},
{
"$lookup": {
"from": "Activity",
"let": {
nodeId: "$nodesLookup.nodeId"
},
"pipeline": [
{
$match: {
$expr: {
$in: [
"$orgUnit",
"$$nodeId"
]
}
}
},
{
$group: {
_id: "$id"
}
}
],
"as": "activity"
}
},
{
$project: {
_id: "$nodeId",
activities: "$activity._id"
}
}
])
Here is the Mongo playground for your reference.

Related

Filter documents that have id in another collection in MongoDB with aggregation framework

So I have two collection. collectionA and collectionB
collection A has following documents
db={
"collectiona": [
{
"_id": "6173ddf33ed09368a094e68a",
"title": "a"
},
{
"_id": "61wefdf33ed09368a094e6dc",
"title": "b"
},
{
"_id": "61wefdfewf09368a094ezzz",
"title": "c"
},
],
"collectionb": [
{
"_id": "6173ddf33ed0wef368a094zq",
"collectionaID": "6173ddf33ed09368a094e68a",
"data": [
{
"userID": "123",
"visibility": false,
"response": false
},
{
"userID": "2345",
"visibility": true,
"response": true
}
]
},
{
"_id": "6173ddf33ed09368awef4e68g",
"collectionaID": "61wefdf33ed09368a094e6dc",
"data": [
{
"userID": "5678",
"visibility": false,
"response": false
},
{
"userID": "674",
"visibility": true,
"response": false
}
]
}
]
}
So What I need is documents from collection A which has response false in collection B
and document should be sorted by first the ones that have visibility false and then the ones that have visibility true
for eg. userID : 123 should get 3 documents
{
"_id": "6173ddf33ed09368a094e68a",
"title": "a"
},
{
"_id": "61wefdf33ed09368a094e6dc",
"title": "b"
},
{
"_id": "61wefdfewf09368a094ezzz",
"title": "c"
},
whereas userID 2345 should get two
{
"_id": "61wefdf33ed09368a094e6dc",
"title": "b"
},
{
"_id": "61wefdfewf09368a094ezzz",
"title": "c"
},
User 674 will receive 3 objects from collection A but second would be in the last as it has visibility true for that document
{
"_id": "6173ddf33ed09368a094e68a",
"title": "a"
},
{
"_id": "61wefdfewf09368a094ezzz",
"title": "c"
},
{
"_id": "61wefdf33ed09368a094e6dc",
"title": "b"
},
MongoDB Playground link : https://mongoplayground.net/p/3rLry0FPlw-
Really appreciate the help. Thanks
You can start from collectionA:
$lookup the collectionB for the record related to the user specified
filter out collectionB documents according to response
assign a helper sortrank field based on the visibility and whether collectionaID is a match
$sort according to sortrank
wrangle back to the raw collection A
db.collectiona.aggregate([
{
"$lookup": {
"from": "collectionb",
let: {
aid: "$_id"
},
"pipeline": [
{
$unwind: "$data"
},
{
$match: {
$expr: {
$and: [
{
$eq: [
"$data.userID",
"2345"
]
},
{
$eq: [
"$collectionaID",
"$$aid"
]
}
]
}
}
}
],
"as": "collB"
}
},
{
$match: {
"collB.data.response": {
$ne: true
}
}
},
{
"$unwind": {
path: "$collB",
preserveNullAndEmptyArrays: true
}
},
{
"$addFields": {
"sortrank": {
"$cond": {
"if": {
$eq: [
"$collB.data.visibility",
false
]
},
"then": 1,
"else": {
"$cond": {
"if": {
$eq: [
"$collB.collectionaID",
"$_id"
]
},
"then": 3,
"else": 2
}
}
}
}
}
},
{
$sort: {
sortrank: 1
}
},
{
$project: {
collB: false,
sortrank: false
}
}
])
Here is the Mongo playground for your reference.

How do I use a different condition if there are empty results on the condition of a $match of a $lookup?

Using the mongoDb Aggregation framework; suppose I wanted to $lookup a set of results in another collection with a condition that if returned no results - would then return the results of another condition. This is what I have.
srp collection
[
{
"_id": ObjectId("5fb6727790f41fef3ee7db87"),
"dates_e": {
"from": ISODate("2021-10-01"),
"to": ISODate("2021-10-03")
}
},
{
"_id": ObjectId("5f034bfa4c0000abdfc7df2e"),
"dates_e": {
"from": ISODate("2021-03-10"),
"to": ISODate("2021-03-15")
}
},
..
]
uth collection
[
{
"_id": ObjectId("5fb6727790f41fef3ee7db88"),
"dateTime": ISODate("2021-10-01"),
"res": 1.7
},
{
"_id": ObjectId("5fb6727790f41fef3ee7db89"),
"dateTime": ISODate("2021-10-02"),
"res": 0.5
},
..
]
The aggregation query (on the srp collection):
[
{
$match: { "_id": ObjectId("5fb6727790f41fef3ee7db87") }
},
{
$lookup: {
"from": "uth",
"let": {
"fromDate": "$dates_e.from",
"toDate": "$dates_e.to"
},
"pipeline": [
$match: {
$expr: {
$and: [
{
$gte: ["$varData_e.dateTime", "$$fromDate"]
},
{
$lt: ["$varData_e.dateTime", "$$toDate"]
}
]
}
}
],
"as": "uth_e"
}
}
]
Which would return:
[
{
"_id": ObjectId("5fb6727790f41fef3ee7db87"),
"dates_e": {
"from": ISODate("2021-10-01"),
"to": ISODate("2021-10-03")
},
"uth_e": [
{
"_id": ObjectId("5fb6727790f41fef3ee7db88"),
"dateTime": ISODate("2021-10-01"),
"res": 1.7
},
{
"_id": ObjectId("5fb6727790f41fef3ee7db89"),
"dateTime": ISODate("2021-10-02"),
"res": 0.5
},
{
"_id": ObjectId("5fb6727790f41fef3ee7db90"),
"dateTime": ISODate("2021-10-03"),
"res": 2.8
}
]
]
So this works just fine. However if the $match was "_id": ObjectId("5f034bfa4c0000abdfc7df2e") and there weren't any results returned (on the $lookup from uth) then I would like to return a set of results for a broader condition:
[
{
$match: { "_id": ObjectId("5f034bfa4c0000abdfc7df2e") }
},
{
$lookup: {
"from": "uth",
"let": {
"fromDate": "$dates_e.from",
"toDate": "$dates_e.to"
},
"pipeline": [
$match: {
$expr: {
$gte: ["$varData_e.dateTime", "$$fromDate"]
}
}
],
"as": "uth_e"
}
}
]
Any help appreciated!

$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.

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'
}

mongodb $lookup 3 level nested document

I'm new to mongo and struggling mightily with the following.
In my mongodb database, there are 3 collections and structured as below.
lv1:
{
"_id": ObjectId("58650f1abbf1cd8804d0abde"),
"name": "lv1_aaa"
}
lv2:
{
"_id": ObjectId("5ba45de41e78c7eb3fdfbfa6"),
"lv1_id": ObjectId("58650f1abbf1cd8804d0abde"),
"name": "lv2_bbb"
}
lv3:
{
"_id": ObjectId("5ba45de41e78c7eb3fdfbfa6"),
"lv1_id": ObjectId("58650f1abbf1cd8804d0abde"),
"lv2_id": ObjectId("58d8c3e1bbf1cd7436117bd6"),
"name": "lv3_ccc"
}
How can I get a data structure below use $lookup
[
{
"_id": ObjectId("58650f1abbf1cd8804d0abde"),
"name": "lv1_aaa",
"children": [
{
"_id": ObjectId("5ba45de41e78c7eb3fdfbfa6"),
"lv1_id": ObjectId("58650f1abbf1cd8804d0abde"),
"name": "lv2_bbb",
"children": [
{
"_id": ObjectId("5ba45de41e78c7eb3fdfbfa6"),
"lv1_id": ObjectId("58650f1abbf1cd8804d0abde"),
"lv2_id": ObjectId("58d8c3e1bbf1cd7436117bd6"),
"name": "lv3_ccc"
},
......
]
},
......
]
},
......
]
Any help would be greatly appreciated!
You can try below aggregation with mongodb 3.6 and above
db.lv1.aggregate([
{ "$sort": { _id: 1 } },
{ "$lookup": {
"from": "lv2",
"let": { "lv1_id": "$_id" },
"pipeline": [
{ "$sort": { index: 1 } },
{ "$match": { "$expr": { "$eq": [ "$lv1_id", "$$lv1_id" ] } } },
{ "$lookup": {
"from": "lv3",
"let": { "lv2_id": "$_id" },
"pipeline": [
{ "$sort": { index: 1 } },
{ "$match": { "$expr": { "$eq": [ "$lv2_id", "$$lv2_id" ] } } }
],
"as": "children"
}}
],
"as": "children"
}}
]);