How can I populate the fields after running the aggregation mongodb - node.js

I am trying to populate the fields in the result that I got from running the $geoNear aggregation but I don't know how can I do it. I tried $lookup but it gave me the same result.
const users = await locations.aggregate([
{
$geoNear: {
near: {
type: "Point",
coordinates: req.body.coordinates,
},
maxDistance: req.body.maxDistance,
distanceField: "dist.calculated",
spherical: true,
},
{
$lookup: {
from: "users",
localField: "userId", // getting empty array of users
foreignField: "_id",
as: "users",
},
}, // getting all users data
{
$project: {
_id: 1,
name: 1,
profession: 1,
},
}, // only getting `_id`
},
]);
Result:
{
{
"_id": "63555c4f29820cf3c7667eb5",
"userId": "63555c2629820cf3c7667eac",
"location": {
"coordinates": [
2.346688,
48.858888
],
"type": "Point"
},
"createdAt": "2022-10-23T15:22:55.820Z",
"updatedAt": "2022-10-23T16:08:59.979Z",
"__v": 2,
"dist": {
"calculated": 42.95013302539912
}
}
}
I want to populate the name field from the users schema and to do so I have used the ref of the users schema in the locations schema.
users schema:
{
name: {
type: String,
required: true,
},
email: {
type: String,
required: true,
unique: true,
index: true,
},
}
location Schema:
{
userId: {
type: mongoose.Schema.ObjectId,
ref: "users",
required: true,
},
location: {
type: {
type: String,
enum: ["Point"],
default: "Point",
},
coordinates: {
type: [Number],
default: [0, 0],
},
},
},
How can I populate fields in my aggregation result?
Also, How can I remove all the fields and output the names and email only from the users schema?

You should include your $lookup stage as following:
{
$lookup: {
from: "users",
localField: "userId",
foreignField: "_id",
as: "user",
}
},
{
"$replaceRoot": {
"newRoot": {
"$arrayElemAt": [
"$user",
0
]
}
}
},
{
$project: {
_id: 1,
name: 1,
profession: 1
}
}
It seems like you tried to match an ObjectId to a String (property name), which will never match. Using the aggregation step above you should receive a populated user array containing exactly one entry. After that you can project the result and transform the array to a plain object (if needed).

Related

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

$lookup returns an empty array mongoose

I am using the following code with $lookup function.
postSchemaModel.aggregate([{
"$geoNear": {
"near": { "type": "Point", "coordinates": [6.7336665, 79.8994071], "Typology": "post" },
"distanceField": "dist.calculated",
"maxDistance": 5000,
"includeLocs": "dist.location",
"spherical": true
}
},
{ "$limit": limit },
{ "$skip": startIndex },
{ "$sort": { "createdAt": -1 } },
{
"$lookup": {
"from": userSchemaModel.collection.name,
"localField": "user_id",
"foreignField": "_id",
"as": "user_id"
}
},
{
"$project": {
"post_data": 1,
"likes": 1,
"commentsCount": 1,
"post_img": 1,
"isUserLiked": 1,
"usersLiked": 1,
'exp_date': 1,
"has_img": 1,
"user_id": "$user_id",
"typology": 1,
"geometry": 1,
"category": 1,
"created": 1,
"createdAt": 1,
"updatedAt": 1,
}
},
]).then(async function(posts) {
//some code here
});
The problem is this gives me an empty array for user_id. The following is one output I receive.
{ _id: 5ee1f89732fd2c33bccfec55,
post_data: 'vvhhh',
likes: 1,
commentsCount: 0,
post_img: null,
isUserLiked: false,
usersLiked: [ 5edf43b93859680cf815e577 ],
exp_date: 2020-06-12T18:30:00.000Z,
has_img: false,
typology: 'chat',
geometry:
{ pintype: 'Point',
_id: 5ee1f89732fd2c33bccfec56,
coordinates: [Array] },
category: [],
created: 1591867543468,
createdAt: 2020-06-11T09:25:43.478Z,
updatedAt: 2020-06-15T10:01:01.133Z,
user_id: [] }
In my case I don't want it to be null and I am expecting a output like below.
{ _id: 5ee1f89732fd2c33bccfec55,
post_data: 'vvhhh',
likes: 1,
commentsCount: 0,
post_img: null,
isUserLiked: false,
usersLiked: [ 5edf43b93859680cf815e577 ],
exp_date: 2020-06-12T18:30:00.000Z,
has_img: false,
typology: 'chat',
geometry:
{ pintype: 'Point',
_id: 5ee1f89732fd2c33bccfec56,
coordinates: [Array] },
category: [],
created: 1591867543468,
createdAt: 2020-06-11T09:25:43.478Z,
updatedAt: 2020-06-15T10:01:01.133Z,
user_id: { img: 'default-user-profile-image.png',
_id: 5edd103214ce223088a59236,
user_name: 'Puka' }
}
My userSchema is something like below
var userSchema = mongoose.Schema({
//some other fields
user_name: {
type: String,
max: 30,
min: 5
},
img: {
type: String,
default: 'default-user-profile-image.png'
},
//some other fields
});
userSchema.plugin(uniqueValidator);
var userSchemaModel = mongoose.model('users', userSchema);
module.exports = {
userSchemaModel,
}
According to the other answers here I tried using mongoose.Types.ObjectId(userId), but it gives complete empty set.
What can be the problem here and it will be really helpful if someone can help me with this as I'm stuck with this for days.
Update :
post schema
var postSchema = mongoose.Schema({
user_id: {
type: String,
required: true,
ref: 'users'
},
//other fields
var postSchemaModel = mongoose.model('posts', postSchema);
module.exports = {
postSchemaModel,
}
Since the data type of user._id(ObjectId) and post.user_id(String) are not the same you can not join those fields using $lookup. You have to make sure they are the same type before doing $lookup
If you are allowed to change the schema, it's recommended to use ObjectId for post.user_id
var postSchema = mongoose.Schema({
user_id: {
type: mongoose.Types.ObjectId,
required: true,
ref: 'users'
},
// ...other fields
But do remember to change the existing data type to ObjectId as well.
If you are really not allowed to change the schema and existing data, for some reason. You can convert the post.user_id to ObjectId in case that the data contains valid hexadecimal representation of ObjectId (available from MongoDB v4.0)
[
// prior pipeline stages
{ "$sort": { "createdAt": -1 } },
{
"$addFields": {
"user_id": { "$toObjectId": "$user_id" }
}
},
{
"$lookup": {
"from": userSchemaModel.collection.name,
"localField": "user_id",
"foreignField": "_id",
"as": "user_id"
}
},
// other stages
As the _id field in mongodb is stored as type ObjectId but in the posts collection user_id is stored as type string, therefore it is not able find the user information and bring blank array.
To resolve this save a plain string version of _id in user collection when a user is created. for example
{
_id: ObjectId("5ee1f89732fd2c33bccfec55"),
doc_id: "5ee1f89732fd2c33bccfec55",
//other user info
}
and then use this doc_id field in $lookup
{
"$lookup": {
"from": userSchemaModel.collection.name,
"localField": "user_id",
"foreignField": "doc_id",
"as": "user_id"
}
}
In this way both user_id and doc_id will be of type string and will not need any type conversion hassles.

Count number of elements to get from lookup query for nested referenced array in mongodb

I have the following categories collection
{
"_id" : ObjectId("5e1d8c3ed06661d2b06a7c46"),
"status" : "active",
"noOfLevels" : 3,
"levels" : [
ObjectId("5e1db2237a265cfe50dfdb18"),
ObjectId("5e1db22b7a265cfe50dfdb19"),
ObjectId("5e1db23b7a265cfe50dfdb1a")
],
"name" : "Fuel",
},
{
"_id" : ObjectId("5e1d8c3ed06661d2b06a7c46"),
"status" : "active",
"noOfLevels" : 3,
"levels" : [
ObjectId("5e1db2237a265cfe50dfdb18"),
ObjectId("5e1db22b7a265cfe50dfdb19"),
ObjectId("5e1db23b7a265cfe50dfdb1a")
],
"name" : "Feeds",
}
In the above collection levels _id has been stored as a reference to populate later.
Level collection has this items:-
{
levelName: { type: String, required: true },
levelDesc: { type: String },
levelNo: { type: Number, required: true },
categoryId: { type: Mongoose.Schema.Types.ObjectId, ref: 'trainingCategories', required: true },
videoId: [{ type: Mongoose.Schema.Types.ObjectId, ref: 'trainingVideoMaster' }],
},
{
levelName: { type: String, required: true },
levelDesc: { type: String },
levelNo: { type: Number, required: true },
categoryId: { type: Mongoose.Schema.Types.ObjectId, ref: 'trainingCategories', required: true },
videoId: [{ type: Mongoose.Schema.Types.ObjectId, ref: 'trainingVideoMaster' }],
}
I am using aggregation and lookup to find items in categories collection, in my final result I need counts for videoIds which is present on each level of the particular categories.
Need final result like this:-
{
name: 'How to save fuel',
catNo: 1,
noOfLevels: 3,
totalVideos: 4
},
{
name: 'Feeds',
catNo: 2,
noOfLevels: 3,
totalVideos: 3
}
You need to $unwind the trainingLevels array to make it an object property and then you can use .dot notation to get the videoId length.
Cat.aggregate([
{ "$match": { "status": "active" } },
{ "$lookup": {
"from": "traininglevels",
"let": { "levels": "$levels" },
"pipeline": [
{ "$match": { "$expr": { "$in": ["$_id", "$$levels"] }}},
{ "$group": {
"_id": null,
"totalVideos": { "$sum": { "$size": "$videoId" }}
}}
]
"as": "trainingLevels"
}},
{ "$unwind": "$trainingLevels" },
{ "$project": {
"noOfLevels": 1,
"name": 1,
"catNo": 1,
"createdAt": 1,
"trainingLevels": "$trainingLevels.totalVideos"
}}
])

aggregate nested array of objects using mongoose

I have the following model and I want to query a specific user on _id field and populate the inbox.messages array with the necessary data that matches the corresponding _id field in the users model and more importantly i also want to group each message by the 'from' field and return that result
const UserSchema = new Schema({
username: {
type: String,
required: true,
},
blockedUsers: {
users: [
{
userId: {type: Schema.Types.ObjectId, ref: 'User', required: true },
}
]
},
favorites: {
users: [
{
userId: {type: Schema.Types.ObjectId, ref: 'User', required: true },
}
]
},
profileViews: {
views: [
{
userId: {type: Schema.Types.ObjectId, ref: 'User', required: true },
date: {type: Date}
}
]
},
inbox: {
messages: [
{
messageId: {type: Schema.Types.ObjectId},
from: {type: Schema.Types.ObjectId, ref: 'User', required: true },
content: {type: String, required: true},
date: {type: Date}
}
]
},
images: {
"imagePaths": [
{
imageId: {type: Schema.Types.ObjectId},
path: { type: String, required: true},
date: {type: Date}
}
],
}
})
what I have so far
let incomingId = '5e29fd75fdfd5320d0e42bc4';
let myUser = await User.aggregate([
{ $match: {"_id": mongoose.Types.ObjectId(incomingId) }},
{ $lookup: { }}
])
Not sure exactly what to put in the $lookup field or if this is even correct.
As a sample I would like the documents to look like:
[
{
"from": "5e240f7480a24e07d832c7bd",
"username":"hable0",
"images": {
imagePaths: [
'images/2020-09-24-Z_34234342_12.jpg'
],
},
"inbox": {
"messages": [
{
"messageId": "5e2a110a21c64d63f451e39e",
"content": "Message content",
"date": "2020-01-23T21:32:58.126Z"
},
{
"messageId": "5e2a111321c64d63f451e3a0",
"content": "Message content",
"date": "2020-01-23T21:33:07.378Z"
},
{
"messageId": "5e2a112321c64d63f451e3a2",
"content": "Message content",
"date": "2020-01-23T21:33:23.036Z"
}
]
}
}
]
You could try the following pipeline with aggregate().
Find the document that matches the id
Unwind inbox.messages
Group by from field
Perform a $lookup to get another document
Perform a $unwind to destruct the array
Specify fields to be included in the output
let myUser = await User.aggregate([
{
$match: { "_id": mongoose.Types.ObjectId(incomingId) }
},
{
$unwind: "$inbox.messages"
},
{
$group: {
_id: { from: "$inbox.messages.from" },
messages: {
$push: {
messageId: "$inbox.messages.messageId"
// Add more info of the message here as needed
}
}
},
},
{
$lookup: {
from: "User",
localField: "_id.from",
foreignField: "_id",
as: "extraUserInfo"
}
},
{
$unwind: "$extraUserInfo"
},
{
$project: {
_id: 0,
from: "$_id.from",
inbox: { messages: "$messages" },
username: "$extraUserInfo.username",
images: "$extraUserInfo.images"
}
}
]);
Sample output:
{
"from": "user1",
"inbox": {
"messages": [{
"messageId": "message1-from-user1"
}]
},
"username": "user1-username",
"images": {
"imagePaths": ["image-path-user1"]
}
} {
"from": "user2",
"inbox": {
"messages": [{
"messageId": "message1-from-user2"
}, {
"messageId": "message2-from-user2"
}, {
"messageId": "message3-from-user2"
}]
},
"username": "user2-username",
"images": {
"imagePaths": ["image-path-user2"]
}
} {
"from": "user3",
"inbox": {
"messages": [{
"messageId": "message1-from-user3"
}, {
"messageId": "message2-from-user3"
}]
},
"username": "user3-username",
"images": {
"imagePaths": ["image-path-user3"]
}
}
Hope this answers part of your question. Though I'm not very clear how you would like to populate the messages array with the user info who sent the messages. But you can perform a $lookup() with a pipeline after $group() operation to attach additional info from the sender to the result.
Read more about $unwind, $group, $project and $lookup.

Mongoose $group & $lookup aggregation returns empty array with $unwind

I have 2 schemas TravelRoute & Flights. I am trying to find the $min of Flight.fare.total_price and return that result with some details from TravelRoute which is ref-ed in the Flights schema. I am using Mongo 3.4.1 and Mongoose 4.8.5
const TravelRoute = new mongoose.Schema({
departureAirport: {
type: String,
required: true,
trim: true,
},
arrivalAirport: {
type: String,
required: true,
trim: true,
},
durationDays: {
type: Number
},
fromNow: {
type: Number
},
isActive: {
type: Boolean
},
class: {
type: String,
enum: ['economy', 'business', 'first', 'any']
}
}
const Flights = new mongoose.Schema({
route: {
type: mongoose.Schema.Types.ObjectId, ref: 'TravelRoute'
},
departure_date: {
type: Date
},
return_date: {
type: Date
},
fare: {
total_price: {
type: Number
}
}
}
I have the following code:
Flights.aggregate(
{$group: {
_id: {
route: '$route',
departure_date: '$departure_date',
return_date: '$return_date'
},
total_price: {$min: '$fare.total_price'}
}
},
{$lookup: {
from: 'TravelRoute',
localField: '_id.route',
foreignField: '_id',
as: 'routes'
}},
{$unwind: '$routes'},
{$project: {
'routes.departureAirport': 1,
'routes.destinationAirport': 1,
'departure_date': 1,
'return_date': 1,
'total_price': 1,
'_id': 1
}},
{$sort: {'total_price': 1}},
function(err, cheapFlights){
if (err) {
log.error(err)
return next(new errors.InvalidContentError(err.errors.name.message))
}
res.send(cheapFlights)
next()
})
The above returns an empty array [] when I use $unwind. When I comment out the $unwind stage, it returns the following:
[
{
"_id": {
"route": "58b30cac0efb1c7ebcd14e0a",
"departure_date": "2017-04-03T00:00:00.000Z",
"return_date": "2017-04-08T00:00:00.000Z"
},
"total_price": 385.6,
"routes": []
},
{
"_id": {
"route": "58ae47ddc30b175150d94eef",
"departure_date": "2017-04-03T00:00:00.000Z",
"return_date": "2017-04-10T00:00:00.000Z"
},
"total_price": 823.68,
"routes": []
}
...
I'm reasonably new to Mongo and Mongoose. I am confounded by the the fact that it returns "routes" even though I don't project that. And it doesn't return departure_date (etc) even though I ask for it? I am not sure I need $unwind as there will only be one TravelRoute per Flight.
Thanks...
Edit
Here was the final solution:
{$lookup: {
from: 'travelroutes', //<-- this is the collection name in the DB
localField: '_id.route',
foreignField: '_id',
as: 'routes'
}},
{$unwind: '$routes'},
{$project: {
departureAirport: '$routes.departureAirport',
arrivalAirport: '$routes.arrivalAirport',
departureDate: '$_id.departure_date',
returnDate: '$_id.return_date',
'total_price': 1,
'routeID': '$_id.route',
'_id': 0
}},
{$sort: {'total_price': 1}},

Resources