MongoDb aggregate the count/sum of 3 fields - mongodb

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.

Related

MongDb how to count of t null fields and the other one with the count of those with non-null fields?

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.

Multiple Counts with single query in mongodb

I am new to Mongo Db and would appreciate some help with this query. I have been sifting through posts here for the past couple of days tearing my hair to see if I could find anything related to my query but with no luck.
I have a collection with documents similar in structure to below :
_id: xyz
Movieid: 123
MovieName: Titanic
ReleaseDate: 2000-01-01
_id: uvw
Movieid: 456
MovieName: Titanic II
ReleaseDate: 2018-01-01
_id: pqr
Movieid: 789
MovieName: Titanic III
ReleaseDate:
I would like to achieve the output as counts for totalmovies, movies with release date, and movies without release date in 3 seperate columns as below:
Total | Released | UnReleased
3 | 2 | 1
I was able to write individual queries to execute the counts, but I am unable to successfully consolidate all that into a single query. The end goal is to create one view producing these counts as output. I have tried using operators such as $and, but can't seem to get the query to work as desired....this is as far as I got :
db.getCollection("Movies").aggregate({
"$and": [
{ "$match": { "ReleaseDate": { "$exists": true } }},
{ "$count": "Total" },
{ "$match": { "ReleaseDate": { "$exists": true, "$nin": [""] } }},
{ "$count": "Released" },
{ "$match": { "ReleaseDate": { "$exists": true, "$in": [""] } }},
{ "$count": "Unreleased" }
]
})
You can try below $facet aggregation
$count aggregation will always give you the counts for only single matching ($match) condition. So you need to further divide your each count into multiple section and that's what the $facet provides by processes multiple aggregation pipelines within a single stage on the same set of input documents.
db.collection.aggregate([
{ "$facet": {
"Total": [
{ "$match" : { "ReleaseDate": { "$exists": true }}},
{ "$count": "Total" },
],
"Released": [
{ "$match" : {"ReleaseDate": { "$exists": true, "$nin": [""] }}},
{ "$count": "Released" }
],
"Unreleased": [
{ "$match" : {"ReleaseDate": { "$exists": true, "$in": [""] }}},
{ "$count": "Unreleased" }
]
}},
{ "$project": {
"Total": { "$arrayElemAt": ["$Total.Total", 0] },
"Released": { "$arrayElemAt": ["$Released.Released", 0] },
"Unreleased": { "$arrayElemAt": ["$Unreleased.Unreleased", 0] }
}}
])
Output
[{
"Total": 3,
"Released": 2,
"Unreleased": 1
}]
You can use below aggregation.
$gt > null - to check whether field exists or not in aggregation expressions.
$cond with $sum to output 0 and 1 based on release date filter.
$add to add both released and unreleased count to output total.
db.Movies.aggregate([
{"$group":{
"_id":null,
"Unreleased":{"$sum":{"$cond":[{"$and":[{"$gt":["$ReleaseDate",null]},{"$ne":["$ReleaseDate",""]}]},0,1]}},
"Released":{"$sum":{"$cond":[{"$and":[{"$gt":["$ReleaseDate",null]},{"$ne":["$ReleaseDate",""]}]},1,0]}}
}},
{"$addFields":{"Total":{"$add":["$Unreleased","$Released"]}}}
])
db.Movies.aggregate(
// Pipeline
[
// Stage 1
{
$group: {
_id: null,
Total: {
$sum: 1
},
docs: {
$push: '$$ROOT'
}
}
},
// Stage 2
{
$project: {
_id: 0,
Total: 1,
Released: {
$filter: {
input: "$docs",
as: "doc",
cond: {
$ne: ["$$doc.ReleaseDate", ""]
}
}
},
Unreleased: {
$filter: {
input: "$docs",
as: "doc",
cond: {
$eq: ["$$doc.ReleaseDate", ""]
}
}
},
}
},
// Stage 3
{
$project: {
Total: 1,
Released: {
$size: '$Released'
},
UnReleased: {
$size: '$Unreleased'
}
}
},
]
);

How count different key according its value in mongo aggregation

I've a collection in mongo and need create a query with statistics for example number of register by name and number of register by key privated that is true and so on.
{
_id : 123,
name: capnis
privated: true
},
{
_id : 124,
name: capnis
privated: false
}, ....
when try do the query in mongo don't know how get this result
{name: capnis,
total: 2,
totalprivatedtrue:1,
totalprivatedfalse:1 }
here i get total but how can get the other?
.aggregate([
{$match: {name: "$name"} },
{$group: {_id:"$name", total: {$sum:1} } }
])
Apply the $cond operator in the $group pipeline step to evaluate the counts based on the privated field, something like the following:
db.collection.aggregate([
{
"$group": {
"_id": "$name",
"total": {
"$sum": 1
},
"totalprivatedtrue": {
"$sum": {
"$cond": [ "$privated", 1, 0 ]
}
},
"totalprivatedfalse": {
"$sum": {
"$cond": [ { "$eq": [ "$privated", false ] }, 1, 0 ]
}
}
}
}
])

Mongo Query to Return only a subset of SubDocuments

Using the example from the Mongo docs:
{ _id: 1, results: [ { product: "abc", score: 10 }, { product: "xyz", score: 5 } ] }
{ _id: 2, results: [ { product: "abc", score: 8 }, { product: "xyz", score: 7 } ] }
{ _id: 3, results: [ { product: "abc", score: 7 }, { product: "xyz", score: 8 } ] }
db.survey.find(
{ id: 12345, results: { $elemMatch: { product: "xyz", score: { $gte: 6 } } } }
)
How do I return survey 12345 (regardless of even if it HAS surveys or not) but only return surveys with a score greater than 6? In other words I don't want the document disqualified from the results based on the subdocument, I want the document but only a subset of subdocuments.
What you are asking for is not so much a "query" but is basically just a filtering of content from the array in each document.
You do this with .aggregate() and $project:
db.survey.aggregate([
{ "$project": {
"results": {
"$setDifference": [
{ "$map": {
"input": "$results",
"as": "el",
"in": {
"$cond": [
{ "$and": [
{ "$eq": [ "$$el.product", "xyz" ] },
{ "$gte": [ "$$el.score", 6 ] }
]}
]
}
}},
[false]
]
}
}}
])
So rather than "contrain" results to documents that have an array member matching the condition, all this is doing is "filtering" the array members out that do not match the condition, but returns the document with an empty array if need be.
The fastest present way to do this is with $map to inspect all elements and $setDifference to filter out any values of false returned from that inspection. The possible downside is a "set" must contain unique elements, so this is fine as long as the elements themselves are unique.
Future releases will have a $filter method, which is similar to $map in structure, but directly removes non-matching results where as $map just returns them ( via the $cond and either the matching element or false ) and is then better suited.
Otherwise if not unique or the MongoDB server version is less than 2.6, you are doing this using $unwind, in a non performant way:
db.survey.aggregate([
{ "$unwind": "$results" },
{ "$group": {
"_id": "$_id",
"results": { "$push": "$results" },
"matched": {
"$sum": {
"$cond": [
{ "$and": [
{ "$eq": [ "$results.product", "xyz" ] },
{ "$gte": [ "$results.score", 6 ] }
]},
1,
0
]
}
}
}},
{ "$unwind": "$results" },
{ "$match": {
"$or": [
{
"results.product": "xyz",
"results.score": { "$gte": 6 }
},
{ "matched": 0 }
}},
{ "$group": {
"_id": "$_id",
"results": { "$push": "$results" },
"matched": { "$first": "$matched" }
}},
{ "$project": {
"results": {
"$cond": [
{ "$ne": [ "$matched", 0 ] },
"$results",
[]
]
}
}}
])
Which is pretty horrible in both design and perfomance. As such you are probably better off doing the filtering per document in client code instead.
You can use $filter in mongoDB 3.2
db.survey.aggregate([{
$match: {
{ id: 12345}
}
}, {
$project: {
results: {
$filter: {
input: "$results",
as: "results",
cond:{$gt: ['$$results.score', 6]}
}
}
}
}]);
It will return all the sub document that have score greater than 6. If you want to return only first matched document than you can use '$' operator.
You can use $redact in this way:
db.survey.aggregate( [
{ $match : { _id : 12345 }},
{ $redact: {
$cond: {
if: {
$or: [
{ $eq: [ "$_id", 12345 ] },
{ $and: [
{ $eq: [ "$product", "xyz" ] },
{ $gte: [ "$score", 6 ] }
]}
]
},
then: "$$DESCEND",
else: "$$PRUNE"
}
}
}
] );
It will $match by _id: 12345 first and then it will "$$PRUNE" all the subdocuments that don't have "product":"xyz" and don't have score greater or equal 6. I added the condition ($cond) { $eq: [ "$_id", 12345 ] } so that it wouldn't prune the whole document before it reaches the subdocuments.

How to return count:0 in an aggregation if there is no match

I am having an issues that I thought it would happen often, but I wasn't able to find enough information during my research.
My problem is that I expect the return of a query to have a given JSON format, but when the match filters out all documents, I get no json.
A simplified example: I would like to have the count if documents that match a given criteria, so I have the following query
db.collection.aggregate( [{
$match: {
type: /^1[.]2[.]3[.].*$/
}
}, {
$group: {
_id: {$ifNull : ["$type", 0]},
count: { $sum: 1 }
}
}]);
If I have at least one document that matches, then the query works:
{ "_id" : "1.2.3", "count" : 44 }
If I have no documents, I would like to receive a json like this:
{ "_id" : "1.5.3", "count" : 0 }
Is this possible?
ps: this is a simplified case, it would not be so easy to handle that on the application side, so I would rather try to adjust my query
If you can know beforehand the value of the key that you are searching for(i.e. 1.2.3, 1.5.3 in your case), here is a workaround using $facet. It first tries to get the documents by $match and store them into an array named results. Depending on the $size of the results array, we either replace it with the $group result (when we have matched records); or replace it with a default count: 0 record with the key you specified.
db.collection.aggregate([
{
"$facet": {
"results": [
{
$match: {
"type": <key you want to search>
}
},
{
$group: {
_id: {
$ifNull: [
"$type",
0
]
},
count: {
$sum: 1
}
}
}
]
}
},
{
"$replaceRoot": {
"newRoot": {
"$cond": {
"if": {
$gt: [
{
"$size": "$results"
},
0
]
},
"then": "$$ROOT",
"else": {
"results": [
{
"_id": <key you want to search>,
"count": 0
}
]
}
}
}
}
},
{
"$unwind": "$results"
},
{
"$replaceRoot": {
"newRoot": "$results"
}
}
])
Mongo Playground