More than one 'in' expression within $map - mongodb

Been recently playing around with an array field within documents of a collection, and was wondering if I could apply more than one expression having in is specified in some way?
$project: {
"new_field": {
$map: {
input: "$test",
as: "item",
in: {$trim: {input: "$$item"}}
}
}
}
And trying to avoid performing another projection I was thinking of doing something like this, but just fails with An object representing an expression must have exactly one field.
$project: {
"new_field": {
$map: {
input: "$test",
as: "item",
in: {$trim: {input: "$$item"}, $concat: ["$$item", "testing"]}
}
}
}
Is my only option to just do another project step with all the fields?

You could apply more than one operation. But the output will not be on top of the first expression' result.
Sample Query:
db.collection.aggregate([
{
$project: {
adjustedGrades: {
$map: {
input: "$quizzes",
as: "grade",
in: [
{
$add: [
"$$grade",
2
]
},
{
$add: [
"$$grade",
3
]
}
]
}
}
}
}
])
Difference:
I specified in as array.
in: [
{
$add: [
"$$grade",
2
]
},
{
$add: [
"$$grade",
3
]
}
]
Sample input:
[
{
_id: 1,
quizzes: [
5,
6,
7
]
},
{
_id: 2,
quizzes: []
},
{
_id: 3,
quizzes: [
3,
8,
9
]
}
]
Sample output:
[
{
"_id": 1,
"adjustedGrades": [
[
7,
8
],
[
8,
9
],
[
9,
10
]
]
},
{
"_id": 2,
"adjustedGrades": []
},
{
"_id": 3,
"adjustedGrades": [
[
5,
6
],
[
10,
11
],
[
11,
12
]
]
}
]
Play

Related

Mongoose aggregate: how to create fields dynamically from the user request

Please someone help me! I can't find the solution in documentation or other topics.
I'm using mongodb aggregation in Mongoose/Nest.js project to return the document data with some formatting and filtering. I have the structure of the mongo document like
{
_id: '1',
outputs: [
{
fileName: 'fileName1',
data: [
{
columnName1: 3,
columnName2: 4,
........
columnName30: 5
},
{
columnName1: 1,
columnName2: 2,
........
columnName30: 3
},
...........
]
},
{
fileName: 'fileName1',
data: [
{
columnName1: 3,
columnName2: 4,
........
columnName30: 5
},
{
columnName1: 1,
columnName2: 2,
........
columnName30: 3
},
...........
]
}
........
]
}
I've already done some formatting, but now I need to include to the response only requested by the user fields (columnNamesToChoose). And filter their values depending on gte, lte of mainColumnName. Inside $project I was going to use some mapping like this, but it doesn't work. Could you please help me to fix this part of code?
...columnNamesToChoose.map((columnName) => ({ [columnName]: {
$map: {
input: {
$filter: {
input: '$outputs.data',
as: 'item',
cond: {
$and: [
{ $gte: [`$$item.${mainColumnName}`, gte] },
{ $lte: [`$$item.${mainColumnName}`, lte] },
],
},
},
},
as: 'file',
in: `$$file.${columnName}`,
},
} })),
This is the full code of aggregation:
mainColumnName = 'column1' (from the body of the user request)
columnNamesToChoose = ['column2', 'column5'] (from the body of the user request)
myModel.aggregate([
{
$match: { _id: Number(id) },
},
{ $unwind: '$outputs' },
{
$match: { 'outputs.fileName': fileName },
},
{
$project: {
_id: '$_id',
fileName: '$outputs.fileName',
[mainColumnName]: {
$map: {
input: {
$filter: {
input: '$outputs.data',
as: 'item',
cond: {
$and: [
{ $gte: [`$$item.${mainColumnName}`, gte] },
{ $lte: [`$$item.${mainColumnName}`, lte] },
],
},
},
},
as: 'file',
in: `$$file.${mainColumnName}`,
},
},
},
},
])
My result:
{
"0": {
"column2": [
4,
2,
1,
5
]
},
"1": {
"column5": [
1,
8,
9,
0
]
},
"_id": 1,
"fileName": "somefilename.txt",
"column1": [
3,
1,
2,
20
],
}
Expected result:
{
"_id": 1,
"fileName": "somefilename.txt",
"column1": [
3,
1,
2,
20
],
"column2": [
4,
2,
1,
5
],
"column5": [
1,
8,
9,
0
],
}
One option is to first $reduce and then $unwind, $match and $group, where the $group stage is built dynamically on the code (for-loop) according to the input:
db.collection.aggregate([
{$match: {_id: id}},
{$project: {
outputs: {
$reduce: {
input: "$outputs",
initialValue: [],
in: {
$concatArrays: [
"$$value",
{$cond: [
{$eq: ["$$this.fileName", fileName]},
"$$this.data",
[]
]
}
]
}
}
}
}
},
{$unwind: "$outputs"},
{$match: {"outputs.columnName1": {$gte: gte, $lte: lte}}},
{$group: {
_id: 0,
column1: {$push: "$outputs.columnName1"},
column2: {$push: "$outputs.columnName2"},
column5: {$push: "$outputs.columnName5"}
}},
{$set: {fileName: fileName}}
])
See how it works on the playground example
On js it will look something like:
const matchStage = {$match: {}};
matchStage.$match[`outputs.${mainColumnName}`] = {$gte: gte, $lte: lte};
const groupStage = {$group: {_id: 0}};
for (const col of columnNamesToChoose ) {
groupStage.$group[col] = {$push: `"$outputs.${col}"`}
};
const aggregation = [
{$match: {_id: id}},
{$project: {
outputs: {$reduce: {
input: "$outputs",
initialValue: [],
in: {$concatArrays: [
"$$value",
{$cond: [
{$eq: ["$$this.fileName", fileName]},
"$$this.data",
[]
]}
]}
}}
}},
{$unwind: "$outputs"},
matchStage,
groupStage,
{$set: {fileName: fileName}}
],
const res = await myModel.aggregate(aggregation)

How can I exclude results that contain a specific element from grouped results?

A: It should be output how many _ids are included by date grouped by date.
B: The number of elements in details in A.
If it has element, count 1. not 0. If the document is as follows, the value counted after excluding from A becomes B
{
_id: ObjectId
details: array //no elements
createdAt: Date
}
C: The count of B becomes C, except when there are specific details.slaesManagerIds among B.
details.salesManagerIds is provided as an array.
For examples,
[ObjecttId("612f57184205db63a3396a9e"), ObjectId("612cb021278f621a222087d7")]
I made query as follows.
https://mongoplayground.net/p/6sBxAmO_31y
It goes well until B. How can I write a query to get C ?
If you write and execute a query that can obtain C through the link above, you should get the following result.
[
{
"A": 2,
"B": 1,
"C": 1,
"_id": "2018-05-19"
},
{
"A": 3,
"B": 3,
"C": 1,
"_id": "2018-05-18"
}
]
use $filter
db.collection.aggregate([
{
$group: {
_id: {
$dateToString: {
format: "%Y-%m-%d",
date: "$createdAt"
}
},
A: {
$sum: 1
},
B: {
$sum: {
$cond: [
{
$and: [
{
$isArray: "$details"
},
{
$gt: [
{
$size: "$details"
},
0
]
}
]
},
1,
0
]
}
},
C: {
$sum: {
$cond: [
{
$and: [
{
$isArray: "$details"
},
{
$gt: [
{
$size: "$details"
},
0
]
},
{
$gt: [
{
$size: {
$filter: {
input: "$details",
as: "d",
cond: {
$and: [
{
$not: [
{
$in: [
"$$d.salesManagerId",
[
ObjectId("612f57184205db63a3396a9e"),
ObjectId("612cb021278f621a222087d7")
]
]
}
]
}
]
}
}
}
},
0
]
}
]
},
1,
0
]
}
}
}
},
{
$sort: {
_id: -1
}
}
])
mongoplayground

Redact nested items in two level arrays

Having some issues understanding the $redact stage and cant seem to understand what i’m doing wrong or just missing. I’ve read the docs but it doesn’t go through my specific need or I'm just not understanding it correctly. I want to redact some nested items based on a id/number in a two level deep array. If i omit the array and use a regular key/value it does work. But not when having the values in an array.
I want to redact/remove all the items (articles) where the ids don’t match the input/query id that i provide. If $redact is not suitable for this i will happy take other solutions, but preferably not with $unwind and $group.
If I have a single key/value as authorId as in this playground it does work.
https://mongoplayground.net/p/V4mOboXp7zR
On the link above is the result i want to achieve but where the authorIds is an array and not key/values.
But if I have multiple Ids in an array it does not work. As this
https://mongoplayground.net/p/pqvJLUfL1f4
Thanks!
Sample data
[
{
"title": "one title",
"articles": [
{
content: "lorem ipsum",
authorIds: [
1
],
},
{
content: "bacon ipsum",
authorIds: [
2,
3,
4
]
},
{
content: "hippsum dippsum",
authorIds: [
3,
5
]
},
{
content: "hippsum dippsum",
authorIds: [
4
]
}
],
}
]
Current non working stage
db.collection.aggregate([
{
"$project": {
title: 1,
articles: 1,
articleCount: {
$size: "$articles"
},
},
},
{
"$redact": {
"$cond": {
"if": {
"$or": [
{
"$eq": [
"$authorIds",
2 // match on this authorId
]
},
{
$gte: [
"$articleCount",
1
]
},
]
},
"then": "$$DESCEND",
"else": "$$PRUNE"
}
}
},
])
Use setIntersection in redact condition.
aggregate
db.collection.aggregate([
{
"$project": {
title: 1,
articles: 1,
articleCount: {
$size: "$articles"
}
}
},
{
"$redact": {
"$cond": {
"if": {
"$or": [
{
$gte: [
"$articleCount",
1
]
},
{
$gt: [
{
$size: {
$setIntersection: [
"$authorIds",
[
2
]
]
}
},
0
]
}
]
},
"then": "$$DESCEND",
"else": "$$PRUNE"
}
}
}
])
data
[
{
"title": "one title",
"articles": [
{
content: "lorem ipsum",
authorIds: [
1
]
},
{
content: "bacon ipsum",
authorIds: [
2,
3,
4
]
},
{
content: "hippsum dippsum",
authorIds: [
3,
5
]
},
{
content: "hippsum dippsum",
authorIds: [
4
]
}
]
}
]
result
[
{
"_id": ObjectId("5a934e000102030405000000"),
"articleCount": 4,
"articles": [
{
"authorIds": [
2,
3,
4
],
"content": "bacon ipsum"
}
],
"title": "one title"
}
]
mongoplayground

How to use $subtract along with conditions to use a default value from an array, if null?

I'm trying to subtract values from final array and start array in my aggregation pipeline. But there are certain exceptional cases that needs some additional logic before subtraction.
Expected Output:
I need to subtract nth value of startarray from nth value of final array
And then, get the total sum of subtracted values
Exceptional Cases:
If nth value of start array is NULL, use a start_default value(from the query)
If nth value of final array is NULL, use value from the final_default array
After some of the aggregation stages, my MongoDB document has this format:
Assuming that start_default value = 1
I have commented the way I expect to perform subtractions in each of the group
{
"data": [
{
"key": "TP-1",
"status_map": [
{
"status": "Closed",
"final": [
6,
3
], // Expected Output
"start": [ // sum:6 [(6-2)+(3-1(start_default))=4+2]
2
],
"final_default": [
4
]
},
{
"status": "Done",
"final": [
4
], // Expected Output
"start": [ // sum:2 [(4-3)+(2(final_default)-1)=1+1]
3,
1
],
"final_default": [
2
]
}
]
},
{
"key": "TP-2",
"status_map": [
{
"status": "Closed",
"final": [
1,
5
], // Expected Output
"start": [], //sum:4 [(1-1(start_default))+(5-1(start_default))=0+4]
"final_default": [
3
]
},
{
"status": "Done",
"final": [], // Expected Output
"start": [ //sum:3 [(5(final_default)-3)+(5(final_default)-4)=2+1]
3,
4
],
"final_default": [
5
]
}
]
}
]
}
Here is my expected output assuming that start_default value = 1
{
"data": [
{
"key": "TP-1",
"status_map": [
{
"status": "Closed",
"sum": 6 //[(6-2)+(3-1(start_default))=4+2]
{
"status": "Done",
"sum": 2 //[(4-3)+(2(final_default)-1)=1+1]
}
]
},
{
"key": "TP-2",
"status_map": [
{
"status": "Closed",
"sum": 4 //[(1-1(start_default))+(5-1(start_default))=0+4]
},
{
"status": "Done",
"sum": 3 //[(5(final_default)-3)+(5(final_default)-4)=2+1]
}
]
}
]
}
How to achieve this use case?
You can start with double $map to rewrite your nested array. You'll also need $reduce since you'll be converting an array into scalar value. Since you need to "pair" two arrays, there's a perfect operator called $zip which can be used even if arrays have different lengths. Pairing final and start for the first subdocument will return:
[ [ 6,2 ], [ 3, null ] ]
which is great because you can use $ifNull to provide a default value.
Your aggregation can look like below:
db.collection.aggregate([
{
$project: {
data: {
$map: {
input: "$data",
as: "d",
in: {
key: "$$d.key",
status_map: {
$map: {
input: "$$d.status_map",
as: "sm",
in: {
status: "$$sm.status",
sum: {
$reduce: {
input: {
$zip: {
inputs: [ "$$sm.final", "$$sm.start" ],
useLongestLength: true
}
},
initialValue: 0,
in: {
$add: [
"$$value",
{
$subtract: [
{ $ifNull: [ { $arrayElemAt: [ "$$this", 0 ] }, { $arrayElemAt: [ "$$sm.final_default" , 0] } ] },
{ $ifNull: [ { $arrayElemAt: [ "$$this", 1 ] }, 1 ] }
]
}
]
}
}
}
}
}
}
}
}
}
}
}
])
Mongo Playground

Averaging across list indicies during aggregation pipeline?

I currently have a MongoDB aggregation pipeline that ends with the following type of synthetic documents
[
{
'_id': '2019-09-10',
'grouped_foos':
[
{... 'foo': [1, 78, 100]},
{... 'foo': [8, 66, 98]},
{... 'foo': [99, 5, 33]},
{... 'foo': [120, 32, 2]}
]
},
{
'_id': '2019-09-09',
'grouped_foos':
[
{... 'foo': [10, 27]},
{... 'foo': [19, 66]}
]
},
{
'_id': '2019-09-08',
'grouped_foos':
[
{... 'foo': [1]}
]
}
]
I would like to continue this pipeline and average the indices of the foo lists together to form documents that look like
[
{
'_id': '2019-09-10',
'avg_foo': [57, 45.25, 58.25]
},
{
'_id': '2019-09-09',
'avg_foo': [14.5, 46.5]
},
{
'_id': '2019-09-08',
'avg_foo': [1]
}
]
Is this type of averaging possible during aggregation? Do I potentially need to $unwind the lists with indexing and assign new _id for uniqueness to make documents that look like
[
{
'_id': UUID,
'old_id': '2019-09-10',
'foo': 1,
'index': 0
},
{
'_id': UUID,
'old_id': '2019-09-10',
'foo': 78,
'index': 1
},
........
]
Basically you can try with $unwind but easier and faster approach would be to use $reduce to $map and $sum all the rows from grouped_foos. Then you'll be able to run another $map and use $divide to get the average.
db.collection.aggregate([
{
$project: {
size: { $size: "$grouped_foos" },
foo_sum: {
$reduce: {
input: "$grouped_foos",
initialValue: [],
in: {
$map: {
input: { $range: [ 0, { $size: "$$this.foo" }, 1 ] },
as: "index",
in: {
$add: [
{ $arrayElemAt: [ "$$this.foo", "$$index" ] },
{ $ifNull: [ { $arrayElemAt: [ "$$value", "$$index" ] }, 0 ] }
]
}
}
}
}
}
}
},
{
$project: {
_id: 1,
avg_foo: {
$map: {
input: "$foo_sum",
in: {
$divide: [ "$$this", "$size" ]
}
}
}
}
}
])
Mongo Playground