Find Top rated product in mongoose - node.js

I have a Review Model and Product Model such as:
const ProductReviewSchema = Schema({
rating: {
type: Number,
min: 1,
max: 5,
required: true,
},
product: {
type: mongoose.Types.ObjectId,
ref: 'ecommerce-product',
},
});
and
const ProductSchema = Schema({ name: String, price: Number });
and I want to display top-rated product I can do this using find method in reviewschema and using loops
const totalResult = await ProductReviewModel.find({}, 'rating _id');
let averageRatingNumerator = 0;
let averageRatingDenominator = 0;
if (totalResult) {
totalResult.forEach((eachRate) => {
averageRatingDenominator += 1;
averageRatingNumerator += eachRate.rating;
});
}
then find the average rating of each product and sort the result in ascending order. Can I do the exact same thing using mongoose aggregate, If yes what pipeline or operator should I use to achieve such a result:
My Expected result may be is:
[
{ product: 'abc', avgRating: 3.5 },
{ product: 'def', avgRating: 3.1 }
];

Search from the productReview Schema and populate the product,
$group by product and get average of rating
$sort by avgRating in descending order
$lookup and join product collection
$unwind deconstruct product
$project to show required fields
const totalResult = await ProductReviewModel.aggregate([
{
$group: {
_id: "$product",
avgRating: { $avg: "$rating" }
}
},
{ $sort: { avgRating: -1 } },
{
$lookup: {
from: "products", // update your actual product collection name
localField: "_id",
foreignField: "_id",
as: "product"
}
},
{ $unwind: "$product" },
{
$project: {
name: "$product.name",
avgRating: 1
}
}
]).exec();
Playground
Search from product Schema and populate productReview,
$lookup join products review collection
$size to get total reviews
$sum to calculate sum of return array of rating
$let to specify both (total, ratings) as variable
$cond check condition if total is not zero then $divide
$sort by avgRating in descending order
const totalResult = await Products.aggregate([
{
$lookup: {
from: "productsReview", // replace your actual review collection name here
localField: "_id",
foreignField: "product",
as: "review"
}
},
{
$project: {
name: 1,
avgRating: {
$let: {
vars: {
total: { $size: "$review" },
ratings: { $sum: "$review.rating" }
},
in: {
$cond: [{ $eq: ["$$total", 0] }, 0, { $divide: ["$$ratings", "$$total"] }]
}
}
}
}
},
{ $sort: { avgRating: -1 } }
]).exec();
Playground

Related

MongoDB Aggregate - How to implement match and lookup

I need to get the data from two collections (expenses, accounts). The data must be between the given date range and amount range.
date, amount, currency, type is in Expenses collection and accountId is in account collection as _id.
I tried the following query but it's not returning anything.
const response = await Expense.aggregate( [
{ $match: {
$and: [
{
currency: "1",
expenseType: "1"
},
{
date: {
$gte: new Date(date.value.from) , $lte: new Date(date.value.to)
},
amount: {
$gt: parseFloat(amount.value.from) , $lt: parseFloat(amount.value.to)
}
}
],
} },
{
$lookup: {
from: "accounts",
localField: "accountId",
foreignField: "_id",
as: "account"
}
}
] )
This is the schema of expenses collection:
Document of expense collection:
Document of account collection:
From the attached expense document, the amount field was a String type but not a Number type.
Data
amount: "20000"
Schema
amount: {
type: Number,
required: true
}
You should revise your Expense document in MongoDB with your schema.
From your Aggregation Query, you need to convert amount from String to Decimal to compare.
Solution 1: With $expr
db.expense.aggregate([
{
$match: {
$and: [
{
currency: "1",
expenseType: "1"
},
{
date: {
$gte: new Date(date.value.from),
$lte: new Date(date.value.to)
},
},
{
$expr: {
$and: [
{
$gt: [
{
"$toDecimal": "$amount"
},
parseFloat(amount.value.from)
]
},
{
$lt: [
{
"$toDecimal": "$amount"
},
parseFloat(amount.value.to)
]
}
]
}
}
],
}
},
{
$lookup: {
from: "accounts",
localField: "accountId",
foreignField: "_id",
as: "account"
}
}
])
Solution 1 on Mongo Playground
Solution 2: Add $set as first stage
db.expense.aggregate([
{
$set: {
amount: {
"$toDecimal": "$amount"
}
}
},
{
$match: {
$and: [
{
currency: "1",
expenseType: "1"
},
{
date: {
$gte: new Date(date.value.from),
$lte: new Date(date.value.to)
},
amount: {
$gt: parseFloat(amount.value.from),
$lt: parseFloat(amount.value.to)
}
}
]
}
},
{
$lookup: {
from: "accounts",
localField: "accountId",
foreignField: "_id",
as: "account"
}
}
])
Solution 2 on Mongo Playground

Aggregate data from different collections

I am currently working on a project that has the following schema using mongoose.
User schema
const userSchema = {
name: string
email: string
medicalVisits: [{type: Schema.ObjectId, ref: "records"}]
createdAt: Date
}
Records schema
const recordSchema = {
medication: [String],
rating: Number
user: [{type: Schema.ObjectId, ref: "user"}]
tests: [{type: Schema.ObjectId, ref: "tests"}]
createdAt: Date
}
Tests schema
testScore: Number
answers: Object
user: [{type: Schema.ObjectId, ref: "user"}]
createdAt: Date
From the little schema above, I have a setup where a patient can take tests multiple times and their respective tests are saved in the Tests collection. Also, the date is recorded for all tests they take. A doctor can request to see a patient's record, in this case, the patient has only one record document that has their tests records embedded in them. Currently, I am faced with the problem of getting a patient's newest and oldest test score alongside their initial details.
I can do a mongoose populate to get all information regarding a user, e.g
await User.findById(userId).populate({
path: "medicalVisits"
model: "records"
populate: {
path: "tests"
model: "test"
}
})
And that operation returns the patient's record and all the tests they have taken since they signed up to date. But when I make such a call to the Database, I just want to retrieve the patient's newest and oldest score. In other words, I want to get the patients, Initial test score, and their most recent test score. I am new to Mongoose aggregation, I tried to use the Mongoose aggregate function, but it returns an empty array, I guess I am missing something.
Currently, this is what my aggregate pipeline looks like.
const user = await Doctor.aggregate([
{ $match: { _id: docId } },
{
$lookup: {
from: "users",
localField: "patients",
foreignField: "_id",
as: "patients",
},
},
{ $unwind: "$patients" },
{ $unwind: "$patients.medicalVisits" },
{
$lookup: {
from: "records",
localField: "patients.user",
foreignField: "_id",
as: "patientRecord",
},
},
{ $unwind: "$patientRecord" },
// { $sort: { createdAt: 1 } },
{
$group: {
_id: docId,
user: { $last: "$patients" },
record: { $last: "$patientRecord"}
},
},
]);
return user[0];
From the above snippet, my intention is:
given a doctor Id, they can see a list of their patients and also see their newest and oldest test score.
Expected Output
const output = {
userId: 6e12euido....
name: "John doe"
email: "john#john.com"
rating: 2
initialTestScore: 10
recentTestScore: 30
}
How do I go about this? Or what could be a better alternative? Thank you very much.
tried my best to understand your case, and I think your aggregation pipeline should be like:
const patientsWithNewestRecord = await Doctor.aggregate([
{ $match: { _id: docId } },
{
$lookup: {
from: "users",
localField: "patients",
foreignField: "_id",
as: "patients",
},
},
// one patient, per doc
{ $unwind: "$patients" },
// one patient with all his/her visit records, per doc
{
$lookup: {
from: "records",
localField: "patients.medicalVisits",
foreignField: "_id",
as: "patientRecords",
},
},
// one patient with one visit record, per doc
{ $unwind: "$patientRecords" },
// sort by patient first, createdAt second
{ $sort: { 'patientRecords.user': 1, 'patientRecords.createdAt': 1 } },
{
$group: {
_id: { patient: '$patientRecords.user' },
user: { $last: "$patients" },
record: { $last: "$patientRecords"}
},
},
]);
this pipeline return a list of a doctor's patients and also see their newest test record. Oldest test record should be in similar war.
Based on these collections (as I understand them from your question):
// doctor collection:
{ _id: "doc1", patients: ["user1"] }
// user collection:
{
_id: "user1", name: "John", email: "john#gmail.com",
medicalVisits: ["record1", "record2"]
}
// record collection:
{ _id: "record1", rating: 2, tests: ["test1", "test2"] }
{ _id: "record2", rating: 4, tests: ["test3"] }
// test collection:
{ _id: "test1", testScore: 12, createdAt: ISODate("2021-12-04") }
{ _id: "test2", testScore: 9, createdAt: ISODate("2021-12-05") }
{ _id: "test3", testScore: 15, createdAt: ISODate("2021-12-24") }
we can apply:
db.doctor.aggregate([
{ $match: { _id: "doc1" } }
{ $lookup: {
from: "user",
localField: "patients", foreignField: "_id",
as: "patients"
}},
{ $unwind: "$patients" }, { $unwind: "$patients.medicalVisits" },
{ $lookup: {
from: "record",
localField: "patients.medicalVisits", foreignField: "_id",
as: "records"
}},
{ $unwind: "$records" }, { $unwind: "$records.tests" },
{ $lookup: {
from: "test",
localField: "records.tests", foreignField: "_id",
as: "tests"
}},
{ $unwind: "$tests" },
{ $sort: { "tests.createdAt": 1 } },
{ $group: {
_id: "$patients._id",
name: { $first: "$patients.name" },
email: { $first: "$patients.email" },
rating: { $first: "$records.rating" },
initialTestScore: { $first: "$tests.testScore" },
recentTestScore: { $last: "$tests.testScore" }
}},
{ $set: { "userId": "$_id" } }, { $unset: "_id" }
])
in order to extract:
{
userId: "user1",
name: "John",
email: "john#gmail.com",
rating: 2,
initialTestScore: 12,
recentTestScore: 15
}
Differences compared to your query:
I $lookup the test collection as it seems you information from there to get both test dates and test scores.
I $sort by test date (createdAt) before the $group by user such that we'll be able to define the right order for selecting the $first and $last test scores.
I extract user's information by using a $first on each group on user's field (since all unwind records for a given user have the same user information): for instance email: { $first: "$patients.email" }
I extract the $first and $last test scores for a user as defined by the $sort order: initialTestScore: { $first: "$tests.testScore" } and recentTestScore: { $last: "$tests.testScore" }.
I finally $set/$unset to rename the _id field into userId
I would suggest to do the following once you have the userId / patientId:
Get their tests (all) from the database in a sorted order
Take the first and last element of the array for your initial and final test report based on the sorting order you have applied (ascending or descending)
If you can just retrieve the user details and all the tests without any sorting, then you can proceed the following way:
Run a loop through all the tests and sort the tests according to test date.
Take the first and last element of the array for your initial and final test report based on the sorting order you have applied (ascending or descending)
You will not be performing the operations on DB end, so there might be a minor speed issue, but the difference would still come out to be in milliseconds unless a user takes a billion tests.
Let me know if this helps, let me know if it doesn't

how to use mongoose aggregate to search an array within a collection

I am trying to group by products in my sales collection and add their totals to know which are the best selling products of my app.
MONGOOSE MODEL
const mongoose = require('mongoose');
const DHCustomerinvoiceSchema = mongoose.Schema({
Saledetail: {
type: Array,
required: true
},
date:{
type: Date,
required: true
},
total:{
type: Number,
required: true
},
pay:{
type: Number,
required: true,
default: 0
},
topay:{
type: Number,
required: true
},
user:{
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'UserDH'
},
customer:{
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'DHcontacto'
},
state:{
type: String,
default: "OWED"
},
created:{
type: Date,
default: Date.now()
},
});
module.exports = mongoose.model('DHCustomerinvoice', DHCustomerinvoiceSchema);
COLLECTION EXAMPLE
{
"id": "5ef6*****",
"Saledetail": [
{
"id": "5ebf*****",
"quantity": 9,
"price": 2000,
"totalline": 18000
}
],
"datesale": "1593129600000",
"grandtotal": 18000,
"user": "5eb9ab******",
"customer": {
"name": "isabella"
},
"state": "PAID"
},
RESOLVER:
mostSellingProducts: async (_,{},ctx)=>{
const Products = await invoice.aggregate([
{ $unwind: "$Saledetail" },
{ $match: { "state" : 'PAID'}},
{ $group: {
_id : "$Saledetail.id",
total: { $sum: '$Saledetail.totalline' }
}},
{
$lookup: {
from: 'dhproducts',
localField: '_id',
foreignField: "_id",
as: "producto"
}
},
{
$limit: 4
},
{
$sort : {total: -1}
}
]);
console.log(Products);
return Products;
},
I have used many methods that actually did not give me this result, but nevertheless I have achieved a positive result in terms of finding my best clients who actually develop it with aggregate, match and group also apply sort and limit ...
but with this example I have not been able to achieve success, and I imagine that it is because the architecture of the collection is distinguished due to the arrangement of the purchase detail
I don't have enough reputation to comment on your question. So I am sharing this as an answer.
I think you can use $elemMatch to search for the item in an array.
const Productos = await Factura.aggregate([{ detlle: { $elemMatch: { $gte: 80, $lt: 85 } } }])
For more detailed info elemMatch
below the answer to my question
mostSellingProducts: async (_,{},ctx)=>{
const Products = await Invoice.aggregate([
{ $unwind: "$Saledetail" },
{ $match: { "state" : 'PAY'}},
{ $group: {
_id : { $toObjectId: "$Saledetail.id" },
total: { $sum: '$Saledetail.totalline' }
}},
{
$lookup: {
from: 'dhproducts',
localField: '_id',
foreignField: "_id",
as: "products"
}
},
{
$limit: 4
},
{
$sort : {total: -1}
}
]);
return Products;
},

Is it possible to use schema methods in mongoose aggregate return value?

I have a special situation and I use aggregate. But I could not use the schema methods in the return value of the aggregate.
Is there a way to solve this problem?
example source:
server:
itemSchema = new Schema({
name: String,
seller: ObjectId,
category: String,
price: Number,
....
});
itemSchema.methods = {
getPriceToString(){
return this.price.toString()
}
...
}
let delay = 1000*60*60*24*30*5;
let item = yield item.aggregate({
{
{
$project:
{
"_id": 1,
...
"created_at" : {
$cond: {
if: { ... },
then: {$subtract:["$created_at",delay]},
else: "$created_at" }
}
}
},
{
$lookup: {
"from": 'users',
"localField": 'seller',
"foreignField": '_id',
"as": 'seller'
},
},
{
$sort:
{
'created_at' : -1
}
},
{
$limit: 40
}
]).exec();
res.send({item:item});
front:
...
a(...)=item.seller.username
...
h2=item.getPriceToString()
error:
item.getPriceToString is not a function

Filtering a subdocument in mongoose containing an array of objects

Im trying to return a list of ratings from a given user, filtered by category.
var UserSchema = new Schema({
...
ratings: [{
item: { type: Schema.Types.ObjectId, ref: 'Items' },
category: String,
rating: Number
}]
If I do the following, I only get the first rating for that category:
var query = User.findOne(userId, 'ratings');
query.select({ 'ratings': { $elemMatch: { 'category': req.query.category }}});
The following also returns only the first rating:
var query = User.find();
query.where({'_id': userId, 'ratings.category': req.query.category});
query.select({ 'ratings.$': 1 });
I was able to aggregate the correct results with the following, however, I dont think that'll work since I cant populate after an aggregation.
var query = User.aggregate(
{ $match: { _id: userId }},
{ $project: { _id: 1, ratings: 1 }},
{ $unwind: '$ratings' },
{ $match: { 'ratings.category': req.query.category }}
);
You could do the population after your aggregation, something like this:
var pipeline = [
{ $match: { _id: userId }},
{ $project: { _id: 1, ratings: 1 }},
{ $unwind: '$ratings' },
{ $match: { 'ratings.category': req.query.category } }
];
User.aggregate(pipeline, function (err, result){
User.populate(result, {path: "ratings.item"}, callback);
});

Resources