I have a collection in MongoDB for my survey results(name=surveyresults). I want to have a query that gives me the number of correct answers based on category, for example, category "Bee" number of correct answers 10.
I tried different ways but these are not results that I want.
I have searched and found this post Group count with MongoDB using aggregation framework useful but not working for me.
This is part of my data in my surveyResults collection :
[{"_id":"0eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOjE5LCJpYXQiOjE1MjQwMDgzOTl9.2YvhnXtCD7-fm4B14k10m6NF7xuv7moCTbekVekkbvY","category":"Wasp","photo":"A_wasp_565","description":"","answer":"Bee","__v":0},{"_id":"1eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOjE5LCJpYXQiOjE1MjQwMDgzOTl9.2YvhnXtCD7-fm4B14k10m6NF7xuv7moCTbekVekkbvY","category":"Wasp","photo":"A_Pompilid_wasp_007","description":"","answer":"Wasp","__v":0},{"_id":"2eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOjE5LCJpYXQiOjE1MjQwMDgzOTl9.2YvhnXtCD7-fm4B14k10m6NF7xuv7moCTbekVekkbvY","category":"Wasp","photo":"wasp_248","description":"","answer":"Wasp","__v":0},{"_id":"3eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOjE5LCJpYXQiOjE1MjQwMDgzOTl9.2YvhnXtCD7-fm4B14k10m6NF7xuv7moCTbekVekkbvY","category":"Fly","photo":"A_butterfly_291","description":"kjlkjlkjlk","answer":"Moth/Butterfly","__v":0},
I want result like this :
[{"category":"Fly","count":3, "correct":1},{"category":"Wasp","count":3, "correct":1},{"category":"Moth/Butterfly","count":4, "correct":2},{"category":"Bee","count":3, "correct":1}]
Now I have these two queries but not giving me correct results :
1.
SurveyResults.aggregate([
{ $group: {
_id: { answer: '$answer', category: '$category' }
}},
{ $group: {
_id: '$_id.answer',
answer_correct: { $sum: 1 }
}},
{ $project: {
_id: 0,
answer: '$_id',
answer_correct: 1
}}
]).exec(callback);
2.
SurveyResults.aggregate([
{
$group:{
_id:"$answer",
count: { $sum : {$cond : { if: { $eq: ["answer", "$category"]}, then: 1, else: 0} }}
}
}]).exec(callback);
Also, I can have the number of answers based on the category by this query:
SurveyResults.aggregate([
{
$group:{
_id:"$answer",
count: { $sum : 1 }
}
}]).exec(callback);
Results:
[{"_id":"Don't know","count":2},{"_id":"Fly","count":3},{"_id":"Wasp","count":3},{"_id":"Moth/Butterfly","count":4},{"_id":"Bee","count":3}]
Here's what you want:
SurveyResults.aggregate([
$group: {
_id: "$category",
"count": { $sum: 1 }, // simply count all questions per category
"correct": {
$sum: { // and sum up the correct ones in a field called "correct"
$cond: [ // ...where "correct ones" means
{ $eq: [ "$category", "$answer" ] }, // that "category" needs to match "answer"
1,
0
]
}
}
}
}, {
$project: { // this is just to effectively rename the "_id" field into "category" - may or may not be needed
_id: 0,
"category": "$_id",
"count": "$count",
"correct": "$correct"
}
}]).exec(callback);
Related
The two fields named name_id and age_id respectively. Now I would like to find a document that does not have both two fields and count the total numbers.
Below is the code I tried, but it did not work.
db.user.aggregate([{ "$group": {
"_id" : { user_id: "$key_id" },
"requestA_count": { "$sum": {
"$cond": [ { "$ifNull": [{"$name_id", false},{"$age_id",false}] }, 1, 0 ]
} },
{ "$project": {
"_id": 0,
"requestA_count": 1,
} }
])
I think this is what your looking for. If you want to count docs that have either name_id or age_id simply change $and to $or.
https://mongoplayground.net/p/cuAVkYnLUTq
db.collection.aggregate([
{$group: {
_id: {
// Group by bool, has both name_id and age_id
hasIdAndAge: {
$and: [
{$toBool: "$name_id"},
{$toBool: "$age_id"}
]
}
},
// Count sum
count: {$sum: 1}
}},
// Rework to only output one object with both counts
{$group: {
_id: null,
has: {
$sum: {$cond: [
"$_id.hasIdAndAge", "$count", 0
]}
},
hasNot: {
$sum: {$cond: [
"$_id.hasIdAndAge", 0, "$count"
]}
}
}}
])
// Outputs
[
{
"_id": null,
"has": 1,
"hasNot": 4
}
]
Using the $match operator seems more fitting. You could do something like this:
db.user.aggregate([
{ $match: {$and: [{name_id: null},{age_id: null}]}},
{ $count: "null_name&age"}
])
I haven't tested it but that should point you in the right direction.
I have a collection, lets call it 'user'. In this collection there is a property entries, which holds a variably sized array of strings,
I want to find out the total number of these strings across my collection.
db.users.find()
> [{ entries: [] }, { entries: ['entry1','entry2']}, {entries: ['entry1']}]
So far I have have made many attempts here are some of my closest.
db.users.aggregate([
{ $project:
{ numberOfEntries:
{ $size: "$entries" } }
},
{ $group:
{_id: { total_entries: { $sum: "$entries"}
}
}
}
])
What this gives me is a list of the users with the total number of entries, now what I want is each of the total_entries figures added up to get my total. Any ideas of what I am doing wrong. Or if there is a better way to start this?
A possible solution could be:
db.users.aggregate([{
$group: {
_id: 'some text here',
count: {$sum: {$size: '$entries'}}
}
}]);
This will give you the total count of all entries across all users and look like
[
{
_id: 'some text here',
count: 3
}
]
I would use $unwind in the case that you want individual entry counts.
That would look like
db.users.aggregate([
{ $unwind: '$entries' },
{$group: {
_id: '$entries',
count: {$sum: 1}
}
])
and this will give you something along the lines of:
[
{
_id: 'entry1',
count: 2
},
{
_id: 'entry2',
count: 1
}
]
In case you want the overall distinct nbr of entries:
> db.users.aggregate([
{ $unwind: "$entries" },
{ $group: { _id: "$entries" } },
{ $count: "total" }
])
{ "total" : 2 }
In case you want the overall nbr of entries:
> db.users.aggregate( [ { $unwind: "$entries" }, { $count: "total" } ] )
{ "total" : 3 }
This makes use of the "unwind" operator which flattens elements of an array from records:
> db.users.aggregate( [ { $unwind: "$entries" } ] )
{ "_id" : ObjectId("5a81a7a1318e1cfc10250430"), "entries" : "entry1" }
{ "_id" : ObjectId("5a81a7a1318e1cfc10250430"), "entries" : "entry2" }
{ "_id" : ObjectId("5a81a7a1318e1cfc10250431"), "entries" : "entry1" }
You were in the right direction though you just needed to specify an _id value of null in the $group stage to calculate accumulated values for all the input documents as a whole i.e.
db.users.aggregate([
{
"$project": {
"numberOfEntries": {
"$size": {
"$ifNull": ["$entries", []]
}
}
}
},
{
"$group": {
"_id": null, /* _id of null to get the accumulated values for all the docs */
"totalEntries": { "$sum": "$numberOfEntries" }
}
}
])
Or with just a single pipeline as:
db.users.aggregate([
{
"$group": {
"_id": null, /* _id of null to get the accumulated values for all the docs */
"totalEntries": {
"$sum": {
"$size": {
"$ifNull": ["$entries", []]
}
}
}
}
}
])
Im trying to get a sum of content a user has liked, shared or commented.
my content model has the following structure.
Content = {
_id: ObjectId(),
author: ObjectId(),
value: <string of post content>
tags: [<trings>],
likes: [array of user ids],
shares: [array of user ids],
comments: [{
ContentId( of the same schema ),
User Id
}]
}
Now, id like to aggregate over the content documents,
and get a result like this.
{
likes: 20,
shares: 10,
comments: 5
}
So, in short, whenever there is a content,
where user id is in likes array,
likes gets incremented by 1.
Same for shares and comments.
Im not sure if this is possible with aggregation framework.
I think not, but maybe some mongodb gurus know better
Quick Edit. Partially based on first post submitted, i made this.
Now, it seems to work, but im sure there is some sort of gotcha, that im missing, since it seems too simple :)
db.getCollection('contents').aggregate( [
{$facet: {
"likes": [
{$match: {likes: ObjectId("596537d6b63edc2318ee9f0c")} },
{$group: {
_id : "$likes",
count: { $sum: 1 }
}},
{ $project: { count: 1} }
],
"shares": [
{$match: {shares: ObjectId("596537d6b63edc2318ee9f0c")} },
{$group: {
_id : "$shares",
count: { $sum: 1 }
}},
{ $project: { count: 1} }
],
"posts": [
{$match: {$and: [
{user: ObjectId("596537d6b63edc2318ee9f0c")},
{parent: {$exists: false} }
]} },
{$group: {
_id : "$_id",
count: { $sum: 1 }
}},
{ $project: { count: 1} }
]
}
}]);
Can you spot something wrong with the code above ?
You can try the below aggregation in 3.4 version.
The below query will $group all the documents and use $in operator to check if the input user_id is in the arrays.
For likes and shares array, use 1 if the user_id is found else is set 0 and $sum to aggregate over all documents.
For comments it is two step process as it is array of arrays.
$grouping step to go over the user_id's in the content document and check input user_id in each element and output 1 if match and else 0.
The comments will have array of array values [[1, 0], [1], [1]] after group stage. Next step is to sum all the values to get the comments count using $reduce operator.
db.collection_name.aggregate([
{
"$group": {
"_id": null,
"likes": {
"$sum": {
"$cond": [
{
"$in": [
user_id,
"$likes"
]
},
1,
0
]
}
},
"shares": {
"$sum": {
"$cond": [
{
"$in": [
user_id,
"$shares"
]
},
1,
0
]
}
},
"comments": {
"$push": {
"$map": {
"input": "$comments",
"as": "comment",
"in": {
"$cond": [
{
"$in": [
user_id,
"$$comment.user_id"
]
},
1,
0
]
}
}
}
}
}
},
{
"$addFields": {
"comments": {
"$reduce": {
"input": "$comments",
"initialValue": 0,
"in": {
"$add": [
"$$value",
{
"$sum": "$$this"
}
]
}
}
}
}
}
])
db.collection.aggregate( [
{
$facet: {
"LikeCategory": [
{ $unwind: "$likes" },
{ $group : {
_id : "$likes",
count: { $sum: 1 }
}
},{ $project : {
userId : "$_id"
_id : 0,
count : 1
}}
],
"ShareCategory": [
{ $unwind: "$shares" },
{ $group : {
_id : "$shares",
count: { $sum: 1 }
}
},{ $project : {
userId : "$_id"
_id : 0,
count : 1
}}
],
"CommentCategory": [
{ $unwind: "$comments" },
{ $group : {
_id : "$comments.userId",
count: { $sum: 1 }
}
},{ $project : {
userId : "$_id"
_id : 0,
count : 1
}}
]
}
}
];
In this way, you will be able to find out the counts. Above code may have some syntax issues, but i hope you will be able to resolve your problem.
So I am looking to group documents in my collection on a specific field, and for the output results of each group, I am looking to include the following:
A count of all documents in the group that match a specific query (i.e. a count of documents that satisfy some expression { "$Property": "Value" })
The total number of documents in the group
(Bonus, as I suspect that this is not easily accomplished) Properties of a document that correspond to a $min/$max accumulator
I am very new to the syntax used to query in mongo and don't quite understand how it all works, but after some research, I've managed to get it down to the following query (please note, I am currently using version 3.0.12 for my mongo db, but I believe we will upgrade in a couple of months time):
db.getCollection('myCollection').aggregate(
[
{
$group: {
_id: {
GroupID: "$GroupID",
Status: "$Status"
},
total: { $sum: 1 },
GroupName: { $first: "$GroupName" },
EarliestCreatedDate: { $min: "$DateCreated" },
LastModifiedDate: { $max: "$LastModifiedDate" }
}
},
{
$group: {
_id: "$_id.GroupID",
Statuses: {
$push: {
Status: "$_id.Status",
Count: "$total"
}
},
TotalCount: { $sum: "$total" },
GroupName: { $first: "$GroupName" },
EarliestCreatedDate: { $min: "$EarliestCreatedDate" },
LastModifiedDate: { $max: "$LastModifiedDate" }
}
}
]
)
Essentially what I am looking to retrieve is the Count for specific Status values, and project them into one final result document that looks like the following:
{
GroupName,
EarliestCreatedDate,
EarliestCreatedBy,
LastModifiedDate,
LastModifiedBy,
TotalCount,
PendingCount,
ClosedCount
}
Where PendingCount and ClosedCount are the total number of documents in each group that have a status Pending/Closed. I suspect I need to use $project with some other expression to extract this value, but I don't really understand the aggregation pipeline well enough to figure this out.
Also the EarliestCreatedBy and LastModifiedBy are the users who created/modified the document(s) corresponding to the EarliestCreatedDate and LastModifiedDate respectively. As I mentioned, I think retrieving these values will add another layer of complexity, so if there is no practical solution, I am willing to forgo this requirement.
Any suggestions/tips would be very much appreciated.
You can try below aggregation stages.
$group
Calculate all the necessary counts TotalCount, PendingCount and ClosedCount for each GroupID
Calculate $min and $max for EarliestCreatedDate and LastModifiedDate respectively and push all the fields to CreatedByLastModifiedBy to be compared later for fetching EarliestCreatedBy and LastModifiedBy for each GroupID
$project
Project all the fields for response
$filter the EarliestCreatedDate value against the data in the CreatedByLastModifiedBy and $map the matching CreatedBy to the EarliestCreatedBy and $arrayElemAt to convert the array to object.
Similar steps for calculating LastModifiedBy
db.getCollection('myCollection').aggregate(
[{
$group: {
_id: "$GroupID",
TotalCount: {
$sum: 1
},
PendingCount: {
$sum: {
$cond: {
if: {
$eq: ["Status", "Pending"]
},
then: 1,
else: 0
}
}
},
ClosedCount: {
$sum: {
$cond: {
if: {
$eq: ["Status", "Closed "]
},
then: 1,
else: 0
}
}
},
GroupName: {
$first: "$GroupName"
},
EarliestCreatedDate: {
$min: "$DateCreated"
},
LastModifiedDate: {
$max: "$LastModifiedDate"
},
CreatedByLastModifiedBy: {
$push: {
CreatedBy: "$CreatedBy",
LastModifiedBy: "$LastModifiedBy",
DateCreated: "$DateCreated",
LastModifiedDate: "$LastModifiedDate"
}
}
}
}, {
$project: {
_id: 0,
GroupName: 1,
EarliestCreatedDate: 1,
EarliestCreatedBy: {
$arrayElemAt: [{
$map: {
input: {
$filter: {
input: "$CreatedByLastModifiedBy",
as: "CrBy",
cond: {
"$eq": ["$EarliestCreatedDate", "$$CrBy.DateCreated"]
}
}
},
as: "EaCrBy",
in: {
"$$EaCrBy.CreatedBy"
}
}
}, 0]
},
LastModifiedDate: 1,
LastModifiedBy: {
$arrayElemAt: [{
$map: {
input: {
$filter: {
input: "$CreatedByLastModifiedBy",
as: "MoBy",
cond: {
"$eq": ["$LastModifiedDate", "$$MoBy.LastModifiedDate"]
}
}
},
as: "LaMoBy",
in: {
"$$LaMoBy.LastModifiedBy"
}
}
}, 0]
},
TotalCount: 1,
PendingCount: 1,
ClosedCount: 1
}
}]
)
Update for Version < 3.2
$filter is also not available in your version. Below is the equivalent.
The comparison logic is the same and creates an array with for every non matching entry the value of false or LastModifiedBy otherwise.
Next step is to use $setDifference to compare the previous array values with array [false] which returns the elements that only exist in the first set.
LastModifiedBy: {
$setDifference: [{
$map: {
input: "$CreatedByLastModifiedBy",
as: "MoBy",
in: {
$cond: [{
$eq: ["$LastModifiedDate", "$$MoBy.LastModifiedDate"]
},
"$$MoBy.LastModifiedBy",
false
]
}
}
},
[false]
]
}
Add $unwind stage after $project stage to change to object
{$unwind:"$LastModifiedBy"}
Similar steps for calculating EarliestCreatedBy
I want to return an aggregate of blog post tags and their total count. My blog posts are stored like so:
{
"_id" : ObjectId("532c323bb07ab5aace243c8e"),
"title" : "Fitframe.js - Responsive iframes made easy",
"tags" : [
"JavaScript",
"jQuery",
"RWD"
]
}
I'm then executing the following pipeline:
printjson(db.posts.aggregate(
{
$project: {
tags: 1,
count: { $add: 1 }
}
},
{
$unwind: '$tags'
},
{
$group: {
_id: '$tags',
count: {
$sum: '$count'
},
tags_lower: { $toLower: '$tags' }
}
},
{
$sort: {
_id: 1
}
}
));
So that the results are sorted correctly I need to sort on a lowercase version of each tag. However, when executing the above code I get the following error:
aggregate failed: {
"errmsg" : "exception: unknown group operator '$toLower'",
"code" : 15952,
"ok" : 0
}
Do I need to do another projection to add the lowercase tag?
Yes, you must add it to the projection. It will not work in the group, only specific operators like $sum ( http://docs.mongodb.org/manual/reference/operator/aggregation-group/ ) are counted as $group operators and capable of being used on that level of the group
You don't need to add another projection ... you could fix it when you do the $group:
db.posts.aggregate(
{
$project: {
tags: 1,
count: { $add: 1 }
}
},
{
$unwind: '$tags'
},
{
$group: {
_id: { tag: '$tags', lower: { $toLower : '$tags' } },
count: {
$sum: '$count'
}
}
},
{
$sort: {
"_id.lower": 1
}
}
)
In the above example, I've preserved the original name and added the lower case version to the _id.
Add another projection step between $unwind and $grop:
...
{$project: {
tags: {$toLower: '$tags'},
count: 1
}}
...
And remove tags_lower from $group