Posting an aggregation as a new document/collection - node.js

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.

Related

Lookup based on match result

I have collection name Services:
[
{
"_id": "61dad1d21aa077c61b7bc2aa",
"name": "HomeMaintenance",
"subServices": [
"61dacb86cb94917c1edcea8f",
"61dad5812881410ba441c401"
],
},
{
"_id": "61dad60b2881410ba441c40e",
"name": "HomeMaintenance",
"subServices": [],
}
]
in another hand I have a subServices Collection like this :
[
{
"_id": "61dacb86cb94917c1edcea8f",
"name": "something",
"title": "something else",
"imageUrl": "",
"__v": 0,
"service": "61dad1d21aa077c61b7bc2aa"
},
{
"_id": "61dad5812881410ba441c401",
"name": "Plumbing",
"title": "Plumbing",
"imageUrl": "",
"__v": 0,
"service": "61dad1d21aa077c61b7bc2aa"
}
]
I came up with a solution with two queries like this
const requestedService = (serviceId)=>{
return servicesModel.findById(id);
};
const ids= requestedService.subServices
const subServicesList = (ids) => {
return subServicesModel.find({
_id: {
$in: ids,
},
});
};
which works perfectly fine, I was wondering is there any way to do these queries with one aggregation pipeline with lookup stage, first find the main services from service collection and then from subServices collection find that subServices of service
something like this
const result = await servicesModel.aggregate([
{
$match: { _id: ObjectId(id) },
},
{
$lookup: {
from: "sub_services",
let: { pid: "$_id" },
pipeline: [
{
$match: {
$expr: {
$in: ["$$pid" //>> id in sub_services modal , //>> "array which we get from match" ],
},
},
},
],
as: "subServices",
},
},
]);
The let is used for declaring the variable from the left document.
Specifies variables to use in the pipeline stages. Use the variable expressions to access the fields from the joined collection's documents that are input to the pipeline.
db.services.aggregate([
{
$match: {
_id: ObjectId(id)
},
},
{
$lookup: {
from: "sub_services",
let: {
subServices: "$subServices"
},
pipeline: [
{
$match: {
$expr: {
$in: [
"$_id",
"$$subServices"
]
},
},
},
],
as: "subServices",
},
},
])
Sample Mongo Playground

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.

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

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

Group by the content of array of string with out order in mongo aggregation

i have a problem with aggregation framework in MongoDB (mongoose) this is the problem. i have the following database scheme.so what i want to do is count number of people who has access through Mobile only , Card only, or both. with out any order,
{
'_id': ObjectId,
'user_access_type': ['Mobile' , 'Card']
}
{
'_id': ObjectId,
'user_access_type': ['Card' , 'Mobile']
}
{
'_id': ObjectId,
'user_access_type': ['Mobile']
}
{
'_id': ObjectId,
'user_access_type': ['Card']
}
Now i am using this but it only groups by the order of the user_access_type array,
[ { "$group" : { "_id": {"User" : "$user_access_type"} , "count": {"$sum" : 1} }]
this is the output:
{
"_id": {
"User": [
"Card",
"Mobile"
]
},
"count": 1
},
{
"_id": {
"_id": "5f7dce2359aaf004985f98eb",
"User": [
"Mobile",
"Card"
]
},
"count": 1
},
{
"_id": {
"User": [
"Mobile"
]
},
"count": 1
},
{
"_id": {
"User": [
"Card"
]
},
"count": 1
},
vs what i want:
{
"_id": {
"User": [
"Card",
"Mobile" // we can say both
]
},
"count": 2 // does not depend on order
},
{
"_id": {
"User": [
"Mobile"
]
},
"count": 1
},
{
"_id": {
"User": [
"Card"
]
},
"count": 1
},
You can use other option as well using $function,
$function can allow to add javascript code, you can use sort() to sort the array
db.collection.aggregate([
{
$addFields: {
user_access_type: {
$function: {
body: function(user_access_type){
return user_access_type.sort();
},
args: ["$user_access_type"],
lang: "js"
}
}
}
},
{
$group: {
_id: "$user_access_type",
count: { $sum: 1 }
}
}
])
Second option,
If user_access_type array having always unique elements then you can use $setUnion operator on user_access_type array as self union, some how this will re-order array in same order,
db.collection.aggregate([
{
$addFields: {
user_access_type: {
$setUnion: "$user_access_type"
}
}
},
{
$group: {
_id: "$user_access_type",
count: { $sum: 1 }
}
}
])
Playground

mongoose 4.7.5: how to fetch subdocuments in array

Am trying to fetch and filter subdocuments in array.
The document has this structure:
{
"_id": {
"$oid": "58bc4fa0fd85f439ee3ce716"
},
"updatedAt": {
"$date": "2017-03-08T20:39:19.390Z"
},
"createdAt": {
"$date": "2017-03-05T17:49:20.455Z"
},
"app": {
"$oid": "58ae10852035431d5a746cbd"
},
"stats": [
{
"meta": {
"key": "value",
"key": "value"
},
"_id": {
"$oid": "58bc4fc4fd85f439ee3ce718"
},
"data": "data",
"updatedAt": {
"$date": "2017-03-05T17:49:56.305Z"
},
"createdAt": {
"$date": "2017-03-05T17:49:56.305Z"
}
},
{
"meta": {
"key": "value",
"key": "value"
},
"_id": {
"$oid": "58c06bf79eaf1f15aafe39d0"
},
"data": "data",
"updatedAt": {
"$date": "2017-03-08T20:39:19.391Z"
},
"createdAt": {
"$date": "2017-03-08T20:39:19.391Z"
}
}
]
}
What i want to get is the subdocuments in the stats array between two dates
I tried mongoose queries chain:
Model.findById(id)
.select('stats')
.where('stats.createdAt').gt(data-value).lt(data-value)
But the result always the full document including the all the subdocuments.
Also I tried aggregation like this:
Model.aggregate({
$match: {
'stats.createdAt': '2017-03-05T17:49:56.305Z'
}
})
The result is always null
Your result is null because '2017-03-05T17:49:56.305Z' is a String, you are looking for a Date : new Date('2017-03-05T17:49:56.305Z')
You can filter subdocuments with a date range with $unwind and $match :
var startDate = new Date('2017-03-05T17:49:56.305Z');
var endDate = new Date('2017-03-08T17:49:56.305Z');
Model.aggregate([{
$match: {
"_id": new mongoose.mongo.ObjectId("58bc4fa0fd85f439ee3ce716"),
"stats.createdAt": {
$gte: startDate,
$lt: endDate
}
}
}, {
$unwind: "$stats"
}, {
$match: {
"stats.createdAt": {
$gte: startDate,
$lt: endDate
}
}
}], function(err, res) {
console.log(res);
})
Or more straightforward with $filter :
Model.aggregate([{
$match: {
"_id": new mongoose.mongo.ObjectId("58bc4fa0fd85f439ee3ce716"),
"stats.createdAt": {
$gte: startDate,
$lt: endDate
}
}
}, {
$project: {
"stats": {
$filter: {
input: "$stats",
as: "stat",
cond: {
$and: [
{ $gte: ["$$stat.createdAt", startDate] },
{ $lte: ["$$stat.createdAt", endDate)] }
]
}
}
}
}
}], function(err, res) {
console.log(res);
})

Resources