Mongo aggregation lookup - mongodb

I am trying to attempt the following
From a log, find all prizes the user entered
return the order numbers and the title
I am using the mongo aggregation framework.
I match by userId
I group the prizes by the same product and then push order numbers
I want to do a look up to the prizes collection to find the title of the prizes.
The look up is returning no elements
Here is the aggregation code
db.pointslogs.aggregate(
// Pipeline
[
// Stage 1
{
$match: {
"user": ObjectId("5aacff47c67f99103bcbf693")
}
},
// Stage 2
{
$group: {
_id: "$productPurchased",
orderNumber: { $push: "$orderNumber" }
}
},
// Stage 3
{
$unwind: {
path : "$orderNumber",
}
},
// Stage 4
{
$lookup: {
"from" : "prizes",
"localField" : "_id",
"foreignField" : "_id",
"as" : "title"
}
},
]
// Created with Studio 3T, the IDE for MongoDB - https://studio3t.com/
);
Here are the models (points log below)
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var schema = new Schema({
orderNumber: {type: String, required: true},
productPurchased: {type: String, require: true},
answer: {type: String},
user: [{type: Schema.Types.ObjectId, ref: 'User'}]
});
module.exports = mongoose.model('PointsLog', schema);
Prizes model below
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var schema = new Schema({
title: {type: String, required: true},
content: {type: String, required: true},
orderNumber: {type: String, required: true},
imageUrl: {type: String, required: true},
stockQty: {type: Number, required: true},
question: {type: String, required: true},
answers: [{type: String, required: true}],
image: {type: String, required: true},
cost: {type: Number, required: true },
//entries: {type: Int},
//user: {type: Schema.Types.ObjectId, ref: 'User'}
user: [{type: Schema.Types.ObjectId, ref: 'User'}]
});
module.exports = mongoose.model('Prize', schema);
Not sure why lookup is returning nothing

1)You can use the upgraded lookup
Below is an Example->
db.pointslogs.aggregate([
// Stage 1
{
$match: {
"user": ObjectId("5aacff47c67f99103bcbf693")
}
},
// Stage 2
{
$group: {
_id: "$productPurchased",
orderNumber: { $push: "$orderNumber" }
}
},
// Stage 3
{
$unwind: {
path : "$orderNumber",
}
},
// Stage 4
{
$lookup:
{
from: "prizes",
let: { oNumber: "$orderNumber" },
pipeline: [
{ $match:{ $expr: { $eq: [ "$orderNumber", "$$oNumber" ] } }
}
},
{ $project: {
title: 1,
content: 1,
orderNumber: 1,
imageUrl: 1,
stockQty: 1,
question: 1,
answers: 1,
image: 1,
cost: 1,
user: 1
} }
],
as: "orderNumber"
}
}
])

Related

How to sort result by number of refs in MongoDB?

I'm creating an inventory application, in which items can be added to an inventories. Items and inventories share a many-to-many relationship. These are my schemas for both:
const itemSchema = mongoose.Schema({
name: {
type: String,
required: true,
},
emoji: {
type: String,
required: true,
},
createdBy: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
default: null,
},
category: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Category',
},
});
const inventorySchema = mongoose.Schema({
owner: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
},
access: {
type: String,
enum: ['PRIVATE', 'PUBLIC'],
default: 'PRIVATE',
},
category: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Category',
},
items: [
{
item: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Item',
},
quantity: {
type: Number,
default: 0,
},
},
],
sharedWith: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Group',
},
],
});
What I require...
I basically want to apply a sort to return me all the items in the order from most added to least added (in all Inventories).
I can create a counter field inside the Item Model and update it every time an item is added or removed from an inventory, but it does not seem like the solution for this. Any help would be appreciated!
You did not provide sample documents but is sounds like this should work:
Keep only necessary fields
$unwind so each item in each inventory is a document
$group items by _id and count the quantity for each.
Format the data and sort by count
db.inventories.aggregate([
{$project: {items: 1, _id: 0}},
{$unwind: "$items"},
{$group: {_id: "$items.item", count: {$sum: "$items.quantity"}}},
{$lookup: {
from: "items",
localField: "_id",
foreignField: "_id",
as: "item"
}
},
{$project: {item: {$mergeObjects: [{$first: "$item"}, {count: "$count"}]}}},
{$replaceRoot: {newRoot: "$item"}},
{$sort: {count: -1}}
])

Get extra field in model summing all records from lookup document

Having this model:
const matchSchema = mongoose.Schema({
location: {type: mongoose.Types.ObjectId, ref: 'Location'},
datetime: Date,
teamAName: String,
teamBName: String,
teamA: [{type: mongoose.Types.ObjectId, ref: 'Player'}],
teamB: [{type: mongoose.Types.ObjectId, ref: 'Player'}],
teamAScore: {type: Number, default: 0},
teamBScore: {type: Number, default: 0},
pichichi: [{type: mongoose.Types.ObjectId, ref: 'Player'}],
mvp: {type: mongoose.Types.ObjectId, ref: 'Player', default:null},
});
"teamA" and "teamB" are lists containing the "_id" of every player. When retrieving a player, I want to retrieve the number of matches that he/she have played. How can I do that? Below my query just retrieving fields from model "Player"
class PlayerController {
getAll(req, res) {
Player.find()
.sort('firstname')
.exec(function(err, players) {
res.send(players);
});
}
}
So, instead of just having this list:
[
{
_id: new ObjectId("6232395d08663294b412d6a1"),
firstname: 'Sam',
lastname: 'Credington',
__v: 0
},
{
_id: new ObjectId("622479f39be8118a52af70e5"),
firstname: 'Santi',
lastname: 'Futsal',
__v: 0
},
{
_id: new ObjectId("6232399608663294b412d6b9"),
firstname: 'Tom',
lastname: 'Hendry',
__v: 0
}
]
I would like to have the amount of matches that every player played:
[
{
_id: new ObjectId("6232395d08663294b412d6a1"),
firstname: 'Sam',
lastname: 'Credington',
matches:6,
__v: 0
},
{
_id: new ObjectId("622479f39be8118a52af70e5"),
firstname: 'Santi',
lastname: 'Futsal',
matches:8,
__v: 0
},
{
_id: new ObjectId("6232399608663294b412d6b9"),
firstname: 'Tom',
lastname: 'Hendry',
matches: 2,
__v: 0
}
]
Here's one way you could do it.
db.players.aggregate([
{
"$lookup": {
"from": "matches",
"let": { "myId": "$_id" },
"pipeline": [
{
"$match": {
"$expr": {
"$in": [ "$$myId", { "$setUnion": [ "$teamA", "$teamB" ] } ]
}
}
},
{ "$count": "numMatches" }
],
"as": "matchCount"
}
},
{
"$set": {
"matches": {
"$ifNull": [ { "$first": "$matchCount.numMatches" }, 0 ]
}
}
},
{ "$unset": "matchCount" }
])
Try it on mongoplayground.net.

Mongoose/Mongodb Aggregate - group and average multiple fields

I have a Post model with 2 fields : date and rating. How would I go about getting an average aggregate rating for each date? So group by date first and then average the rating across all posts for that date. I need to do this within mongoose but their docs are so difficult to understand.
const PostSchema = new Schema({
date: {
type: String,
default: getToday() //this is just a new Date() formatted
},
rating: {
type: Number,
required: true
}
},
)
This gives me the average across all dates but I can't figure out how to filter it by date:
Post.aggregate([
{ $group: { _id: null, avgRating: { $avg: '$rating' }}}
])
.then(function (res) {
console.log(res[0]["avgRating"]);
})
This worked for me:
Post.aggregate([
{ $group: { _id: "$date", avgRating: { $avg: '$rating' }}}
]).
then(function (res) {
console.log(res);
})
Output:
[
{ _id: 'Aug 18, 2021', avgRating: 3.0212234706616727 },
{ _id: 'Aug 19, 2021', avgRating: 2.9680319680319682 },
{ _id: 'Aug 20, 2021', avgRating: 3.023976023976024 },
{ _id: 'Aug 17, 2021', avgRating: 2.9600665557404326 },
{ _id: 'Aug 21, 2021', avgRating: 3.072661217075386 }
]
BUT it would be great if I could somehow filter this based on other factors. For example, each post has an author (reference to User model). How would I go about filtering based on the author's country.name or gender?
User model:
const userSchema = new Schema({
email: {
type: String,
required: true,
unique: true
},
birthday: {
type: Date,
required: true,
},
gender:{
type: String,
required: true
},
country:{
name: {
type: String,
required: true
},
flag: {
type: String,
// default: "/images/flags/US.png"
}
},
avatar: AvatarSchema,
displayName: String,
bio: String,
coverColor: {
type: String,
default: "#343a40"
},
posts: [
{
type: Schema.Types.ObjectId,
ref: "Post"
}
],
comments: [
{
type: Schema.Types.ObjectId,
ref: "Comment"
}
],
postedToday: {
type: Boolean,
default: false
},
todaysPost: {
type: String
}
})
Something like this
Post.aggregate([
{$match: {"date": today}},
{$group: {_id: {"country": "$author.country.name"}, avgRating: {$avg: "$rating"}}}
]).then(function(res) {
console.log(res)
})

referencing documents only by the mongo objectID?

is it possible to reference another value other than the mongo generated _id?
User Model
uid: {type: String, required: true},
channel_pub: {type: String},
channel_groups: [{type: String}],
auth_key: {type: String},
channels: [{
name: {
type: String,
ref: 'channel'
}
}]
Channel Model
name: {type: String, required: true},
uid: [{
type: String,
ref: 'user',
required: true
}]
I am trying to reference the actual channel name in the user document.
You can do this with Populate Virtuals since mongoose 4.5.0 :
var UserSchema = new mongoose.Schema({
uid: { type: String, required: true }
}, {
toJSON: {
virtuals: true
}
});
var ChannelSchema = new mongoose.Schema({
name: { type: String, required: true },
uid: [{
type: String,
ref: 'User',
required: true
}]
});
UserSchema.virtual('channels.data', {
ref: 'Channel',
localField: 'channels.name',
foreignField: 'name'
});
Here the local field is channels.name, the Channel object will be populated in channels.data.
For instance a find with channels.data populated :
User.find({}).populate('channels.data').exec(function(error, res) {
console.log(JSON.stringify(res, null, 4));
});
will give :
[{
"_id": "588a82ff7fe89686fd2210b0",
"uid": "user1",
"channels": [{
"data": {
"_id": "588a80fd7fe89686fd2210a8",
"name": "channel1",
"uid": []
},
"name": "channel1"
}, {
"data": {
"_id": "588a80fd7fe89686fd2210a9",
"name": "channel2",
"uid": []
},
"name": "channel2"
}],
"id": "588a82ff7fe89686fd2210b0"
}
...
]

MongoDB utilizing an aggregate average result

I have a collection of documents that looks like this:
var schema = new Schema({
name: {type: String, required: true},
gradeLevels: {type: String},
subject: {type: String},
standard: {type: String},
description: {type: String},
price: {type: String},
numberOfInternalReviews: {type: Number},
reviews: [{
content: {type: String},
rating: {type: Number},
user: {type: Schema.Types.ObjectId, ref: 'User'},
userFirstName: {type: String},
softDelete: {type : Boolean}
}],
averageUserRating: {type: Number}
});
I have them created an aggregate formula that produces the average of the ratings for each review, as such:
db.applications.aggregate([
{ $unwind: "$reviews" },
{ $group: { _id: "$_id", reviews: { $push: "$reviews" },
averageUserRating: { $avg: "$reviews.rating" } } },
{ $project: { _id: 0, _id: "$_id", reviews: 1, averageUserRating: 1 } }]);
This does produce the correct results of the average, as needed, within the console.
What I need to happen is to push the result into the original object's averageUserRating field.
When I then look at the application document, the averageUserRating is still returning null.
How can I make this happen?
maybe it is a type inb project, so: change this:
{ $project: { _id: 0, _id: "$_id", reviews: 1, averageUserRating: 1 } }]);
to:
{ $project: { _id: "$_id", reviews: 1, averageUserRating: 1 } }]);