Return custom results using the Aggregate framework - mongodb

I've the following Schemas in my Node.js app:
let CategorySchema = mongoose.Schema({
name: { type: String, required: true }
});
let UserSchema = mongoose.Schema({
firstName: { type: String, required: true },
lastName: { type: String, required: true }
});
let CustomerSchema = mongoose.Schema({
name: { type: String, required: true }
});
let VendorSchema = mongoose.Schema({
userID: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
category: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Category' }],
products: [ { type: mongoose.Schema.Types.ObjectId, ref: 'Product' } ],
name: { type: String, required: true }
});
let ProductSchema = mongoose.Schema({
vendorID: { type: mongoose.Schema.Types.ObjectId, ref: 'Vendor' },
name: { type: String, index: true, required: true },
customerReviews: [{
stars: { type: Number, required: false },
review: { type: String, required: false },
customerId: { type: mongoose.Schema.Types.ObjectId, ref: 'Customer', required: true }
}]
});
and I am trying to query the Vendors collection to return the following result:
{
_id: "dsi9dsik129dkdsdsds",
userData: {
_id: "dsa9dskd2kwd29dkkss",
firstName: "Michael",
lastName: "White"
},
categoryData: {
_id: "e9dids91i239dskds91",
name: "Category A"
},
productsData: [ {
_id: "3132139i31j32131",
vendorID: "dsi9dsik129dkdsdsds",
name: "Product ABC",
customerReviews [{
stars: "5",
review: "Good",
customerData: {
_id: "zds91i232131j2321j",
name: "John silver"
}
},
{
stars: "3",
review: "Bad",
customerData: {
_id: "ldso91232131j2321j",
name: "Mark Spenser"
}
}]
} ],
name: "Vendor XYZ"
}
I've built the following query on Vendors collection (meaning Vendors.aggregate(...) ) yet I am not sure how to format the returned results as well as retrieve Customers data in customers review, so I was wondering if anyone can help? Thanks.
(
{ $lookup: { from: "users", localField: "userID", foreignField: "_id", as: "userData" } },
{ $lookup: { from: "categories", localField: "category", foreignField: "_id", as: "categoryData" } },
{ $lookup: { from: "products", localField: "products", foreignField: "_id", as: "productsData" } },
{ $group: { _id: null, content: { $push: '$$ROOT' },count: { $sum: 1 } } },
{ $project: { content: { $slice: [ '$content', 0, 10 ] }, count: 1, _id: 0 } },
)
Test:
Category:
{
"_id" : ObjectId("5de7fc530ce9d05be4024170"),
"categoryName" : "Glass",
"__v" : 0
}
User:
{
"_id" : ObjectId("5d6671ae7be3be4e18ebe9bb"),
"firstName" : "Mark",
"lastName" : "Smith",
"__v" : 0
}
Customer:
{
"_id" : ObjectId("5dd7cb11f4b2544253368f24"),
"customerName" : "Michael White",
"__v" : 0
}
Vendor:
{
"_id" : ObjectId("5de7fc6a0ce9d05be4024171"),
"category" : [
ObjectId("5de7fc530ce9d05be4024170")
],
"products" : [
ObjectId("5de8474ccd0bbc05256db819")
],
"userID" : ObjectId("5d6671ae7be3be4e18ebe9bb"),
"vendorName" : "Michael White",
"__v" : 0
}
Product:
{
"_id" : ObjectId("5de8474ccd0bbc05256db819"),
"vendorID" : ObjectId("5de7fc6a0ce9d05be4024171"),
"name" : "Red Sause",
"customerReviews" : [
{
"moderated" : false,
"_id" : ObjectId("5de7fcf20ce9d05be4024175"),
"customerId" : ObjectId("5dd7cb11f4b2544253368f24"),
"stars" : 3,
"review" : "Didn't like it that much :( ",
"date" : ISODate("2019-12-04T18:37:38.253Z")
}
],
"__v" : 0
}
Note: I've added entity name before name in the test above, ex. vendorName / customerName just to avoid confusion between the field 'name' across multiple collections

You can try below aggregation from 3.6 version.
{"$lookup":{
"from": "users",
"localField": "userID",
"foreignField": "_id",
"as": "userData"
}},
{"$unwind":"$userData"},
{"$lookup":{
"from": "categories",
"localField": "category",
"foreignField": "_id",
"as": "categoryData"
}},
{"$lookup":{
"from":"products",
"let":{"products":"$products"},
"pipeline":[
{"$match":{"$expr":{"$in":["$_id","$$products"]}}},
{"$unwind":{"path":"$customerReviews", "preserveNullAndEmptyArrays":true}},
{"$lookup":{
"from":"customers",
"localField":"customerReviews.customerId",
"foreignField":"_id",
"as":"customerData"
}},
{"$unwind":{"path":"$customerData", "preserveNullAndEmptyArrays":true}},
{"$group":{
"_id":"$_id",
"vendorID": {"$first":"$vendorID"},
"name": {"$first":"$name"},
"customerReviews":{
"$push":{
"stars": "$customerReviews.stars",
"review": "$customerReviews.review",
"customerData":"$customerData"
}
}
}}
],
"as":"productsData"
}}

Related

How to apply $lookup with conditions in mongodb?

I am using mongodb databases and want to apply $lookup on 2 collections but with specific conditions.
I have one collection named Company like this
new Schema({
name: String,
benefit: String,
benefitDesc: String,
company_url: String,
logoUrl: String,
coverUrl: String,
desc: String,
createdAt: String,
categoryId: { type: Schema.Types.ObjectId, ref: 'categories' },
})
And another collection named Referrallinks like this
new Schema({
referral_link: String,
referral_code: String,
isLink: Number,
offer_name: String,
offer_desc: String,
user_email: String,
companyId: { type: Schema.Types.ObjectId, ref: 'companies' },
addedByAdmin: { type: Boolean, default: true },
number_of_clicks: Number,
referral_country: String,
link_status: String,
categoryId: { type: Schema.Types.ObjectId, ref: 'categories' },
number_of_clicks: { type: Number, default: 0 },
createdAt: String,
updatedAt: String,
userId: { type: Schema.Types.ObjectId, ref: 'users' }
})
Now when i apply this $lookup
Company.aggregate([{
$lookup: {
from: 'referrallinks',
localField: '_id',
foreignField: 'companyId',
as: 'referrals'
}
}])
So i am getting all companies with referrals as array like this
[
{name : '', benefit : '', benefiDesc : '', referrals : []},
{name : '', benefit : '', benefiDesc : '', referrals : []},
{name : '', benefit : '', benefiDesc : '', referrals : []},
.....
]
But i want to have only referrals in each company's referrals array that have link_status value Approved.
**Note : ** I also want to have category object with company and tried this aggregation but it gives error
{
from: "referrallinks",
let: { company_id: "$_id" },
pipeline: [{
$match: {
$expr: {
$and: [
{ $eq: [ "$$company_id", "$companyId" ] },
{ $eq: [ "$link_status", "Approved" ] }
]
}
},
{
$lookup : {
from : "categories",
let : {"category_id" : "$categoryId"},
pipeline : [
{
$match : {
$expr : {
$eq : ["category_id","$_id"]
}
}
}
],
as : "company"
}
}
}],
as: "referrals"
}
How can i achieve this?
Use $lookup with custom pipeline to specify additional filtering condition:
Company.aggregate([{
$lookup: {
from: "referrallinks",
let: { company_id: "$_id" },
pipeline: [{
$match: {
$expr: {
$and: [
{ $eq: [ "$$company_id", "$companyId" ] },
{ $eq: [ "$link_status", "Approved" ] }
]
}
}
}],
as: "referrals"
}
}])
EDIT: to run nested $lookup your code should look like below:
Company.aggregate([{
$lookup: {
from: "referrallinks",
let: { company_id: "$_id" },
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: [ "$$company_id", "$companyId" ] },
{ $eq: [ "$link_status", "Approved" ] }
]
}
}
},
{
$lookup: {
from: "categories",
localField: "categoryId",
foreignField: "_id",
as: "company"
}
}
],
as: "referrals"
}
}])

How to aggregate lookup sub array documents in mongoDB, and projecting the result?

I have 2 Collections
Posts
Users
Posts collection contains comments array which stores { userId, comment } object, along with other information.
users ollection contains user's information.
I want to return the complete result.
Ex:
{
"postId":"xvzeee",
"post": "Good Morning",
"likedBy":[
12342234,
23456534
]
"comments": [
{
"comment": "very good morning",
"userName": "Max"
},
{
"comment": "v. GM",
"userName": "Suraj"
}
]
}
My Approach to achieve the above result is.
db.wall.aggregate([
{ $lookup: { from: 'profiles', localField: 'likedBy', foreignField: 'profileId', as: 'likedBy' } },
{ $lookup: { from: 'profiles', localField: 'comments.commentedBy', foreignField: 'profileId', as: 'commentedUser' } },
{
$project:{
"likedBy.name": 1,
"likedBy.profileId": 1,
createdBy: 1,
createdAt: 1,
updatedAt: 1,
comments : [{
comment: "$comments.comment",
user: "$commentedUser.name"
}],
"commentedUser.name" : 1
}
}
])
The result is coming like below:
{
"_id" : ObjectId("5dfdbb129f644213c413eb18"),
"likedBy" : [
{
"profileId" : "96444206",
"name" : "Vinay3"
},
{
"profileId" : "400586806",
"name" : "Dev"
}
],
"createdBy" : "96444206",
"commentedUser" : [
{
"name" : "Vinay3"
},
{
"name" : "Dev"
}
],
/*Facing problem in comment array*/
"comments" : [
{
"comment" : [
"Super-awesome",
"FAB",
],
"user" : [
"Vinay3",
"Dev"
]
}
]
}
The comment should look like :
[{
...
"comments" : [
{
"comments" : [
{
comment :"Super-awesome",
user: "Vinay3",
profileId: "11111..."
},
{
comment: "FAB",
user: "Dev",
profileId: "2222..."
}
],
}
]
}]
The Posts Collection looks like this :
[{
"_id" : ObjectId("5dfdbb129f699913c413eb18"),
"post":"Good Morning",
"createdBy" : "96444206",
"postId" : "D9644s5h8m",
"likedBy" : [
"96444206",
"40058680"
],
"comments" : [
{
"commentId" : "COM9644",
"commentedBy" : "96444206",
"comment" : "Super-awesome"
},
{
"commentId" : "COM9644",
"commentedBy" : "96444206",
"comment" : "#FAB"
},
{
"commentId" : "COM00587",
"commentedBy" : "400586806",
"comment" : "marvelous"
}
],
"createdAt" : ISODate("2019-12-21T11:56:26.944+05:30"),
"updatedAt" : ISODate("2019-12-21T12:12:35.047+05:30"),
"__v" : 0
}, {...}, {...}]
User Profiles Collection
[{
"_id" : ObjectId("5dd4ff3abe53181160efa446"),
"accountStatus" : "CONFIRMED",
"profileId" : "400586806",
"name" : "Dev",
"email" : "dev#xyz.com",
"createdAt" : ISODate("2019-11-20T14:24:18.692+05:30"),
"updatedAt" : ISODate("2019-12-20T16:58:06.041+05:30"),
"__v" : 0
}, {...}, {...} ]
How to achieve this, any help will much be appreciated.
However, I'm able to give a solution to my problem like below.
If there any better solution than this will much be appreciated.
db.wall.aggregate([
{ $unwind: '$comments'},
{ $lookup: { from: 'profiles', localField: 'comments.commentedBy', foreignField: 'profileId', as: 'comments.user' } },
{ $unwind: '$comments.user' },
{
$group: {
_id:'$_id',
postId: { $first: "$postId"},
likedBy: { $first: "$likedBy"},
createdBy: { $first: '$createdBy'},
comments: { $push : '$comments' },
createdAt: { $first: "$createdAt" },
updatedAt: { $first: "$updatedAt" },
}
},{
$project: {
_id:1,
postId: 1,
"likedBy.name": 1,
"likedBy.profileId": 1,
createdBy: 1,
comments: {
$map : {
input: '$comments',
as: 'com',
in: {
commentId: "$$com.commentId",
comment: "$$com.comment",
name: "$$com.user.name"
}
}
},
createdAt: 1,
updatedAt: 1,
}
}
])
I want to add isActive => true || false field to the comments object.
now the challenge is how to filter only isActive => true comments ?
Here is my approach
db.posts.aggregate([
{
$unwind: '$likedBy',
},
{
$unwind: '$comments',
},
{
$lookup: {
from: 'users',
localField: 'likedBy',
foreignField: 'profileId',
as: 'likedByUser'
},
},
{
$unwind: '$likedByUser'
},
{
$lookup: {
from: 'users',
localField: 'comments.commentedBy',
foreignField: 'profileId',
as: 'commentByUser'
},
},
{
$unwind: '$commentByUser'
},
{
$group: {
"_id": '$postId',
"post": { $first: 'post'} ,
comments: {
$push: {
"commentId": "$comments.commentId",
"commentedBy": "$likedByUser.name",
"comment": "$comments.comment"
}
},
likes: {
$push: {
"likedByName": "$likedByUser.name",
"likeById": "$likedBy"
}
}
}
},
{
$project: {
"_id": 0,
"postId":'$_id',
"post": 1,
"comments": 1,
"likes": 1
}
}
])

Count articles grouping by tags mongodb

I had a lot of articles with a field called tags, and is an array of tags _ids, and for statistics purpose I want to count how many articles we had by each tag. If tags were a simple tag _id, it's easy because I could group by tag, but is an array of tags, and I can't group by that field.
First I try with this:
db.note.aggregate([{$match: {
publishedAt: {
$gte: ISODate('2018-01-01'),
$lte: ISODate('2019-01-01')
}
}}, {$group: {
_id: "$tags",
"total": {
"$sum": 1
}
}}, {$lookup: {
from: 'tags',
localField: '_id',
foreignField: '_id',
as: 'tag'
}}, {$unwind: {
path: "$tag"
}}, {$project: {
total: 1,
"tag.name": 1
}}, {$sort: {
total: -1
}}])
But that doesn't work, that query, group by tags group, so I try to do this:
{
'$match': {
'publishedAt': {
'$gte': new Date(req.body.gte),
'$lte': new Date(req.body.lte)
}
}
},
{
'$unwind': {
'path': '$tags'
}
}, {
'$group': {
'_id': '$tags',
'total': {
'$sum': 1
}
}
}, {
'$lookup': {
'from': 'tags',
'localField': '_id',
'foreignField': '_id',
'as': 'tag'
}
}, {
'$project': {
'total': 1,
'tag.name': 1
}
}, {
'$sort': {
'total': -1
}
},
{
'$unwind': {
'path': '$tag'
}
}
)
But the problem with this, that group for the first tag from the array and I miss all other tags in that array.
What do you think will be the solution?
I had a lot of articles with a field called tags, and is an array of
tags _ids, and for statistics purpose I want to count how many
articles we had by each tag.
You can try this (I am assuming the following input documents):
notes:
{ _id: 1, name: "art-1", author: "ab", tags: [ "t1", "t2" ] },
{ _id: 2, name: "art-2", author: "cd", tags: [ "t1", "t3" ] },
{ _id: 3, name: "art-3", author: "wx", tags: [ "t4", "t3" ] },
{ _id: 4, name: "art-4", author: "yx", tags: [ "t1" ] }
tags:
{ _id: 1, id: "t1", name: "t1's name" },
{ _id: 2, id: "t2", name: "t2's name" },
{ _id: 3, id: "t3", name: "t3's name" },
{ _id: 4, id: "t4", name: "t4's name" }
The Query:
db.tags.aggregate( [
{
$lookup: {
from: "notes",
localField: "id",
foreignField: "tags",
as: "tag_matches"
}
},
{ $project: { id: 1, name: 1, _id: 0, count: { $size: "$tag_matches" } } }
] )
The Output:
{ "id" : "t1", "name" : "t1's name", "count" : 3 }
{ "id" : "t2", "name" : "t2's name", "count" : 1 }
{ "id" : "t3", "name" : "t3's name", "count" : 2 }
{ "id" : "t4", "name" : "t4's name", "count" : 1 }

Check follow status in aggregate mongo / mongoose

I have this schema for users where followers/followed is array and the reference the same schema
var userSchema = new Schema({
username: { type: String, unique: true, trim: true, required: true },
password: { type: String, required: true },
followers: [{ type: Schema.Types.ObjectId, ref: "users" }],
followed: [{ type: Schema.Types.ObjectId, ref: "users" }],
registered: { type: Date, default: Date.now },
admin: { type: Number, default: 0 }
});
What I am looking for to return the follow status, if the _id is contains in followed array give me for example follow_status: 1
[
{
$match: { username: new RegExp(username, "i") }
},
{
$unwind: "$followers"
},
{
$lookup: {
from: "users",
localField: "followers",
foreignField: "_id",
as: "info"
}
},
{
$unwind: "$info"
},
{
$project: {
info: {
_id: 1,
username: 1,
avatar: { $ifNull: ["$avatar", ""] },
fullname: { $ifNull: ["$fullname", ""] }
}
}
},
{
$replaceRoot: { newRoot: "$info" }
},
{
$limit: 1000
}
]
Current pipeliens result
[
{
"_id": "5a906653f52e66c9c7a23cb6",
"username": "User1"
},
{
"_id": "5a908eb564a726cf8ec7e0a3",
"username": "User2"
}
]

multiple $lookup stages in mongodb aggregation

I'm working with the followings mongoose schemas:
Question schema:
var Question = new Schema({
title: String,
content: String,
createdBy: {
type: Schema.ObjectId,
ref: 'User',
required: true
},
answers: {
type: [ { type: Schema.Types.ObjectId, ref: 'Answer' } ]
}
});
Answer Shchema:
var Answer = new Schema({
content: {
type: String,
require: 'Content cannot be empty.'
},
createdBy: {
type: Schema.Types.ObjectId,
ref: 'User'
},
isBest: {
type: Boolean,
default: false
},
createdAt: {
type: Date,
default: Date.now
},
votes: {
type: Number,
default: 0
},
comments: {
type: [{ type: Schema.Types.ObjectId, ref: 'Comment' }]
}
});
and Comment Schema:
var Comment = new Schema({
content: {
type: String,
required: [true, 'Content cannot be empty']
},
createdBy: {
type: Schema.Types.ObjectId,
ref: 'User'
},
createdAt: {
type: Date,
default: Date.now
}
});
Basically what I'm trying to do is doing a $lookup for answers and for comments array in every answer, and then in $project stage try to add an isOwner field that is going to be true if the user logged is the owner of the answer or comment. This is what I' trying:
Question.aggregate([
{
$match: { '_id': { $eq: questionId } }
},
{
$lookup: {
from: 'answers',
localField: 'answers',
foreignField: '_id',
as: 'answers'
}
},{
$lookup:{
from: 'comments',
localField: 'answers.comments',
foreignField: '_id',
as: 'comments'
}
}, {
$project: {
title: true,
content: true,
createdBy: true,
createdAt: true,
isOwner: { $eq : ['$createdBy', currentUser] },
answers: true,
answers: {
isOwner: { $eq : ['$createdBy', currentUser] },
content: true,
createdBy: true,
createdAt: true,
comments: {
content: true,
createdAt: true,
createdBy: true,
isOwner: { $eq : ['$createdBy', currentUser] }
}
}
}
}
])
This is the ouput that I'm expecting:
{
"_id": "58a7be2c98a28f18acaa4be4",
"title": "Some title",
"createdAt:": "2017-03-03T05:13:41.061Z",
"content": "some content",
"isOwner": true,
"createdBy": {
"_id": "58a3a66c088fe517b42775c9",
"name": "User name",
"image": "imgUrl"
},
"answers": [
{
"_id": "58a3a66c088fe517b42775c9",
"content": "an answer content",
"createdAt": "2017-03-03T05:13:41.061Z",
"isBest": false,
"isOwner": false,
"createdBy":{
"_id": "58a3a66c088fe517b42775c9",
"name": "another user",
"image": "another image"
},
"comments": [
{
"_id": "58aa104a4254221580832a8f",
"content": "some comment content",
"createdBy": {
"_id": "58a3a66c088fe517b42775c9",
"name": "another user",
"image": "another image"
},
}
]
}
]
}
I'm using mongodb 3.4.2
You can try addFields stage to add the isOwner field for all the relations.
Question.aggregate([{
$match: {
'_id': {
$eq: questionId
}
}
}, {
$addFields: {
"isOwner": {
$eq: ['$createdBy', currentUser]
}
}
}, { // No unwind needed as answers is scalar of array values.
$lookup: {
from: 'answers',
localField: 'answers',
foreignField: '_id',
as: 'answers'
}
}, {
$addFields: {
"answers.isOwner": {
$eq: ['$createdBy', currentUser]
}
}
}, {
$unwind: "$answers" //Need unwind here as comments is array of scalar array values
}, {
$lookup: {
from: 'comments',
localField: 'answers.comments',
foreignField: '_id',
as: 'comments'
}
}, {
$addFields: {
"comments.isOwner": {
$eq: ['$createdBy', currentUser]
}
}
}, {
$addFields: { // Move the comments back to answers document
"answers.comments": "$comments"
}
}, {
$project: { // Remove the comments embedded array.
"comments": 0
}
}, {
$group: {
_id: null,
isOwner: {
$first: "$isOwner"
},
answers: {
$push: "$answers"
}
}
}])
THe problem with your code is that you have not unwind the answeeres array before lookup
Please check below comment
Question.aggregate([
{
$match: { '_id': { $eq: questionId } }
},
{
$lookup: {
from: 'answers',
localField: 'answers',
foreignField: '_id',
as: 'answers'
}
},
{$unwind : "$answers"}, // <-- Check here
{
$lookup:{
from: 'comments',
localField: 'answers.comments',
foreignField: '_id',
as: 'comments'
}
},
{
$group : {
_id : null,
title: {$first : true},
content: {$first :true},
createdBy: {$first :true},
createdAt: {$first :true},
isOwner: { $eq : ['$createdBy', currentUser] },
answersStatus: {$first :true},
answers : {$push : $answer}
}
}
])