Pull array within array - mongodb

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 } } } }
)

Related

Aggregate documents where objects in array matches multiple conditions

I have a collection with documents similar to such:
{
"_id": ObjectId("xxxxx"),
"item": [
{ "property": ["attr1", "+1"] },
{ "property": ["attr2", "-1"] }
]
}
{
"_id": ObjectId("xxxxy"),
"item": [
{ "property": ["attr1", "-1"] },
{ "property": ["attr2", "0"] }
]
}
{
"_id": ObjectId("xxxxz"),
"item": [
{ "property": ["attr1", "0"] },
{ "property": ["attr2", "+1"] }
]
}
Preferably using an aggregation pipeline, is there any way to match the document if and only if any one of the properties match more than one condition?
For example, I want a query where one object in the array matches both of these conditions:
("item.property": "attr1") AND ("item.property": /^\+/)
That is, a single property where it contains "attr1" and an element that starts with "+".
However, using my current query that looks like this:
collection.aggregate(
{ $match:
{ $and:
[
{ "item.property": "attr1" },
{ "item.property": /^\+/ }
]
}
}
This would match both the first and last document because both contain a property with "attr1" and an element that stats with "+". However, I do not want this query to match the last document, since the element that starts with "+" does not belong to the same object in the array.
Is there any way to achieve this behavior using the aggregation framework?
You can use the below query with $elemMatch to match the array's both values.
Something like
db.collection_name.aggregate({
"$match": {
"item": {
"$elemMatch": {
"property.0": "attr1",
"property.1": /^\+/
}
}
}
});
Also, you can use $all operator if you don't want to reference array index.
db.collection_name.aggregate({
"$match": {
"item": {
"$elemMatch": {
"property": {
"$all": [
"attr1",
/^\+/
]
}
}
}
}
});

Query by field value, not value in field array

The following snippet shows three queries:
find all the documents
find the documents containing a field a containing either the string "x" or an array containing the string "x"
find the documents containing a field a containing an array containing the string "x"
I was not able to find the documents containing a field a containing the string "x", not inside an array.
> db.stuff.find({},{_id:0})
{ "a" : "x" }
{ "a" : [ "x" ] }
> db.stuff.find({a:"x"},{_id:0})
{ "a" : "x" }
{ "a" : [ "x" ] }
> db.stuff.find({a:{$elemMatch:{$eq:"x"}}},{_id:0})
{ "a" : [ "x" ] }
>
MongoDB basically does not care if the data at a "given path" is actually in an array or not. If you want to make the distinction, then you need to "tell it that":
db.stuff.find({ "a": "x", "$where": "return !Array.isArray(this.a)" })
This is what $where adds to the bargain, where you can supply a condition that explicitly asks "is this an array" via Array.isArray() in JavaScript evaluation. And the JavaScript NOT ! assertion reverses the logic.
An alternate approach is to add the $exists check:
db.stuff.find({ "a": "x", "a.0": { "$exists": false } })
Which also essentially asks "is this an array" by looking for the first element index. So the "reverse" false case means "this is not an array".
Or even as you note you can use $elemMatch to select only the array, but "negate" that using $not:
db.stuff.find({ "a": { "$not": { "$elemMatch": { "$eq": "x" } } } })
Though probably "not" the best of options since that also "negates index usage", which the other examples all strive to avoid by at least including "one" positive condition for a match. So it's for the best to include the "implicit AND" by combining arguments:
db.stuff.find({
"a": { "$eq": "x", "$not": { "$elemMatch": { "$eq": "x" } } }
})
Or for "aggregation" which does not support $where, you can test using the $isArray aggregation operator should your MongoDB version ( 3.2 or greater ) support it:
db.stuff.aggregate([
{ "$match": { "a": "x" } },
{ "$redact": {
"$cond": {
"if": { "$not": { "$isArray": "$a" } },
"then": "$$KEEP",
"else": "$$PRUNE"
}
}}
])
Noting that it is good practice to supply "regular" query conditions as well where possible, and in all cases.
Also noting that querying the BSON $type does not typically work in this case, since the "contents" of the array itself are in fact a "string", which is what the $type operator is going to consider, and thus not report that such an array is in fact an array.

Mongo Sort by Count of Matches in Array

Lets say my test data is
db.multiArr.insert({"ID" : "fruit1","Keys" : ["apple", "orange", "banana"]})
db.multiArr.insert({"ID" : "fruit2","Keys" : ["apple", "carrot", "banana"]})
to get individual fruit like carrot i do
db.multiArr.find({'Keys':{$in:['carrot']}})
when i do an or query for orange and banana, i see both the records fruit1 and then fruit2
db.multiArr.find({ $or: [{'Keys':{$in:['carrot']}}, {'Keys':{$in:['banana']}}]})
Result of the output should be fruit2 and then fruit1, because fruit2 has both carrot and banana
To actually answer this first, you need to "calculate" the number of matches to the given condition in order to "sort" the results to return with the preference to the most matches on top.
For this you need the aggregation framework, which is what you use for "calculation" and "manipulation" of data in MongoDB:
db.multiArr.aggregate([
{ "$match": { "Keys": { "$in": [ "carrot", "banana" ] } } },
{ "$project": {
"ID": 1,
"Keys": 1,
"order": {
"$size": {
"$setIntersection": [ ["carrot", "banana"], "$Keys" ]
}
}
}},
{ "$sort": { "order": -1 } }
])
On an MongoDB older than version 3, then you can do the longer form:
db.multiArr.aggregate([
{ "$match": { "Keys": { "$in": [ "carrot", "banana" ] } } },
{ "$unwind": "$Keys" },
{ "$group": {
"_id": "$_id",
"ID": { "$first": "$ID" },
"Keys": { "$push": "$Keys" },
"order": {
"$sum": {
{ "$cond": [
{ "$or": [
{ "$eq": [ "$Keys", "carrot" ] },
{ "$eq": [ "$Keys", "banana" ] }
]},
1,
0
]}
}
}
}},
{ "$sort": { "order": -1 } }
])
In either case the function here is to first match the possible documents to the conditions by providing a "list" of arguments with $in. Once the results are obtained you want to "count" the number of matching elements in the array to the "list" of possible values provided.
In the modern form the $setIntersection operator compares the two "lists" returning a new array that only contains the "unique" matching members. Since we want to know how many matches that was, we simply return the $size of that list.
In older versions, you pull apart the document array with $unwind in order to perform operations on it since older versions lacked the newer operators that worked with arrays without alteration. The process then looks at each value individually and if either expression in $or matches the possible values then the $cond ternary returns a value of 1 to the $sum accumulator, otherwise 0. The net result is the same "count of matches" as shown for the modern version.
The final thing is simply to $sort the results based on the "count of matches" that was returned so the most matches is on "top". This is is "descending order" and therefore you supply the -1 to indicate that.
Addendum concerning $in and arrays
You are misunderstanding a couple of things about MongoDB queries for starters. The $in operator is actually intended for a "list" of arguments like this:
{ "Keys": { "$in": [ "carrot", "banana" ] } }
Which is essentially the shorthand way of saying "Match either 'carrot' or 'banana' in the property 'Keys'". And could even be written in long form like this:
{ "$or": [{ "Keys": "carrot" }, { "Keys": "banana" }] }
Which really should lead you to if it were a "singular" match condition, then you simply supply the value to match to the property:
{ "Keys": "carrot" }
So that should cover the misconception that you use $in to match a property that is an array within a document. Rather the "reverse" case is the intended usage where instead you supply a "list of arguments" to match a given property, be that property an array or just a single value.
The MongoDB query engine makes no distinction between a single value or an array of values in an equality or similar operation.

Update a field in a deeply nested document in 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
});
});

how to match the last value of array in mongo db? [duplicate]

I have a sample document like shown below
{
"_id" : "docID",
"ARRAY" : [
{
"k" : "value",
"T" : "20:15:35",
"I" : "Hai"
},
{
"K" : "some value",
"T" : "20:16:35",
"I" : "Hello"
},
{
"K" : "some other value",
"T" : "20:15:35",
"I" : "Update"
}
]
}
I am trying to update the last element in the "ARRAY" based on field "ARRAY.T"(which is only field i know at the point of update), but what my problem is first element in the array matches the query and its ARRAY.I field is updated.
Query used to update:
db.collection.update( { _id: "docID","ARRAY.T" : "20:15:35"},
{ $set: { "ARRAY.$.I": "Updated value" }
})
Actually i don't know index of the array where to update so i have to use ARRAY.I in the query, is there any way to to tell Mongodb to update the first element matched the query from last of the array.
I understand what you are saying in that you want to match the last element in this case or in fact process the match in reverse order. There is no way to modify this and the index stored in the positional $ operator will always be the "first" match.
But you can change your approach to this, as the default behavior of $push is to "append" to the end of the array. But MongoDB 2.6 introduced a $position modifier so you can in fact always "pre-pend" to the array meaning your "oldest" item is at the end.
Take this for example:
db.artest.update(
{ "array": { "$in": [5] } },
{ "$push": { "array": { "$each": [5], "$position": 0 } }},
{ "upsert": true }
)
db.artest.update(
{ "array": { "$in": [5] } },
{ "$push": { "array": { "$each": [6], "$position": 0 } }},
{ "upsert": true }
)
This results in a document that is the "reverse" of the normal $push behavior:
{ "_id" : ObjectId("53eaf4517d0dc314962c93f4"), "array" : [ 6, 5 ] }
Alternately you could apply the $sort modifier when updating your documents in order to "order" the elements so they were reversed. But that may not be the best option if duplicate values are stored.
So look into storing your arrays in "reverse" if you intend to match the "newest" items "first". Currently that is your only way of getting your "match from last" behavior.