Mongodb $project: $filter sub-array - mongodb

There is an items (mongoose) schema that looks like this (simplified to what it matters to the question):
{
brand: {
name: String,
},
title: String,
description: [{ lang: String, text: String }],
shortDescription: [{ lang: String, text: String }],
variants: {
cnt: Number,
attrs: [
{
displayType: String,
displayContent: String,
displayName: [{ lang: String, text: String }],
name: String,
},
],
}
}
I'm trying to filter the items by language, so I've constructed the following query:
db.items.aggregate([
{ $match: { 'description.lang': 'ca', 'shortDescription.lang': 'ca' } },
{ $project: {
'brand.name': 1,
title: 1,
description: {
'$filter': {
input: '$description',
as: 'description',
cond: { $eq: ['$$description.lang', 'ca'] }
}
},
shortDescription: {
'$filter': {
input: '$shortDescription',
as: 'shortDescription',
cond: { $eq: ['$$shortDescription.lang', 'ca'] }
}
},
'variants.cnt': 1,
'variants.attrs': 1
} }
])
And it works as expected: it filters description and shortDescription by language. Right now I'm wondering if it could be possible to filter every variants.attrs.$.displayName as well. Is there any way to do it?
I've been trying to $unwind variant.attrs but I get completly lost when trying to $group again and I'm not really sure if this is the best way...

You are nearly there. Try these steps:
use $unwind stage before $project stage to expand the outer array of documents, i.e. variants.attrs
Add filter for the sub array variants.attrs.displayName in the $project stage.
You will have to project all the sub fields of variants key.
Next add $group stage and group by all the elements except the sub-array. Use $push to rebuild the sub array in group by stage.
Lastly, add $project stage to rebuild the document to its original structure.
db.items.aggregate([
{ $match: { 'description.lang': 'ca', 'shortDescription.lang': 'ca' } },
{ $unwind : "$variants.attrs" },
{ $project: {
'_id' : 1,
'brand.name': 1,
title: 1,
description: {
'$filter': {
input: '$description',
as: 'description',
cond: { $eq: ['$$description.lang', 'ca'] }
}
},
shortDescription: {
'$filter': {
input: '$shortDescription',
as: 'shortDescription',
cond: { $eq: ['$$shortDescription.lang', 'ca'] }
}
},
'variants.attrs.displayName' : {
'$filter' : {
input: '$variants.attrs.displayName',
as: 'variants_attrs_displayName',
cond: { $eq : ['$$variants_attrs_displayName.lang','ca']}
}
},
'variants.cnt': 1,
'variants.attrs.displayType': 1,
'variants.attrs.displayContent' : 1,
'variants.attrs.name' : 1
}
} , { $group:
{
_id : {
_id: "$_id",
title: "$title",
brand:"$brand",
description:"$description",
shortDescription:"$shortDescription",
variants_cnt : "$variants.cnt"
},
variants_attrs : { $push :
{
displayType : "$variants.attrs.displayType",
displayContent : "$variants.attrs.displayContent",
displayName : "$variants.attrs.displayName",
name: "$variants.attrs.name"
}
}
}
},
{ $project :
{
"_id" : 0,
brand : "$_id.brand",
title : "$_id.title",
description : "$_id.description",
shortDescription : "$_id.shortDescription",
variants : {
cnt : "$_id.variants_cnt" ,
attrs : "$variants_attrs"
}
}
}
])
Depending on your use case, you should reconsider your data model design to avoid duplication of filter values. i.e.
'description.lang': 'ca', 'shortDescription.lang': 'ca', 'variants.attrs.displayName.lang': 'ca'

Related

Mongodb aggregate to return result only if the lookup field has length

I have two collections users and profiles. I am implementing a search with the following query:
User.aggregate(
[
{
$match: {
_id: { $ne: req.user.id },
isDogSitter: { $eq: true },
profileId: { $exists: true }
}},
{
$project: {
firstName: 1,
lastName: 1,
email: 1,
isDogSitter: 1,
profileId: 1,
}},
{
$lookup: {
from: "profiles",
pipeline: [
{
$project: {
__v: 0,
availableDays: 0,
}},
{
$match: {
city: search
}}
],
as: "profileId",
}}
],
(error, result) => {
console.log("RESULT ", result);
}
);
What this does is that its searches for the city in the profiles collection and when there is not search match then profileId becomes an empty array. What I really want is that if the profileId is an empty array then I don't want to return the other fields in the documents too. It should empty the array. Below is my current returned result.
RESULT [
{
_id: 60cabe38e26d8b3e50a9db21,
isDogSitter: true,
firstName: 'Test',
lastName: 'Sitter',
email: 'test#user.com',
profileId: []
}
]
Add $match pipeline stage after the $lookup pipeline stage and
add the empty array condition check over there.
User.aggregate(
[
{
$match: {
_id: { $ne: req.user.id },
isDogSitter: { $eq: true },
profileId: { $exists: true }
}},
{
$project: {
firstName: 1,
lastName: 1,
email: 1,
isDogSitter: 1,
profileId: 1,
}},
{
$lookup: {
from: "profiles",
pipeline: [
{
$project: {
__v: 0,
availableDays: 0,
}},
{
$match: {
city: search
}}
],
as: "profileId",
}}
{
$match: { // <-- Newly added $match condition
"profileId": {"$ne": []}
},
},
],
(error, result) => {
console.log("RESULT ", result);
}
);

Join two collections with id stored in array of objects in mongodb

I have two collections with name School & Students
School Collection
{
_id: ObjectId("60008e81d186a82fdc4ff2b7"),
name: String,
branch: String,
class: [{
"active" : true,
"_id" : ObjectId("6001e6871d985e477b61b43f"),
"name" : "I",
"order" : 1
},
{
"active" : true,
"_id" : ObjectId("6001e68f1d985e477b61b444"),
"name" : "II",
"order" : 2
}]
}
Student Collection
{
_id: ObjectId("6002def815eccd53a596f830"),
schoolId: ObjectId("60008e81d186a82fdc4ff2b7"),
sessionId: ObjectId("60008e81d186a82fdc4ff2b9"),
class: ObjectId("6001e6871d985e477b61b43f"),
}
I want to get the data of Student Collection in single query.
I have class id stored in Student Collection and data against that id is stored in School Collection under class key, which is array of objects.
Can you please help me in getting the class object in student collection with this id?
Output i want:
data: {
_id: ObjectId("6002def815eccd53a596f830"),
schoolId: ObjectId("60008e81d186a82fdc4ff2b7"),
sessionId: ObjectId("60008e81d186a82fdc4ff2b9"),
class: ObjectId("6001e6871d985e477b61b43f"),
classData: [{
"active" : true,
"_id" : ObjectId("6001e6871d985e477b61b43f"),
"name" : "I",
"order" : 1
}]
}
So I tried this but it didn't work:
const students = await this.studentModel.aggregate([
{
$lookup: {
from: 'School',
let: { classId: '$class' },
pipeline: [
{
$match: {
$expr: { $eq: ['$$classId', '$class._id'] },
},
},
],
as: 'classData',
},
},
]);
$lookup, pass schoolId and class in let,
$match school id condition
$filter to iterate loop of class and filter specific class object
$arrayElemAt will get object from returned result from $filter
$replaceRoot to replace object to root
const students = await this.studentModel.aggregate([
{
$lookup: {
from: "School",
let: {
schoolId: "$schoolId",
classId: "$class"
},
pipeline: [
{ $match: { $expr: { $eq: ["$$schoolId", "$_id"] } } },
{
$replaceRoot: {
newRoot: {
$arrayElemAt: [
{
$filter: {
input: "$class",
cond: { $eq: ["$$this._id", "$$classId"] }
}
},
0
]
}
}
}
],
as: "classData"
}
}
])
Playground

MongoDB - How to tell if every document has an exactly matching element array after an aggregate match

I have a collection of documents similar to this:
{
Name : "Name1",
Product : 1012,
Titles : [ {
Id: 5,
Title: "FirstTitle"
},
{
Id: 75,
Title: "SecondTitle"
}
},
{
Name : "Name1",
Product : 2014,
Titles : [ {
Id: 5,
Title: "FirstTitle"
},
{
Id: 75,
Title: "SecondTitle"
}
}
I'm matching by the Name with an aggregate to get all documents with the same name. Then if all matches have the exact same set of Titles, I want that set.
{
Name : "Name1,
TitlesVaries : false
Titles : [ {
Id: 5,
Title: "FirstTitle"
},
{
Id: 75,
Title: "SecondTitle"
}
}
If they are different I want to know that.
{
Name : "Name1"
TitlesVaries : true
Titles : null
}
I'm having trouble comparing the set/array titles for each document to see if they are all exactly the same after I've don't my aggregate/match. Some documents can have empty/null arrays of Titles and if they are all empty/null that's a match
The aggreagtion might be
$addToSet helps to remove duplicates. So we will have two array which is a Set (Titles) and an original array ('original')
Compare both array. If both are not equal, then there is a various.
The script is
db.collection.aggregate([
{
$group: {
_id: "$Name",
Titles: { $addToSet: "$Titles" },
original: { $push: "$Titles" }
}
},
{
$project: {
Titles: {
$cond: [
{
$or: [
{
$ne: [
{ $size: "$Titles" },
{ $size: "$original" }
]
},
{
$eq: [ { $size: "$original" }, 1 ]
}
]
},
"$Titles",
null
]
},
TitlesVaries: {
$ne: [
{ $size: "$Titles" }, { $size: "$original" }
]
}
}
}
])
Working Mongo playground

MongoDB lookup when foreign field is an array

I've searched the internet and StackOverflow, but I cannot find the answer or even the question.
I have two collections, reports and users. I want my query to return all reports and indicate if the specified user has that report as a favorite in their array.
Reports Collection
{ _id: 1, name:"Report One"}
{ _id: 2, name:"Report Two"}
{ _id: 3, name:"Report Three"}
Users Collection
{_id: 1, name:"Mike", favorites: [1,3]}
{_id: 2, name:"Tim", favorites: [2,3]}
Desired Result for users.name="Mike"
{ _id: 1, name:"Report One", favorite: true}
{ _id: 2, name:"Report Two", favorite: false}
{ _id: 3, name:"Report Three", favorite: true}
All of the answers I can find use $unwind on the local (reports) field, but in this case the local field isn't an array. The foreign field is the array.
How can I unwind the foreign field? Is there a better way to do this?
I saw online that someone suggested making another collection favorites that would contain:
{ _id: 1, userId: 1, reportId: 1 }
{ _id: 2, userId: 1, reportId: 3 }
{ _id: 3, userId: 2, reportId: 2 }
{ _id: 4, userId: 2, reportId: 3 }
This method seems like it should be unnessesary. It should be simple to join onto an ID in a foreign array, right?
You can use $lookup with custom pipeline which will give you 0 or 1 result and then use $size to convert an array to single boolean value:
db.reports.aggregate([
{
$lookup: {
from: "users",
let: { report_id: "$_id" },
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: [ "$name", "Mike" ] },
{ $in: [ "$$report_id", "$favorites" ] }
]
}
}
}
],
as: "users"
}
},
{
$project: {
_id: 1,
name: 1,
favorite: { $eq: [ { $size: "$users" }, 1 ] }
}
}
])
Alternatively if you need to use MongoDB version lower than 3.6 you can use regular $lookup and then use $filter to get only those users where name is Mike:
db.reports.aggregate([
{
$lookup: {
from: "users",
localField: "_id",
foreignField: "favorites",
as: "users"
}
},
{
$project: {
_id: 1,
name: 1,
favorite: { $eq: [ { $size: { $filter: { input: "$users", as: "u", cond: { $eq: [ "$$u.name", "Mike" ] } } } }, 1 ] }
}
}
])
"_id" : ObjectId("611fc392cfadfbba65d4f4bd"),
"t_name" : "Bahadur",
"t_age" : "22",
"trch" : "java",
"StudentsDetails" : [
{
"_id" : ObjectId("611fc41ccfadfbba65d4f4be"),
"s_name" : "Asin",
"s_age" : "18",
"trch" : "java",
"tsid" : ObjectId("611fc392cfadfbba65d4f4bd")
},
{
"_id" : ObjectId("611fc8f1a815fb2c737ae31f"),
"s_name" : "sonu",
"s_age" : "18",
"tsid" : ObjectId("611fc392cfadfbba65d4f4bd")
},
{
"_id" : ObjectId("611fc915a815fb2c737ae320"),
"s_name" : "monu",
"s_age" : "19",
"tsid" : ObjectId("611fc392cfadfbba65d4f4bd")
}
]
}
Create Trainer Collection
Create Scholar Collection
//query
db.Trainer.aggregate(
[`enter code here`
{`enter code here`
$lookup:`enter code here`
{`enter code here`
from: "scholar",`enter code here`
localField: "_id",`enter code here`
foreignField: "tsid",`enter code here`
as: "StudentsDetails"`enter code here`
}`enter code here`
}`enter code here`
]`enter code here`
).pretty();

Mongo DB - Second Level Search - elemMatch

I am trying to fetch all records (and count of all records) for a structure like the following,
{
id: 1,
level1: {
level2:
[
{
field1:value1;
},
{
field1:value1;
},
]
}
},
{
id: 2,
level1: {
level2:
[
{
field1:null;
},
{
field1:value1;
},
]
}
}
My requirement is to fetch the number of records that have field1 populated (atleast one in level2). I need to say fetch all the ids or the number of such ids.
The query I am using is,
db.table.find({},
{
_id = id,
value: {
$elemMatch: {'level1.level2.field1':{$exists: true}}
}
}
})
Please suggest.
EDIT1:
This is the question I was trying to ask in the comment. I was unable to elucidate in the comment properly. Hence, editing the question.
{
id: 1,
level1: {
level2:
[
{
field1:value1;
},
{
field1:value1;
},
]
}
},
{
id: 2,
level1: {
level2:
[
{
field1:value2;
},
{
field1:value2;
},
{
field1:value2;
}
]
}
}
{
id: 3,
level1: {
level2:
[
{
field1:value1;
},
{
field1:value1;
},
]
}
}
The query we used results in
value1: 4
value2: 3
I want something like
value1: 2 // Once each for documents 1 & 3
value2: 1 // Once for document 2
You can do that with the following find query:
db.table.find({ "level1.level2" : { $elemMatch: { field1 : {$exists: true} } } }, {})
This will return all documents that have a field1 in the "level1.level2" structure.
For your question in the comment, you can use the following aggregation to "I had to return a grouping (and the corresponding count) for the values in field1":
db.table.aggregate(
[
{
$unwind: "$level1.level2"
},
{
$match: { "level1.level2.field1" : { $exists: true } }
},
{
$group: {
_id : "$level1.level2.field1",
count : {$sum : 1}
}
}
]
UPDATE: For your question "'value1 - 2` At level2, for a document, assume all values will be the same for field1.".
I hope i understand your question correctly, instead of grouping only on the value of field1, i added the document _id as an xtra grouping:
db.table.aggregate(
[
{
$unwind: "$level1.level2"
},
{
$match: {
"level1.level2.field1" : { $exists: true }
}
},
{
$group: {
_id : { id : "$_id", field1: "$level1.level2.field1" },
count : {$sum : 1}
}
}
]
);
UPDATE2:
I altered the aggregation and added a extra grouping, the aggregation below gives you the results you want.
db.table.aggregate(
[
{
$unwind: "$level1.level2"
},
{
$match: {
"level1.level2.field1" : { $exists: true }
}
},
{
$group: {
_id : { id : "$_id", field1: "$level1.level2.field1" }
}
},
{
$group: {
_id : { id : "$_id.field1"},
count : { $sum : 1}
}
}
]
);