Update the existing array values in a Document in Mongodb - 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"
]
}
}
}
}
}
])

Related

MongoDB addFields based on condition (array of array)

Let's say I have a sample object as below
[
{
"status": "ACTIVE",
"person": [
{
"name": "Jim",
"age": "2",
"qualification": [
{
"type": "education",
"degree": {
"name": "bachelor",
"year": "2022"
}
},
{
"type": "certification",
"degree": {
"name": "aws",
"year": "2021"
}
}
]
}
]
}
]
Now, I need to add a field "score" only when the qualification.type == bachelor
This is the query I tried but could not get the proper result. Not sure what mistake I am doing. Any help is highly appreciated. Thank you in advance.
db.collection.aggregate([
{
$addFields: {
"person.qualification.score": {
$reduce: {
input: "$person.qualification",
initialValue: "",
in: {
$cond: [
{
"$eq": [
"$$this.type",
"bachelor"
]
},
"80",
"$$REMOVE"
]
}
}
}
}
}
])
$set - Set person array field.
1.1. $map - Iterate person array and return a new array.
1.1.1. $mergeObjects - Merge current iterate (person) document and the document with qualification.
1.1.1.1. $map - Iterate the qualification array and return a new array.
1.1.1.1.1. $cond - Match type of current iterate qualification document. If true, merge current iterate qualification document and the document with score field via $mergeObjects. Else remain the existing document.
db.collection.aggregate([
{
$set: {
person: {
$map: {
input: "$person",
as: "p",
in: {
$mergeObjects: [
"$$p",
{
qualification: {
$map: {
input: "$$p.qualification",
in: {
$cond: {
if: {
$eq: [
"$$this.type",
"education"
]
},
then: {
$mergeObjects: [
"$$this",
{
score: "80"
}
]
},
else: "$$this"
}
}
}
}
}
]
}
}
}
}
}
])
Sample Mongo Playground

Can't access sub array fields in $map

I have an object like so
{
"_id": "62334a3c86f03ce0985f11a1",
"stops": [
{
"location": {
"baseLocation": "622e29bd0a56c69f81e0e6e9",
"extraLocation": "622e29bd0a56c69f81e0e6eb"
},
"timesData": [
{
"wType": "date"
}
],
},
{
"location": {
"baseLocation": "622e70a59975be022276178c",
"extraLocation": "622e70a59975be022276178e"
},
"timesData": [
{
"wType": "date"
}
],
}
},
],
}
I need to find the index of objects that have a baseLocation equal to a value and which have a wType of date and I do so using a $map like the one below
{
$map: {
input: "$stops",
in: {
$and: [
{
$eq: ["$$this.location.baseLocation", mongoose.Types.ObjectId(from.id)]
},
{
$eq: ["$$this.timesData.0.wType", "date"]
}
]
}
}
},
I get no matches, but if I do it like this
{
$map: {
input: "$stops",
in: {
$eq: ["$$this.location.baseLocation", mongoose.Types.ObjectId(from.id)]
}
}
},
It works, so I guess the problem is that I can't access "$$this.timesData.0.wType"
How about using $arrayElemAt:
$map: {
input: "$stops",
in: {
$and: [
{
$eq: [
"$$this.location.baseLocation",
mongoose.Types.ObjectId(from.id)]
]
},
{
$eq: [
{
"$arrayElemAt": [
"$$this.timesData",
0
]
},
{
"wType": "date"
}
]
}
]
}
}
}
}
You can check it on the playground: https://mongoplayground.net/p/p5eRv0pXYXM

MongoDB get all documents where array values match

I have the following structure of document:
{
"input": {
"fields": [
{
"name": "last_name_hebrew",
"text": "test1",
},
],
},
"output": {
"fields": [
{
"name": "last_name_hebrew",
"text": "test1"
},
],
},
},
I want to get all documents, where fields has object that has name of value last_name_hebrew as with text value of the output.fields.
For example in the given structure it would return this documents because input.fields.name is last_name_hebrew and text is equal to the text in output.
Note I cannot guarantee that fields array in either input or output will have name: last_name_hebrew in the array.
How can I do so?
This is my try to first force the arrays to have document with name of last_name_hebrew:
db.collection.find({
"input.fields": {
$elemMatch: {
"name": "last_name_hebrew"
}
},
"output.fields": {
$elemMatch: {
"name": "last_name_hebrew"
}
},
})
But now I need to compare the text values.
Your first 2 condition with $elemMatch is correct
add expression match, first find the matching element that having last_name_hebrew name from input using $filter and get first element from that filtered result using $arrayElemAt, same process for output field and then match both object using $eq
db.collection.find({
"input.fields": { $elemMatch: { "name": "last_name_hebrew" } },
"output.fields": { $elemMatch: { "name": "last_name_hebrew" } },
$expr: {
$eq: [
{
$arrayElemAt: [
{
$filter: {
input: "$input.fields",
cond: { $eq: ["$$this.name", "last_name_hebrew"] }
}
},
0
]
},
{
$arrayElemAt: [
{
$filter: {
input: "$output.fields",
cond: { $eq: ["$$this.name", "last_name_hebrew"] }
}
},
0
]
}
]
}
});
Playground
Second option: if you want to go with more specific to match exact 2 fields name and text both just need to add $let operator to return fields from filter,
db.collection.find({
"input.fields": { $elemMatch: { "name": "last_name_hebrew" } },
"output.fields": { $elemMatch: { "name": "last_name_hebrew" } },
$expr: {
$eq: [
{
$let: {
vars: {
input: {
$arrayElemAt: [
{
$filter: {
input: "$input.fields",
cond: { $eq: ["$$this.name", "last_name_hebrew"] }
}
},
0
]
}
},
in: { name: "$$input.name", text: "$$input.text" }
}
},
{
$let: {
vars: {
output: {
$arrayElemAt: [
{
$filter: {
input: "$output.fields",
cond: { $eq: ["$$this.name", "last_name_hebrew"] }
}
},
0
]
}
},
in: { name: "$$output.name", text: "$$output.text" }
}
}
]
}
})
Playground
Third option: for more specific to check both fields in loop,
first filter the matching elements by name in input field using $filter
pass above filter result in another filter
filter to match name and text field in output field, if its not [] empty then return filter result
$ne to check return result is not [] empty
db.collection.find({
"input.fields": { $elemMatch: { "name": "last_name_hebrew" } },
"output.fields": { $elemMatch: { "name": "last_name_hebrew" } },
$expr: {
$ne: [
{
$filter: {
input: {
$filter: {
input: "$input.fields",
cond: { $eq: ["$$this.name", "last_name_hebrew"] }
}
},
as: "i",
cond: {
$ne: [
{
$filter: {
input: "$output.fields",
cond: {
$and: [
{ $eq: ["$$this.name", "$$i.name"] },
{ $eq: ["$$this.text", "$$i.text"] }
]
}
}
},
[]
]
}
}
},
[]
]
}
})
Playground
You will have to use an aggregation pipeline to achieve this, there are several ways to do so, here is one example:
db.collection.aggregate([
{
$match: {
$expr: {
$gt: [
{
$size: {
$filter: {
input: "$input.fields",
as: "inputField",
cond: {
$and: [
{
$eq: [
"$$inputField.name",
"last_name_hebrew"
]
},
{
"$setIsSubset": [
[
"$$inputField.text"
],
"$output.fields.text"
]
}
]
}
}
}
},
0
]
}
}
}
])
Mongo Playground
One thing to note is that with this query there are no restrictions on the output.fields.name (as it was not required), if you do require the names to match then you can drop the .text field in the $setIsSubset operator.

MongoDB remove objects from multiple nested arrays

I have a very complex json document in mongo like below:
{
"Category": [
{
"Name" : "",
"Description": "",
"SubCategory" : [
{
"Name": "",
"Description": "",
"Services": [
{"ServiceA": [
{
"Name": "",
"Description": "",
"CodeA": "1234"
}
]},
{"ServiceB" : [
{
"Name": "",
"Description": "",
"CodeBC": "ABCD",
"Key": ""
}
]},
{"ServiceC": [
{
"Name": "",
"Description": "",
"CodeBC": "ABCD",
"Section": [
{
"Name": "",
"Description": ""
}
]
}
]}
]
}
]
}
]
}
Now I want to retrieve the same document from Mongo but I want it to match only those objects inside ServiceA having CodeA = "1234" and those inside ServiceB and ServiceC having CodeBC = "ABCD".
I want it to remove all other objects inside ServiceA, ServiceB and ServiceC not matching the above condition and retrieve the document while maintaining the structure as it is.
Please Note : In the above example I just showed arrays containing single objects and fields that I want to retrieve but in real case it's very complex.
Try nested $map to iterate loop in your nested arrays, $mergeObjects to merge current object and new that we want to filter,
At the end in Services array filter documents on the base of condition in $filter, if filtered is null then that field will be removed using $ifNull
db.collection.aggregate([
{
$set: {
Category: {
$map: {
input: "$Category",
as: "c",
in: {
$mergeObjects: [
"$$c",
{
SubCategory: {
$map: {
input: "$$c.SubCategory",
as: "sc",
in: {
$mergeObjects: [
"$$sc",
{
Services: {
$map: {
input: "$$sc.Services",
as: "s",
in: {
$mergeObjects: [
"$$s",
{
ServiceA: {
$ifNull: [
{
$filter: {
input: "$$s.ServiceA",
cond: { $eq: ["$$this.CodeA", "1234"] }
}
},
"$$REMOVE"
]
},
ServiceB: {
$ifNull: [
{
$filter: {
input: "$$s.ServiceB",
cond: { $eq: ["$$this.CodeBC", "ABCD"] }
}
},
"$$REMOVE"
]
},
ServiceC: {
$ifNull: [
{
$filter: {
input: "$$s.ServiceC",
cond: { $eq: ["$$this.CodeBC", "ABCD"] }
}
},
"$$REMOVE"
]
}
}
]
}
}
}
}
]
}
}
}
}
]
}
}
}
}
}
])
Playground

MongoDB 4.2 combine aggregation pipeline update with array filters

I saw that MongoDB 4.2 introduces aggregation pipeline updates, which allows you to set document fields based on other fields in the document.
Considering the following document
{
ean: "12345",
orderedQty: 2,
fulfilledQty: 1,
"status": "pending"
}
I could use the following command to increment the fulfilledQty by 1 and if the orderedQty matches the fulfilledQty set the status accordingly:
db.collection.findOneAndUpdate({}, [
{
"$set": {
"orderedQty": {
"$add": [ "$fulfilledQty", 1 ]
}
},
"$set": {
"status": {
"$cond": {
"if": { "$eq": ["$orderedQty", "$fulfilledQty"] },
"then": "fulfilled",
"else": "pending"
}
}
}
}
])
My question: How would i perform this on an array. Say I have a document like this:
_id: "test",
items: [
{ean: "12345", orderedQty: 2, fulfilledQty: 1, "status": "pending"},
{ean: "67891", orderedQty: 1, fulfilledQty: 1, "status": "fulfilled"}
]
Given I have the params ean = 12345 and an increase value by 1. How could I target the specific array item with EAN 12345, increase the fulfilledQty by 1 and set the status? I want to only chance the status field and fulfilledQty field and leave the rest of the items array as is. So expected outcome would be:
_id: "test",
items: [
{ean: "12345", orderedQty: 2, fulfilledQty: 2, "status": "fulfilled"},
{ean: "67891", orderedQty: 1, fulfilledQty: 1, "status": "fulfilled"}
]
I found the following workflow (works only for mongodb 4.2+), but it's amazingly verboseā€¦
Given that there are two variables, an item identifier (called ean) and a quantity that was shipped (called fulfilledQty)
collection.update({}, [
{
$set: {
items: {
$map: {
input: "$items",
as: "item",
in: {
$mergeObjects: [
"$$item",
{
fulfilledQty: {
$switch: {
branches: [
{
case: {
$eq: ["$$item.ean", ean]
},
then: {
$toInt: {
$add: ["$$item.fulfilledQty", fulfilledQty]
}
}
}
],
default: "$$item.fulfilledQty"
}
}
}
]
}
}
}
}
},
{
$set: {
items: {
$map: {
input: "$items",
as: "item",
in: {
$mergeObjects: [
"$$item",
{
status: {
$cond: {
if: {
$eq: ["$$item.orderedQty", "$$item.fulfilledQty"]
},
then: "fulfilled",
else: "$$item.status"
}
}
}
]
}
}
}
}
}
]);
I used a switch statement since in my use case I have multiple different EANs. Downside is that I had to use a $map operation, so it always iterates over the whole items array.