Join fields when not all have values - mongodb

I want to modify a field through a projection stage in the aggregation pipeline, this field is combination of other fields values separated by (-)
if the field is null or empty of missing it will not be added to the cocatenated string
{$project:{
//trial-1:
finalField:{
$concat["$field1",'-','$field2','-','$field3',...]
//problem1: $concat will return null if any of it's arguments is null or missing
//problem2: if all the fields are exist with non-null values, the delimiter will exists even if the field dosen't
}
//trial-2:
finalField:{
$concat:[
{$cond:[{field1:null},'',{$concat:['$field1','-']},..]
//the problem: {field1:null} fails if the field dosen't exixt (i.e the expression gives true)
//trial-3
finalField:{
$concat:[
{$cond:[{$or:[{field1:null},{field:{$exists:true}},'',
{$concat:['$field1','-']}
]}]}
]
}
]
}
//trial-4 -> using $reduce instead of $concate (same issues)
}

You basically want $ifNull. It's "sort of" like $exists but for aggregation statements, where it returns a default value when the field expression returns null, meaning "not there":
{ "$project": {
"finalField": {
"$concat": [
{ "$ifNull": [ "$field1", "" ] },
"-",
{ "$ifNull": [ "$field2", "" ] },
"-",
{ "$ifNull": [ "$field3", "" ] }
]
}
}}
For example with data like:
{ "field1": "a", "field2": "b", "field3": "c" },
{ "field1": "a", "field2": "b" },
{ "field1": "a", "field3": "c" }
You get, without any error producing of course:
{ "finalField" : "a-b-c" }
{ "finalField" : "a-b-" }
{ "finalField" : "a--c" }
If you want something fancier, then you would instead dynamically work with the names, as in:
{ "$project": {
"finalField": {
"$reduce": {
"input": {
"$filter": {
"input": { "$objectToArray": "$$ROOT" },
"cond": { "$ne": [ "$$this.k", "_id" ] }
}
},
"initialValue": "",
"in": {
"$cond": {
"if": { "$eq": [ "$$value", "" ] },
"then": { "$concat": [ "$$value", "$$this.v" ] },
"else": { "$concat": [ "$$value", "-", "$$this.v" ] }
}
}
}
}
}}
Which can be aware of what fields were actually present and only attempt to join those:
{ "finalField" : "a-b-c" }
{ "finalField" : "a-b" }
{ "finalField" : "a-c" }
You can even manually specify the list of fields if you don't want the $objectToArray over the document or sub-document:
{ "$project": {
"finalField": {
"$reduce": {
"input": {
"$filter": {
"input": ["$field1", "$field2", "$field3"],
"cond": { "$ne": [ "$$this", null ] }
}
},
"initialValue": "",
"in": {
"$cond": {
"if": { "$eq": [ "$$value", "" ] },
"then": { "$concat": [ "$$value", "$$this" ] },
"else": { "$concat": [ "$$value", "-", "$$this" ] }
}
}
}
}
}}

Related

$mergeObjects requires object inputs, but input is of type array

I am trying to add one property if current user has permission or not based on email exists in array of objects.
My input data looks like below.
[
{
nId: 0,
children0: [
{
nId: 3,
access: [
{
permission: "view",
email: "user1#email.com"
}
]
},
{
nId: 4,
access: [
{
permission: "view",
email: "user2#email.com"
}
]
}
]
}
]
https://mongoplayground.net/p/xZmRGFharAb
[
{
"$addFields": {
"children0": {
"$map": {
"input": "$children0.access",
"as": "accessInfo",
"in": {
"$cond": [
{
"$eq": [
"$$accessInfo.email",
"user1#email.com"
]
},
{
"$mergeObjects": [
"$$accessInfo",
{
"hasAccess": true
}
]
},
{
"$mergeObjects": [
"$$accessInfo",
{
"hasAccess": false
}
]
},
]
}
}
}
}
}
]
I also tried this answer as following, but that is also not merging the object.
https://mongoplayground.net/p/VNXcDnXl_sZ
Try this:
db.collection.aggregate([
{
"$addFields": {
"children0": {
"$map": {
"input": "$children0",
"as": "accessInfo",
"in": {
nId: "$$accessInfo.nId",
access: "$$accessInfo.access",
hasAccess: {
"$cond": {
"if": {
"$ne": [
{
"$size": {
"$filter": {
"input": "$$accessInfo.access",
"as": "item",
"cond": {
"$eq": [
"$$item.email",
"user1#email.com"
]
}
}
}
},
0
]
},
"then": true,
"else": false
}
}
}
}
}
}
}
])
Here, we use one $map to loop over children0 and then we filter the access array to contain only elements with matching emails. If the filtered array is non-empty, we set hasAccess to true.
Playground link.

mongodb aggregation get max number of negative sequence in array

I need to get the max count of negative sequence from array via aggregation , example documents:
{
"id": 1,
x: [ 1,1,-1,-1,1,1,1,-1,-1,-1,-1]
},
{
"id": 2,
x: [ 1,-1,-1,1,1,1,-1 ]
}
expected result:
{"id": 1,x:4},
{"id": 2,x:2}
Please, advice?
You can use $reduce to iterate the array and $cond to apply your logic (consecutive negatives)
The carrier is in format
{
previous: // previous value to compare for continuity
acc: // number of consecutive negatives in the current sequence
max: // length of the longest sequence
}
$let is to memoise current accumulator to reuse in the max calculation. It's optional yet convenient:
db.collection.aggregate([
{
"$set": {
"x": {
"$reduce": {
"input": "$x",
"initialValue": {
previous: 0,
acc: 0,
max: 0
},
"in": {
$let: {
vars: {
result: {
"$cond": {
"if": {
"$and": [
{
"$lt": [
"$$this",
0
]
},
{
"$lt": [
"$$value.previous",
0
]
}
]
},
"then": {
"$add": [
"$$value.acc",
1
]
},
"else": {
"$cond": {
"if": {
"$lt": [
"$$this",
0
]
},
"then": 1,
"else": 0
}
}
}
}
},
in: {
previous: "$$this",
acc: "$$result",
max: {
"$cond": {
"if": {
$gt: [
"$$value.max",
"$$result"
]
},
"then": "$$value.max",
"else": "$$result"
}
}
}
}
}
}
}
}
},
{
"$set": {
x: "$x.max"
}
}
])
Try it on mongoplayground.net.
Here's another way to do it. The general idea is to $reduce the sequence to a string and then $split to make an array filled with strings of each run. Then map the array of strings to an array of string lengths and then take the max.
db.collection.aggregate({
"$project": {
"_id": 0,
"id": 1,
"x": {
"$max": {
"$map": {
"input": {
$split: [
{
"$reduce": {
"input": "$x",
"initialValue": "",
"in": {
$concat: [
"$$value",
{
"$cond": [
{
"$gt": [
"$$this",
0
]
},
"p",
"n"
]
}
]
}
}
},
"p"
]
},
"in": {
"$strLenBytes": "$$this"
}
}
}
}
}
})
Try it on mongoplayground.net.

Remove null MongoDB aggregation

I want to just put blank if the field is null.
Data:
"history": [{
"status": "Not Processed",
"createdAt": {
"$date": "2021-01-26T00:16:26.018Z"
},
"updatedAt": {
"$date": "2021-01-26T00:16:26.018Z"
}
}, {
"status": "Processed",
"updatedAt": {
"$date": "2021-01-26T00:17:25.725Z"
},
"createdAt": {
"$date": "2021-01-26T00:17:25.725Z"
}
}],
Input:
{
"$reduce": {
"input": "$history",
"initialValue": null,
"in": {
"$cond": {
"if": {
$eq: [
"$$this.status",
"Processed"
]
},
"then": "$$this.createdAt",
"else": "$$value"
}
}
}
}
Some of my data doesn't have a processed status in history array. I do not want to include null in my data. FYR: I'm doing this in mongodb charts.
Play
You can use boolean expressions in the condition of if statement.
db.collection.aggregate([
{
$project: {
"res": {
"$reduce": {
"input": "$history",
"initialValue": null,
"in": {
"$cond": {
"if": {
$and: [
{
$eq: [
"$$this.status",
"Processed"
]
},
{
$ne: [
"$$this.status",
null
]
}
]
},
"then": "$$this.createdAt",
"else": "$$value"
}
}
}
}
}
}
])

MongoDB how to filter in nested array

I have below data. I want to find value=v2 (remove others value which not equals to v2) in the inner array which belongs to name=name2. How to write aggregation for this? The hard part for me is filtering the nestedArray which only belongs to name=name2.
{
"_id": 1,
"array": [
{
"name": "name1",
"nestedArray": [
{
"value": "v1"
},
{
"value": "v2"
}
]
},
{
"name": "name2",
"nestedArray": [
{
"value": "v1"
},
{
"value": "v2"
}
]
}
]
}
And the desired output is below. Please note the value=v1 remains under name=name1 while value=v1 under name=name2 is removed.
{
"_id": 1,
"array": [
{
"name": "name1",
"nestedArray": [
{
"value": "v1"
},
{
"value": "v2"
}
]
},
{
"name": "name2",
"nestedArray": [
{
"value": "v2"
}
]
}
]
}
You can try,
$set to update array field, $map to iterate loop of array field, check condition if name is name2 then $filter to get matching value v2 documents from nestedArray field and $mergeObject merge objects with available objects
let name = "name2", value = "v2";
db.collection.aggregate([
{
$set: {
array: {
$map: {
input: "$array",
in: {
$mergeObjects: [
"$$this",
{
$cond: [
{ $eq: ["$$this.name", name] }, //name add here
{
nestedArray: {
$filter: {
input: "$$this.nestedArray",
cond: { $eq: ["$$this.value", value] } //value add here
}
}
},
{}
]
}
]
}
}
}
}
}
])
Playground
You can use the following aggregation query:
db.collection.aggregate([
{
$project: {
"array": {
"$concatArrays": [
{
"$filter": {
"input": "$array",
"as": "array",
"cond": {
"$ne": [
"$$array.name",
"name2"
]
}
}
},
{
"$filter": {
"input": {
"$map": {
"input": "$array",
"as": "array",
"in": {
"name": "$$array.name",
"nestedArray": {
"$filter": {
"input": "$$array.nestedArray",
"as": "nestedArray",
"cond": {
"$eq": [
"$$nestedArray.value",
"v2"
]
}
}
}
}
}
},
"as": "array",
"cond": {
"$eq": [
"$$array.name",
"name2"
]
}
}
}
]
}
}
}
])
MongoDB Playground

Get property with highest value from key value pair

In MongoDB, I have documents with a structure like this:
{
_id: "123456...", // an ObjectId
name: "foobar",
classification: {
class_1: 0.45,
class_2: 0.11,
class_3: 0.44
}
}
Using the aggregation pipeline, is it possible to give me an object that contains the highest classification? So, given the above, I would like something like this as result:
{
_id: "123456...", // an ObjectId
name: "foobar",
classification: "class_1"
}
I thought I could use $unwind but the classification property is not an array.
For what it's worth: I know there will always be three properties in classification, so it's ok to hard-code the keys in the query.
You should probably note here that every technique applied is essentially based on "coercion" of the "key/value" pairs into an "array" format for comparison and extraction. So the real lesson to learn is is that your document "should" in fact store this as an "array" instead. But onto the techniques.
If you have MongoDB 3.4 then you can use $objectToArray to turn the "keys" into an array so you can get the value:
Dynamic
db.collection.aggregate([
{ "$addFields": {
"classification": {
"$arrayElemAt": [
{ "$map": {
"input": {
"$filter": {
"input": { "$objectToArray": "$classification" },
"as": "c",
"cond": {
"$eq": [
"$$c.v",
{ "$max": {
"$map": {
"input": { "$objectToArray": "$classification" },
"as": "c",
"in": "$$c.v"
}
}}
]
}
}
},
"as": "c",
"in": "$$c.k",
}},
0
]
}
}}
])
Otherwise just to the transformation as you iterate the cursor if you do not really need it for further aggregation. As a basic JavaScript example:
db.collection.find().map(d => Object.assign(
d,
{ classification: Object.keys(d.classification)
.filter(k => d.classification[k] === Math.max.apply(null,
Object.keys(d.classification).map(k => d.classification[k])
))[0]
}
));
And that's also the same basic logic that you apply using mapReduce if you were actually aggregating something.
Both produce:
/* 1 */
{
"_id" : "123456...",
"name" : "foobar",
"classification" : "class_1"
}
HardCoding
On the "hardcoding" case which you say is okay. Then you can construct like this with $switch by supplying $max with each of the values:
db.collection.aggregate([
{ "$addFields": {
"classification": {
"$let": {
"vars": {
"max": {
"$max": [
"$classification.class_1",
"$classification.class_2",
"$classification.class_3"
]
}
},
"in": {
"$switch": {
"branches": [
{ "case": { "$eq": [ "$classification.class_1", "$$max" ] }, "then": "class_1" },
{ "case": { "$eq": [ "$classification.class_2", "$$max" ] }, "then": "class_2" },
{ "case": { "$eq": [ "$classification.class_3", "$$max" ] }, "then": "class_3" },
]
}
}
}
}
}}
])
Which gives rise to then actually being able to write that out longer using $cond, and then the only real constraint is the change in $max for MongoDB 3.2, which allowed an array of arguments as opposed to it's previous role as an "accumulator only":
db.collection.aggregate([
{ "$addFields": {
"classification": {
"$let": {
"vars": {
"max": {
"$max": [
"$classification.class_1",
"$classification.class_2",
"$classification.class_3"
]
}
},
"in": {
"$cond": {
"if": { "$eq": [ "$classification.class_1", "$$max" ] },
"then": "class_1",
"else": {
"$cond": {
"if": { "$eq": [ "$classification.class_2", "$$max" ] },
"then": "class_2",
"else": "class_3"
}
}
}
}
}
}
}}
])
If you were "really" constrained then you could "force" the "max" through a separate pipeline stage using $map and $unwind on the array then $group again. This would make the operations compatible with MongoDB 2.6:
db.collection.aggregate([
{ "$project": {
"name": 1,
"classification": 1,
"max": {
"$map": {
"input": [1,2,3],
"as": "e",
"in": {
"$cond": {
"if": { "$eq": [ "$$e", 1 ] },
"then": "$classification.class_1",
"else": {
"$cond": {
"if": { "$eq": [ "$$e", 2 ] },
"then": "$classification.class_2",
"else": "$classification.class_3"
}
}
}
}
}
}
}},
{ "$unwind": "$max" },
{ "$group": {
"_id": "$_id",
"name": { "$first": "$name" },
"classification": { "$first": "$classification" },
"max": { "$max": "$max" }
}},
{ "$project": {
"name": 1,
"classification": {
"$cond": {
"if": { "$eq": [ "$classification.class_1", "$max" ] },
"then": "class_1",
"else": {
"$cond": {
"if": { "$eq": [ "$classification.class_2", "$max" ] },
"then": "class_2",
"else": "class_3"
}
}
}
}
}}
])
And going really ancient, then we can instead $unwind from $const, which was (and still is) a "hidden" and undocumented operator equal in function to $literal (which is technically aliased to it) in modern versions, but also with the alternate syntax to $cond as an "array" ternary operation this then becomes compatible with all versions since the aggregation framework existed:
db.collection.aggregate([
{ "$project": {
"name": 1,
"classification": 1,
"temp": { "$const": [1,2,3] }
}},
{ "$unwind": "$temp" },
{ "$group": {
"_id": "$_id",
"name": { "$first": "$name" },
"classification": { "$first": "$classification" },
"max": {
"$max": {
"$cond": [
{ "$eq": [ "$temp", 1 ] },
"$classification.class_1",
{ "$cond": [
{ "$eq": [ "$temp", 2 ] },
"$classification.class_2",
"$classification.class_3"
]}
]
}
}
}},
{ "$project": {
"name": 1,
"classification": {
"$cond": [
{ "$eq": [ "$max", "$classification.class_1" ] },
"class_1",
{ "$cond": [
{ "$eq": [ "$max", "$classification.class_2" ] },
"class_2",
"class_3"
]}
]
}
}}
])
But it is of course possible, even if extremely messy.
You can use $indexOfArray operator to find the $max value in classification followed by projecting the key. $objectToArray to convert classification embedded doc into array of key value pairs in 3.4.4 version.
db.collection.aggregate([
{
"$addFields": {
"classification": {
"$let": {
"vars": {
"classificationkv": {
"$objectToArray": "$classification"
}
},
"in": {
"$let": {
"vars": {
"classificationmax": {
"$arrayElemAt": [
"$$classificationkv",
{
"$indexOfArray": [
"$$classificationkv.v",
{
"$max": "$$classificationkv.v"
}
]
}
]
}
},
"in": "$$classificationmax.k"
}
}
}
}
}
}
])
In the end, I went with a more simple solution, but not as generic as the other ones posted here. I used this a switch case statement:
{'$project': {'_id': 1, 'name': 1,
'classification': {'$switch': {
'branches': [
{'case': {'$and': [{'$gt': ['$classification.class_1', '$classification.class_2']},
{'$gt': ['$classification.class_1', '$classification.class_3']}]},
'then': "class1"},
{'case': {'$and': [{'$gt': ['$classification.class_2', '$classification.class_1']},
{'$gt': ['$classification.class_2', '$classification.class_3']}]},
'then': "class_2"},
{'case': {'$and': [{'$gt': ['$classification.class_3', '$classification.class_1']},
{'$gt': ['$classification.class_3', '$classification.class_2']}]},
'then': "class_3"}],
'default': ''}}
}}
This works for me, but the other answers might be a better option, YMMV.