Filter query for fetching product based on categories in MERN application? - mongodb

This is my Product Schema
const product = mongoose.Schema({
name: {
type: String,
},
category:{
type: ObjectId,
ref: Category
}
}
I want to return the product based on filters coming from the front end.
For example: Lets consider there are 2 categories Summer and Winter. When the user wants to filter product by Summer Category an api call would me made to http://localhost:8000/api/products?category=summer
Now my question is, how do I filter because from the frontend I am getting category name and there is ObjectId in Product Schema.
My attempt:
Project.find({category:categoryName})

If I've understood correctly you can try one of these queries:
First one is using pipeline into $lookup to match by category and name in one stage like this:
yourModel.aggregate([
{
"$lookup": {
"from": "category",
"let": {
"category": "$category",
},
"pipeline": [
{
"$match": {
"$expr": {
"$and": [
{
"$eq": [
"$id",
"$$category"
]
},
{
"$eq": [
"$name",
"Summer"
]
}
]
}
}
}
],
"as": "categories"
}
},
{
"$match": {
"categories": {
"$ne": []
}
}
}
])
Example here
Other way is using normal $lookup and then $filter the array returned by join.
yourModel.aggregate([
{
"$lookup": {
"from": "category",
"localField": "category",
"foreignField": "id",
"as": "categories"
}
},
{
"$match": {
"categories": {
"$ne": []
}
}
},
{
"$set": {
"categories": {
"$filter": {
"input": "$categories",
"as": "c",
"cond": {
"$eq": [
"$$c.name",
"Summer"
]
}
}
}
}
}
])
Example here
Note how both queries uses $match: { categories: { $ne : []}}, this is because if the lookup doesn't find any result it returns an empty array. By the way, this stage is optional.
Also you can add $unwind or $arrayElemAt index 0 or whatever to get only one value from the array.
Also, to do it in mongoose you only need to replace "Summer" with your variable.

Related

MongoDB nested aggregation

So, I have two collections User and Book, and I want to aggregate them to receive an output (shown at the end of the post) for a specific User ID.
below are the collections:
User
Each document contains User ID, Name of the User and an array containing ID of a document from Book collection and a Boolean property read.
[
{
_id: ObjectId('adfa2sd5'),
name: "Mia",
books: [
{
bid: "154854",
read: true
},
{
bid: "5475786",
read: false
}
]
},
{
_id: ObjectId('uai5as5a'),
name: "Jack",
books: [
{
bid: "5475786",
read: true
}
]
}
]
Book
Each document possesses a book ID and name of the book.
[
{
_id: ObjectId('154854'),
name: "The Great Gatsby"
},
{
_id: ObjectId('5475786'),
name: "Frankenstein"
},
]
Output:
The output contains the User ID, along an array book_list which contains detail of each book (id, name) from the documents of Book collection based on the books.bid from User document and read field which was along books.bid.
[
{
_id: ObjectId('adfa2sd5'),
book_list: [
{
_id: ObjectId('154854'),
name: "The Great Gatsby",
read: true
},
{
_id: ObjectId('5475786'),
name: "Frankenstein",
read: false
}
]
}
]
Here's one way you could do it.
db.users.aggregate([
{
"$match": {
"_id": ObjectId("fedcba9876543210fedcba98")
}
},
{
"$lookup": {
"from": "books",
"localField": "books.bid",
"foreignField": "_id",
"as": "bookList"
}
},
{
"$set": {
"books": {
"$map": {
"input": "$books",
"as": "book",
"in": {
"$mergeObjects": [
"$$book",
{
"name": {
"$getField": {
"field": "name",
"input": {
"$first": {
"$filter": {
"input": "$bookList",
"cond": {"$eq": ["$$this._id", "$$book.bid"]}
}
}
}
}
}
}
]
}
}
}
}
},
{"$unset": ["bookList", "name"]}
])
Try it on mongoplayground.net.

MongoDB : How to loop on a field in order to lookup each value?

I need to loop on products field in order to lookup on products collection and check product category. If category is equal some value, i m adding a new Field on my current document.
Here a sample of my first document :
{
_id:(objId),
'products':[
{productId:987678},
{productId:3456765}
}
And my products documents :
{_id:(objId), category:1, name:2}
If category is correct, i use addField to add this on my first document:
category:true;
I can't figure out how to do this. Anyone can help me, please ?
https://mongoplayground.net/p/_KeECXkJTfB , here we have unwinded products before we do lookup then compared the category found and then we set the category true if found.
db.product.aggregate([
{
"$unwind": "$products"
},
{
"$lookup": {
"from": "category",
"localField": "products.productId",
"foreignField": "_id",
"as": "inventory_docs"
}
},
{
"$unwind": {
path: "$inventory_docs",
preserveNullAndEmptyArrays: true
}
},
{
"$addFields": {
"products.category": {
$cond: {
if: {
"$gt": [
{
$strLenCP: {
"$toString": {
"$ifNull": [
"$inventory_docs.category",
""
]
}
}
},
0
]
},
then: true,
else: false
}
}
}
}
])

$match in $lookup pipeline always returns all the documents, not filtering

I have two collections.
First Collection
{ _id:"601d07fece769400012f1280",
FieldName:"Employee",
Type:"Chapter" }
Second Collection
_id : "601d11905617082d7049153a",
SurveyId : "601d118e5617082d70491539",
TemplateName : "",
Elements : [
{
_id : "601d07fece769400012f1280",
FieldName : "Employee",
Type : "Chapter"
},
{
_id : "601d07fece769400012f1281",
FieldName : "Contract",
Type : "Chapter"
}]
When I do the lookup
'$lookup': {
'from': 'SecondCollection',
'localField': 'FieldName',
'foreignField': 'Elements.FieldName',
'as': 'SurveyInfo'
}
I will get the correct result, but I get the "Total size of documents in SecondCollection matching pipeline's $lookup stage exceeds 16793600 bytes" sometimes.
So I changed my approach to join the second collection with the pipeline, so I get only the field I need.
"from": 'SecondCollection',
"let": { "fieldname": "$fieldname" },
"pipeline": [
{ "$match":
{ "$expr":
{ "$eq": ["$elements.fieldname", "$$fieldname"] }}},
{ "$project": { "SurveyId": 1}}
],
"as": 'SurveyInfo'
Now the problem is this returns all the SecondCollection documents. Not returning the matching documents.
I am would like to get the below result
_id:"601d07fece769400012f1280",
FieldName:"Employee",
Type:"Chapter",
SurveyInfo: [
{
_id:"601d11905617082d7049153a",
SurveyId:"601d118e5617082d70491539"
}
]
I am not able to figure out the issue. Please help me.
Few Fixes,
You have to try $in instead of $eq because $Elements.FieldName will return array of string
Need to correct fields name in let and $match condition
db.FirstCollection.aggregate([
{
"$lookup": {
"from": "SecondCollection",
"let": { "fieldname": "$FieldName" },
"pipeline": [
{ "$match": { "$expr": { "$in": ["$$fieldname", "$Elements.FieldName"] } } },
{ "$project": { "SurveyId": 1 } }
],
"as": "SurveyInfo"
}
}
])
Playground
To match nested condition, you can try,
$reduce to iterate loop of Elements.Children.FieldName nested array and we are going to merge nested level array in single array of sting, using $concatArrays
db.FirstCollection.aggregate([
{
"$lookup": {
"from": "SecondCollection",
"let": { "fieldname": "$FieldName" },
"pipeline": [
{
"$match": {
"$expr": {
"$in": [
"$$fieldname",
{
$reduce: {
input: "$Elements.Children.FieldName",
initialValue: [],
in: { $concatArrays: ["$$this", "$$value"] }
}
}
]
}
}
},
{ "$project": { "SurveyId": 1 } }
],
"as": "SurveyInfo"
}
}
])
Playground

MongoDB add field with keys from object

I have the following two collections:
{
"organizations": [
{
"_id": "1",
"name": "foo",
"users": { "1": "admin", "2": "member" }
},
{
"_id": "2",
"name": "bar",
"users": { "1": "admin" }
}
],
"users": [
{
"_id": "1",
"name": "john smith"
},
{
"_id": "2",
"name": "bob johnson"
}
]
}
The following query works to merge the users into members when I just use an array of the user ids to match, however, the users prop is an object.
{
"collection": "organizations",
"command": "aggregate",
"query": [
{
"$lookup": {
"from": "users",
"localField": "users",
"foreignField": "_id",
"as": "members"
}
}
]
}
What I'm hoping to do is lookup by id then create a members array from the results with the user object including the role (value of the users objects:
{
"_id": "1",
"name": "foo",
"users": {
"1": "admin",
"2": "member"
},
"members": [
{
"_id": "1",
"name": "john smith",
"role": "admin"
},
{
"_id": "2",
"name": "bob johnson",
"role": "user"
}
]
}
Here's the sandbox I have setup: https://mongoplayground.net/p/yhRpeRvJf3u
You really need to change your schema design, this will cause the performance on retrieving data,
$addFields to add new field usersArray convert users object to array using $objectToArray, the format will be k(key) and v(value),
$lookup to join users collection, set localField name to usersArray.k
$addFields, remove usersArray field using $$REMOVE,
$map iterate loop of members array and $reduce to iterate loop of usersArray and get matching role as per _id and merge current fields and role field using $mergeObjects
db.organizations.aggregate([
{
$addFields: {
usersArray: {
$objectToArray: "$users"
}
}
},
{
"$lookup": {
"from": "users",
"localField": "usersArray.k",
"foreignField": "_id",
"as": "members"
}
},
{
$addFields: {
usersArray: "$$REMOVE",
members: {
$map: {
input: "$members",
as: "m",
in: {
$mergeObjects: [
"$$m",
{
role: {
$reduce: {
input: "$usersArray",
initialValue: "",
in: { $cond: [{ $eq: ["$$this.k", "$$m._id"] }, "$$this.v", "$$value"] }
}
}
}
]
}
}
}
}
}
])
Playground
First of all, the problem with your query is you want to use a KEY to do the $lookup, then the members field always gonna be empty.
You are trying to use users as local field, but users is an object, so you need the key (users.1, users.2, ... )
To do this you need to use $objectToArray, which create an object array with two fields: k and v for key and value. So now, you can $lookup with the field users.k.
To get the query you need $unwind before $lookup because you also want the users filed into the new document.
With the new object created using $objectToArray, you can do $unwind to get the values in differents documents. And then $lookup to get the "join".
Here, localField uses the value k created by $objectToArray (the object key).
After that, $set to add the field with the role and $group again into one document.
Ive used _id to get the values without changes between stages, and into members push the members in each collection.
And then, $project to output the values you want. In this case, tha calues "stored" into _id and the array members in "one level" using $reduce.
So, the query you need I think is this:
db.organizations.aggregate([
{
"$match": {
"_id": "1"
}
},
{
"$set": {
"usersArray": {
"$objectToArray": "$users"
}
}
},
{
"$unwind": "$usersArray"
},
{
"$lookup": {
"from": "users",
"localField": "usersArray.k",
"foreignField": "_id",
"as": "members"
}
},
{
"$set": {
"members.role": "$usersArray.v"
}
},
{
"$group": {
"_id": {
"_id": "$_id",
"users": "$users",
"name": "$name"
},
"members": {
"$push": "$members"
}
}
},
{
"$project": {
"members": {
"$reduce": {
"input": "$members",
"initialValue": [],
"in": {
"$concatArrays": [
"$$value",
"$$this"
]
}
}
},
"users": "$_id.users",
"name": "$_id.name",
"_id": "$_id._id"
}
}
])
Example here

Merge the original array of objects into the "as" field after a $lookup

I have a hero collection where each hero document looks like the following:
{
_id:'the-name-of-the-hero',
name: 'Name of Hero',
(...), //other properties to this hero
relations: [
{
hero: 'the-id-of-another-hero',
relationType: 'trust'
},
{
hero: 'yet-another-id-of-another-hero',
relationType: 'hate'
}
]
}
The relations.hero points to an _id of another hero. I needed to grab some more information of the related heroes, therefore I used aggregate $lookup to match each against the "hero" collection, to grab it's name (and other data, but project simplified for the question). Here the currently working query, docummented:
let aggregate = db.collection('hero').aggregate([
// grabbing an specific hero
{ $match: { _id } },
//populate relations
{
$lookup: {
from: 'hero',
let: { letId: '$relations.hero' }, //create a local variable for the pipeline to use
// localField: "relations.hero", //this would bring entire hero data, which is unnecessary
// foreignField: "_id", //this would bring entire hero data, which is unnecessary
pipeline: [
//match each $relations.hero (as "$$letId") in collection hero's (as "from") $_id
{ $match: { $expr: { $in: ['$_id', '$$letId'] } } },
//grab only the _id and name of the matched heroes
{ $project: { name: 1, _id: 1 } },
//sort by name
{ $sort:{ name: 1 } }
],
//replace the current relations with the new relations
as: 'relations',
},
}
]).toArray(someCallbackHere);
In short, $lookup on hero collection using a pipeline that match each of relations.hero and bring back only the _id and name (which has the real name to be printed on UI) and replace current relations with this new relations, generating the document as:
{
_id:'the-name-of-the-hero',
name: 'Name of Hero',
(...), //other properties to this hero
relations: [
{
_id: 'the-id-of-another-hero',
name: 'The Real Name of Another Hero',
},
{
_id: 'yet-another-id-of-another-hero',
name: 'Yet Another Real Name of Another Hero',
}
]
}
The question:
What can I add on the pipeline to make it merge the matched heroes with the original relations, in order to not only have the projected _id and name, but also the original relationType? That is, have the following result:
{
_id:'the-name-of-the-hero',
name: 'Name of Hero',
(...), //other properties to this hero
relations: [
{
_id: 'the-id-of-another-hero',
name: 'The Real Name of Another Hero',
relationType: 'trust' //<= kept from the original relations
},
{
_id: 'yet-another-id-of-another-hero',
name: 'Yet Another Real Name of Another Hero',
relationType: 'hate' //<= kept from the original relations
}
]
}
I tried exporting as: 'relationsFull' and then tried to $push with $mergeObjects as part of a next step into the aggregation but no luck. I tried to do the same as a pipeline step (instead of a new aggregate step) but always end up relations as empty array..
How would I write a new aggregation step to merge old relations objects with the new looked-up relations?
Note: Consider MongoDB 3.6 or later (that is, $unwind array is not needed, at least for the $lookup). I'm querying using Node.js driver, if that info matters.
You can use below aggregation
db.collection("hero").aggregate([
{ "$match": { _id } },
{ "$unwind": "$relations" },
{ "$lookup": {
"from": "hero",
"let": { "letId": "$relations.hero" },
"pipeline": [
{ "$match": { "$expr": { "$eq": ["$_id", "$$letId"] } } },
{ "$project": { "name": 1 } }
],
"as": "relation"
}},
{ "$unwind": "$relation" },
{ "$addFields": { "relations.name": "$relation.name" }},
{ "$group": {
"_id": "$_id",
"relations": { "$push": "$relations" },
"name": { "$first": "$name" },
"rarity": { "$first": "$rarity" },
"classType": { "$first": "$classType" }
}}
])
Or alternate you can use this as well
db.collection("hero").aggregate([
{ "$match": { _id } },
{ "$lookup": {
"from": "hero",
"let": { "letId": "$relations.hero" },
"pipeline": [
{ "$match": { "$expr": { "$in": ["$_id", "$$letId"] } } },
{ "$project": { "name": 1 } }
],
"as": "lookupRelations"
}},
{ "$addFields": {
"relations": {
"$map": {
"input": "$relations",
"as": "rel",
"in": {
"$mergeObjects": [
"$$rel",
{ "name": { "$arrayElemAt": ["$lookupRelations.name", { "$indexOfArray": ["$lookupRelations._id", "$$rel._id"] }] }}
]
}
}
}
}}
])
Well, I think we should use different name for the as field.From there, we can use the following expression the the $addFields stage.
{
"$addFields": {
"relations": {
"$reduce": {
"input": {
"$reduce": {
"input": {
"$zip": {
"inputs": [
"$relations",
"$relheros"
]
}
},
"initialValue": [
],
"in": {
"$concatArrays": [
"$$value",
"$$this"
]
}
}
},
"initialValue": {
},
"in": {
"$mergeObjects": [
"$$value",
"$$this"
]
}
}
}
}
}
Note that the relheros here is the as field.
We really should not $unwind and $group here, before $unwind is cheap but $group is expensive.