lookup for tree hierarchy in mongo - mongodb

i have collections which represent a tree hiearchy meta, site and geolocation collections, one meta can have multiple sites and one site can have multiple geolocation
meta collection
{
"_id": "1",
"meta_id": 1,
"meta_name": "yankung"
}
site collection
{
"_id": "1",
"meta_id": 1,
"site_id" :2,
"site_name": "uoop"
}
geo collection
{
"_id": "1",
"site_id": 2,
"geo_id" :3,
"geo_name": "toop"
}
i have to get the final result like this
{
"_id": "1",
"meta_id": 1,
"meta_name": "yankung",
"sites": [
{
"site_id": 2,
"site_name": "uoop",
"geos:": [
{
"geo_id": 3,
"geo_name": "toop"
},
{
"geo_id": 4,
"geo_name": "toop"
}
]
},
{
"site_id": 1000,
"site_name": "uoop",
"geos:": [
{
"geo_id": 5,
"geo_name": "toop"
},
{
"geo_id": 6,
"geo_name": "toop"
}
]
}
]
}
i tried using aggregation query with lookup and unwind was able to segregate sites and geos as list , thought of getting the required result from application level, but would have to iterate through each document and add which will increase the time complexity, any help on how should i proceed?
this is what i was able to achieve
{
"_id": "1",
"meta_id": 1,
"meta_name": "yankung",
"sites": [
{ "site_id": 2, "site_name": "uoop"
},
{"site_id": 1000,"site_name": "uoop"
}
],
"geos:": [
{ "geo_id": 5,"geo_name": "toop"
},
{"geo_id": 6,"geo_name": "toop"
}
]
}

The trick is to use $lookup with join conditions and uncorrelated subqueries. By this way you can define $lookup inside $lookup.
Here's the query :
db.meta_collection.aggregate([
{
$lookup: {
from: "site_collection",
let: {
meta: "$meta_id"
},
pipeline: [
{
$match: {
$expr: {
$eq: [
"$meta_id",
"$$meta"
]
}
}
},
{
$lookup: {
from: "geo_collection",
let: {
site: "$site_id"
},
pipeline: [
{
$match: {
$expr: {
$eq: [
"$site_id",
"$$site"
]
}
}
},
],
as: "geos"
}
}
],
as: "sites"
}
}
])
You can test it here

Related

Mongodb lookup pipeline, comparing a field with an array

I have this sample data:
[
{
"customers": [
{"id": 100, "name": "a"},
{"id": 200, "name": "b"},
{"id": 300, "name": "c"},
{"id": 400, "name": "d"}
],
"sales": [
{
"sale_id": 9999,
"persons_related": [
{"id": 100},
{"id": 900},
{"id": 800}
]
},
{
"sale_id": 9998,
"persons_related": [
{"id": 500},
{"id": 550},
{"id": 560}
]
},
]
}
]
It represents two collections, customers and sales.
Imagine that I am working with the customers collection, I have selected just the customer 100
db.collection.aggregate([
{ $project: {
"customer": { "$arrayElemAt": [ "$customers", 0 ]
}}
}
Which returns:
[
{
"_id": ObjectId("5a934e000102030405000000"),
"customer": {
"id": 100,
"name": "a"
}
}
]
And I want to find up the sales where this id appears, so I lookup against the same collection, adding this stage to the pipeline:
{ $lookup: {
from: "collection",
let: { id: "$customer.id" },
pipeline: [
{ $match: {
$and: [
{ $expr: { $in: [ "$$id", "$sales.persons_related.id" ] } }
]
}}
],
as: "sales"
}
}
I need to use this lookup version (the one with let and pipeline, and not the other one with
localField/foreignField) because I need to add additional filters in the match stage of the pipeline. However, this part:
{ $expr: { $in: [ "$$id", "$sales.persons_related.id" ] } }
Doesn't work as expected, I have tried with other operators ($eq) with same result.
The expected output (using a pipeline in the lookup) should be:
[
{
"_id": ObjectId("5a934e000102030405000000"),
"customer": {
"id": 100,
"name": "a"
},
"sales": [
{
"sale_id": 9999,
"persons_related": [
{"id": 100},
{"id": 900},
{"id": 800}
]
}
]
}
]
Can you please lend me a hand? You can test on this mongo playground
Just FYI mongoplayground provides a multiple collection option, so instead of trying to hack your syntax to simulate it you can just us the dropdown at the top right side to change it.
Your syntax is fine, here is a working playground example:
db.customers.aggregate([
{
$match: {
id: 100
}
},
{
$lookup: {
from: "sales",
let: {
id: "$id"
},
pipeline: [
{
$match: {
$and: [
{
$expr: {
$in: [
"$$id",
"$persons_related.id"
]
}
}
]
}
}
],
as: "sales"
}
}
])
$lookup also flattens arrays so you can just use the simpler syntax for it and it simplifies the code:
db.customers.aggregate([
{
$match: {
id: 100
}
},
{
$lookup: {
from: "sales",
localField: "id",
foreignField: "persons_related.id",
as: "sales"
}
}
])
Mongo Playground

MongoDB $lookup on array of objects

Categories
{
"_id" : ObjectId("61740086893f048528d166b9"),
"name": "Category1",
"tracks" : [
"61c65353565a2d9a1cd3020d",
"61c74518962dc3efb96c3438",
"61c74775703176a6f72df444"
]
}
Tracks
{
"_id" : ObjectId("61c65353565a2d9a1cd3020d"),
"name" : "Track1",
"categoryId" : ObjectId("61740086893f048528d166b9"),
"creatorId" : ObjectId("61c6478304e98ed63e8ee7d3"),
"thumbnailId" : ObjectId("61c65353565a2d9a1cd3020c"),
"plays" : [],
"media" : {
"type" : "wav",
"url" : ""
},
"status" : "approved",
"downloads" : [],
"uploadedDate" : 1640387411
}
Assuming that I have 5 categories and each category has many tracks ID, I wanna get N last tracks for each category so I used this code below
categories.aggregate([
{
$project: {
tracks: { $slice: ["$tracks", -2] },
},
},
]
And the response is
[
{
"_id": "61740086893f048528d166b9",
"tracks": [
"61c74518962dc3efb96c3438",
"61c74775703176a6f72df444"
]
},
{
"_id": "61740094893f048528d166c1",
"tracks": []
},
{
"_id": "617400a0893f048528d166cb",
"tracks": []
}
]
So far it's good, but the question is how can I replace each category's tracks from an array of IDs to an array of objects?
I tried $loopup but I probably didn't implement the localField correctly.
Expected result
[
{
"_id": "61740086893f048528d166b9",
"tracks": [
{
"_id": ObjectId("61c74518962dc3efb96c3438")
...
},
{
"_id": ObjectId("61c74775703176a6f72df444")
...
}
]
},
{
"_id": "61740094893f048528d166c1",
"tracks": []
},
{
"_id": "617400a0893f048528d166cb",
"tracks": []
}
]
***** UPDATE *****
I'm trying to replace the creatorId by createdBy which is an object of the users from the users collection
Users
{
"_id": ObjectId("61c6478304e98ed63e8ee7cb"),
"email": "USER888#gmail.com",
"username": "USER999",
"tracks": [
ObjectId("61c65353565a2d9a1cd3020d"),
],
}
The expected result should be
[
{
"_id": "61740086893f048528d166b9",
"tracks": [
{
"_id": ObjectId("61c74518962dc3efb96c3438"),
"createdBy": {
"_id": "userId"
...
},
...
},
{
"_id": ObjectId("61c74775703176a6f72df444"),
"createdBy": {
"_id": "userId"
...
}
...
}
]
},
{
"_id": "61740094893f048528d166c1",
"tracks": []
},
{
"_id": "617400a0893f048528d166cb",
"tracks": []
}
]
In addition to the solution below by ray, I added the code here https://mongoplayground.net/p/8AjmnL-vhtz
The createdBy is at the top level but not under every track
$lookup is the correct way for you to find the corresponding object in Tracks collection. Why your code does not work is that you are storing strings in tracks array in Categories collection; while the _id of Tracks collection is ObjectId. There will be no $lookup result as the datatypes do not match. What you can do is converting the strings to ObjectId by using $toObjectId in a $map, and then do the $lookup
db.categories.aggregate([
{
$project: {
tracks: {
$slice: [
"$tracks",
-2
]
}
}
},
{
$project: {
tracks: {
"$map": {
"input": "$tracks",
"as": "t",
"in": {
"$toObjectId": "$$t"
}
}
}
}
},
{
"$lookup": {
"from": "tracks",
let: {
t: "$tracks"
},
pipeline: [
{
$match: {
$expr: {
"$in": [
"$_id",
"$$t"
]
}
}
}
],
"as": "tracks"
}
}
])
Here is the Mongo playground for your reference.

Display the conditionnal size of an array with the others fields of a mongodb document

I have a collection of fridges and I would like to have some fields from each fridge matching a condition plus the 'conditionnal size' of the items in this fridge.
This is an example of my DB :
db={
"fridges": [
{
_id: 1,
items: [
{
itemId: 1,
name:"beer"
},
{
itemId: 2,
name: "chicken"
}
],
brand:"Bosch",
size:195,
cooler:true,
color:"grey"
},
{
_id: 2,
items: [
{
itemId: 1,
name:"beer"
},
{
itemId: 2,
name: "chicken"
},
{
itemId: 3,
name: "lettuce"
}
],
brand:"Electrolux",
size:200,
cooler:true,
color:"white"
},
]
}
I want to get fridges with these mutuals conditions ('and' condition):
brand is $in ["Bosch","Samsung"]
color is $in ["grey","white"]
In addition :
The number of items with a name $in ["beer","lettuce"]
And finally :
Removing some fields like the size and items of the result.
In our example, the excepted output would be :
{
_id:1
itemsNumber:1,
brand:"Bosch",
cooler:true,
color:"grey"
}
Explanations :
We removed the field items and size, itemsNumber counts the number of beers and lettuce from items array. And we only keep the first fridge its brand is Bosch and it's grey.
This what I have so far :
db.fridges.aggregate([
{
"$match": {
$and: [
{
"brand": {
$in: [
"Bosch",
"Samsung"
]
}
},
{
"color": {
$in: [
"grey",
"white"
]
}
}
]
}
},
{
"$project": {
"itemsNumber": {
$size: "$items" // This is not good
},
brand: 1,
cooler: 1,
color: 1
}
}
])
Which returns me :
[
{
"_id": 1,
"brand": "Bosch",
"color": "grey",
"cooler": true,
"itemsNumber": 2
}
]
Counting the items matching with either beer or lettuce is my main problem.
This is an executable example.
Thanks in advance !
I found out how to make it work. Thank you #joe for suggesting to use filter this was indeed the solution.
Here is the complete query :
db.fridges.aggregate([
{
$match: {
$and: [
{
"brand": {
$in: [
"Bosch",
"Samsung"
]
}
},
{
"color": {
$in: [
"grey",
"white"
]
}
}
]
}
},
{
$project: {
"itemsNumber": {
"$filter": {
"input": "$items",
"as": "item",
"cond": {
$in: [
"$$item.name",
[
"beer",
"lettuce"
]
]
}
}
},
brand: 1,
cooler: 1,
color: 1
}
}
])
Runnable example.

Lookup for a referenced document in an array of embedded documents

I got two collections.
One contains an array of objects. These objects own a field with an id to a document in another collection.
The goal is to "replace" the ref by the document. Sounds simple but I have no clue how to archive this.
E.G.
Collection "Product"
{ "_id": 1,
"alias": "ProductA"
},
{ "_id": 2,
"alias": "ProductC"
}
Collection "Order"
{ "_id": 5765,
"cart": [
{
"product": 1,
"qty": 7
}, {
"product": 2,
"qty": 6
}
]
}
What I need by a query is this:
{ "_id": 5765,
"cart": [
{
"product": {
"_id": 1,
"alias": "ProductA"
},
"qty": 7
}, {
"product": {
"_id": 2,
"alias": "ProductC"
},
"qty": 6
}
]
}
I tried a simple lookup, but the array will only contains the products. What do I need to change?
{
$lookup: {
from: "products",
let: {
tmp: "$cart.product"
},
pipeline: [
{
$match: {
$expr: {
$in: ["$_id", "$$tmp"]
}
}
}
],
as: "cart.product"
}
}
Thanks for your help.
I added a new $addFields stage to transform the output from the $lookup stage - it gets the desired output:
db.order.aggregate([
{
$lookup: {
from: "product",
let: {
tmp: "$cart.product"
},
pipeline: [
{
$match: {
$expr: {
$in: ["$_id", "$$tmp"]
}
}
}
],
as: "products"
}
},
{
$addFields: {
cart: {
$map: {
input: "$cart", as: "ct",
in: {
product: {
$arrayElemAt: [
{ $filter: {
input: "$products", as: "pr",
cond: {
$eq: [ "$$ct.product", "$$pr._id" ]
}
}
}, 0 ]
},
qty: "$$ct.qty"
}
}
}
}
}
] ).pretty()

After applying $filter in mongo, project specific nested array attributes

The collection I'm trying to query has documents with the following structure -
{
"_id": 1,
"topA": "topAValue",
"topB": "topBValue",
"topC": "topCValue",
"nestedDocArray": [
{
"attr1": "a",
"attr2": "b",
"attr3": "c"
},
{
"attr1": "a5",
"attr2": "b5",
"attr3": "c5"
},
{
"attr1": "a1000",
"attr2": "b1000",
"attr3": "c1000"
}
]
}
I'm trying to query this document with "_id": 1 with a requirement to project only certain attributes. In addition to this, the requirement is to only fetch nestedDocArray which matches the condition "attr1": "a5".
The query I tried is as below -
db.testCollection.aggregate(
[
{
"$match": {
"_id": 1
}
},
{
"$project": {
"topA": 1,
"nestedDocArray": {
"$filter": {
"input": "$nestedDocArray",
"as": "nestedDocArray",
"cond": {
"$eq": [
"$$nestedDocArray.attr1",
"a5"
]
}
}
}
}
}
]
);
The response of this query looks something like below -
{
"_id": 1,
"topA": "topAValue",
"nestedDocArray": [
{
"attr1": "a5",
"attr2": "b5",
"attr3": "c5"
}
]
}
This is fine. This has managed to project attributes topA and nestedDocArray.
I further want to only project nestedDocArray.attr2.
The output i'm looking for is like below.
{
"_id": 1,
"topA": "topAValue",
"nestedDocArray": [
{
"attr2": "b5"
}
]
}
How can I modify the query to achieve the same?
You can use $map with $filter to reshape your data:
db.testCollection.aggregate([
{
$match: { _id: 1 }
},
{
$project: {
topA: 1,
nestedDocArray: {
$map: {
input: {
$filter: {
input: "$nestedDocArray",
as: "nestedDocArray",
cond: {
$eq: [ "$$nestedDocArray.attr1", "a5" ]
}
}
},
as: "item",
in: {
attr2: "$$item.attr2"
}
}
}
}
}
])