Grouping in MongoDb using aggregate - mongodb

I am a beginner to MongoDB and I found the Aggregate function hard to understand.
I read many topics and tried many things, however I couldn't get the results I am looking for.
Actually, I have two schema as:
1) Faculty.js
const FacultySchema = new Schema({
name: {
type: String,
required: true
}
});
2) Semester.js
const SemesterSchema = new Schema({
name: {
type: String,
required: true
},
faculty: {
type: Schema.Types.ObjectId,
ref: 'faculties'
}
});
Semester collection
[
{
"_id": ObjectId("5bf82da745209d0d48a91b62"),
"name": "1st Semester",
"faculty": ObjectId("5bf7f39a1972dd0b6c74de7d"),
"__v": 0
},
{
"_id": ObjectId("5bf8c3f945209d0d48a91b63"),
"name": "2nd Semester",
"faculty": ObjectId("5bf7f39a1972dd0b6c74de7d"),
"__v": 0
},
{
"_id": ObjectId("5bf8c3fe45209d0d48a91b64"),
"name": "3rd Semester",
"faculty": ObjectId("5bf7f39a1972dd0b6c74de7d"),
"__v": 0
},
{
"_id": ObjectId("5bf8c40345209d0d48a91b65"),
"name": "4th Semester",
"faculty": ObjectId("5bf7f39a1972dd0b6c74de7d"),
"__v": 0
}
]
What I want to group is all those semesters as an array having same faculty id in one place.
Something like:
[
{faculty: "BBA", semesters: ['first', 'second', 'third']},
{faculty: "BCA", semesters: ['first', 'second', 'third']}
];
How can I achieve this??

You can use $group aggregation to first find the distinct faculties and then $lookup to get the names of the faculties from the Faculties collection
Semester.aggregate([
{ "$group": {
"_id": "$faculty",
"semesters": { "$push": "$name" }
}},
{ "$lookup": {
"from": "faculties",
"let": { "facultyId": "$_id" },
"pipeline": [
{ "$match": { "$expr": { "$eq": ["$_id", "$$facultyId"] }}}
],
"as": "faculty"
}},
{ "$project": {
"semesters": 1, "faculty": { "$arrayElemAt": ["$faculty.name", 0] }
}}
])
Or you can use $lookup first and then $grouping the distinct names
Semester.aggregate([
{ "$lookup": {
"from": "Faculty",
"let": { "facultyId": "$_id" },
"pipeline": [
{ "$match": { "$expr": { "$eq": ["$_id", "$$facultyId"] }}}
],
"as": "faculty"
}},
{ "$unwind": "$faculty" },
{ "$group": {
"_id": "$faculty.name",
"semesters": { "$push": "$name" }
}},
{ "$project": {
"semesters": 1, "faculty": "$_id", "_id": 0 }
}}
])

Related

Find one user then get their ranking based on their total points using MongoDB

So I got the following data:
Users collection
{
_id: ObjectId("62a2a0422ec90fea68390aaa"),
name: 'Robert Yamashita',
username: 'robyama',
email: 'robert.yamashita#rocketmail.com',
},
{
_id: ObjectId("62a2a0452ec90fea68390aad"),
name: 'Charles X',
username: 'cvx',
email: 'charles.xxx#rocketmail.com',
}
Points collection
{
userId: ObjectId("62a2a0422ec90fea68390aaa"),
action: 'Liked a post',
points: 10,
}
{
userId: ObjectId("62a2a0422ec90fea68390aaa"),
action: 'Liked a post',
points: 10,
}
{
userId: ObjectId("62a2a0452ec90fea68390aad"),
action: 'Liked a comment',
points: 5,
}
I created a pipeline to get the total points of username robyama using the following query:
db.users.aggregate([
{ $match: { username: 'robyama' } },
{
$lookup: {
from: 'points',
localField: '_id',
foreignField: 'user',
as: 'userPoints'
}
},
{
$unwind: '$userPoints'
},
{
$group: {
_id: {
name: '$name',
email: '$email',
username: '$username',
},
count: { $sum: '$userPoints.points' }
}
}
]);
I got the following result:
{
"_id": {
"name": "Robert Yamashita",
"email": "robert.yamashita#rocketmail.com",
"username": "robyama",
},
"count": 20
}
This is exactly what I needed but I wanted to add a ranking field to the returned query since Robert has 20 points and Charles only has 5. So ideally I want the result to be this:
{
"_id": {
"name": "Robert Yamashita",
"email": "robert.yamashita#rocketmail.com",
"username": "robyama",
},
"count": 20
"rank": 1
}
What should I add to my pipeline to get the above output? Any help would be greatly appreciated!
Here's another way to do it. There's only one "$lookup" with one embedded "$group" so it should be fairly efficient. The "$project" seems a bit contrived, but it gives the output in the format you want.
db.users.aggregate([
{
"$match": {
"username": "robyama"
}
},
{
"$lookup": {
"from": "points",
"as": "sortedPoints",
"pipeline": [
{
"$group": {
"_id": "$userId",
"count": {"$sum": "$points"}
}
},
{"$sort": {"count": -1}}
]
}
},
{
"$unwind": {
"path": "$sortedPoints",
"includeArrayIndex": "idx"
}
},
{
"$match": {
"$expr": {
"$eq": ["$_id", "$sortedPoints._id"]
}
}
},
{
"$project": {
"_id": {
"name": "$name",
"username": "$username",
"email": "$email"
},
"count": "$sortedPoints.count",
"rank": {
"$add": ["$idx", 1]
}
}
}
])
Try it on mongoplayground.net.
Well, this is one way of doing it.
Perform join using $lookup and calculate counts for each user.
Sort the elements by counts in desc order.
Group documents by _id as NULL and push them all in an array.
Unwind the array, along with getting row numbers.
Find your required document and calculate the rank using row number.
db.users.aggregate([
{
$lookup: {
from: "points",
localField: "_id",
foreignField: "userId",
as: "userPoints"
}
},
{
$unwind: "$userPoints"
},
{
$group: {
_id: {
name: "$name",
email: "$email",
username: "$username",
},
count: {
$sum: "$userPoints.points"
}
}
},
{
"$sort": {
count: -1
}
},
{
"$group": {
"_id": null,
"docs": {
"$push": "$$ROOT",
}
}
},
{
"$unwind": {
path: "$docs",
includeArrayIndex: "rownum"
}
},
{
"$match": {
"docs._id.username": "robyama"
}
},
{
"$addFields": {
"docs.rank": {
"$add": [
"$rownum",
1
]
}
}
},
{
"$replaceRoot": {
"newRoot": "$docs"
}
}
])
This is the playground link.

How can i optimize my query i have written to find the Users and there last order details using aggregate, it shows me timeout as the dataset is large

I have a query as below, what it does it creates a link between two documents and find the last order date and users details like email, phone, etc. but on large data set it shows me timeout error any help would be much appreciated, and thanks in advance for the help
db.users.aggregate([
{
"$lookup": {
"from": "orders",
"let": {
"id": "$_id"
},
"pipeline": [
{
"$addFields": {
"owner": {
"$toObjectId": "$owner"
}
}
},
{
"$match": {
$expr: {
$eq: [
"$owner",
"$$id"
]
}
}
},
],
"as": "orders"
}
},
{
"$unwind": {
path: "$orders",
preserveNullAndEmptyArrays: false,
includeArrayIndex: "arrayIndex"
}
},
{
"$group": {
"_id": "$_id",
"order": {
"$last": "$orders.createdAt"
},
"userInfo": {
"$mergeObjects": {
name: "$name",
email: "$email",
phone: "$phone",
orderCount: "$orderCount"
}
}
}
},
{
"$project": {
name: "$userInfo.name",
email: "$userInfo.email",
phone: "$userInfo.phone",
orderCount: "$userInfo.orderCount",
lastOrder: "$order",
}
}
]
)
my documents look like the following for orders
{
"_id": ObjectId("607fbeeb0a752a66a7af40eb"),
"address": {
"loc": [
-1,
3
],
"_id": "5d35d55d3d081f486d0d401c",
"apartment": "",
"description": "ACcdg dfef"
},
"approvedAt": ISODate("2021-04-21T11:28:05.295+05:30"),
"assignedAt": null,
"billingAddress": {
"description": ""
},
"createdAt": ISODate("2021-04-21T11:28:04.449+05:30"),
"creditCard": "",
"deliveryDate": "04/21/21",
"deliveryDateObj": ISODate("2021-04-21T12:27:58.746+05:30"),
"owner": "609bd5831b912947ea51a9ac",
"products": [
"5a070c079b"
],
"updatedAt": ISODate("2021-04-21T11:28:05.295+05:30"),
}
and for users, it is like below
{
"_id": ObjectId("609bd5831b912947ea51a9ac"),
"updatedAt": ISODate("2021-05-12T18:47:55.291+05:30"),
"createdAt": ISODate("2021-05-12T18:47:55.213+05:30"),
"email": "1012#gmail.com",
"phone": "123",
"dob": "1996-04-10",
"password": "",
"stripeID": "",
"__t": "Customer",
"name": {
"first": "A",
"last": "b"
},
"orderCount": 1,
"__v": 0,
"forgottenPassword": ""
}
convert _id to string in lookup's let and you can remove $addFields from lookup pipeline
add $project stage in lookup pipeline and show only required fields
$project to show required fields and get last / max createdAt date use $max, you don't need to $unwind and $group operation
db.users.aggregate([
{
$lookup: {
from: "orders",
let: { id: { $toString: "$_id" } },
pipeline: [
{ $match: { $expr: { $eq: ["$owner", "$$id"] } } },
{
$project: {
_id: 0,
createdAt: 1
}
}
],
"as": "orders"
}
},
{
$project: {
email: 1,
name: 1,
orderCount: { $size: "$orders" },
phone: 1,
lastOrder: { $max: "$orders.createdAt" }
}
}
])
Playground
SUGGESTION:
You can save owner id in orders as objectId instead of string and whenever new order arrive store it as objectId, you can prevent conversation operator $toString operation
create an index in owner field to make lookup process faster.
I have figured out that after using createIndex for the owner field which is used to compare the owner in the orders from the users _id filed, so just after adding an db.orders.createIndex({ owner: 1 }), the query will run much faster and smoother

$lookup in nested array

I need a MongoDB query to return the aggregation result from a collection of events, users and confirmations.
db.events.aggregate([
{
"$match": {
"_id": "1"
}
},
{
"$lookup": {
"from": "confirmations",
"as": "confirmations",
"let": {
"eventId": "$_id"
},
"pipeline": [
{
"$match": {
"$expr": {
"$eq": [
"$eventId",
"$$eventId"
]
}
}
},
{
"$lookup": {
"from": "users",
"as": "user",
"let": {
"userId": "$confirmations.userId"
},
"pipeline": [
{
"$match": {
"$expr": {
"$eq": [
"$_id",
"$$userId"
]
}
}
},
]
},
},
]
}
}
])
Desired
[
{
"_id": "1",
"confirmations": [
{
"_id": "1",
"eventId": "1",
"user": {
"_id": "1",
"name": "X"
},
"userId": "1"
},
{
"_id": "2",
"eventId": "1",
"user": {
"_id": "2",
"name": "Y"
},
"userId": "2"
}
],
"title": "foo"
}
]
Everything works except the embedded user in confirmations array. I need the output to show the confirmations.user, not an empty array.
Playgound: https://mongoplayground.net/p/jp49FW59WCv
You made mistake in variable declaration of inner $lookup. Try this Solution:
db.events.aggregate([
{
"$match": {
"_id": "1"
}
},
{
$lookup: {
from: "confirmations",
let: { "eventId": "$_id" },
pipeline: [
{
$match: {
"$expr": {
$eq: ["$eventId", "$$eventId"]
}
}
},
{
$lookup: {
from: "users",
let: { "userId": "$userId" },
pipeline: [
{
$match: {
$expr: {
$eq: ["$_id", "$$userId"]
}
}
}
],
as: "user"
}
},
{ $unwind: "$user" }
],
as: "confirmations"
}
}
])
Also instead of $unwind of user inside inner $lookup you can use:
{
$addFields: {
user: { $arrayElemAt: ["$user", 0] }
}
}
since $unwind will not preserve empty results from previous stage by default.

How to Populate data using Mongodb Aggregate framework?

I have a current MongoDB Aggregate framework pipeline from a previous question and I am unable to add populate query to grab user profile by id.
My code is below
Product.aggregate([
{
$group: {
_id: {
hub: "$hub",
status: "$productStatus",
},
count: { $sum: 1 },
},
},
{
$group: {
_id: "$_id.hub",
counts: {
$push: {
k: "$_id.status",
v: "$count",
},
},
},
},
{
$group: {
_id: null,
counts: {
$push: {
k: { $toString: "$_id" },
v: "$counts",
},
},
},
},
{
$addFields: {
counts: {
$map: {
input: "$counts",
in: {
$mergeObjects: [
"$$this",
{ v: { $arrayToObject: "$$this.v" } },
],
},
},
},
},
},
{
$replaceRoot: {
newRoot: { $arrayToObject: "$counts" },
},
},
]);
and got the following result
[
{
"5fe75679e6f7a62ddaf5b2e9": {
"in progress": 5,
"Cancelled": 4,
"return": 1,
"on the way": 3,
"pending": 13,
"Delivered": 4
}
}
]
Expected Output
I need to grab user information from user collections using the hubId "5fe75679e6f7a62ddaf5b2e9" and expect a final result of the form below
[
{
"hub": {
"photo": "avatar.jpg",
"_id": "5fe75679e6f7a62ddaf5b2e9",
"name": "Dhaka Branch",
"phone": "34534543"
},
"statusCounts": {
"in progress": 5,
"Cancelled": 4,
"return": 1,
"on the way": 3,
"pending": 13,
"Delivered": 4
}
}
]
First id is user id and available in user collections.
You need to tweak your aggregate pipeline a little bit and include new pipeline stage like $lookup that populates the
hub
Product.aggregate([
{ "$group": {
"_id": {
"hubId": "$hubId",
"status": "$productStatus"
},
"count": { "$sum": 1 }
} },
{ "$group": {
"_id": "$_id.hubId",
"statusCounts": {
"$push": {
"k": "$_id.status",
"v": "$count"
}
}
} },
{ "$lookup": {
"fron": "users",
"localField": "_id",
"foreignField": "_id",
"as": "user"
} },
{ "$project": {
"user": { "$arrayElemAt": ["$user", 0] },
// "hub": { "$first": "$hub" },
"statusCounts": { "$arrayToObject": "$statusCounts" }
} }
])
To project only some fields in the user profile, you can update your $lookup pipeline to have the form
{ "$lookup": {
"from": "users",
"let": { "userId": "$_id" },
"pipeline": [
{ "$match": {
"$expr": { "$eq": ["$_id", "$$userId"] }
} },
{ "$project": {
"name": 1,
"phone": 1,
"photo": 1
} }
],
"as": "user"
} }

How to check if an element exists in a document and return true or false depending on it?

I have an aggregation query in which I use $lookup to get data from other collections. But I cannot understand how do I get a boolean value if a $match is found.
Schema
const likesSchema = new mongoose.Schema({
user: {
id: {
type: String,
required: true,
},
name: {
type: String,
required: true,
},
},
storyID: {
type: String,
required: true,
}
}, {
timestamps: true
});
Complete Query
const user_id = req.authorizedUser.sub;
const stories = await Story.aggregate([
{
$lookup: {
from: "comments",
localField: "storyID",
foreignField: "storyID",
as: "comments"
},
},
{
$lookup: {
from: "likes",
let: {storyID: "$storyID"},
pipeline: [
{
$match: {
$expr: { $eq: ["$$storyID", "$storyID"] }
}
},
{
$facet: {
"total": [{ $count: "count" }],
"byMe": [{
$match: {
$expr: { $eq: ["$user.id", user_id] } // Need boolean value if found/ not found
}
}]
}
}
],
as: "likes"
}
},
Snippet of Response
"likes": [
{
"total": [
{
"count": 2
}
],
"byMe": [
{
"_id": "5d04fe8e982bb50bbcbd2b48",
"user": {
"id": "63p6PpPyOh",
"name": "Ayan Dey"
},
"storyID": "b0g5GA6ZJFKkJcnJlp6w8qGR",
"createdAt": "2019-06-15T14:19:58.531Z",
"updatedAt": "2019-06-15T14:19:58.531Z",
"__v": 0
}
]
}
]
Required Response
"likes": {
"total": 2,
"byMe": true
}
You can use below aggregation
{ "$lookup": {
"from": "likes",
"let": { "storyID": "$storyID" },
"pipeline": [
{ "$match": { "$expr": { "$eq": ["$$storyID", "$storyID"] }}}
],
"as": "likes1"
}},
{ "$addFields": {
"likes.total": { "$size": "$likes1" },
"likes.byMe": { "$ne": [{ "$indexOfArray": ["$likes1.user.id", user_id] }, -1] }
}},
{ "$project": { "likes1": 0 }}
Or
{ "$lookup": {
"from": "likes",
"let": { "storyID": "$storyID" },
"pipeline": [
{ "$match": { "$expr": { "$eq": ["$$storyID", "$storyID"] }}},
{ "$facet": {
"total": [{ "$count": "count" }],
"byMe": [{ "$match": { "$expr": { "$eq": ["$user.id", user_id] }}}]
}}
{ "$project": {
"total": {
"$ifNull": [{ "$arrayElemAt": ["$total.count", 0] }, 0 ]
},
"byMe": { "$ne": [{ "$size": "$byMe" }, 0] }
}}
],
"as": "likes"
}},
{ "$unwind": "$likes" }