MongoDB Replace specific array values - arrays

In MongoDB, I have a movie collection that has an array of languages , e.g.
languages: [0:USA, 1: German, 2: French, ...etc]
The array values are not in any particular order.
How can I now update an array value based on some specific value? Let's say I want to update all "French" and replace it with "Francais" for the entire collection. How can I do that?

Use the positional $ operator which identifies the element in the languages array to update without explicitly specifying its position in the array i.e. instead of knowing the position in advance and updating the element as:
db.movies.updateMany(
{ "languages": "French" },
{ "$set": { "languages.2": "Francais" } }
)
you can just use the $ operator as:
db.movies.updateMany(
{ "languages": "French" },
{ "$set": { "languages.$": "Francais" } }
)
Alternatively using the aggregation pipeline for update operations:
db.movies.updateMany(
{ "languages": "French" },
[
{ "$set": {
"languages": {
"$map": {
"input": "$languages",
"in": {
"$cond": [
{ "$eq": ["$$this", "French"] },
"Francais",
"$$this"
]
}
}
}
} }
]
)

In case you have duplicate entries in your array like this:
"languages" : [
"USA",
"German",
"French",
"French"
]
You can use the following syntax to replace all occurrences starting with MongoDB v3.5.12 (documentation)
db.movies.updateMany(
{ "languages": "French" },
{ "$set": { "languages.$[filter]": "Francais" } },
{ "arrayFilters": [ { "filter": "French" } ] }
)
Strictly speaking, the { "languages": "French" } filter is not needed. It will, however, speed up things by leveraging an index on "languages" in case there is one. If that's not needed/wanted, passing a simple {} value as the first parameter works as well.

Related

Finding documents in mongodb collection by order of elements index of array field

Array field in collection:
"fruits": [ "fruits": [ "fruits": [
{"fruit1": "banana"}, {"fruit2": "apple"}, {"fruit3": "pear"},
{"fruit2": "apple"}, {"fruit4": "orange"}, {"fruit2": "apple"},
{"fruit3": "pear"}, {"fruit1": "banana"}, {"fruit4": "orange"},
{"fruit4": "orange"} {"fruit3": "pear"} {"fruit1": "banana"}
]
I need to find those documents in collections, where "banana" signed before "apple". Does mongodb allows to compare elements in array just like :
if (fruits.indexOf('banana') < fruits.indexOf('apple')) return true;
Or maybe there is any other method to get result i need?
MongoDB's array query operations do not support any positional search as you want.
You can, however, write a $where query to do what you want:
db.yourCollection.find({
$where: function() {
return (this.fruits.indexOf('banana') < this.fruits.indexOf('apple'))
}
})
Be advised though, you won't be able to use indexes here and the performance will be a problem.
Another approach you can take is to rethink the database design, if you can specify what it is you're trying to build, someone can give you specific advise.
One more approach: pre-calculate the boolean value before persisting to DB as a field and query on true / false.
Consider refactoring your schema if possible. The dynamic field names(i.e. fruit1, fruit2...) make it unnecessarily complicated to construct a query. Also, if you require frequent queries by array index, you should probably store your array entries in individual documents with some sort keys to facilitate sorting with index.
Nevertheless, it is achievable through $unwind and $group the documents again. With includeArrayIndex clause, you can get the index inside array.
db.collection.aggregate([
{
"$unwind": {
path: "$fruits",
includeArrayIndex: "idx"
}
},
{
"$addFields": {
fruits: {
"$objectToArray": "$fruits"
}
}
},
{
"$addFields": {
"bananaIdx": {
"$cond": {
"if": {
$eq: [
"banana",
{
$first: "$fruits.v"
}
]
},
"then": "$idx",
"else": "$$REMOVE"
}
},
"appleIdx": {
"$cond": {
"if": {
$eq: [
"apple",
{
$first: "$fruits.v"
}
]
},
"then": "$idx",
"else": "$$REMOVE"
}
}
}
},
{
$group: {
_id: "$_id",
fruits: {
$push: {
"$arrayToObject": "$fruits"
}
},
bananaIdx: {
$max: "$bananaIdx"
},
appleIdx: {
$max: "$appleIdx"
}
}
},
{
$match: {
$expr: {
$lt: [
"$bananaIdx",
"$appleIdx"
]
}
}
},
{
$unset: [
"bananaIdx",
"appleIdx"
]
}
])
Mongo Playground

Mongo Query to modify the existing field value with new value + array of objects

I want to update many documents based on the condition in MongoDB.
MODEL is the collection which has the document with below information.
"info": [
{
"field1": "String1",
"field2": "String2"
},
{
"field1": "String1",
"field2": "String_2"
}
],
"var": "x"
I need to update all the "String1" value of field1 with "STRING_NEW". I used the below query to update but not working as expected.
db.model.updateMany(
{ "info.field1": { $exists: true } },
[
{ "$set": {
"info": {
"$map": {
"input": "$info.field1",
"in": {
"$cond": [
{ "$eq": ["$$this.field1", "String1"] },
"STRING_NEW",
$$this.field1
]
}
}
}
} }
]
)
Please have a look and suggest if anything is to be modified in the above query.
Solution 1
With the update with aggregation pipeline, you should iterate the object in info array and update the iterated object by merging the current object with new field1 field via $mergeObjects.
db.model.updateMany({
"info.field1": "String1"
},
[
{
"$set": {
"info": {
"$map": {
"input": "$info",
"in": {
"$cond": [
{
"$eq": [
"$$this.field1",
"String1"
]
},
{
$mergeObjects: [
"$$this",
{
"field1": "STRING_NEW"
}
]
},
"$$this"
]
}
}
}
}
}
])
Demo Solution 1 # Mongo Playground
Solution 2
Can also work with $[<identifier>] positional filtered operator and arrayFilters.
db.model.updateMany({
"info.field1": "String1"
},
{
"$set": {
"info.$[info].field1": "STRING_NEW"
}
},
{
arrayFilters: [
{
"info.field1": "String1"
}
]
})
Demo Solution 2 # Mongo Playground

How to convert a field, which is an array of objects with K-V pairs to array of arrays with only values?

I have a collection in MongoDB which has a field called "geometry" with latitude and longitude like this :
{
"abc":"xyz",
"geometry" : [
{
"lat" : 45.0,
"lng" : 25.0
},
{
"lat" : 46.0,
"lng" : 26.0
}
]
}
I want to convert the field geometry into something like this, to be compliant with the GeoJSON format:
{
"abc":"xyz",
"geometry": {
"type": "LineString",
"coordinates": [
[
25.0,
45.0
],
[
26.0,
46.0
]
]
}
}
The operation essentially involves taking an array of objects with two K/V pairs and pick only the values and store them as array of arrays(with the order reversed- so value of "lng" comes first).
My failed attempts:
I tried using an aggregate and tried to project the following:
"geometry": {"type":"LineString", "coordinates":["$points.lng","$points.lat"] }
which gave me a result similar to:
"geometry": {
"type": "LineString",
"coordinates": [
[
25.0,
26.0
],
[
45.0,
46.0
]
]
}
I've tried working with this and modifying data record by record, but the results are not consistent. And, I'm trying to avoid going through every record and changing the structure one by one. Is there a way to do this efficiently ?
You would think that the following code should theoretically work:
db.collection.aggregate({
$project: {
"abc": 1, // include the "abc" field in the output
"geometry": { // add a new geometry sub-document
"type": "LineString", // with the hardcoded "type" field
"coordinates": {
$map: {
"input": "$geometry", // transform each item in the "geometry" array
"as": "this",
"in": [ "$$this.lng", "$$this.lat" ] // into an array of values only, ith "lng" first, "lat" second
}
}
}
}
}, {
$out: "result" // creates a new collection called "result" with the transformed documents in it
})
However, the way MongoDB works at this stage as per SERVER-37635 is that the above query results in a surprising output where the coordinates field contains the desired result several times. So the following query can be used to generate the desired output instead:
db.collection.aggregate({
$addFields: {
"abc": 1,
"geometry2": {
"type": "LineString",
"coordinates": {
$map: {
"input": "$geometry",
"as": "this",
"in": [ "$$this.lng", "$$this.lat" ]
}
}
}
}
}, {
$project: {
"abc": 1,
"geometry": "$geometry2"
}
}, {
$out: "result"
})
In the comments section of the JIRA ticket mentioned above, Charlie Swanson mentions another workaround which uses $let to "trick" MongoDB into interpreting the query in the desired way. I re-post it here (note that it's missing the $out part):
db.collection.aggregate([
{
$project: {
"abc": 1,
"geometry": {
$let: {
vars: {
ret: {
"type": "LineString",
"coordinates": {
$map: {
"input": "$geometry",
"as": "this",
"in": [
"$$this.lng",
"$$this.lat"
]
}
}
}
},
in: "$$ret"
}
}
}
}
])

Aggregate - $push an Array Expression in $group

I have this aggregation pipeline:
{
$project: {
type: '$_id.type',
datapoints: [
{$multiply: ['$_id.timestamp', 1000]},
'$count',
],
},
},
{
$group: {
_id: {
type: '$type',
},
datapoints: {$push: '$datapoints'},
},
},
... which brought me to a following question:
How could I possibly append/push/add/etc an array to datapoints in $group (what could let me avoid $project stage))?
If I try putting the same expression for datapoints in the $project under $push in the $group it gives me an error
It's a fair point because you cannot just simply notate with "bracket" [] notation as available from MongoDB 3.2 into a $push.
It's a bit of a hack, but you can basically wrap the expression with $map instead of using the [] notation:
{ "$group": {
"_id": "$_id.type",
"datapoints": {
"$push": {
"$map": {
"input": [1],
"in": [
{ "$multiply": ["$_id.timestamp", 1000] },
"$count"
]
}
}
}
}}
Or even $concatArrays if your MongoDB supports it:
{ "$group": {
"_id": "$_id.type",
"datapoints": {
"$push": {
"$concatArrrays": [
[
{ "$multiply": ["$_id.timestamp", 1000] },
"$count"
]
]
}
}
}}
Or even $setUnion, which is actually probably the shortest to type, but it may not retain the order of elements if that is important to you:
{ "$group": {
"_id": "$_id.type",
"datapoints": {
"$push": {
"$setUnion": [
[
{ "$multiply": ["$_id.timestamp", 1000] },
"$count"
]
]
}
}
}}
Trying to simply do :
"datapoint": { "$push": [{ "$mulitply": [ ...
Will of course result in an error
"errmsg" : "The $push accumulator is a unary operator",
And this sort of expression would be syntactically incorrect:
"datapoint": { "$push": { ["a"] }
So you need an "expression" of some sort which is "unary" and not "list based" and returns a BSON Array in result. The above operators cover those cases.

Update array content within another array that don't have key

I have mongoDB content as below:
[
{
"_id":{
"$oid":"57c6699711bd6a0976cabe8a"
},
"ID":"1111",
"FullName":"AAA",
"Category":[
{
"CategoryId":{
"$oid":"57c66ebedcba0f63c1ceea51"
},
"_id":{
"$oid":"57e38a8ad190ea1100649798"
},
"Value":[
{
"Name":""
}
]
},
{
"CategoryId":{
"$oid":"57c3df061eb1e59d3959cc40"
},
"_id":{
"$oid":"57e38a8ad190ea1100649797"
},
"Value":[
[
"111",
"XXXX",
"2005"
],
[
"1212",
"YYYY",
"2000"
],
[
"232323",
"ZZZZZ",
"1999"
]
]
}
]
},
{
"_id":{
"$oid":"57c6699711bd6a0976cabe8a"
},
"ID":"1111",
"FullName":"BBB",
"Category":[
{
"CategoryId":{
"$oid":"57c66ebedcba0f63c1ceea51"
},
"_id":{
"$oid":"57e38a8ad190ea1100649798"
},
"Value":[
{
"Name":""
}
]
},
{
"CategoryId":{
"$oid":"57c3df061eb1e59d3959cc40"
},
"_id":{
"$oid":"57e38a8ad190ea1100649797"
},
"Value":[
[
"4444",
"XXXX",
"2005"
],
[
"7777",
"GGGG",
"2000"
],
[
"8888",
"ZZZZZ",
"1999"
]
]
}
]
}
]
Here I have an array named 'Category' where it contains objects with different category id.
I need to
select a particular category id - '57c3df061eb1e59d3959cc40'
From the above selected Category, we get 'Value' array
From Value array need to find if the second value is equal to 'ZZZZZ' ie. value[1] == 'ZZZZZ'
And now, update the matched value arrays with a new value at the end
Eg:
[
"232323",
"ZZZZZ",
"1999"
]
should be updated to
[
"232323",
"ZZZZZ",
"1999",
"update1"
]
and
[
"8888",
"ZZZZZ",
"1999"
]
should be updated to
[
"8888",
"ZZZZZ",
"1999",
"update1"
]
I have tried as below:
resume.update({
"Category.CategoryId": new ObjectId('57c3df191eb1e59d3959cc43'),
"Category.Value.$.1": 'ZZZZZ'
},
{"$set": {"Category.Value.$.3": "update1"}
}, function(err, resData){
res.send(resData);
});
But, nothing gets updated. Its there any way to get this work. Please help to update the inner array.
Thanks in advance.
Your goal is not possible at the moment since you need to update two positional elements.
There is a JIRA trackable for the sort of behaviour you want here: https://jira.mongodb.org/browse/SERVER-831
It's a problem since you need to match two elements positions:
the Category element with the matched CategoryId
the Value element in the Value array of arrays
If one of these wouldn't be an array it would have been possible.
Anyway, Your update try above was wrong. IF this feature was possible (and it is not!!!) it would have been something like this:
db.resume.update(
{
Category: {
$elemMatch: {
CategoryId: ObjectId('57c3df061eb1e59d3959cc40'),
Value: {
$elemMatch: {
'1': 'ZZZZZ'
}
}
}
}
},
{
$push: {
'Category.$.Value.$': 'update1'
}
}
)
The positional $ operator should be used during the update and not the find like you did, and it will update the first element that matched the query.
Doing the above will return the error:
Too many positional (i.e. '$') elements found in path 'Category.$.Value.$'
Because of the missing feature I explained at the top.
So, currently (version 3.2) you will not be able to do this unless you change your schema.

Resources