Can you please help? I'm trying to aggregate data over the past 12 months by both ALL publication data specific to a certain publisher and per publication to return a yearly graph data analysis based on the subscription type.
Here's a snapshot of the Subscriber model:
const SubscriberSchema = new Schema({
publication: { type: Schema.Types.ObjectId, ref: "publicationcollection" },
subType: { type: String }, // Print, Digital, Bundle
subStartDate: { type: Date },
subEndDate: { type: Date },
});
Here's some data for the reader (subscriber) collection:
{
_id: ObjectId("5dc14d3fc86c165ed48b6872"),
publication: ObjectId("5d89db9d82273f1d18970deb"),
subStartDate: "2019-11-20T00:00:00.000Z",
subtype: "print"
},
{
_id: ObjectId("5dc14d3fc86c165ed48b6871"),
publication: ObjectId("5d89db9d82273f1d18970deb"),
subStartDate: "2019-11-19T00:00:00.000Z",
subtype: "print"
},
{
_id: ObjectId("5dc14d3fc86c165ed48b6870"),
publication: ObjectId("5d89db9d82273f1d18970deb"),
subStartDate: "2019-11-18T00:00:00.000Z",
subtype: "digital"
},
{
_id: ObjectId("5dc14d3fc86c165ed48b6869"),
publication: ObjectId("5d8b36c3148c1e5aec64662c"),
subStartDate: "2019-11-19T00:00:00.000Z",
subtype: "print"
}
The publication model has plenty of fields but the _id and user fields are the only point of reference in the following queries.
Here's some data for the publication collection:
// Should use
{ "_id": {
"$oid": "5d8b36c3148c1e5aec64662c"
},
"user": {
"$oid": "5d24bbd89f09024590db9dcd"
},
"isDeleted": false
},
// Should use
{ "_id": {
"$oid": "5d89db9d82273f1d18970deb"
},
"user": {
"$oid": "5d24bbd89f09024590db9dcd"
},
"isDeleted": false
},
// Shouldn't use as deleted === true
{ "_id": {
"$oid": "5d89db9d82273f1d18970dec"
},
"user": {
"$oid": "5d24bbd89f09024590db9dcd"
},
"isDeleted": true
},
// Shouldn't use as different user ID
{ "_id": {
"$oid": "5d89db9d82273f1d18970dfc"
},
"user": {
"$oid": "5d24bbd89f09024590db9efd"
},
"isDeleted": true
}
When I do a lookup on a publication ID with the following, I'm getting perfect results:
Subscriber.aggregate([
{
$match: {
$and: [
{ 'publication': mongoose.Types.ObjectId(req.params.id) },
],
"$expr": { "$eq": [{ "$year": "$subStartDate" }, new Date().getFullYear()] }
}
},
{
/* group by year and month of the subscription event */
$group: {
_id: { year: { $year: "$subStartDate" }, month: { $month: "$subStartDate" }, subType: "$subType" },
count: { $sum: 1 }
},
},
{
/* sort descending (latest subscriptions first) */
$sort: {
'_id.year': -1,
'_id.month': -1
}
},
{
$limit: 100,
},
])
However, when I want to receive data from the readercollections (Subscriber Model) for ALL year data, I'm not getting the desired results (if any) from all of the things I'm trying (I'm posting the best attempt result below):
Publication.aggregate([
{
$match:
{
user: mongoose.Types.ObjectId(id),
isDeleted: false
}
},
{
$project: {
_id: 1,
}
},
{
$lookup: {
from: "readercollections",
let: { "id": "$_id" },
pipeline: [
{
$match:
{
$expr: {
$and: [
{ $eq: ["$publication", "$$id"] },
{ "$eq": [{ "$year": "$subStartDate" }, new Date().getFullYear()] }
],
}
}
},
{ $project: { subStartDate: 1, subType: 1 } }
],
as: "founditem"
}
},
// {
// /* group by year and month of the subscription event */
// $group: {
// _id: { year: { $year: "$founditem.subStartDate" }, month: { $month: "$foundtitem.subStartDate" }, subType: "$founditem.subType" },
// count: { $sum: 1 }
// },
// },
// {
// /* sort descending (latest subscriptions first) */
// $sort: {
// '_id.year': -1,
// '_id.month': -1
// }
// },
], function (err, result) {
if (err) {
console.log(err);
} else {
res.json(result);
}
})
Which returns the desired data without the $group (commented out) but I need the $group to work or I'm going to have to map a dynamic array based on month and subtype which is completely inefficient.
When I'm diagnosing, it looks like this $group is the issue but I can't see how to fix as it works in the singular $year/$month group. So I tried the following:
{
/* group by year and month of the subscription event */
$group: {
_id: { year: { $year: "$subStartDate" }, month: { $month: "$subStartDate" }, subType: "$founditem.subType" },
count: { $sum: 1 }
},
},
And it returned the $founditem.subType fine, but any count or attempt to get $year or $month of the $founditem.subStartDate gave a BSON error.
The output from the single publication ID lookup in the reader collection call that works (and is plugging into the line graph perfectly) is:
[
{
"_id": {
"year": 2019,
"month": 11,
"subType": "digital"
},
"count": 1
},
{
"_id": {
"year": 2019,
"month": 11,
"subType": "print"
},
"count": 3
}
]
This is the output I'd like for ALL publications rather than just a single lookup of a publication ID within the reader collection.
Thank you for any assistance and please let me know if you need more details!!
Related
I would like to "create" new documents, to keep only the contents of the activities.
I've looked at $project and $unwide, but I didn't find any appropriate result.
Here is my MongoDB query:
const activities = await friendModel.aggregate([
{
$match: {
$or: [
{
ReceiverId: ActorId,
Status: 1,
},
{
RequesterId: ActorId,
Status: 1,
},
],
},
},
{
$set: {
fieldResult: {
$cond: {
if: {
$eq: [ "$ReceiverId", ActorId ],
},
then: "$RequesterId",
else: "$ReceiverId",
},
},
},
},
{
$lookup: {
from: "activities",
localField: "fieldResult",
foreignField: "ActorId",
as: "activity",
},
},
{
$unwind: {
path: "$activity",
preserveNullAndEmptyArrays: true
}
},
{ $sort: { "activity._Date": -1 } },
{ $skip: request.pageindex * 3 },
{ $limit: 3 },
]);
Here is what I would like to do: https://sourceb.in/7u0tirIstt
Here is what I get for the moment: https://sourceb.in/Ped2xY5Ml8
Thank you in advance for your help!
You should be able to add $project as follows:
...
{
$project: {
"_id":"$_id",
"ActivityId": "$activity._id",
"ActorId": "$activity.ActorId",
"Type": "$activity.Type",
"_Date": "$activity._Date",
"MovieId": "$activity.MovieId",
"FriendId": "$activity.FriendId",
"ContestId": "$activity.ContestId",
"LookId": "$activity.LookId",
"__v": "$__v"
},
},
...
You can access values with $, and map nested values by accessing them directly
Also would recommend you doing a limit before you do lookup to speed it up a bit
I am trying to show results where the sum of records is greater or equal to 4 and the status matches a string. If I leave off the status field it works fine but adding it in always gives me an empty array even when there should be data.
const bookings = await Booking.aggregate([
{
$group: {
_id: {
$dateToString: {
format: "%Y/%m/%d",
date: "$bookingDate",
},
},
totalBookings: {
$sum: 1,
},
},
},
{
$match: {
totalBookings: {
$gte: 4,
},
status: "Accepted",
},
},
]);
Each booking will have it's own status. So you need to add that as part of $group
{
$group: {
_id: {
status: "$status",
"d": {
$dateToString: {
format: "%Y/%m/%d",
date: "$bookingDate",
}
},
},
totalBookings: {
$sum: 1,
},
}
}
Then you need to change your match as below
$match: {
totalBookings: {
$gte: 4,
},
"_id.status": "Accepted",
}
It will give you all the Accepted Booking on the given BookinDate which is >= 4.
I want to get all objects in a radius and also for each single of those objects their average rating and total ratings. I've got both queries working but I'm looking to combine these 2 into one.
LocationSchema
const LocationObject = new Schema({
name: String
location: {
type: {
type: String,
enum: ['Point'],
default: 'Point',
required: true
},
coordinates: {
type: [Number],
required: true
}
}
})
ratingSchema
const Rating = new Schema({
locationObject: { type: Schema.Types.ObjectId, ref: 'LocationObject' },
average: Number,
})
locationQuery
const objects = await LocationObject.find({
location: {
$geoWithin: {
$centerSphere: [[lon, lat, radius]
}
}
})
RatingAggregation for single LocationObject
const result = await Rating.aggregate([
{
"$match": {
"locationObject": objectID
}
},
{
"$facet": {
"numbers": [
{
"$group": {
"_id": null,
"totalRating": {
"$sum": "$average"
},
"totalItemCount": {
"$sum": 1.0
}
}
}
],
}
},
{
"$unwind": "$numbers"
},
{
"$project": {
"_id": null,
"avgRating": {"$divide": ["$numbers.totalRating", "$numbers.totalItemCount"]},
"totalRatings": "$numbers.totalItemCount"
}
}
])
The final result should return an array with locationObjects which each has an average and totalRatings added.
mongo playground: https://mongoplayground.net/p/JGuJtB5bZV4
Expected result
[
{
name: String,
location: {
coordinates: [Number, Number],
},
avgRating: Number,
totalRatings: Number
},
{
name: String,
location: {
coordinates: [Number, Number],
}
}
]
As per your latest playground, you could achieve using this
db.locationObject.aggregate([
{
"$match": {
"location": {
"$geoWithin": {
"$centerSphere": [
[
6.064953,
52.531348
],
0.0012
]
}
}
}
},
{
"$lookup": { //You need to bring both the collection data together
"from": "Rating",
"localField": "_id",
"foreignField": "locationObject",
"as": "locRatings"
}
},
{
$unwind: "$locRatings"
},
{
"$group": { //you can simplify the other pipelines
"_id": "$_id",
"field": {
"$avg": "$locRatings.average"
},
"totalItemCount": {
"$sum": 1.0
}
}
}
])
To preserve the document fields, you need to use accumulators as in this playground
{
"$group": {
"_id": "$_id",
"field": {
"$avg": "$locRatings.average"
},
"totalItemCount": {
"$sum": 1.0
},
"locations": {
"$addToSet": "$location"
}
}
}
you can keep empty/null arrays in unwind stage as below
playground
{
$unwind: {
"path": "$locRatings",
"preserveNullAndEmptyArrays": true
}
},
You can add a project stage to ignore null values if needed.
For a project where we have actions and donations. We store the donations in an array in the related action. For the connection we use Mongoose.
The schema for an action is as follows, for readability I've removed some fields which are not related to this problem:
const donationSchema = new Schema(
{
id: {
type: String,
unique: true,
required: true,
index: true,
},
amount: { type: Number },
status: {
type: String,
enum: ['pending', 'collected', 'failed'],
default: 'pending',
},
},
{ timestamps: true, versionKey: false, _id: false },
);
const schema = new Schema(
{
donations: { type: [donationSchema], default: [] },
target: { type: Number, default: 0 },
collected: { type: Number, default: 0 },
},
{
timestamps: true,
versionKey: false,
},
);
const Action = model<IAction>('Action', schema);
Let say I have an Action with three donations, one in every state:
{
"_id": "6098fb22101f22cfcbd31e3b"
"target": 10000,
"collected": 25,
"donations": [
{
"uuid": "dd90f6f1-56d7-4d8b-a51f-f9e5382d3cd9",
"amount": 25,
"status": "collected"
},
{
"uuid": "eea0ac5e-1e52-4eba-aa1f-c1f4d072a37a",
"amount": 10,
"status": "failed"
},
{
"uuid": "215237bd-bfe6-4d5a-934f-90e3ec9d2aa1",
"amount": 50,
"status": "pending"
}
]
}
Now I want to update the pending donation to collected.
This would be
Action.findOneAndUpdate(
{
_id: '6098fb22101f22cfcbd31e3b',
'donations.id': '215237bd-bfe6-4d5a-934f-90e3ec9d2aa1',
},
{
$set: {
'donations.$.status': 'collected',
},
},
{
upsert: false,
returnOriginal: false,
}
).then((action) => console.log(action);
I want to update the status to collected, but also update the collected so that it is the same as all the donations with status equal to collected. I thought of using the $inc operator, but this keeps saying that donations.$.amount is not a number and therefore not able to increment collected.
Is there a way to do this in the same update call? The reason why I cannot get the object and just count collected amount is that maybe two donation callbacks occur at the same time, so we don't want the to overwrite the previous given amount.
This aggregation can help you I believe:
db.collection.aggregate([
{
"$match": {
_id: "6098fb22101f22cfcbd31e3b"
}
},
{
"$set": {
"donations.status": {
"$reduce": {
"input": "$donations",
"initialValue": {
uuid: "215237bd-bfe6-4d5a-934f-90e3ec9d2aa1"
},
"in": {
$cond: [
{
$eq: [
"$$this.uuid",
"$$value.uuid"
]
},
"collected",
"$$this.status"
]
}
}
}
}
},
{
"$set": {
"collected": {
"$reduce": {
"input": "$donations",
"initialValue": "$collected",
"in": {
$cond: [
{
$eq: [
"$$this.status",
"collected"
]
},
{
$sum: [
"$$value",
"$$this.amount"
]
},
"$$value"
]
}
}
}
}
}
])
Edit: Above aggregation wasn't properly update status field to "collected" dunno why..
But update query below should work. I couldn't test it too. So, please let me know if something goes wrong.
db.collection.update({
"_id": "6098fb22101f22cfcbd31e3b"
},
{
"$set": {
"donations.$[element].status": "collected",
"$inc": {
"donations.$[element].amount": {
"$cond": [
{
"$eq": [
"donations.$[element].status",
"collected"
]
},
"donations.$[element].amount",
"collected"
]
}
}
}
},
{
"arrayFilters": [
{
"element.uuid": "215237bd-bfe6-4d5a-934f-90e3ec9d2aa1"
}
]
})
I'm attempting to count the number of times two separate fields are true. I have two values "clickedWouldRecommend" and "clickedWouldNotRecommend". These values are defaulted too FALSE. When a button is clicked in the interface, they are set too TRUE. I'm trying to see how many clickedWouldRecommend = true and how many clickedWouldNotRecommend = true for each branch.name.
db.appointments.aggregate([
{
$match: {
$and: [
{
'branch.org_id': '100000'
},
{ "analytics.clickedWouldRecommend": true },
// Add OR statement to include analytics.clickedWouldNotRecommend = true?
]
}
},
{
$group: {
_id: '$branch.name',
wouldRecommend: { $sum: 1 }
}
}
])
This provides results similar to:
{
"_id": [ 'Clinic Name' ],
"wouldRecommend": 115.0
}
I need to modify the query to also look for cases where analytics.clickedWouldNotRecommend is set to true. I'm trying to get output similar to this ( also notice removing the array from _id if possible ):
{
"name": 'Clinic Name'
"wouldRecommend": 115,
"wouldNotRecommend": 10
},
{
"name": 'Second Clinic Name'
"wouldRecommend": 200,
"wouldNotRecommend": 12
}
Here is the truncated model / schema:
{
branch: [
{
name: {
type: String,
required: true
},
clinic_id: {
type: String,
required: true
},
org_id: {
type: String
}
}
],
analytics: {
clickedWouldRecommend: {
type: Boolean,
default: false
},
clickedWouldNotRecommend: {
type: Boolean,
default: false
}
},
date: {
type: Date,
default: Date.now
}
};
You can use below aggregations
db.appointments.aggregate([
{ "$match": { "branch.org_id": "100000" }},
{ "$unwind": "$branch" },
{ "$facet": {
"wouldRecommend": [
{ "$match": { "analytics.clickedWouldRecommend": true }},
{ "$group": { "_id": "$branch.name" }}
],
"wouldNotRecommend": [
{ "$match": { "analytics.clickedWouldNotRecommend": true }},
{ "$group": { "_id": "$branch.name" }}
]
}}
])
Or
db.appointments.aggregate([
{ "$match": { "branch.org_id": "100000" }},
{ "$unwind": "$branch" },
{ "$group": {
"_id": "$branch.name",
"wouldRecommend": {
"$sum": {
"$cond": [{ "$eq": ["$analytics.clickedWouldRecommend", true] }, 1, 0]
}
},
"wouldNotRecommend": {
"$sum": {
"$cond": [{ "$eq": ["$analytics.clickedWouldRecommend", true]}, 1, 0]
}
}
}}
])