Mongoose - Filter Subdocuments inside multiple documents and get as a plain array - mongodb

I have an ecommerce app that has Products with multiple variants. So, variants are stored inside an array in each Product Object.
[
{
title: "Test"
description: "test description",
....
....
variants: [
{
color: "red",
size: "L"
....
....
price: 500
},
{
color: "red",
size: "M"
....
....
price: 500
},
{
color: "red",
size: "S"
....
....
price: 500
},
]
}
]
Is there a way to filter these products variants by its ID and return as a plain array of variants ignoring they're from different parents but with parent details ?
[
{
color: "red",
size: "L"
....
....
price: 500,
parent: {
title: "Test"
description: "test description"
}
},
{
color: "red",
size: "M"
....
....
price: 500,
parent: {
title: "Test"
description: "test description"
}
},
{
color: "red",
size: "S"
....
....
price: 500,
parent: {
title: "Test"
description: "test description"
}
},
{
color: "orange",
size: "S"
....
....
price: 500,
parent: {
title: "Test 2"
description: "test description 2"
}
},
]

Query1
add one field named parent with the fields that are in the root, except variants
remove those fields, keep the variants
map to add the parent field inside the array variants
unwind variants and replace the root
Playmongo
aggregate(
[{"$set": {"parent": {"title": "$title", "description": "$description"}}},
{"$unset": ["title", "description"]},
{"$set":
{"variants":
{"$map":
{"input": "$variants",
"in": {"$mergeObjects": ["$$this", {"parent": "$parent"}]}}}}},
{"$unwind": "$variants"},
{"$replaceRoot": {"newRoot": "$variants"}}])
Query2
same as above just with less stages, 1 set to do the 3 first stages
Playmongo
aggregate(
[{"$set":
{"title": "$$REMOVE",
"description": "$$REMOVE",
"variants":
{"$map":
{"input": "$variants",
"in":
{"$mergeObjects":
["$$this",
{"parent":
{"title": "$title", "description": "$description"}}]}}}}},
{"$unwind": "$variants"}, {"$replaceRoot": {"newRoot": "$variants"}}])

Related

$addFields is not adding value in document

Query is as follows and result is given below:
What I want is I am adding field called name, in which I want categoryObj[0].categoryName but it is empty.
Tried categoryObj.$.categoryName but giving error.
Once name is obtained as I want i will exclude categoryObj with project opertator.
Thanks for help in advance
let itemsByCategory = await VendorItem.aggregate([
{$match: {vendor: vendorId}},
{$lookup: {
from: "vendorcategories",
localField: "category",
foreignField: "_id",
as: 'categoryDetails'
}},
{$group:{
"_id":"$category",
"count":{"$sum":1},
"items":{"$push":"$$ROOT"},
"categoryObj":{"$addToSet":"$categoryDetails"}
}},
{$project: {"items.categoryDetails":0}},
{$addFields: {"categoryName" : "$categoryObj.categoryName"}},
//{$project: {"categoryObj":0}},
]);
and the result is as follows
{
"itemsByCategory": [
{
"_id": "62296d612a1462a7d5e4b86b",
"count": 1,
"menuItems": [
{
"_id": "622971fa4fda7b4c792a7812",
"category": "62296d612a1462a7d5e4b86b",
"vendor": "62296c6f2a1462a7d5e4b863",
"item": "Dahi Chaat",
"price": 30,
"inStock": true,
"variants": [
{
"variantName": "With Sev",
"variantPrice": 40,
"_id": "622975b9f7bdf6c2a3b7703c"
}
],
"toppings": [
{
"name": "cheese",
"price": 10,
"inStock": true,
"_id": "62297766ff9f01d236c60736"
}
],
"categoryDetails": [
{
"_id": "62296d612a1462a7d5e4b86b",
"categoryName": "Snacks",
"categoryDescription": "Desciption changed!",
"vendor": "621c6c944d6d79e83219e59a",
"__v": 0
}
]
}
],
"categoryObj": [
[
{
"_id": "62296d612a1462a7d5e4b86b",
"categoryName": "Snacks",
"categoryDescription": "Desciption changed!",
"vendor": "621c6c944d6d79e83219e59a",
"__v": 0
}
]
],
"name": []
}
]
}
You can add an $unwind phase in order to "loop" all objects inside "categoryObj", but you will need to group it back afterwards:
{"$addFields": {orig_id: "$_id"}},
{"$unwind": "$categoryObj"},
{"$addFields": {"name": {"$arrayElemAt": ["$categoryObj", 0]}}},
{"$group": {_id: "$orig_id", name: {$push: "$name.categoryName"},
menuItems: {$first: "$menuItems"}, count: {$first: "count"},
}
}
See playground here:
https://mongoplayground.net/p/wsH2Y0UZ_FH

How to find documents from multiple collection with similar field value

I have two collections:
Product
{ id: "1", model: "Light1", category: "Light"},
{ id: "2", model: "Light3", category: "Light"},
{ id: "3", model: "Lock1", category: "Lock"},
Item
{ id: "1", model: "Light1", category: "Light", color: "Blue"},
{ id: "2", model: "Light2", category: "Light", color: "Blue"},
{ id: "3", model: "Lock1", category: "Lock", color: "Blue"},
{ id: "4", model: "Light3", category: "Light", color: "Blue"}
{ id: "5", model: "Lock2", category: "Lock", color: "Blue"},
I want to find documents from the Item collection containing both model and category from the product collection.
From the example above, I want to get this so called new collection:
{ id: "1", model: "Light1", category: "Light", color: "Blue"},
{ id: "3", model: "Lock1", category: "Lock", color: "Blue"},
{ id: "4", model: "Light3", category: "Light", color: "Blue"}
You can try this aggregation query:
First $lookup from Item collection to join collections. This lookup uses a pipeline where you match the desired values: Local model is equal to foreign model and local category is equal to foreign category. This produces an array as output: if there is not any match the array will be empty.
So you can $match to not shown empty result array.
And use $project to output the values you want.
db.Item.aggregate([
{
"$lookup": {
"from": "Product",
"let": {
"model": "$model",
"category": "$category"
},
"pipeline": [
{
"$match": {
"$and": [
{
"$expr": {
"$eq": [
"$model",
"$$model"
]
}
},
{
"$expr": {
"$eq": [
"$category",
"$$category"
]
}
}
]
}
}
],
"as": "result"
}
},
{
"$match": {
"result": {
"$ne": []
}
}
},
{
"$project": {
"_id": 0,
"result": 0
}
}
])
Example here

Comparing 2 fields from $project in a mongoDB pipeline

In a previous post I created a mongodb query projecting the number of elements matching a condition in an array. Now I need to filter this number of elements depending on another field.
This is my db :
db={
"fridges": [
{
_id: 1,
items: [
{
itemId: 1,
name: "beer"
},
{
itemId: 2,
name: "chicken"
}
],
brand: "Bosch",
size: 195,
cooler: true,
color: "grey",
nbMax: 2
},
{
_id: 2,
items: [
{
itemId: 1,
name: "beer"
},
{
itemId: 2,
name: "chicken"
},
{
itemId: 3,
name: "lettuce"
}
],
brand: "Electrolux",
size: 200,
cooler: true,
color: "white",
nbMax: 2
},
]
}
This is my query :
db.fridges.aggregate([
{
$match: {
$and: [
{
"brand": {
$in: [
"Bosch",
"Electrolux"
]
}
},
{
"color": {
$in: [
"grey",
"white"
]
}
}
]
}
},
{
$project: {
"itemsNumber": {
$size: {
"$filter": {
"input": "$items",
"as": "item",
"cond": {
$in: [
"$$item.name",
[
"beer",
"lettuce"
]
]
}
}
}
},
brand: 1,
cooler: 1,
color: 1,
nbMax: 1
}
}
])
The runnable example.
Which gives me this :
[
{
"_id": 1,
"brand": "Bosch",
"color": "grey",
"cooler": true,
"itemsNumber": 1,
"nbMax": 2
},
{
"_id": 2,
"brand": "Electrolux",
"color": "white",
"cooler": true,
"itemsNumber": 2,
"nbMax": 2
}
]
What I expect is to keep only the results having a itemsNumber different from nbMax. In this instance, the second fridge with _id:2 would not match the condition and should not be in returned. How can I modify my query to get this :
[
{
"_id": 1,
"brand": "Bosch",
"color": "grey",
"cooler": true,
"itemsNumber": 1,
"nbMax": 2
}
]
You can put a $match stage with expression condition at the end of your query,
$ne to check both fields should not same
{
$match: {
$expr: { $ne: ["$nbMax", "$itemsNumber"] }
}
}
Playground

How to get distinct combinations of two fields from a collection when one of the fields is in an array of subdocuments

From a collection consisting of documents representing products similar to the following:
[
{
code: "0WE3A5CMY",
name: "lorem",
category: "voluptas",
variants: [
{
color: "PapayaWhip",
stock: 17,
barcode: 4937310396997
},
{
color: "RoyalBlue",
stock: 13,
barcode: 9787252504890
},
{
color: "DodgerBlue",
stock: 110,
barcode: 97194456959791
}
]
},
{
code: "0WE3A5CMX",
name: "ipsum",
category: "temporibus",
variants: [
{
color: "RoyalBlue",
stock: 113,
barcode: 23425202111840
},
{
color: "DodgerBlue",
stock: 10,
barcode: 2342520211841
}
]
},
{
code: "0WE3A5CMZ",
name: "dolor",
category: "temporibus",
variants: [
{
color: "MaroonRed",
stock: 17,
barcode: 3376911253701
},
{
color: "RoyalBlue",
stock: 12,
barcode: 3376911253702
},
{
color: "DodgerBlue",
stock: 4,
barcode: 3376911253703
}
]
}
]
I would like to retrieve distinct combinations of variants.color and category. So the result should be:
[
{
category: 'voluptas',
color: 'PapayaWhip',
},
{
category: 'voluptas',
color: 'RoyalBlue',
},
{
category: 'voluptas',
color: 'DodgerBlue',
},
{
category: 'temporibus',
color: 'RoyalBlue',
},
{
category: 'temporibus',
color: 'DodgerBlue',
}
]
Based on some cursory research I think I will have to use an aggregate but I've never worked with those and could use some help. I've tried the solution at How to efficiently perform "distinct" with multiple keys?
I've tried the method mentioned by jcarter in the comments but it doesn't solve my problem. If I do:
db.products.aggregate([
{
$group: {
_id: {
"category": "$category",
"color": "$variants.color"
}
}
}
])
I get the result:
[
{
"_id": {
"category": "temporibus",
"color": [
"MaroonRed",
"RoyalBlue",
"DodgerBlue"
]
}
},
{
"_id": {
"category": "temporibus",
"color": [
"RoyalBlue",
"DodgerBlue"
]
}
},
{
"_id": {
"category": "voluptas",
"color": [
"PapayaWhip",
"RoyalBlue",
"DodgerBlue"
]
}
}
]
Which isn't what I need.
Since variants is an array you need to unwind it & group on two fields to get unique docs based on category + 'variants.color' combo.
As group stage results something like :
[
{
"_id": {
"category": "voluptas",
"color": "DodgerBlue"
}
},
{
"_id": {
"category": "voluptas",
"color": "PapayaWhip"
}
}
]
then using $replaceRoot stage you can make _id object field as root for each document to get desired result.
Query :
db.collection.aggregate([
{
$unwind: "$variants"
},
{
$group: { _id: { "category": "$category", "color": "$variants.color" } }
},
{
$replaceRoot: { newRoot: "$_id" }
}
])
Test : mongoplayground

Can I avoid using the same $match criteria twice when using $unwind?

Take the following data as an example:
{
_id: 1,
item: "abc",
stock: [
{ size: "S", color: "red", quantity: 25 },
{ size: "S", color: "blue", quantity: 10 },
{ size: "M", color: "blue", quantity: 50 }
]
}
{
_id: 2,
item: "def",
stock: [
{ size: "S", color: "blue", quantity: 20 },
{ size: "M", color: "blue", quantity: 5 },
{ size: "M", color: "black", quantity: 10 },
{ size: "L", color: "red", quantity: 2 }
]
}
{
_id: 3,
item: "ijk",
stock: [
{ size: "M", color: "blue", quantity: 15 },
{ size: "L", color: "blue", quantity: 100 },
{ size: "L", color: "red", quantity: 25 }
]
}
Say I'm going to filter out the stocks that matches the criteria size = 'L'. I already have a multikey index on the stock.size field.
In the aggregation pipeline, if I use the following two operations:
[{$unwind: {path: "$stock"}},
{$match: {"stock.size": "L"}}]
I will get the desired results, but when the db gets very large, the $unwind step will have to scan the whole collection, without utilizing the existing index, which is very inefficient.
If I reverse the order of the $unwind and $match operations, the $match will utilize the index to apply an early filtering, but the final result will not be as desired: it will fetch the extra stocks that are not of size L, but have sibling L-sized stocks that belong to the same item.
Would I have to use the same $match operation twice, i.e. both before and after the $unwind, to make it both utilizing the index and return the correct results?
Yes you can use $match stage twice in the aggregation pipeline but here only the first $match stage will use the index second one will do the collscan.
[
{ "$match": { "stock.size": "L" }},
{ "$unwind": { "path": "$stock" }},
{ "$match": { "stock.size": "L" }}
]
If you want to avoid the $match twice then use $filter aggregation
[
{ "$match": { "stock.size": "L" } },
{ "$addFields": {
"stock": {
"$filter": {
"input": "$stock",
"as": "st",
"cond": { "$eq": ["$stock.size", "L"] }
}
}
}}
]