Update a field in a deeply nested document in Mongodb - mongodb

The MongoDB document structure in question looks like this :
{
"_id": ObjectId("54247a68fab6b6775d000062"),
"owner": "1",
"version": "Version 1",
"name": "Test20",
"u_at": ISODate("2014-09-25T20:26:16.140Z"),
"c_at": ISODate("2014-09-25T20:26:16.140Z"),
"canvases": [
{
"_id": ObjectId("54247a68fab6b6775d000063"),
"nodes": [
{
"_id": ObjectId("54247a68fab6b6775d000060"),
"filePathTemplate": "LETSDOEMAIL"
},
{
"_id": ObjectId("54247a68fab6b6775d000061"),
"filePathTemplate": "LETSDOFACEBOOK"
}
]
}
]
}
I am struggling primarily with two things:
Searching for a specific node and get only the node back in result. Following is the query I am currently using (after browsing all related SO questions):
db.getCollection("coll").find({_id: ObjectId("54247a68fab6b6775d000062")}, {canvases:{$elemMatch:{nodes:{$elemMatch:{_id: ObjectId("54247a68fab6b6775d000060")}}}}})
But this gives back the canvas, containing the node searched for, instead of node.
{
"_id": ObjectId("54247a68fab6b6775d000062"),
"canvases": [
{
"_id": ObjectId("54247a68fab6b6775d000063"),
"nodes": [
{
"_id": ObjectId("54247a68fab6b6775d000060"),
"filePathTemplate": "LETSDOEMAIL"
},
{
"_id": ObjectId("54247a68fab6b6775d000061"),
"filePathTemplate": "LETSDOFACEBOOK"
}
]
}
]
}
As a result of above mentioned issue, updating a field in a node document is also a problem. This is the query I have got from other SO questions but to no avail:
db.getCollection("coll").update({canvases: {$elemMatch:{nodes:{$elemMatch:{_id: ObjectId("54247a68fab6b6775d000060")}}}}}, {$set: {"canvases.$.nodes.$.filePathTemplate": "21"}})
Any help would be appreciated.

Question 1:
Only the first $elemMatch in the second parameter of .find(arg1, arg2) is effective to position element of array, that is, only one element of canvases can be positioned, excluding nodes'. So .find() is improper to do this kind of task in my opinion.
But I think you can reach the target by following method:
db.coll.aggregate([ {
$match : {
_id : ObjectId("54247a68fab6b6775d000062")
}
}, {
$redact : {
$cond : [ {
$or : [ {
$gt : [ "$canvases", [] ]
}, {
$gt : [ "$nodes", [] ]
}, {
$eq : [ "$_id", ObjectId("54247a68fab6b6775d000060") ]
} ]
}, "$$DESCEND", "$$PRUNE" ]
}
} ]);
Question 2:
Positional operator $ can not use in nested array;
So when you use $ after an array, all the rest of path must be "solid"; it means if there are nested array behind $, you must explicitly write out its index to point out which element you want to update.
Suppose all _id in all document level of this collection is unique.
Following code for your reference:
var searchKey = ObjectId("54247a68fab6b6775d000060");
var filePathTemplate = "21";
db.getCollection("coll").find({
canvases : {
$elemMatch : {
nodes : {
$elemMatch : {
_id : searchKey
}
}
}
}
}, {
"canvases.$" : 1
}).forEach(function(doc) {
// We need to find out the index of that sub-document which is required to
// updated in nodes, because nodes is probably very big.
var i = 0;
var nodes = doc.canvases[0].nodes; // Only one element in canvases from abover query.
for (; i < nodes.length && nodes[i]._id.str != searchKey.str; ++i);
var key = "canvases.$.nodes." + i + ".filePathTemplate"; // Only one "$" can be used.
var updateNodePart = {};
updateNodePart[key] = filePathTemplate;
db.getCollection("coll").update({
canvases : {
$elemMatch : {
nodes : {
$elemMatch : {
_id : searchKey
}
}
}
}
}, {
$set : updateNodePart
});
});

Related

how to lower case the value of unique:true keys in mongodb?

I have created a mongodb and by mistake have entered duplicate values in the form of capital and small case letters.
I have made the index unique. MongoDB is case sensitive and hence, considered the capital letter and small letter as different values.
Now my problem is the database have got around 32 GB. and I came across this issue. Kindly help me.
Here is the sample:
db.tt.createIndex({'email':1},{unique:true})
> db.tt.find().pretty()
{
"_id" : ObjectId("591d706c0ef9acde11d7af66"),
"email" : "g#gmail.com",
"src" : [
{
"acc" : "ln"
},
{
"acc" : "drb"
}
]
}
{
"_id" : ObjectId("591d70740ef9acde11d7af68"),
"email" : "G#gmail.com",
"src" : [
{
"acc" : "ln"
},
{
"acc" : "drb"
},
{
"acc" : "dd"
}
]
}
How I can make the email as lowercase and assign the src values to the original one. Kindly help me.
you can achive this using $toLower aggregation operator like this :
db.tt.aggregate([
{
$project:{
email:{
$toLower:"$email"
},
src:1
}
},
{
$unwind:"$src"
},
{
$group:{
_id:"$email",
src:{
$addToSet:"$src"
}
}
},
{
$project:{
_id:0,
email:"$_id",
src:1
}
},
{
$out:"anotherCollection"
}
])
$addToSet allow to keep oly one distinct occurence of src items
this will write this document to a new collection named anotherCollection:
{ "email" : "g#gmail.com", "src" : [ { "acc" : "dd" }, { "acc" : "drb" }, { "acc" : "ln" } ] }
Note that with $out, you can averwrite directly your tt collection, however before doing this make sure to understand what your doing because all previous data will be lost
The most efficient way I can think of to merge the data is run an aggregation and loop the result to write back to the collection in bulk operations:
var ops = [];
db.tt.aggregate([
{ "$unwind": "$src" },
{ "$group": {
"_id": { "$toLower": "$email" },
"src": { "$addToSet": "$src" },
"ids": { "$addToSet": "$_id" }
}}
]).forEach(doc => {
var id = doc.ids.shift();
ops = [
...ops,
{
"deleteMany": {
"filter": { "_id": { "$in": doc.ids } }
}
},
{
"updateOne": {
"filter": { "_id": id },
"update": {
"$set": { "email": doc._id },
"$addToSet": { "src": { "$each": doc.src } }
}
}
},
];
if ( ops.length >= 500 ) {
db.tt.bulkWrite(ops);
ops = [];
}
});
if ( ops.length > 0 )
db.tt.bulkWrite(ops);
In steps, that's $unwind the array items so they can be merged via $addToSet, under a $group on using $toLower on the email value. You also want to keep the set of unique source document ids.
In the loop you shift the first _id value off of doc.ids and update that document with the lowercase email and the revised "src" set. Using $addToSet here makes the operation write safe with any other updates that might occur to the document.
Then the other operation in the loop deletes the other documents that shared the same converted case email, so there are no duplicates. Actually do that one first. The default "ordered" operations make sure this is fine.
And do it in the shell, since it's a one-off operation and is really just as simple as listing as shown.

Filter sub-document array using substring as criteria

My collection:
{
title: 'Computers',
maincategories:[
{
title: 'Monitors',
subcategories:[
{
title: '24 inch',
code: 'AFG'
}
]
}
]
}
I want query the code. The code is just the first part so I want to have all subcategories that contains the given search. So AFG101 would return this subcategories.
My query:
module.exports = (req, res) => {
var q = {
'maincategories.subcategories': {
$elemMatch: {
code: 'AFG101'
}
}
};
var query = mongoose.model('TypeCategory').find(q, {'maincategories.$': 1, 'title': 1});
query.exec((err, docs) => {
res.status(200).send(docs);
});
};
My problem:
How do I search for a part of a string? AFG101 should return all subcategories with property code containing any part of the string. So in this case, AFG would be a hit. Same as in this sql question: MySQL: What is a reverse version of LIKE?
How do I project the subcategories. Current query returns all subcategories. I only want to returns those hitting.
The best way to do this is in MongoDB 3.4 using the $indexOfCP string aggregation operator.
let code = "afg101";
db.collection.aggregate([
{ "$project": {
"title": 1,
"maincategories": {
"$map": {
"input": "$maincategories",
"as": "mc",
"in": {
"$filter": {
"input": "$$mc.subcategories",
"as": "subcat",
"cond": {
"$gt": [
{
"$indexOfCP": [
code,
{ "$toLower": "$$subcat.code" }
]
},
-1
]
}
}
}
}
}
}}
])
which returns:
{
"_id" : ObjectId("582cba57e6f570d40d77b3a8"),
"title" : "Computers",
"maincategories" : [
[
{
"title" : "24 inch",
"code" : "AFG"
}
]
]
}
You can read my other answers to similar question 1, 2 and 3.
From 3.2 backward, the only way to do this is with mapReduce.
db.collection.mapReduce(
function() {
var code = 'AFG101';
var maincategories = this.maincategories.map(function(sdoc) {
return {
"title": sdoc.title,
"subcategories": sdoc.subcategories.filter(function(scat) {
return code.indexOf(scat.code) != -1;
}
)};
});
emit(this._id, maincategories);
},
function(key, value) {},
{ "out": { "inline": 1 }
})
which yields something like this:
{
"results" : [
{
"_id" : ObjectId("582c9a1aa358615b6352c45a"),
"value" : [
{
"title" : "Monitors",
"subcategories" : [
{
"title" : "24 inch",
"code" : "AFG"
}
]
}
]
}
],
"timeMillis" : 15,
"counts" : {
"input" : 1,
"emit" : 1,
"reduce" : 0,
"output" : 1
},
"ok" : 1
}
Well, just like your question has two parts, I could think of two separate solution, however I don't see a way to join them together.
For first part $where can be used to do a reverse regex, but it's dirty, it's an overkill and it can't use any indexes, since $where runs on each documents.
db.TypeCategory.find({$where:function(){for(var i in this.maincategories)
{for(var j in this.maincategories[i].subcategories)
{if("AFG101".indexOf(this.maincategories[i].subcategories[j].code)>=0)
{return true}}}}},{"maincategories.subcategories.code":1})
Even if you use this option, it would need couple of boundary check and it cannot project two level of nested array. MongoDB doesn't support such projection (yet).
For that purpose we might go for aggregation
db.TypeCategory.aggregate([{$unwind:"$maincategories"},
{$unwind:"$maincategories.subcategories"},
{$match:{"maincategories.subcategories.code":"AFG"}},
{$group:{_id:"$_id","maincategories":{$push:"$maincategories"}}}
])
However I don't think there is a way to do reverse regex check in aggregation, but I might be wrong too. Also this aggregation is costly since there are two unwinds which can lead to overflow the memory limit for aggregation for a really large collection.
You can use $substr and do it
db.getCollection('cat').aggregate([
{"$unwind" : "$maincategories"},
{"$unwind" : "$maincategories.subcategories"},
{"$project" :
{"maincategories" : 1,
"title":1,"sub" : {"$substr" :["$maincategories.subcategories.code",0,3]}}},
{"$match" : {"sub" : "AFG"}},
{"$project" :
{"maincategories" : 1,
"title":1}
}
])

MongoDB: Create Object in Aggregation result

I want to return Object as a field in my Aggregation result similar to the solution in this question. However in the solution mentioned above, the Aggregation results in an Array of Objects with just one item in that array, not a standalone Object. For example, a query like the following with a $push operation
$group:{
_id: "$publisherId",
'values' : { $push:{
newCount: { $sum: "$newField" },
oldCount: { $sum: "$oldField" } }
}
}
returns a result like this
{
"_id" : 2,
"values" : [
{
"newCount" : 100,
"oldCount" : 200
}
]
}
}
not one like this
{
"_id" : 2,
"values" : {
"newCount" : 100,
"oldCount" : 200
}
}
}
The latter is the result that I require. So how do I rewrite the query to get a result like that? Is it possible or is the former result the best I can get?
You don't need the $push operator, just add a final $project pipeline that will create the embedded document. Follow this guideline:
var pipeline = [
{
"$group": {
"_id": "$publisherId",
"newCount": { "$sum": "$newField" },
"oldCount": { "$sum": "$oldField" }
}
},
{
"$project" {
"values": {
"newCount": "$newCount",
"oldCount": "$oldCount"
}
}
}
];
db.collection.aggregate(pipeline);

Pull array within array

Similar to Find document with array that contains a specific value, but i'm trying to pull it.
db.getCollection('users').find({'favorites':{$elemMatch:{0:5719}}}, {"favorites.$": 1})
returns this:
{
"_id" : "FfEj5chmviLdqWh52",
"favorites" : [
[
5719,
"2016-03-21T17:46:01.441Z",
"a"
]
]
}
even after this returned 1:
Meteor.users.update(this.userId, {$pull: {'favorites':{$elemMatch:{0:movieid}}}})
It doesn't work because $pull is trying to remove a matching element from the "favorites" array. What you want to do is remove from the "array inside the array" of favorites.
For this you need a positional match to point to the nth inner element, then a very careful $pull expression to actually remove that element:
Meteor.users.update(
{ "favorites": { "$elemMatch": { "$elemMatch": { "$eq": 5719 } } } },
{ "$pull": { "favorites.$": 5719 } }
)
The "double" $elemMatch with the $eq operator is a bit more expressive than { 0: 5719 } since it is not "locked" into the first position only and is actually looking at the matching value. But you can write it that way if you must, or if you "really mean" to match that value in the first position only.
Note that the "index" returned from the match in the positional $ argument is actually that of the "outer" array. So to pull from the
Of course if there is only ever one nested array element within, the you might as well just write:
{ "$pull": { "favorites.0": 5719 } }
Using the direct "first index" position, since you know the inner array will always be there.
In either case, your object updates correctly:
{
"_id" : "FfEj5chmviLdqWh52",
"favorites" : [
[
"2016-03-21T17:46:01.441Z",
"a"
]
]
}
If you are trying to $pull the entire array entry from favorites, then the $eleMatch just needs to be dialed back one element:
Meteor.users.update(
{ "_id": this.userId },
{ "$pull": { "favorites": { "$elemMatch": { "$eq": 5719 } } } }
)
Or even:
Meteor.users.update(
{ "_id": this.userId },
{ "$pull": { "favorites": { "$elemMatch": { "0": 5719 } } } }
)
Noting that:
{ "_id": this.userId },
Is the long form that we generally use as a "query" selector, and especially when we want criteria "other than" the _id of the document. MiniMongo statements require at "least" the _id of the document though.
The rest of the statement has one "less" $elemMatch because the $pull already applies to the array.
That removes the whole matched element from the outer array:
{
"_id" : "FfEj5chmviLdqWh52",
"favorites" : []
}
This is the first code i found that actually works:
Meteor.users.update(Meteor.userId(), {$pull: {favorites: {$in: [i]}}})
Apparently $in does partial matching. It seems safer than the working code from this answer:
Meteor.users.update(
{ "_id": this.userId },
{ "$pull": { "favorites": { "$elemMatch": { "$eq": i } } } }
)

Mongodb: find documents with array field that contains more than one SAME specified value

There is three documents in collection test:
// document 1
{
"id": 1,
"score": [3,2,5,4,5]
}
// document 2
{
"id": 2,
"score": [5,5]
}
// document 3
{
"id": 3,
"score": [5,3,3]
}
I want to fetch documents that score field contains [5,5].
query:
db.test.find( {"score": {"$all": [5,5]}} )
will return document 1, 2 and 3, but I only want to fetch document 1 and 2.
How can I do this?
After reading your problem I personally think mongodb not supported yet this kind of query. If any one knows about how to find this using mongo query they defiantly post answers here.
But I think this will possible using mongo forEach method, so below code will match your criteria
db.collectionName.find().forEach(function(myDoc) {
var scoreCounts = {};
var arr = myDoc.score;
for (var i = 0; i < arr.length; i++) {
var num = arr[i];
scoreCounts[num] = scoreCounts[num] ? scoreCounts[num] + 1 : 1;
}
if (scoreCounts[5] >= 2) { //scoreCounts[5] this find occurrence of 5
printjsononeline(myDoc);
}
});
Changed in version 2.6.
The $all is equivalent to an $and operation of the specified values; i.e. the following statement:
{ tags: { $all: [ "ssl" , "security" ] } }
is equivalent to:
{ $and: [ { tags: "ssl" }, { tags: "security" } ] }
I think you need to pass in a nested array -
So try
db.test.find( {"score": {"$all": [[5,5]]}} )
Source
Changed in version 2.6.
When passed an array of a nested array (e.g. [ [ "A" ] ] ), $all can now match documents where the field contains the nested array as an element (e.g. field: [ [ "A" ], ... ]), or the field equals the nested array (e.g. field: [ "A" ]).
http://docs.mongodb.org/manual/reference/operator/query/all/
You can do it with an aggregation. The first step can use an index on { "score" : 1 } but the rest is hard work.
db.test.aggregate([
{ "$match" : { "score" : 5 } },
{ "$unwind" : "$score" },
{ "$match" : { "score" : 5 } },
{ "$group" : { "_id" : "$_id", "sz" : { "$sum" : 1 } } }, // use $first here to include other fields in the results
{ "$match" : { "sz" : { "$gte" : 2 } } }
])