Mongoose Aggregation of a field that optionally exists in mongoose - rating calculation - node.js

My product document looks thus:
{
"_id": {
"$oid": "60999af1160b0eebed51f203"
},
"name": "Olive Skin care On1",
"business": {
"$oid": "609fa1d25adc157a33c59098"
},
"ratings": [{
"_id": {
"$oid": "60bdb541d6212ec44e62273c"
},
"user": {
"$oid": "5fdce4bd75dbe4864fcd5001"
},
"rating": 5
}]
}
I have this mongoose query to get product details alongside the product rating. Some products have ratings field while others do not. When I make a query as shown here, it returns a response as expected with calculated average rating. The response looks thus:
[
{
"_id": "609a657f2bf43c290fb22df8",
"name": "Olive babay Oil",
"business": "6079ed084d9ab0c3171317ea",
"averageRating": 5
}
]
Here is the query:
const productArray = await Product.aggregate([
{
$match: {
_id: mongooseProductId,
},
},
{ $unwind: "$ratings" },
{
$project: {
averageRating: { $avg: "$ratings.rating" },
name: 1,
business: 1,
},
},
]);
However if the same product above is modified by removing the ratings field, the query below will return an empty array.
How do I write my query to ensure that whether the ratings field exists or not, I do not get an empty array provided that the matching criteria is met.
Meaning that I can get an expected response like this when the ratings field doesn't exist on my product document:
[
{
"_id": "609a657f2bf43c290fb22df8",
"name": "Olive babay Oil",
"business": "6079ed084d9ab0c3171317ea",
"averageRating": null
}
]
And this when the rating field exists:
[
{
"_id": "609a657f2bf43c290fb22df8",
"name": "Olive babay Oil",
"business": "6079ed084d9ab0c3171317ea",
"averageRating": 5
}
]

Based on #turivishal's comment. The query below solved the problem.
const productArray = await Product.aggregate([
{
$match: {
_id: mongooseProductId,
},
},
{ $unwind:{ path: "$ratings", preserveNullAndEmptyArrays: true } },
{
$project: {
averageRating: { $avg: "$ratings.rating" },
name: 1,
business: 1,
},
},
]);

Related

How to use $match after $lookup and $unwind so that it can check all the fields including the array for a keyword/string?

I have collections named
products
`
{
"_id": {
"$oid": "1"
},
"companyId": [
{
"$oid": "2"
}
],
"Title": "abcd",
"Caption": "abc",
},{
"_id": {
"$oid": "2"
},
"companyId": [
{
"$oid": "3"
}
],
"Title": "milk",
"Caption": "aa",
}
`
companies
`
{
"_id": {
"$oid": "2"
},
"name": "cathub",
"url": "cathub.com",
"__v": 0
},
"_id": {
"$oid": "3"
},
"name": "Amule",
"url": "amule.com",
"__v": 0
`
here the products collection have companyId as foreign key of _id from companies collection.What i need is that when i search for a perticular string in products,it needed to search all fields including companies which is joined.for example if my keyword is "Amule",then it needed to search in title and caption and companies.name also.if it found matching then we need to return the products document of _id:2
I tried with the following
{ $lookup:{ form:"companies", localField:"companyId", foriegnField:"_id", as :"result" } }
then
{ $unwind:{ path:"$result" } }
but i am not able to perform $match after that.Because it shows error and only allow to use $match
only in the begining.Please help to solve this issue(i need to solve this issue using TEXT index)
Complete query
model.aggregate([
{
$match: {
$text: {
$search: "Amul",
},
},
},
{
$lookup: {
from: "companies",
localField: "companyId",
foreignField: "_id",
as: "company",
},
},
{
$unwind: "$company",
},
{
$match: {
$text:{
$search: "Amul"}
},
},
},
$group: {
_id: {
_id: "$_id",
Title: "$Title",
},
comapany: {
$push: "$company",
},
},
]}
if the string 'Amul'present in any field of "products" then return the document or 'Amul' is present in the "name" field of joined "company" joined using $lookup then also return the parent document
note:-
'model' is the products collection

Posting an aggregation as a new document/collection

In my client I have a form that is sent and stored in Mongo. Made an aggregation to get the name of the people that selected a same place, date and time. Now I would like to create a Mongo document containing all matches as collections so whenever there is a match in place, date and time of people you can get it in a collection. This is what I have so far:
router.get('/match', async (req, res) => {
const matchs = await Forms.aggregate([
{
$group: {
_id: { Date: "$date", Time: "$time", Place: "$place" },
Data: { $addToSet: {Name: "$firstName", Surname:"$surname"}},
count: { $sum: 1 }
}
},
{
$match: {
count: { $gte: 2}
}
},
]);
res.json(matchs)
});
This is the result that I would like to store in Mongo:
{
"_id": {
"Date": "2022-04-20",
"Time": "15:00",
"Place": "Mall"
},
"Data": [
{
"Name": "Carl",
"Surname": "Man"
},
{
"Name": "Christian",
"Surname": "Max"
}
],
"count": 2
}
{
"_id": {
"Date": "2022-04-20",
"Time": "13:00",
"Place": "Restaurant"
},
"Data": [
{
"Name": "Felix",
"Surname": "Sad"
},
{
"Name": "Liu",
"Surname": "Lam"
}
],
"count": 2
}
You can use $out as the last stage in your pipeline. In the following example, matching_collection will contain the result of your pipeline.
{ $out : "matching_collection" }
https://www.mongodb.com/docs/v4.2/reference/operator/aggregation/out/
You can also check $merge, it could be helpful as well.

How to fetch the last n months record(consider only the last entry per month) in MongoDB

I am new to MongoDb. I need help to fetch the last n month record, there might be multiple entry per month but the query needs to return only the last entry per month.
For e.g lets say if n is 3 and userId is userId1 (that means return last 3 month record for userId1).
Sample inputs in the collection:
[
{
"_id": objectId("aaaaaa"),
"userId": "userId1",
"processedAt": "2021-06-01T12:16:49.349Z"
},
{
"_id": objectId("bbbbb"),
"userId": "userId1",
"processedAt": "2021-10-11T12:16:49.349Z"
},
{
"_id": objectId("ccccc"),
"userId": "userId1",
"processedAt": "2021-10-25T12:16:49.349Z"
},
{
"_id": objectId("eeeee"),
"userId": "userId1",
"processedAt": "2021-09-12T12:16:49.349Z"
},
{
"_id": objectId("fffff"),
"userId": "userId1",
"processedAt": "2021-09-28T12:16:49.349Z"
},
{
"_id": objectId("ggggg"),
"userId": "userId1",
"processedAt": "2021-09-23T12:16:49.349Z"
},
{
"_id": objectId("hhhhh"),
"userId": "userId1",
"processedAt": "2021-07-23T12:16:49.349Z"
},
{
"_id": objectId("iiiii"),
"userId": "userId2",
"processedAt": "2021-09-29T12:16:49.349Z"
},
{
"_id": objectId("jjjjj"),
"userId": "userId1",
"processedAt": "2022-01-29T12:16:49.349Z"
},
{
"_id": objectId("kkkkk"),
"userId": "userId1",
"processedAt": "2022-02-29T12:16:49.349Z"
},
]
Expected Result: Should return by userId, limit n months(fetch only the last saved entry of the month) and the ascending order of the month of processedAt:
[{
"_id": objectId("ccccc"),
"userId": "userId1",
"processedAt": "2021-10-25T12:16:49.349Z"
},
{
"_id": objectId("jjjjj"),
"userId": "userId1",
"processedAt": "2022-01-29T12:16:49.349Z"
},
{
"_id": objectId("kkkkk"),
"userId": "userId1",
"processedAt": "2022-02-29T12:16:49.349Z"
}
]
I have tried below query however which is returning all the records. I want query needs to consider only the last entry per month. I have been using mongojs driver v4.1.2
db.collection(collection_name)
.find({ userId: userId }, { projection: { _id: 0 } })
.sort({ processedAt: -1 })
.limit(n)
.toArray()
Starting from MongoDB 5.0,
You can use $setWindowFields to aggregate a "rank" for the "partition" / "group" (i.e. the month in your example) and only choose the document with top rank.
The ranking can be defined as processedAt: -1 as you want to keep only the latest record in the month with highest rank.
{
"$setWindowFields": {
"partitionBy": {
"$dateToString": {
"date": "$processedAt",
"format": "%Y-%m"
}
},
"sortBy": {
"processedAt": -1
},
"output": {
"rank": {
$rank: {}
}
}
}
}
Here is the Mongo playground for your reference.
For MongoDB 3.6+,
As the sample dataset is using ISODate format, it is possible to sort and group the field by leftmost 7 characters (i.e. yyyy-MM). Keeping only the first document inside the month group should do the tricks.
{
$sort: {
processedAt: -1
}
},
{
"$addFields": {
"month": {
"$substrCP": [
"$processedAt",
0,
7
]
}
}
},
{
$group: {
_id: "$month",
last: {
$first: "$$ROOT"
}
}
}
Here is the Mongo playground.

Mongodb aggregate distinct with unique and sort

I have a Ranks collection with documents which looks like this:
[
{
"_id": "1",
"url": "ex1.com",
"keyword": "k1",
"rank": 19,
"createdAt": "2021-06-02",
"user": "616c542660d23fc17469b47e"
},
{
"_id": "2",
"url": "ex1.com",
"keyword": "k1",
"rank": 14,
"createdAt": "2021-06-01",
"user": "616c542660d23fc17469b47e"
},
{
"_id": "3",
"url": "ex1.com",
"keyword": "k2",
"rank": 8,
"createdAt": "2021-05-01",
"user": "616c542660d23fc17469b47e"
},
{
"_id": "4",
"url": "ex2.com",
"keyword": "k3",
"rank": 4,
"createdAt": "2021-05-01",
"user": "616c542660d23fc17469b47e"
}
]
users collection with documents which looks like this:
[
{
_id: "616c542660d23fc17469b47e",
email: "some#email.com"
}
]
I'm trying to run an aggregation which will return each user object + user's data array that grouped by url, each url object has keywords array that includes unique and last (by date) rank keyword
This is what I tried but the query returns all url's keywords, how can i make it return unique and last (by createdAt date) keywords
Rank.aggregate([
{
$match: {}
},
{
$lookup: {
from: 'users',
localField: 'user',
foreignField: '_id',
as: 'user'
}
},
{
$project: {
user: {
$arrayElemAt: ['$user', 0]
},
url: '$url',
keyword: '$keyword',
rank: '$rank',
createdAt: '$createdAt',
}
},
{
$sort: {
createdAt: -1
}
},
{
$group: {
_id: '$user._id',
user: {
$first: '$user'
},
data: {
$push: {
id: '$_id',
url: '$url',
keyword: '$keyword',
rank: '$rank',
createdAt: '$createdAt',
}
}
}
}
])
Expected output:
[{
user: {
_id: "616c542660d23fc17469b47e",
email: "some#email.com"
},
data: [
{
url: "ex1.com",
keywords: [
{
keyword: "k1",
rank: 19,
createdAt: "2021-06-02",
},
{
keyword: "k2",
rank: 8,
createdAt: "2021-05-01"
},
]
},
{
url: "ex2.com",
keywords: [
{
keyword: "k3",
rank: 4,
createdAt: "2021-05-01"
},
]
}
]
}]
Here it is the solution that I came out with. Playground
Full explanation:
We group by "$url","$user" and "$keyword" to get the unique combinations of this fields. AT this point waht we want is only the unique keywords, but we have to use the user and url fields, becouse we would groupBy those later too.Because we order them by createdAt, if we get the first document it will be the last one created.
{
"$sort": {
"createdAt": 1
}
},
{
"$group": {
"_id": [
"$url",
"$user",
"$keyword"
],
"keywords": {
$first: "$$ROOT"
}
}
},
Then we will format this keyword information a bit to group it by url. This step will give us the keywords per URL.
{
"$project": {
"url": "$keywords.url",
"user": "$keywords.user",
"keywords": "$keywords",
"_id": 0
}
},
{
"$group": {
"_id": [
"$user",
"$url"
],
"data": {
$push: "$$ROOT"
}
}
},
Finally we will group the URLs by user. Notice that we have grouped by URL and by user in each groupBy in order to not lose those fields.
{
"$project": {
"url": {
$first: "$data.keywords.url"
},
"user": {
$first: "$data.keywords.user"
},
"keywords": "$data.keywords",
"_id": 0
}
},
{
"$group": {
"_id": "$user",
"data": {
$push: "$$ROOT"
}
}
},
At this step we have almost all the information we needed grouped together. We would perform a lookUp to get the email from the Users collection and do the final mapping to remove some redundant data.
{
$lookup: {
from: "users",
localField: "_id",
foreignField: "_id",
as: "user"
}
},
{
"$unwind": "$user"
},
{
"$project": {
"_id": 0,
"data.user": 0,
"data.keywords._id": 0,
"data.keywords.url": 0,
"data.keywords.user": 0
}
},

MongoDB aggregation : Group by Category and sum up the amount

I have the following structure in my collection (you don't have to mind the status) :
{
"_id": {
"$oid": "5e6355e71b14ee00175698cb"
},
"finance": {
"expenditure": [
{
"status": true,
"_id": { "$oid": "5e63562d1b14ee00175698df" },
"amount": { "$numberInt": "100" },
"category": "Sport"
},
{
"status": true,
"_id": { "$oid": "5e6356491b14ee00175698e0" },
"amount": { "$numberInt": "200" },
"category": "Sport"
},
{
"status": true,
"_id": { "$oid": "5e63565b1b14ee00175698e1" },
"amount": { "$numberInt": "50" },
"category": "Outdoor"
},
{
"status": true,
"_id": { "$oid": "5e63566d1b14ee00175698e2" },
"amount": { "$numberInt": "400" },
"category": "Outdoor"
}
]
}
}
My previos command was this:
User.aggregate([
{ $match: {_id: req.user._id} },
{ $unwind: '$finance.expenditure' },
{ $match: {'finance.expenditure.status': true} },
{ $sort: {'finance.expenditure.currentdate': -1} },
{
$group: {
_id: '$_id',
expenditure: { $push: '$finance.expenditure' }
}
}
])
With this I just get every single expenditure back.
But now I want to group the expenditures by their category and sum up the amount of every single expenditure for their group.
So it should look like this:
{ "amount": 300 }, "category": "Sport" },
{ "amount": 450 }, "category": "Outdoor" }
Thanks for your help
Instead of grouping on _id field group on category field & sum amount field:
db.collection.aggregate([
{ $match: {_id: req.user._id}},
{
$unwind: "$finance.expenditure"
},
{
$match: {
"finance.expenditure.status": true
}
},
{
$sort: {
"finance.expenditure.currentdate": -1
}
},
{
$group: {
_id: "$finance.expenditure.category",
amount: {
$sum: "$finance.expenditure.amount"
}
}
},
{
$project: {
_id: 0,
category: "$_id",
amount: 1
}
}
])
Test : MongoDB-Playground

Resources