Get the average value with mongoose query - node.js

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

Related

Mongoose calculating average from another collection

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.

Mongoose: Sum of all points queried by a particular user

I have a collection in Mongoose called Points where I have a history of all points of a user
This is the point schema
const PointSchema = mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
reason: {
type: String,
required: true,
},
points: {
type: Number,
required: true,
},
time: {
type: Date,
default: Date.now,
},
});
I want a JSON output like this
{
"points": 30,
"history": [
{ Point object },
{ Point object },
{ Point object },
]
}
How can I get the sum of all the points queried by a particular user, and get the output as above if I have the particular user's id?
You could do a simple aggregation where you first $match the user-id, then $sum all the points and finally push each document to the history array:
db.collection.aggregate([
{
"$match": {
user: "<user-id-to-match>"
}
},
{
"$group": {
"_id": "$user",
"points": {
$sum: "$points"
},
history: {
"$push": "$$ROOT"
}
}
},
{
$project: {
_id: 0
}
}
])
Here's an example I've created on mongoplayground: https://mongoplayground.net/p/Q_TtcI_dkZu

Cascaded join using mongoose node js

I am trying to fetch data from MongoDB using Node Js. I have three schemas: Projects, Users, and Teams.
I need to retrieve the project details based on it's type with the worker users.
I got stuck in making join for these schemas:
Projects:
const Project = new Schema({
projectName: { type: String, required: true, trim: true },
type: { type: String, required: true, trim: true },
teamID: { type: Schema.Types.ObjectId, required: true },
});
Teams
const Team = new Schema({
teamId: { type: Schema.Types.ObjectId, required: true, trim: true },
users: { type: [Schema.Types.ObjectId], required: true, trim: true },
teamName: { type: String, required: true },
});
Users:
const User = new Schema({
userId: { type: Schema.Types.ObjectId, required: true, trim: true },
name: { type: String, required: true, trim: true },
profilePicture: { type: String, required: true, trim: true },
});
I am trying to find a way to get
[
{
projectName: "s",
type: "w",
users: ["Jon", "Ali", "Mark"]
},
{
projectName: "a",
type: "w",
users: ["Jon", "Mark"]
}, {
projectName: "s",
type: "w",
users: ["Jon", "Ali", "Mark"]
},
]
I tried to use $lookup, but I can not use it because the relation is complex many to many relations.
Is there a way more efficient than retrieving all users, all teams, and all projects?
I think there is no other efficient way except aggregation and without lookup we can't join collections, You can use nested lookup,
$match condition for type
$lookup to join Team collection using teamID
$match teamID
$lookup to join User collection using users array
$project to convert user's name array using $map
$addFields to get users array in users using $arrayElemAt
db.Project.aggregate([
{ $match: { type: "w" } },
{
$lookup: {
from: "Team",
let: { teamID: "$teamID" },
as: "users",
pipeline: [
{ $match: { $expr: { $eq: ["$$teamID", "$teamId"] } } },
{
$lookup: {
from: "User",
localField: "users",
foreignField: "userId",
as: "users"
}
},
{
$project: {
users: {
$map: {
input: "$users",
in: "$$this.name"
}
}
}
}
]
}
},
{ $addFields: { users: { $arrayElemAt: ["$users.users", 0] } } }
])
Playground
Second possible way, you can combine $project and $addFields stages in single stage,
{
$addFields: {
users: {
$arrayElemAt: [
{
$map: {
input: "$users.users",
in: "$$this.name"
}
},
0
]
}
}
}
Playground

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

Check if sub document is found in $project

I am trying to find all the trips provided by a company, grouped by the driver of the bus, and check if a given passenger was part of the trip.
Content is an array that can have reference to multiple models: User, Cargo, etc.
I can somewhat achieve my desired result using:
traveled: { $in: [ passengerId, "$content.item" ] },
But i want to confirm that the matched id is indeed a 'User'(passenger). I have tried:
traveled: { $and: [
{ $in: [ passengerId, "$content.item" ] },
{ $in: [ `Passenger`, "$content.kind" ] },
]},
But it also matches if the passed id has a kind of 'Cargo' when there is another content with a kind of 'User' is inside the array.
// Trip
const schema = Schema({
company: { type: Schema.Types.ObjectId, ref: 'Company', required: false },
driver: { type: Schema.Types.ObjectId, ref: 'User', required: true },
description: { type: String, required: true },
....
content: [{
kind: { type: String, required: true },
item: { type: Schema.Types.ObjectId, refPath: 'content.kind', required: true }
}]
});
const Trip = mongoose.model('Trip', schema, 'trips');
Trip.aggregate([
{ $match: { company: companyId } },
{
$project: {
_id: 1,
driver: 1,
description: 1,
traveled: { $in: [ passengerId, "$content.item" ] },
// traveled: { $and: [
// { $in: [ passengerId, "$content.item" ] },
// { $in: [ `Passenger`, "$content.kind" ] },
// ]},
}
},
{
$group : {
_id: "$driver",
content: {
$push: {
_id: "$_id",
description: "$description",
traveled: "$traveled",
}
}
},
}
]).then(console.log).catch(console.log);
There is no $elemMatch operator in $project stage. So to use mimic similar functionality you can create $filter with $size being $gt > 0.
Something like
"traveled":{
"$gt":[
{"$size":{
"$filter":{
"input":"$content",
"as":"c",
"cond":{
"$and":[
{"$eq":["$$c.item",passengerId]},
{"$eq":["$$c.kind","User"]}
]
}
}
}},
0
]
}

Resources