Multiple $lookup and sort nested arrays in MongoDB - mongodb

I am learning mongodb, i searched for some tips at whole internet, but i still cant cant get the proper result.
The only thing i have to do is join 2 collections.
Let me introduce the problem.
COLLECTIONS
Artists
{
_id: 1,
Name: 'Artists one'
}
Albums
{
_id: 1,
title: "Album 01",
year
artists_id: 1
}
{
_id: 2,
title: "Album 02",
year: 2020,
artists_id: 1
}
Tracks
{
albums_id: 1,
track_number: 1,
title: 'Track 01',
time: 123
}
{
albums_id: 1,
track_number: 2,
title: 'Track 02',
time: 123
}
{
albums_id: 2,
track_number: 1,
title: 'Track 01',
time: 123
}
{
albums_id: 2,
track_number: 2,
title: 'Track 02',
time: 123
}
WHAT I WANT TO ACHIVE ?
a query should return result like below.
Albums should be sorted by year ascending.
Tracks should be sorted by track_number ascending (or descending whatever i wish)
{
Name: 'Artists one',
Albums: [
{
title: "Album 01",
tracks: [
{
title: 'Track 01'
},
{
title: 'Track 02'
}
]
},
{
title: "Album 02",
tracks: [
{
title: 'Track 01'
},
{
title: 'Track 02'
}
]
}
]
}
WHAT I END UP WITH ?
I can successfully print all data with sorted albums, but i don't know how to unwind tracks to sort them by track_number and group it again like in code up
db.artists.aggregate([
{
$lookup:
{
from: "albums",
localField: "_id",
foreignField: "artists_id",
as: "albums"
}
},
{
$unwind: "$albums"
},
{
$lookup:
{
from: "tracks",
localField: "albums._id",
foreignField: "albums_id",
as: "albums.tracks"
}
},
{
$sort:
{
"albums.year": 1
}
},
{
$group:
{
_id : "$_id",
"Name" : { $first: "$Name" },
albums: { $push: "$albums" }
}
},
{
$project:
{
"_id":0,
"Name":1,
"albums": {"title":1, "tracks": {"title":1}}
}
}
]).pretty()
WHAT I NEED
I know it can't be hard, i just still try to understund the aggregation framework. I will be really greatfull if someone can show me how to make this work - also if u can additionaly explain how to achive result consistent with the assumptions i mentioned before but with look:
{
Name: 'Artists one',
Albums: [
{
title: "Album 01",
tracks: ['Track 01' 'Track 02']
},
{
title: "Album 02",
tracks: ['Track 01' 'Track 02']
}
]
}
The code just would help me very much in understanding aggregation framework.

Because of sorting the query is a bit complex
db.artists.aggregate([
{
$lookup: {
from: "albums",
localField: "_id",
foreignField: "artists_id",
as: "albums"
}
},
{
$unwind: "$albums"
},
{
$lookup: {
from: "tracks",
localField: "albums._id",
foreignField: "albums_id",
as: "albums.tracks"
}
},
{
$unwind: "$albums.tracks"
},
{
$sort: {
"albums.tracks.track_number": 1
}
},
{
$group: {
_id: {
_id: "$_id",
Name: "$Name",
albumId: "$albums._id",
albumTitle: "$albums.title",
albumYear: "$albums.year"
},
albumsTracks: {
$push: "$albums.tracks"
}
}
},
{
$project: {
_id: "$_id._id",
Name: "$_id.Name",
albumId: "$_id.albumId",
albumTitle: "$_id.albumTitle",
albumYear: "$_id.albumYear",
tracks: "$albumsTracks"
}
},
{
$sort: {
albumYear: 1
}
},
{
$group: {
_id: {
_id: "$_id",
Name: "$Name"
},
Albums: {
$push: {
title: "$albumTitle",
tracks: "$tracks"
}
}
}
},
{
$project: {
_id: "$_id._id",
Name: "$_id.Name",
"Albums.tracks.title": 1
}
}
]).pretty()
In general, if you see such a overhead, it's a signal to think different structure to store your data. For example you may want to combine all data in one collection if you're sure single record won't exceed 16 mb at some point of time.

Since MongoDB 3.6, you can use conditional $lookup. For each albums, you fetch tracks.
db.artists.aggregate([
{
$lookup: {
from: "albums",
let: {
"artists_id": "$_id"
},
pipeline: [
{
$match: {
$expr: {
$eq: [
"$artists_id",
"$$artists_id"
]
}
}
},
{
$sort: {
year: 1
}
},
{
$lookup: {
from: "tracks",
let: {
"albums_id": "$_id"
},
pipeline: [
{
$match: {
$expr: {
$eq: [
"$albums_id",
"$$albums_id"
]
}
}
},
{
$sort: {
track_number: 1
}
}
],
as: "tracks"
}
},
{
$project: {
_id: 0,
title: 1,
tracks: {
$map: {
input: "$tracks",
in: "$$this.title"
}
}
}
}
],
as: "Albums"
}
},
{
$unset: "_id"
}
])
MongoPlayground

Related

MongoDB: Aggregation/Grouping for poll votes

Am trying to create a poll results aggregation
I have two collections
poll - here is one document
{
"_id": {
"$oid": "636027704f7a15587ef74f26"
},
"question": "question 1",
"ended": false,
"options": [
{
"id": "1",
"option": "option 1"
},
{
"id": "2",
"option": "option 2"
},
{
"id": "3",
"option": "option 3"
}
]
}
Vote - here is one document
{
"_id": {
"$oid": "635ed3210acbf9fd14af8fd1"
},
"poll_id": "636027704f7a15587ef74f26",
"poll_option_id": "1",
"user_id": "1"
}
and i want to perform an aggregate query to get poll results
so am doing the following query
db.vote.aggregate(
[
{
$addFields: {
poll_id: { "$toObjectId": "$poll_id" }
},
},
{
$lookup: {
from: "poll",
localField: "poll_id",
foreignField: "_id",
as: "details"
}
},
{
$group:
{
_id: { poll_id: "$poll_id", poll_option_id: "$poll_option_id" },
details: { $first: "$details" },
count: { $sum: 1 }
}
},
{
$addFields: {
question: { $arrayElemAt: ["$details.question", 0] }
}
},
{
$addFields: {
options: { $arrayElemAt: ["$details.options", 0] }
}
},
{
$group: {
_id: "$_id.poll_id",
poll_id: { $first: "$_id.poll_id" },
question: { $first: "$question" },
options: { $first: "$options" },
optionsGrouped: {
$push: {
id: "$_id.poll_option_id",
count: "$count"
}
},
count: { $sum: "$count" }
}
}
]
)
That is giving me this form of results
{ _id: ObjectId("636027704f7a15587ef74f26"),
poll_id: ObjectId("636027704f7a15587ef74f26"),
question: 'question 1',
options:
[ { id: '1', option: 'option 1' },
{ id: '2', option: 'option 2' },
{ id: '3', option: 'option 3' } ],
optionsGrouped:
[ { id: '1', count: 2 },
{ id: '2', count: 1 } ],
count: 3 }
So what am interested in i want to have the results looking like ( like merging both options & options Group)
{ _id: ObjectId("636027704f7a15587ef74f26"),
poll_id: ObjectId("636027704f7a15587ef74f26"),
question: 'question 1',
optionsGrouped:
[ { id: '1', option: 'option 1', count: 2 },
{ id: '2', option: 'option 2', count: 1 },
{ id: '3', option: 'option 3', count: 0 } ],
count: 4 }
Another question is the DB structure acceptable overall or i can represent that in a better way ?
One option is to group first and use the $lookup later, in order to fetch less data from the poll collection. After the $lookup, use $map with $cond to merge the arrays:
db.vote.aggregate([
{$group: {
_id: {poll_id: {$toObjectId: "$poll_id"}, poll_option_id: "$poll_option_id"},
count: {$sum: 1}
}},
{$group: {
_id: "$_id.poll_id",
counts: {
$push: {count: "$count", option: {$concat: ["option ", "$_id.poll_option_id"]}}
},
countAll: {$sum: "$count"}
}},
{$lookup: {
from: "poll",
localField: "_id",
foreignField: "_id",
as: "poll"
}},
{$project: {poll: {$first: "$poll"}, counts: 1, countAll: 1}},
{$project: {
optionsGrouped: {
$map: {
input: "$poll.options",
in: {$mergeObjects: [
"$$this",
{$cond: [
{$gte: [{$indexOfArray: ["$counts.option", "$$this.option"]}, 0]},
{$arrayElemAt: ["$counts", {$indexOfArray: ["$counts.option", "$$this.option"]}]},
{count: 0}
]}
]}
}
},
count: "$countAll",
question: "$poll.question"
}}
])
See how it works on the playground example
I had reworked the query to match my desires
and this query is achieving the question i have asked
db.poll.aggregate([
{
$addFields: {
_id: {
$toString: "$_id"
}
}
},
{
$lookup: {
from: "poll_vote",
localField: "_id",
foreignField: "poll_id",
as: "votes"
}
},
{
$replaceRoot: {
newRoot: {
$let: {
vars: {
count: {
$size: "$votes"
},
options: {
$map: {
input: "$options",
as: "option",
in: {
$mergeObjects: [
"$$option",
{
count: {
$size: {
$slice: [
{
$filter: {
input: "$votes",
as: "v",
cond: {
$and: [
{
$eq: [
"$$v.poll_option_id",
"$$option._id"
]
}
]
}
}
},
0,
100
]
}
}
},
{
checked: {
$toBool: {
$size: {
$slice: [
{
$filter: {
input: "$votes",
as: "v",
cond: {
$and: [
{
$eq: [
"$$v.user_id",
2
]
},
{
$eq: [
"$$v.poll_option_id",
"$$option._id"
]
}
]
}
}
},
0,
100
]
}
}
}
}
]
}
}
}
},
"in": {
_id: "$_id",
question: "$question",
count: "$$count",
ended: "$ended",
options: "$$options"
}
}
}
}
},
{
$addFields: {
answered: {
$reduce: {
input: "$options",
initialValue: false,
in: {
$cond: [
{
$eq: [
"$$this.checked",
true
]
},
true,
"$$value"
]
}
}
}
}
}
])

MongoDB: Optimal joining of one to many relationship

Here is a hypothetical case of orders and products.
'products' collection
[
{
"_id": "61c53eb76eb2dc65de621bd0",
"name": "Product 1",
"price": 80
},
{
"_id": "61c53efca0a306c3f1160754",
"name": "Product 2",
"price": 10
},
... // truncated
]
'orders' collection:
[
{
"_id": "61c53fb7dca0579de038cea8", // order id
"products": [
{
"_id": "61c53eb76eb2dc65de621bd0", // references products._id
"quantity": 1
},
{
"_id": "61c53efca0a306c3f1160754",
"quantity": 2
},
]
}
]
As you can see, an order owns a list of product ids. When I pull an order's details I also need the product details combined like so:
{
_id: ObjectId("61c53fb7dca0579de038cea8"),
products: [
{
_id: ObjectId("61c53eb76eb2dc65de621bd0"),
quantity: 1,
name: 'Product 1',
price: 80
},
{
_id: ObjectId("61c53efca0a306c3f1160754"),
quantity: 2,
name: 'Product 2',
price: 10
},
... // truncated
]
}
Here is the aggregation pipleline I came up with:
db.orders.aggregate([
{
$match: {_id: ObjectId('61c53fb7dca0579de038cea8')}
},
{
$unwind: {
path: "$products"
}
},
{
$lookup: {
from: 'products',
localField: 'products._id',
foreignField: '_id',
as: 'productDetail'
}
},
{
$unwind: {
path: "$productDetail"
}
},
{
$group: {
_id: "$_id",
products: {
$push: {$mergeObjects: ["$products", "$productDetail"]}
}
}
}
])
Given how the data is organized I'm doubting if the pipeline stages are optimal and could do better (possibility of reducing the number of stages, etc.). Any suggestions?
As already mentioned in comments the design is poor. You can avoid multiple $unwind and $group, usually the performance should be better with this:
db.orders.aggregate([
{ $match: { _id: "61c53fb7dca0579de038cea8" } },
{
$lookup: {
from: "products",
localField: "products._id",
foreignField: "_id",
as: "productDetail"
}
},
{
$project: {
products: {
$map: {
input: "$products",
as: "product",
in: {
$mergeObjects: [
"$$product",
{
$first: {
$filter: {
input: "$productDetail",
cond: { $eq: [ "$$this._id", "$$product._id" ] }
}
}
}
]
}
}
}
}
}
])
Mongo Playground

$lookup to iterate specific Fields

I am trying to join specific fields from product while performing all request in addtocart. I don't know how to update lookup for this requirement. below I have updated my product collection and add to cart collection. Can anyone suggest me how to do this?
Add to Cart Collection:
add_to_cart_products: [
{
product: ObjectId('5f059f8e0b4f3a5c41c6f54d'),
product_quantity: 5,
product_item: ObjectId('5f4dddaf8596c12de258df20'),
},
],
add_to_cart_product_total: 5,
add_to_cart_discount: 50,
Product Collection:
{
_id: ObjectId('5f059f8e0b4f3a5c41c6f54d'),
product_name: 'La Gioiosa Prosecco',
product_description: 'No Description',
product_overview: 'No Overview',
product_items: [
{
product_item_number: '781239007465',
product_price: 14.99,
product_images: ['pro03655-781239007465-1.png'],
product_item_is_active: true,
_id: ObjectId('5f4dddaf8596c12de258f021'),
},
{
product_item_number: '850651005110',
product_price: 12.99,
product_images: ['default.png'],
product_item_is_active: true,
_id: ObjectId('5f4dddaf8596c12de258df20'),
},
],
product_created_date: ISODate('2020-07-08T10:29:05.892Z'),
product_status_is_active: true,
},
In my AddToCart Schema Lookup
lookups: [
{
from: 'shop_db_products',
let: { productId: '$add_to_cart_products.product', purchaseQuantity: '$add_to_cart_products.product_quantity' },
pipeline: [
{
$match: { $expr: { $in: ['$_id', '$$productId'] } },
},
{
$lookup: {
from: 'shop_db_products',
localField: 'product_id',
foreignField: '_id',
as: 'product',
},
},
{
$project: {
product_id: '$$productId',
product_purchase_quantity: '$$purchaseQuantity',
product_name: true,
},
},
{
$unwind: '$product_id',
},
{
$unwind: '$product_purchase_quantity',
},
],
as: 'add_to_cart_products',
model: 'ProductModel',
},
],
Current Result:
"add_to_cart_products": [
{
"product_name": "Avery Coconut Porter",
"product_id": "5f059f8e0b4f3a5c41c6f54d",
"product_purchase_quantity": 5
}
],
"add_to_cart_product_total": 5,
"add_to_cart_discount": 50,
Expected Result:
"add_to_cart_products": [
{
"product_name": "Avery Coconut Porter",
"product_id": "5f059f8e0b4f3a5c41c6f54d",
"product_item":[
"product_price": 12.99,
"product_images": ["default.png"],
],
"product_purchase_quantity": 5
}
],
"add_to_cart_product_total": 5,
"add_to_cart_discount": 50,
You can try,
$unwind deconstruct add_to_cart_products array
$lookup with shop_db_products collection pass required fields in let
$match productId equal condition
$project to show required fields, and get product item from array product_items using $filter to match product_item_id, and $reduct to get specific fields from product_item
$unwind deconstruct add_to_cart_products array
$group by _id and get specific fields and construct add_to_cart_products array
db.add_to_cart.aggregate([
{ $unwind: "$add_to_cart_products" },
{
$lookup: {
from: "shop_db_products",
let: {
productId: "$add_to_cart_products.product",
purchaseQuantity: "$add_to_cart_products.product_quantity",
product_item_id: "$add_to_cart_products.product_item"
},
pipeline: [
{ $match: { $expr: { $eq: ["$_id", "$$productId"] } } },
{
$project: {
product_name: 1,
product_id: "$_id",
product_purchase_quantity: "$$purchaseQuantity",
product_item: {
$reduce: {
input: {
$filter: {
input: "$product_items",
cond: { $eq: ["$$product_item_id", "$$this._id"] }
}
},
initialValue: {},
in: {
product_price: "$$this.product_price",
product_images: "$$this.product_images"
}
}
}
}
}
],
as: "add_to_cart_products"
}
},
{ $unwind: "$add_to_cart_products" },
{
$group: {
_id: "$_id",
add_to_cart_discount: { $first: "$add_to_cart_discount" },
add_to_cart_product_total: { $first: "$add_to_cart_product_total" },
add_to_cart_products: { $push: "$add_to_cart_products" }
}
}
])
Playground

aggregation lookup and match a nested array

Hello i am trying to join two collections...
#COLLECTION 1
const valuesSchema= new Schema({
value: { type: String },
})
const categoriesSchema = new Schema({
name: { type: String },
values: [valuesSchema]
})
mongoose.model('categories', categoriesSchema )
#COLLECTION 2
const productsSchema = new Schema({
name: { type: String },
description: { type: String },
categories: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'categories',
}]
})
mongoose.model('productos', productsSchema )
Now, what i pretend to do is join these collections and have an output like this.
#Example Product Document
{
name: 'My laptop',
description: 'Very ugly laptop',
categories: ['5f55949054f3f31db0491b5c','5f55949054f3f31db0491b5b'] // these are _id of valuesSchema
}
#Expected Output
{
name: 'My laptop',
description: 'Very ugly laptop',
categories: [{value: 'Laptop'}, {value: 'PC'}]
}
This is what i tried.
{
$lookup: {
from: "categories",
let: { "categories": "$categories" },
as: "categories",
pipeline: [
{
$match: {
$expr: {
$in: [ '$values._id','$$categories']
},
}
},
]
}
}
but this query is not matching... Any help please?
You can try,
$lookup with categories
$unwind deconstruct values array
$match categories id with value id
$project to show required field
db.products.aggregate([
{
$lookup: {
from: "categories",
let: { cat: "$categories" },
as: "categories",
pipeline: [
{ $unwind: "$values" },
{ $match: { $expr: { $in: ["$values._id", "$$cat"] } } },
{
$project: {
_id: 0,
value: "$values.value"
}
}
]
}
}
])
Playground
Since you try to use the non-co-related queries, I appreciate it, you can easily achieve with $unwind to flat the array and then $match. To regroup the array we use $group. The $reduce helps to move on each arrays and store some particular values.
[
{
$lookup: {
from: "categories",
let: {
"categories": "$categories"
},
as: "categories",
pipeline: [
{
$unwind: "$values"
},
{
$match: {
$expr: {
$in: [
"$values._id",
"$$categories"
]
},
}
},
{
$group: {
_id: "$_id",
values: {
$addToSet: "$values"
}
}
}
]
}
},
{
$project: {
categories: {
$reduce: {
input: "$categories",
initialValue: [],
in: {
$concatArrays: [
"$$this.values",
"$$value"
]
}
}
}
}
}
]
Working Mongo template

How to do lookup on an aggregated collection in mongodb that is being grouped?

For some reason, I can't retrieve the author name from another collection on my aggregate query.
db.getCollection('books').aggregate([
{
$match: {
authorId: { $nin: [ObjectId('5b9a008575c50f1e6b02b27b'), ObjectId('5ba0fb3275c50f1e6b02b2f5'), ObjectId('5bc058b6ae9a2a4d6df330b1')]},
isBorrowed: { $in: [null, false] },
status: 'ACTIVE',
},
},
{
$lookup: {
from: "authors",
localField: "authorId", // key of author id in "books" collection
foreignField: "_id", // key of author id in "authors" collection
as: "bookAuthor",
}
},
{
$group: {
_id: {
author: '$authorId',
},
totalSalePrice: {
$sum: '$sale.amount',
},
},
},
{
$project: {
author: '$_id.author',
totalSalePrice: '$totalSalePrice',
authorName: '$bookAuthor.name', // I can't make this appear
_id: 0,
},
},
{ $sort: { totalSalePrice: -1 } },
])
Any advice on where I had it wrong? Thanks for the help.
Two things that are missing here: you need $unwind to convert bookAuthor from an array into single object and then you need to add that object to your $group stage (so that it will be available in next stages), try:
db.getCollection('books').aggregate([
{
$match: {
authorId: { $nin: [ObjectId('5b9a008575c50f1e6b02b27b'), ObjectId('5ba0fb3275c50f1e6b02b2f5'), ObjectId('5bc058b6ae9a2a4d6df330b1')]},
isBorrowed: { $in: [null, false] },
status: 'ACTIVE',
},
},
{
$lookup: {
from: "authors",
localField: "authorId",
foreignField: "_id",
as: "bookAuthor", // this will be an array
}
},
{
$unwind: "$bookAuthor"
},
{
$group: {
_id: {
author: '$authorId',
},
bookAuthor: { $first: "$bookAuthor" },
totalSalePrice: {
$sum: '$sale.amount',
},
},
},
{
$project: {
author: '$_id.author',
totalSalePrice: '$totalSalePrice',
authorName: '$bookAuthor.name',
_id: 0,
},
},
{ $sort: { totalSalePrice: -1 } },
])
Actually you have lost the bookAuthor field in the $group stage. You have to use $first accumulator to get it in the next $project stage.
{ "$group": {
"_id": { "author": "$authorId" },
"totalSalePrice": { "$sum": "$sale.amount" },
"authorName": { "$first": "$bookAuthor" }
}},
{ "$project": {
"author": "$_id.author",
"totalSalePrice": "$totalSalePrice",
"authorName": { "$arrayElemAt": ["$bookAuthor.name", 0] }
"_id": 0,
}}