I have a collection of documents holding a list of feedbacks for different items. It looks something like this:
{
{
item: "item_1"
rating: "neutral"
comment: "some comment"
},
{
item: "item_2"
rating: "good"
comment: "some comment"
},
{
item: "item_1"
rating: "good"
comment: "some comment"
},
{
item: "item_1"
rating: "bad"
comment: "some comment"
},
{
item: "item_3"
rating: "good"
comment: "some comment"
},
}
I want a way to find out how many different ratings each item got.
so the output should look something like this:
{
{
item: "item_1"
good: 12
neutral: 10
bad: 67
},
{
item: "item_2"
good: 2
neutral: 45
bad: 8
},
{
item: "item_3"
good: 1
neutral: 31
bad: 10
}
}
This is what I've done
db.collection(collectionName).aggregate(
[
{
$group:
{
_id: "$item",
good_count: {$sum: {$eq: ["$rating", "Good"]}},
neutral_count:{$sum: {$eq: ["$rating", "Neutral"]}},
bad_count:{$sum: {$eq: ["$rating", "Bad"]}},
}
}
]
)
The format of the output looks right, but the counts are always 0.
I'm wondering what's the properway of summing things up by looking at the distinct values of the same field?
Thanks!
You were very close, but of course $eq just returns a true/false value, so to make that numeric you need $cond:
db.collection(collectionName).aggregate([
{ "$group" : {
"_id": "$item",
"good_count": {
"$sum": {
"$cond": [ { "$eq": [ "$rating", "good" ] }, 1, 0]
}
},
"neutral_count":{
"$sum": {
"$cond": [ { "$eq": [ "$rating", "neutral" ] }, 1, 0 ]
}
},
"bad_count": {
"$sum": {
"$cond": [ { "$eq": [ "$rating", "bad" ] }, 1, 0 ]
}
}
}}
])
As a "ternary" operator $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.
Also note that "case" is sensitive for $eq. If you have varing case then you likely want $toLower in the expressions:
"$cond": [ { "$eq": [ { "$toLower": "$rating" }, "bad" ] }, 1, 0 ]
On a slightly different note, the following aggregation is usually more flexible to different possible values and runs rings around the conditional sums in terms of performance:
db.collection(collectionName).aggregate([
{ "$group": {
"_id": {
"item": "$item",
"rating": { "$toLower": "$rating" }
},
"count": { "$sum": 1 }
}},
{ "$group": {
"_id": "$_id.item",
"results": {
"$push": {
"rating": "$_id.rating",
"count": "$count"
}
}
}}
])
That would instead give output like this:
{
"_id": "item_1"
"results":[
{ "rating": "good", "count": 12 },
{ "rating": "neutral", "count": 10 }
{ "rating": "bad", "count": 67 }
]
}
It's all the same information, but you did not have to explicitly match the values and it does execute much faster this way.
Related
There are 2 array fields after I looked up in MongoDB aggregation pipeline.
the first one
[
{
"colorId": "60828a1b216b0972da695f2a",
"name": "Exellent",
"description": "Great work"
}
]
and the second one
[
{
"_id": "60828a1b216b0972da695f2a",
"colorName": "Green",
"hexColorCodes": "#2D9D78",
"sequence": 1,
"isActivated": true,
"created_at": "2021-04-23T08:49:31.729Z",
"updated_at": "2021-04-23T08:49:31.729Z",
"__v": 0,
"isDefault": true
}
]
the result I want is
[
{
"colorId": "60828a1b216b0972da695f2a",
"name": "Exellent",
"description": "Great work",
"colorName": "Green",
"hexColorCodes": "#2D9D78"
}
]
then I want to map colorName and hexColorCodes to the first array. Here is my aggregate pipeline
db.collection.aggregate([
{
$lookup: {
from: "color_tags",
localField: "colors.colorId",
foreignField: "_id",
as: "tempColors",
},
},
{
$addFields: {
stages3: {
$map: {
input: "$colors",
in: {
$mergeObjects: [
"$$this",
{
$arrayElemAt: [
"$tempColors",
{
$indexOfArray: [
"$tempColors._id",
"$$this.colors.colorId",
],
},
],
},
],
},
},
},
},
}
])
but the result is not what I expected. It mapped with incorrect id. Please suggest.
$map to iterate loop of first array
$filter to iterate loop of second array and match colorId with _id and return matching result
$arrayElemAt to get first matching element
$mergeObjects to merge current object with return result from second array
{
$project: {
first: {
$map: {
input: "$first",
as: "f",
in: {
$mergeObjects: [
"$$f",
{
$arrayElemAt: [
{
$filter: {
input: "$second",
cond: { $eq: ["$$this._id", "$$f.colorId"] }
}
},
0
]
}
]
}
}
}
}
}
If you want to result specific fields then add a $project stage at the end,
{
$project: {
"first.colorId": 1,
"first.name": 1,
"first.description": 1,
"first.colorName": 1,
"first.hexColorCodes": 1
}
}
Playground
I need to extract all the attributes which are of numeric types. For example, if the different attributes are
{
age: 32
gender: "female"
year: 2020
name: "Abc"
}
My query should return ["age","year"]
I think the below query should help you out.
db.test.aggregate([
// Remove this `$limit` stage if your Collection schema is dynamic and you want to process all the documents instead of just one
{
"$limit": 1
},
{
"$project": {
"arrayofkeyvalue": {
"$filter": {
"input": {"$objectToArray":"$$ROOT"},
"as": "keyValPairs",
"cond": {
"$in": [{"$type": "$$keyValPairs.v"}, ["double", "int", "long"]],
// Change the above line to the following to get only `int` keys instead of `int, double` and `long`:
// "$eq": [{"$type": "$$keyValPairs.v"}, "int"],
}
}
}
}
},
{
"$group": {
"_id": null,
"unique": {"$addToSet": "$arrayofkeyvalue.k"}
}
},
{
"$project": {
"_id": 0,
"intKeyNames": {
"$reduce": {
input: "$unique",
initialValue: [],
in: {$setUnion : ["$$value", "$$this"]}
}
}
}
},
])
The above query result will be something like this:
{
"intKeyNames" : [
"_id",
"abc",
"paymentMonth",
"paymentYear",
"value"
]
}
I want to create a mongo db view from two collections with a new value that is a sum of values from one of the collection according to an operation from another collection.
Below is the structure:
/* First collection */
{
"product": "test",
"labels" : [
{"code": "label1", "value": 42},
{"code": "label2", "value": 50}
]
}
/* Second collection */
{
"code": "label3",
"calculation" : [
{"label" : "label1", "operation":"+"},
{"label" : "label2", "operation":"-"}
]
}
In my aggregated collection i want a new field that would be label1 - label2.
{
"product" : "test",
"labels" : [
{"code": "label1", "value": 42},
{"code": "label2", "value": 50}
],
"vlabels" : [
{"code": "label3", "value": -8}
]
}
Although it is possible. I doubt it would be optimal, if you don't need further processing on the database, I suggest you do this at the application layer.
However, I have attempted to do this as an exercise. This approach would check only for the "-" operator and assign a negative value, other operator will use the existing value.
/* First collection: "products" */
/* Second collection: "vlabels" */
db.products.aggregate([
{
$lookup: {
from: "vlabels", // lookup calculation from vlabels
let: {
labels: "$labels"
},
pipeline: [
{
$set: {
calculation: {
$map: {
input: "$calculation", // map over calculation in vlabels
as: "calc",
in: {
operation: "$$calc.operation",
product: {
$arrayElemAt: [
{
$filter: {
input: "$$labels", // filter for matching product labels and get the first element using $arrayAlemAt to get the value
as: "label",
cond: {
$eq: ["$$calc.label", "$$label.code"]
}
}
},
0
]
}
}
}
}
}
},
{
$project: {
_id: false,
code: "$code",
value: {
$reduce: { // reducing by adding all values in calculation array, use negative value on "-" operator
input: "$calculation",
initialValue: 0,
in: {
$add: [
"$$value",
{
$cond: [
{
$eq: ["-", "$$this.operation"]
},
{
$multiply: [
-1,
{ $ifNull: ["$$this.product.value", 0] }
]
},
{ $ifNull: ["$$this.product.value", 0] }
]
}
]
}
}
}
}
}
],
as: "vlabels"
}
}
])
Mongo Playground
I am trying to count distinct(not unique) or Emp No in same department.but getting error
query failed: unknown group operator '$group'
here is my code
https://mongoplayground.net/p/UvYF9NB7vZx
db.collection.aggregate([
{
$group: {
_id: "$Department",
total: {
"$group": {
_id: "$Emp No"
}
}
}
}
])
Expected output
[
{
"_id": "HUAWEI”,
“total”:1
},
{
"_id": "THBS”,
“total”:2
}
]
THBShave two different Emp No A10088P2C and A20088P2C
HUAWEI have only one Emp No A1016OBW
so, $group is Pipeline stage, you can only use it in upper level.
But for your required output there is lots of ways i believe,
we can do something like this as well:
db.collection.aggregate([
{
$group: {
_id: {
dept: "$Department",
emp: "$Emp No"
},
total: {
"$sum": 1
}
}
},
{
$group: {
_id: "$_id.dept",
total: {
"$sum": 1
}
}
}
])
Here, in first stage we are grouping with Department and its Emp No , and also we are having count of how many Emp No is in each dept.
[this count you can remove though as we are not using it.]
result of this stage will be:
[
{
"_id": {
"dept": "THBS",
"emp": "A10088P2C"
},
"total": 2
},
{
"_id": {
"dept": "THBS",
"emp": "A20088P2C"
},
"total": 1
},
{
"_id": {
"dept": "HUAWEI",
"emp": "A1016OBW"
},
"total": 3
}
]
next on top of this part data, i'm grouping again, with the dept. which comes in $_id.dept, and making count in the same way, which gives the result in your required format.
[
{
"_id": "HUAWEI",
"total": 1
},
{
"_id": "THBS",
"total": 2
}
]
Demo
Is it possible to have facet to return as an object instead of an array? It seems a bit counter intuitive to need to access result[0].total instead of just result.total
code (using mongoose):
Model
.aggregate()
.match({
"name": { "$regex": name },
"user_id": ObjectId(req.session.user.id),
"_id": { "$nin": except }
})
.facet({
"results": [
{ "$skip": start },
{ "$limit": finish },
{
"$project": {
"map_levels": 0,
"template": 0
}
}
],
"total": [
{ "$count": "total" },
]
})
.exec()
Each field you get using $facet represents separate aggregation pipeline and that's why you always get an array. You can use $addFields to overwrite existing total with single element. To get that first item you can use $arrayElemAt
Model
.aggregate()
.match({
"name": { "$regex": name },
"user_id": ObjectId(req.session.user.id),
"_id": { "$nin": except }
})
.facet({
"results": [
{ "$skip": start },
{ "$limit": finish },
{
"$project": {
"map_levels": 0,
"template": 0
}
}
],
"total": [
{ "$count": "total" },
]
})
.addFields({
"total": {
$arrayElemAt: [ "$total", 0 ]
}
})
.exec()
You can try this as well
Model
.aggregate()
.match({
"name": { "$regex": name },
"user_id": ObjectId(req.session.user.id),
"_id": { "$nin": except }
})
.facet({
"results": [
{ "$skip": start },
{ "$limit": finish },
{
"$project": {
"map_levels": 0,
"template": 0
}
}
],
"total": [
{ "$count": "total" },
]
})
.addFields({
"total": {
"$ifNull": [{ "$arrayElemAt": [ "$total.total", 0 ] }, 0]
}
})
.exec()
imagine that you want to pass the result of $facet to the next stage, let's say $match. well $match accepts an array of documents as input and return an array of documents that matched an expression, if the output of $facet was just an element we can't pass its output to $match because the type of output of $facet is not the same as the type of input of $match ($match is just an example). In my opinion it's better to keep the output of $facet as array to avoid handling those types of situations.
PS : nothing official in what i said