Accessing $map variable in $cond - mongodb

I have some data in a MongoDB collection that looks something like this:
db.test.insertOne(
{ "interactions": [
{
data: "keep",
prompt: "prompt 1"
},
{
other: "keep",
prompt: "prompt 2"
},
{
field: "no prompt"
}
]}
)
I want to iterate over all the interactions, and set prompts to an array containing the current value of prompt. Something like this:
{
"interactions": [
{
"data": "keep",
"prompt": "prompt 1",
"prompts": [ "prompt 1" ]
},
{
"other": "keep",
"prompt": "prompt 2",
"prompts": [ "prompt 2" ]
},
{
"field": "no prompt"
}
]
}
I've been trying to do a updateMany() with an aggregation pipeline, but am getting an error that I don't understand. This is my update:
db.test.updateMany(
{},
[{
$set: {
interactions: {
$map: {
input: "$interactions",
as: "interaction",
in: {
$mergeObjects: [
"$$interaction",
{
$cond: {
if: { "$$interaction.prompt": { $exists: true } },
then: {
prompts: [ "$$interaction.prompt" ]
},
else: {}
}
}
]
}
}
}
}
}]
)
Running this I get an error:
Unrecognized expression '$$interaction.prompt'.
Running the update without the $cond works:
db.test.updateMany(
{},
[{
$set: {
interactions: {
$map: {
input: "$interactions",
as: "interaction",
in: {
$mergeObjects: [
"$$interaction",
{
prompts: [ "$$interaction.prompt" ]
}
]
}
}
}
}
}]
)
but after that operation, the array element that didn't have a prompt has an array with a null value:
{
"interactions": [
{
"data": "keep",
"prompt": "prompt 1",
"prompts": ["prompt 1"]
},
{
"other": "keep",
"prompt": "prompt 2",
"prompts": ["prompt 2"]
},
{
"field": "no prompt",
"prompts": [null]
}
]
}
I don't want prompts set on an element that didn't have prompt.
So why can't I access $$interactions.prompt within the $cond?
Alternatively, if there's a better way to accomplish what I want to do, please let me know.

In the aggregation pipeline, you can't use the $exists operator.
And you must start with the $ operator but not with variable $$.
Instead, you check whether $$interaction.prompt is neither undefined nor null.
db.collection.updateMany({},
[
{
$set: {
interactions: {
$map: {
input: "$interactions",
as: "interaction",
in: {
$mergeObjects: [
"$$interaction",
{
$cond: {
if: {
$not: {
$in: [
"$$interaction.prompt",
[
undefined,
null
]
]
}
},
then: {
prompts: [
"$$interaction.prompt"
]
},
else: {}
}
}
]
}
}
}
}
}
])
Demo # Mongo Playground

For some reason, using $not and $in didn't work in my database, although it did in the Playground. After finding an alternative here: https://stackoverflow.com/a/25515046/1303158, my final solution is:
db.collection.update({},
[
{
$set: {
interactions: {
$map: {
input: "$interactions",
as: "interaction",
in: {
$mergeObjects: [
"$$interaction",
{
$cond: {
if: {
$gt: [
"$$interaction.prompt",
null
]
},
then: {
prompts: [
"$$interaction.prompt"
]
},
else: {}
}
}
]
}
}
}
}
}
],
{
multi: true
})
Demo # Mongo Playground

Related

MongoDB Aggregation - Show all sub documents with some information from parent document

My invoices collection looks something like this:
[
{
inv_name: "Client 1",
inv_date: 2022-12-20T05:09:09.803Z,
inv_ready: false,
inv_payments: [
{
id: "123",
pay_amount: 32.45
},
{
id: "456",
pay_amount: 55.60
}
]
},
{
inv_name: "Client 2",
inv_date: 2022-12-19T05:09:09.803Z,
inv_ready: true,
inv_payments: [
{
id: "459",
pay_amount: 67.45
},
{
id: "556",
pay_amount: 30.60
}
]
}
]
I know how to create an array of just the sub docs using $project and $unwind. The goal is to just add some data from the parent document to each object like this.
[
{
"client": "Client 1",
"pay_amount": 32.45,
"pay_date": "123"
},
{
"client": "Client 1",
"pay_amount": 55.6,
"pay_date": "456"
},
{
"client": "Client 2",
"pay_amount": 67.45,
"pay_date": "459"
},
{
"client": "Client 2",
"pay_amount": 30.6,
"pay_date": "556"
}
]
Here is what I tried:
db.invoices.aggregate([
{
"$project": {
_id: 0,
"inv_payments": {
$reduce: {
input: "$inv_payments",
initialValue: [],
in: {
$concatArrays: [
[
{
client: "$this.name",
pay_id: "$$this.id",
pay_amount: "$$this.pay_amount"
}
],
"$inv_payments"
]
}
}
}
}
},
{
$unwind: "$inv_payments"
},
{
$replaceRoot: {
newRoot: "$inv_payments"
}
}
])
I think I am close. I am not sure where the bug is. Any ideas would be greatly appreciated!
Besides the typo error mentioned in the comment, the way you used the $reduce operator is incorrect. You didn't work with the accumulator variable ($$value).
Rather than using $reduce, use the $map operator which is simpler in your scenario:
Solution 1: $map
{
"$project": {
_id: 0,
"inv_payments": {
$map: {
input: "$inv_payments",
in: {
client: "$inv_name",
pay_date: "$$this.id",
pay_amount: "$$this.pay_amount"
}
}
}
}
}
Demo ($map) # Mongo Playground
Solution 2: $reduce
For $reduce operator, your query should be:
{
"$project": {
_id: 0,
"inv_payments": {
$reduce: {
input: "$inv_payments",
initialValue: [],
in: {
$concatArrays: [
[
{
client: "$inv_name",
pay_date: "$$this.id",
pay_amount: "$$this.pay_amount"
}
],
"$$value"
]
}
}
}
}
}
Demo ($reduce) # Mongo Playground

Update the existing array values in a Document in Mongodb

I want to update the subject field that starts with http://test and consists of /base in the value with the value without base
for example "http://test/timespan/base/1", as it starts with https://test and has base in it. Then i want to update it to http://test/timespan/1
so at last the subject array becomes :
"subject": [
"http://test/concept/114",
"http://test/timespan/1",
"http://wikidata/1233"
]
Document sample
{
"_id": {
"$oid": "1233"
},
"type": "Entity",
"subject": [
"http://test/concept/114",
"http://test/timespan/base/1",
"http://wikidata/1233"
]
}
You can use a conditional update, like so:
db.collection.updateMany({},
[
{
$set: {
subject: {
$map: {
input: {
$ifNull: [
"$subject",
[]
]
},
in: {
$cond: [
{
$regexMatch: {
input: "$$this",
regex: "http:\\/\\/test.*base"
}
},
{
$replaceAll: {
input: "$$this",
find: "base/",
replacement: ""
}
},
"$$this"
]
}
}
}
}
}
])

How to remove field conditionally mongoodb

I have a collection and its documents look like:
{
_id: ObjectId('111111111122222222223333'),
my_array: [
{
id: ObjectId('777777777788888888889999')
name: 'foo'
},
{
id: ObjectId('77777777778888888888555')
name: 'foo2'
}
//...
]
//more attributes
}
However, some documents have my_array: [{}] (with one element which is an empty array).
How can I add conditionally a projection or remove it?
I have to add it to a mongo pipeline at the end of the query, and I want to get my_array only when it has at least one element which is not an empty object. If there's an empty object remove it.
I tried with $cond and $eq in a projection stage but it is not supported. Any suggestion to solve this?
Suppose you have documents like this with my_array field:
{ "my_array" : [ ] }
{ "my_array" : [ { "a" : 1 } ] } // #(1)
{ "my_array" : null }
{ "some_fld" : "some value" }
{ "my_array" : [ { } ] }
{ "my_array" : [ { "a" : 2 }, { "a" : 3 } ] } // #(2)
And, the following aggregation will filter and the result will have the two documents (1) and (2):
db.collection.aggregate([
{
$match: {
$expr: {
$and: [
{ $eq: [ { $type: "$my_array" }, "array" ] },
{ $gt: [ { $size: "$my_array" }, 0 ] },
{ $ne: [ [{}], "$my_array" ] }
]
}
}
}
])
This also works with a find method:
db.collection.find({
$expr: {
$and: [
{ $eq: [ { $type: "$my_array" }, "array" ] },
{ $gt: [ { $size: "$my_array" }, 0 ] },
{ $ne: [ [{}], "$my_array" ] }
]
}
})
To remove the my_array field, from a document when its empty, then you try this aggregation:
db.collection.aggregate([
{
$addFields: {
my_array: {
$cond: [
{$and: [
{ $eq: [ { $type: "$my_array" }, "array" ] },
{ $gt: [ { $size: "$my_array" }, 0 ] },
{ $ne: [ [{}], "$my_array" ] }
]},
"$my_array",
"$$REMOVE"
]
}
}
}
])
The result:
{ }
{ "my_array" : [ { "a" : 1 } ] }
{ }
{ "a" : 1 }
{ }
{ "my_array" : [ { "a" : 2 }, { "a" : 3 } ] }
You can't do that in a query, however in an aggregations you can add $filter to you pipeline, like so:
db.collection.aggregate([
{
$project: {
my_array: {
$filter: {
input: "$my_array",
as: "elem",
cond: {
$ne: [
{},
"$$elem"
]
}
}
}
}
}
])
Mongo Playground
However unless this is "correct" behavior I suggest you clean up your database, it's much simpler to maintain "proper" structure than to update all your queries everywhere.
You can use this update to remove these objects:
db.collection.update({
"myarray": {}
},
[
{
"$set": {
"my_array": {
$filter: {
input: "$my_array",
as: "elem",
cond: {
$ne: [
{},
"$$elem"
]
}
}
}
}
},
],
{
"multi": false,
"upsert": false
})
Mongo Playground

How to use $addFields in mongo to add elements to just existing documents?

I am trying to add a new field to an existing document by using a combination of both $ifnull and $cond but an empty document is always added at the end.
Configuration:
[
{
line: "car",
number: "1",
category: {
FERRARI: {
color: "blue"
},
LAMBORGHINI: {
color: "red"
}
}
},
{
line: "car",
number: "2",
category: {
FERRARI: {
color: "blue"
}
}
}
]
Query approach:
db.collection.aggregate([
{
$match: {
$and: [
{ line: "car" },
{ number: { $in: ["1", "2"] } }
]
}
},
{
"$addFields": {
"category.LAMBORGHINI.number": {
$cond: [
{ "$ifNull": ["$category.LAMBORGHINI", false] },
"$number",
"$$REMOVE"
]
}
}
},
{
$group: {
_id: null,
CATEGORIES: {
$addToSet: "$category.LAMBORGHINI"
}
}
}
])
Here is the link to the mongo play ground:
https://mongoplayground.net/p/RUnu5BNdnrR
I tried the mentioned query but I still get that ugly empty set added at the end.
$$REMOVE will remove last field/key, from your field category.LAMBORGHINI.number the last field is number that is why it is removing number from the end, you can try another approach,
specify just category.LAMBORGHINI, if condition match then it will return object of current category.LAMBORGHINI and number object after merging using $mergeObjects
{
"$addFields": {
"category.LAMBORGHINI": {
$cond: [
{ "$ifNull": ["$category.LAMBORGHINI", false] },
{
$mergeObjects: [
"$category.LAMBORGHINI",
{ number: "$number" }
]
},
"$$REMOVE"
]
}
}
}
Playground

how to project field in array with mongodb

my collection in mongo db like this:
{
name:"mehdi",
grades:
[
{
a:1,
b:[2,3,4],
c:3,
d:4,
e:5
},
{
a:11,
b:[22,33,44],
c:33,
d:44,
e:55
}
]
}
I want to get a result with project op to give me a specific field in an array like this:
{
name:"mehdi",
grades:
[
{
a:1,
b:2
},
{
a:11,
b:22
}
]
}
how can I do this?
You can use $map to select a,b fields using $type to determine whether it's an array or number:
db.collection.aggregate([
{
$project: {
grades: {
$map: {
input: "$grades",
in: {
a: { $cond: [ { $eq: [ { $type: "$$this.a" }, "array" ] }, { $arrayElemAt: [ "$$this.a", 0 ] }, "$$this.a" ] },
b: { $cond: [ { $eq: [ { $type: "$$this.b" }, "array" ] }, { $arrayElemAt: [ "$$this.b", 0 ] }, "$$this.b" ] },
}
}
}
}
}
])
Mongo Playground