I have a collection with documents like this:
[
{
"user_id": 1,
"prefs": [
"item1",
"item2",
"item3",
"item4"
]
},
{
"user_id": 2,
"prefs": [
"item2",
"item5",
"item3"
]
},
{
"user_id": 3,
"prefs": [
"item4",
"item3",
"item7"
]
}
]
What I want is to write an aggregation which will get a user_id and producer a list containing all users mapped to the number of same prefs in their lists. for example if I run the aggregation for user_id = 1, I have to get:
[
{
"user_id": 2,
"same": 1
},
{
"user_id": 3,
"same": 2
}
]
You cannot write any query here with input as simple as "user_id": 1 here, but you can retrieve the document for that user and then get a comparison of that data to the other documents you are retrieving:
var doc = db.collection.findOne({ "user_id": 1 });
db.collection.aggregate([
{ "$match": { "user_id": { "$ne": 1 } } },
{ "$project": {
"_id": 0,
"user_id": 1
"same": { "$size": { "$setIntersection": [ "$prefs", doc.prefs ] } }
}}
])
Which is one approach, but also not that much different to comparing each document in the client:
function intersect(a,b) {
var t;
if (b.length > a.length) t = b, b = a, a = t;
return a.filter(function(e) {
if (b.indexOf(e) != -1) return true;
});
}
var doc = db.collection.findOne({ "user_id": 1 });
db.collection.find({ "user_id": { "$ne": 1 } }).forEach(function(mydoc) {
printjson({
"user_id": mydoc.user_id,
"same": intersect(mydoc.prefs, doc.prefs).length
});
});
It's the same thing. You are not really "aggregating" anything here but just making comparisons of one documents content against the other. Of course you can ask the aggregation framework to do something like "filter" out anything that does not have a similar match:
var doc = db.collection.findOne({ "user_id": 1 });
db.collection.aggregate([
{ "$match": { "user_id": { "$ne": 1 } } },
{ "$project": {
"_id": 0,
"user_id": 1
"same": { "$size": { "$setIntersection": [ "$prefs", doc.prefs ] } }
}},
{ "$match": { "same": { "$gt": 0 } }}
])
Though actually that would be more efficient to remove any documents with a zero count before doing the projection:
var doc = db.collection.findOne({ "user_id": 1 });
db.collection.aggregate([
{ "$match": { "user_id": { "$ne": 1 } } },
{ "$redact": {
"$cond": {
"if": { "$gt": [
{ "$size": { "$setIntersection": [ "$prefs", doc.prefs ] } },
0
]},
"then": "$$KEEP",
"else": "$$PRUNE"
}
}},
{ "$project": {
"_id": 0,
"user_id": 1
"same": { "$size": { "$setIntersection": [ "$prefs", doc.prefs ] } }
}}
])
And at least then that would make some sense to do the server processing.
But otherwise, it's all pretty much the same, with possibly a "little" more overhead on the client working out the "intersection" here.
Related
Here's a very simple data example.
[
{
"aaa": true,
"bbb": 111,
},
{
"aaa": false,
"bbb": 111,
}
]
Then, what query should be executed so that I can get the result like this?
[
{
"_id": "0",
"bbb_sum": 222,
"aaa_and": false,
"aaa_or": true
}
]
Actually, I've tried with a query like this
db.collection.aggregate([
{
"$group": {
"_id": "0",
"bbb_sum": {
"$sum": "$bbb"
},
"aaa_and": {
"$and": ["$aaa", true]
},
"aaa_or": {
"$or": ["$aaa", false]
}
}
}
])
But the Mongo Playground complains query failed: (Location40237) The $and accumulator is a unary operator, that's quite confusing.
You can also find this simple test case here https://mongoplayground.net/p/8dqtXJ93vIx
Also, I've searched for similar questions on both Google and Stackoverflow, but I can't find one.
Thanks in advance!
Not like "$sum","$and" and "$or" are not aggregation operators that can be used in "$group". You can temporary save all the "aaa" field into an array and then use "$project" operator to process the data.
db.collection.aggregate([
{
"$group": {
"_id": "0",
"sum": {
"$sum": "$bbb"
},
"aaa_all": {
"$push": "$aaa"
}
}
},
{
"$project": {
"sum": 1,
"aaa_and": {
"$allElementsTrue": "$aaa_all"
},
"aaa_or": {
"$anyElementTrue": "$aaa_all"
}
}
}
])
Here is the case: https://mongoplayground.net/p/Y-Fs_Ch9lwk
$min and $max operators actually work with booleans too, false being considered smaller than true
thinking of them as 0 and 1 might be easier to understand :
$min: [a,…,n] will return 1/true only if all elements are 1/true => this is a AND
$max: [a,…,n] will return 0/false only if all elements are 0/false => this is a OR
(the operators will return booleans if input booleans, the analogy with numbers is only for the sake of comprehension)
So your request can simply become :
db.collection.aggregate([
{
"$group": {
"_id": "0",
"bbb_sum": {
"$sum": "$bbb"
},
"aaa_and": {
"$min": "$aaa"
},
"aaa_or": {
"$max": "$aaa"
}
}
}
])
You can do some logics as below
db.collection.aggregate([
{
"$group": {//Group by desired id
"_id": null,
"sum": {//Sum the value
"$sum": "$bbb"
},
"aaa_and": {
"$sum": {
"$cond": {
"if": {
"$eq": [
"$aaa",
true
]
},
"then": 1, //If true returns 1
"else": 0 // else 0
}
}
},
"total": { //helper to do the logic
$sum: 1
}
}
},
{
$project: {
aaa_and: {
"$eq": [//If total matches with number of true, all are true
"$total",
"$aaa_and"
]
},
aaa_or: {
"$ne": [//if value greater than 0, then there is at least one true
"$aaa_and",
"0"
]
},
sum: 1
}
}
])
playground
I have a user collection:
[
{"_id": 1,"name": "John", "age": 25, "valid_user": true}
{"_id": 2, "name": "Bob", "age": 40, "valid_user": false}
{"_id": 3, "name": "Jacob","age": 27,"valid_user": null}
{"_id": 4, "name": "Amelia","age": 29,"valid_user": true}
]
I run a '$facet' stage on this collection. Checkout this MongoPlayground.
I want to talk about the first output from the facet stage. The following is the response currently:
{
"user_by_valid_status": [
{
"_id": false,
"count": 1
},
{
"_id": true,
"count": 2
},
{
"_id": null,
"count": 1
}
]
}
However, I want to restructure the output in this way:
"analytics": {
"invalid_user": {
"_id": false
"count": 1
},
"valid_user": {
"_id": true
"count": 2
},
"user_with_unknown_status": {
"_id": null
"count": 1
}
}
The problem with using a '$project' stage along with 'arrayElemAt' is that the order may not be definite for me to associate an index with an attribute like 'valid_users' or others. Also, it gets further complicated because unlike the sample documents that I have shared, my collection may not always contain all the three categories of users.
Is there some way I can do this?
You can use $switch conditional operator,
$project to show value part in v with _id and count field as object, k to put $switch condition
db.collection.aggregate([
{
"$facet": {
"user_by_valid_status": [
{
"$group": {
"_id": "$valid_user",
"count": { "$sum": 1 }
}
},
{
$project: {
_id: 0,
v: { _id: "$_id", count: "$count" },
k: {
$switch: {
branches: [
{ case: { $eq: ["$_id", null] }, then: "user_with_unknown_status" },
{ case: { $eq: ["$_id", false] }, then: "invalid_user" },
{ case: { $eq: ["$_id", true] }, then: "valid_user" }
]
}
}
}
}
],
"users_above_30": [{ "$match": { "age": { "$gt": 30 } } }]
}
},
$project stage in root, convert user_by_valid_status array to object using $arrayToObject
{
$project: {
analytics: { $arrayToObject: "$user_by_valid_status" },
users_above_30: 1
}
}
])
Playground
Collection exists as below:
[
{"currentLocation": "Chennai", "baseLocation": "Bengaluru"},
{"currentLocation": "Chennai", "baseLocation": "Bengaluru"},
{"currentLocation": "Delhi", "baseLocation": "Bengaluru"},
{"currentLocation": "Chennai", "baseLocation": "Chennai"}
]
Expected Output:
[
{"city": "Chennai", "currentLocationCount": 3, "baseLocationCount": 1},
{"city": "Bengaluru", "currentLocationCount": 0, "baseLocationCount": 3},
{"city": "Delhi", "currentLocationCount": 1, "baseLocationCount": 0}
]
What I have tried is:
db.getCollection('users').aggregate([{
$group: {
"_id": "$baselocation",
baseLocationCount: {
$sum: 1
}
},
}, {
$project: {
"_id": 0,
"city": "$_id",
"baseLocationCount": 1
}
}])
Got result as:
[
{"city": "Chennai", "baseLocationCount": 1},
{"city": "Bengaluru", "baseLocationCount": "3"}
]
I'm not familiar with mongo, so any help?
MongoDB Version - 3.4
Neil Lunn and myself had a lovely argument over this topic the other day which you can read all about here: Group by day with Multiple Date Fields.
Here are two solutions to your precise problem.
The first one uses the $facet stage. Bear in mind, though, that it may not be suitable for large collections because $facet produces a single (potentially huge) document that might be bigger than the current MongoDB document size limit of 16MB (which only applies to the result document and wouldn't be a problem during pipeline processing anyway):
collection.aggregate(
{
$facet:
{
"current":
[
{
$group:
{
"_id": "$currentLocation",
"currentLocationCount": { $sum: 1 }
}
}
],
"base":
[
{
$group:
{
"_id": "$baseLocation",
"baseLocationCount": { $sum: 1 }
}
}
]
}
},
{ $project: { "result": { $setUnion: [ "$current", "$base" ] } } }, // merge results into new array
{ $unwind: "$result" }, // unwind array into individual documents
{ $replaceRoot: { newRoot: "$result" } }, // get rid of the additional field level
{ $group: { "_id": "$_id", "currentLocationCount": { $sum: "$currentLocationCount" }, "baseLocationCount": { $sum: "$baseLocationCount" } } }, // group into final result)
{ $project: { "_id": 0, "city": "$_id", "currentLocationCount": 1, "baseLocationCount": 1 } } // group into final result
)
The second one works based on the $map stage instead:
collection.aggregate(
{
"$project": {
"city": {
"$map": {
"input": [ "current", "base" ],
"as": "type",
"in": {
"type": "$$type",
"name": {
"$cond": {
"if": { "$eq": [ "$$type", "current" ] },
"then": "$currentLocation",
"else": "$baseLocation"
}
}
}
}
}
}
},
{ "$unwind": "$city" },
{
"$group": {
"_id": "$city.name",
"currentLocationCount": {
"$sum": {
"$cond": {
"if": { "$eq": [ "$city.type", "current" ] },
"then": 1,
"else": 0
}
}
},
"baseLocationCount": {
"$sum": {
"$cond": {
"if": { "$eq": [ "$city.type", "base" ] },
"then": 1,
"else": 0
}
}
}
}
}
)
I have a Rating model with a book and rating value to it. I would like to get all the ratings count (ratings vary from 1 to 5) for each book in the database.
My schema simply looks like -
{
"_id": ObjectId("57e112312a52fe257e5d1d5c"),
"book": ObjectId("57e111142a52fe257e5d1d42"),
"rating": 4
}
{
"_id": ObjectId("57e7a002420d22d6106a4715"),
"book": ObjectId("57e111142a52fe257e5d1d42"),
"rating": 5
}
{
"_id": ObjectId("57e7a4cd98bfdb5a11962d54"),
"book": ObjectId("57e111142a52fe257e5d17676"),
"rating": 5
}
{
"_id": ObjectId("57e7a4cd98bfdb5a11962d54"),
"book": ObjectId("57e111142a52fe257e5d17676"),
"rating": 1
}
Currently, i have only been able to get to this point where i can get the no of ratings for each book but it doesn't specify exactly the rating value count.
This is my current query -
db.ratings.aggregate([
{$match: {book: {$in: [ObjectId("57e111142a52fe257e5d1d42"), ObjectId('57e6bef7cad79fa38555c643')]}}},
{$group: {_id: {book: "$book", value: "$value"} } },
{$group: {_id: "$_id.book", total: {$sum: 1}}},
])
The output is this -
{
"result": [
{
"_id": ObjectId("57e6bef7cad79fa38555c643"),
"total": 2
},
{
"_id": ObjectId("57e111142a52fe257e5d1d42"),
"total": 2
}
],
"ok": 1
}
However, i want to club all the documents and get a result with the count of ratings for each value of the rating field, something like below. The whole point is that i just want the count of ratings for each value for each book.
{
result: [
{
_id: "57e111142a52fe257e5d17676",
5_star_ratings: 1,
4_star_ratings: 3,
3_star_ratings: 4,
2_star_ratings: 1,
1_star_ratings: 0,
},
{
_id: "57e111142a52fe257e5d1d42",
5_star_ratings: 10,
4_star_ratings: 13,
3_star_ratings: 7,
2_star_ratings: 8,
1_star_ratings: 19,
}
.
.
.
.
]
}
How do i go about this?
Accomplishing the task require a $group pipeline that uses the $cond operator in the $sum accumulator operator. The $cond operator will evaluate a logical condition based on its first argument (if) and then returns the second argument where the evaluation is true (then) or the third argument where false (else). This converts the true/false logic into 1 and 0 numerical values that feed into $sum respectively:
{
"$sum": {
"$cond": [ { "$eq": [ "$rating", 1 ] }, 1, 0 ]
}
}
As a resulting operation, you might want to run the following aggregation pipeline:
var pipeline = [
{
"$match": {
"book": {
"$in": [
ObjectId("57e111142a52fe257e5d1d42"),
ObjectId('57e6bef7cad79fa38555c643')
]
}
}
},
{
"$group": {
"_id": "$book",
"5_star_ratings": {
"$sum": {
"$cond": [ { "$eq": [ "$rating", 5 ] }, 1, 0 ]
}
},
"4_star_ratings": {
"$sum": {
"$cond": [ { "$eq": [ "$rating", 4 ] }, 1, 0 ]
}
},
"3_star_ratings": {
"$sum": {
"$cond": [ { "$eq": [ "$rating", 3 ] }, 1, 0 ]
}
},
"2_star_ratings": {
"$sum": {
"$cond": [ { "$eq": [ "$rating", 2 ] }, 1, 0 ]
}
},
"1_star_ratings": {
"$sum": {
"$cond": [ { "$eq": [ "$rating", 1 ] }, 1, 0 ]
}
}
}
},
]
db.ratings.aggregate(pipeline)
For a more flexible and better performant approach which executes much faster than the above, consider running an alternative pipeline as follows
db.ratings.aggregate([
{
"$match": {
"book": {
"$in": [
ObjectId("57e111142a52fe257e5d1d42"),
ObjectId('57e6bef7cad79fa38555c643')
]
}
}
},
{
"$group": {
"_id": {
"book": "$name",
"rating": "$rating"
},
"count": { "$sum": 1 }
}
},
{
"$group": {
"_id": "$_id.book",
"counts": {
"$push": {
"rating": "$_id.rating",
"count": "$count"
}
}
}
}
])
I have documents like:
{
"platform":"android",
"install_date":20151029
}
platform - can have one value from [android|ios|kindle|facebook ] .
install_date - there are many install_dates
There are also many fields.
Aim : I am calculating installs per platform on particular date.
So I am using group by in aggregation framework and make counts by platform. Document should look like like:
{
"install_date":20151029,
"platform" : {
"android":1000,
"ios": 2000,
"facebook":1500
}
}
I have done like:
db.collection.aggregate([
{
$group: {
_id: { platform: "$platform",install_date:"$install_date"},
count: { "$sum": 1 }
}
},
{
$group: {
_id: { install_date:"$_id.install_date"},
platform: { $push : {platform :"$_id.platform", count:"$count" } }
}
},
{
$project : { _id: 0, install_date: "$_id.install_date", platform: 1 }
}
])
which Gives document like:
{
"platform": [
{
"platform": "facebook",
"count": 1500
},
{
"platform": "ios",
"count": 2000
},
{
"platform": "android",
"count": 1000
}
],
"install_date": 20151027
}
Problem:
Projecting array to single object as "platform"
With MongoDb 3.4 and newer, you can leverage the use of $arrayToObject operator to get the desired result. You would need to run the following aggregate pipeline:
db.collection.aggregate([
{ "$group": {
"_id": {
"date": "$install_date",
"platform": { "$toLower": "$platform" }
},
"count": { "$sum": 1 }
} },
{ "$group": {
"_id": "$_id.date",
"counts": {
"$push": {
"k": "$_id.platform",
"v": "$count"
}
}
} },
{ "$addFields": {
"install_date": "$_id",
"platform": { "$arrayToObject": "$counts" }
} },
{ "$project": { "counts": 0, "_id": 0 } }
])
For older versions, take advantage of the $cond operator in the $group pipeline step to evaluate the counts based on the platform field value, something like the following:
db.collection.aggregate([
{ "$group": {
"_id": "$install_date",
"android_count": {
"$sum": {
"$cond": [ { "$eq": [ "$platform", "android" ] }, 1, 0 ]
}
},
"ios_count": {
"$sum": {
"$cond": [ { "$eq": [ "$platform", "ios" ] }, 1, 0 ]
}
},
"facebook_count": {
"$sum": {
"$cond": [ { "$eq": [ "$platform", "facebook" ] }, 1, 0 ]
}
},
"kindle_count": {
"$sum": {
"$cond": [ { "$eq": [ "$platform", "kindle" ] }, 1, 0 ]
}
}
} },
{ "$project": {
"_id": 0, "install_date": "$_id",
"platform": {
"android": "$android_count",
"ios": "$ios_count",
"facebook": "$facebook_count",
"kindle": "$kindle_count"
}
} }
])
In the above, $cond takes a logical condition as it's first argument (if) and then returns the second argument where the evaluation is true (then) or the third argument where false (else). This makes true/false returns into 1 and 0 to feed to $sum respectively.
So for example, if { "$eq": [ "$platform", "facebook" ] }, is true then the expression will evaluate to { $sum: 1 } else it will be { $sum: 0 }