Mongoose calculating average from another collection - node.js

I am new to MongoDB/Mongoose. I am trying to do a search based on a key string for a 'Resource' which will return a list of resources based on average of ratings for that resource. I am having a hard time calculating and returning the average. This is my schema.
Resource Schema:
const ResourceSchema = mongoose.Schema({
title: {
type: String,
required: true,
},
type: {
type: String,
required: true,
},
url: {
type: String,
required: true,
},
createdDate: {
type: Date,
default: Date.now,
},
});
module.exports = mongoose.model("Resource", ResourceSchema);
Rating Schema:
const RatingSchema = mongoose.Schema({
resourceId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Resource",
},
createdDate: {
type: Date,
default: Date.now,
},
rating: {
type: Number,
required: true,
min: 1,
max: 5,
},
review: {
type: String,
required: true,
},
});
module.exports = mongoose.model("Rating", RatingSchema);
Each Resource will have multiple Ratings. I am trying to calculate the average of ratings in my list of fetched Resources and add it to the search results.
This is what I have for my search:
Resource.find({
$or: [
{ title: { $regex: req.params.searchStr.toLowerCase(), $options: "i" } },
{ url: { $regex: req.params.searchStr.toLowerCase(), $options: "i" } },
],
})

Here's one way you could do it.
db.resources.aggregate([
{ // filter resources
"$match": {
"title": {
"$regex": "passenger",
"$options": "i"
},
"url": {
"$regex": "https",
"$options": "i"
}
}
},
{ // get ratings for resource
"$lookup": {
"from": "ratings",
"localField": "_id",
"foreignField": "resourceId",
"pipeline": [
{
"$project": {
"_id": 0,
"rating": 1
}
}
],
"as": "ratings"
}
},
{ // calculate average
"$set": {
"avgRating": { "$avg": "$ratings.rating" }
}
},
{ // don't need ratings array anymore
"$unset": "ratings"
}
])
Try it on mongoplayground.net.

Related

populate a field in aggregation result

const commentSchema = new mongoose.Schema({
name: {
type: String,
default: "anonymous",
},
comment: {
type: String,
required: true,
},
post: {
type: mongoose.Schema.Types.ObjectId,
ref: "Post",
required: true,
},
}, {
timestamps: true
});
const trending = await Comment.aggregate([{
$group: {
_id: "$post",
total: {
$sum: 1
},
},
}, ]);
Result of aggregation
[{
"_id": "61eb55808551961dc737c00c",
"total": 5
},
{
"_id": "61eb4a490a894ac62bd833ab",
"total": 2
}
]
hello, please is there a way i can populate the field _id after using aggregation, i want to get all the fields of post not just the _id?

How to return count array of subdocuments mongoose

I want to get the total amount of comments from Place models and I couldn't find a way to get it because I don't want to populate the comment with GET /places.
This is my place model
const placeSchema = mongoose.Schema(
{
type: { type: String },
english: { type: String },
province_code: { type: String },
district_code: { type: String, required: true },
commune_code: { type: String },
village_code: { type: String },
lat: { type: Number },
lon: { type: Number },
body: { type: String },
images: [{ type: String }],
comments: [{ type: mongoose.Types.ObjectId, ref: Comment }],
},
{
timestamps: {
createdAt: 'created_at',
updatedAt: 'updated_at',
},
}
)
I use this query
data = await Place
.find()
.limit(5)
.skip(0)
.populate('comments')
.exec()
I want to get the response like this
{
"data": [
{
"images": [],
"comments": 6,
"type": "place",
"english": "99Boko",
"province_code": "23",
"district_code": "2302",
"commune_code": "230202",
"village_code": "23020202",
"lat": 135.2039,
"lon": 104.01734762756038,
"body": "<b>This place is really good</b>",
"created_at": "2021-07-20T17:41:52.202Z",
"updated_at": "2021-07-20T17:41:52.202Z",
"id": "60f70ae08e54941530d14c4c"
},
]}
Does anybody know the solution to get this kind of response ?
I have figured out to get the comment length is to use virtual count
placeSchema.virtual('comment_length', {
ref: Comment, // model to use for matching
localField: 'comments', // from `localField` i.e., Place
foreignField: '_id', // is equal to `foreignField` of Comment schema
count: true, //only get the number of docs
})
placeSchema.set('toObject', { virtuals: true })
placeSchema.set('toJSON', { virtuals: true })
and use this query
data = await Place
.find().populate({ path: 'comment_length', count: true })
.exec()

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;
},

Get the average value with mongoose query

I'm getting a list of 'spot' with mongoose filtered by location and other stuff, with the code below which works fine.
But I want the value of 'rate' to be a $avg (average) of all reviews and not the list of the reviews. It's an aggregation of another collection.
this is what I get:
{
"_id":"5f0ade7d1f84460434524d3d",
"name":"Fly",
...
"rate":[
{"_id":"5f0bfca64ca1cc02ffe48faf","spot_id":"5f0ade7d1f84460434524d3d","rate":3},
{"_id":"5f0bfdb44ca1cc02ffe48fb0","spot_id":"5f0ade7d1f84460434524d3d","rate":2},
{"_id":"5f0bfdb44ca1cc02ffe48fb1","spot_id":"5f0ade7d1f84460434524d3d","rate":1}
]
},
but I would like this kind of result:
{
"_id":"5f0ade7d1f84460434524d3d",
"name":"Fly",
...
"rate": 2
},
I tried many different things, and I guess I need to use $group but can't figure out how to get the right output.
the reviews schema:
const reviewsSchema = new mongoose.Schema({
_id: {
type: ObjectId,
required: true,
},
spot_id: {
type: ObjectId,
required: true,
},
rate: {
type: Number,
required: true,
},
})
the spot Schema
const spotsSchema = new mongoose.Schema({
_id: {
type: ObjectId,
required: true,
},
name: {
type: String,
required: true,
},
...
})
The code:
Spots.aggregate
([
{
$geoNear: {
near: { type: "Point", coordinates: [ parseFloat(longitude), parseFloat(latitude) ] },
distanceField: "location",
maxDistance: parseInt(distance) * 1000,
query: {
$and: [
{ $or : filter },
{ $or : closed },
{ published: true }
]
}
}
},
{ $match: {} },
{
$lookup:{
from: 'reviews',
localField: '_id',
foreignField: 'spot_id',
as: 'rate'
}
},
])
You're really close, you just have to actually calculate the avg value which can be done using $map and $avg like so:
{
$addFields: {
rate: {
$avg: {
$map: {
input: "$rate",
as: "datum",
in: "$$datum.rate"
}
}
}
}
}
MongoPlayground

MongoDB: A complex query with array input

I'm stuck at finding a solution for the following query.
1) a user can select many categories and subcategories.
2) the user can see all other users how are selected the same categories and subcategories within a certain radius.
Here is the Schema of the user
const userSchema = new Schema(
{
image: { type: String, default: 'NA' },
firstName: { type: String, default: 'first name' },
lastName: { type: String, default: 'last name' },
email: { type: String, lowercase: true, unique: true, trim: true },
password: { type: String, min: 6 },
gender: { type: String, emun: ['male','female','other'] },
about: { type: String, default: 'about you' },
address: {
zipCode: { type: Number, default: 000000 },
place: { type: String, default: 'place' },
street: { type: String, default: 'street' },
country: { type: String, default: 'Country' },
location: {
type: { type: String, default:'Point'},
coordinates: { type:[Number], index:'2dsphere', default:[0,0] }
}
},
interests: [
{
_id : false,
category: {
id: { type: Schema.Types.ObjectId, ref: 'Category' },
name: { type: String }
},
subCategory: [
{
_id : false,
id: { type: Schema.Types.ObjectId, ref: 'Subcategory' },
name: { type: String }
}
]
}
]
}
);
In my controller here is what I tried
homeData: async (req, res, next) => {
const limit = Number(req.params.limit);
const { latitude, longitude, minDistance, maxDistance } = getUserCurrentLocation(req);
const usersWithSameInterests = await User.aggregate([
{
"$geoNear": {
"near": {
"type": "Point",
"coordinates": [longitude, latitude]
},
"distanceField": "distance",
"minDistance": minDistance,
"maxDistance": maxDistance,
"spherical": true,
"query": { "location.type": "Point" }
}
},
{
"$match": { "interests": { "$elemMatch": {name: 'Sports'} }} // hard coded for testing
},
{ "$sort": { "distance": 1 } },
{ "$limit" : limit },
{
"$project": {
"_id": 1,
"image": 1,
"firstName":1,
"lastName":1,
"distance": 1,
"createdAt": 1
}
}
]);
return respondSuccess(res, null, {
newNotification: false,
usersWithSameInterests: usersWithSameInterests
});
},
The response i'm getting is
{
"success": true,
"message": "query was successfull",
"data": {
"newNotification": false,
"usersWithSameInterests": []
}
}
Sample categories and subcategories
Category: Sports
Subcategories: Cricket, Football, Hockey, Tennis
Category: Learning Languages
Subcategories: English, German, Spanish, Hindi
looking forward for much-needed help.
thank you.
It seems that you have a few mismatched columns.
On the $geonear pipeline, the line "query": { "location.type": "Point" } should be: 'query': {'address.location.type': 'Point'}.
And on the $match pipeline, the line { "interests": { "$elemMatch": {name: 'Sports'} } should be 'interests': { '$elemMatch:' {'category.name': 'Sports'} }
Edit:
To match multiple interests on the category and subcategory field, You can use the $in operator on the $match pipeline. Like this:
{
'interests.category.name': { $in: ['Sports'] },
'interests.subCategory.name': {$in: ['Soccer']}
}
It'll return anyone that have Sports in the category name, and Soccer on subcategory name.

Resources