mongo: update matched elements of array that is in other array - mongodb

I have next schema of mongo document:
{
"array1" : [
{
"array2" : [
{
"id" : 1,
"field" : 2
}
]
}
]
}
I want to find elements in array2 with {id: 1} and update its "field". I tried next:
db.filmCitation.update({ "array1.array2": {$elemMatch: {"id": 1, field: {$gte: 2}} } }, {$set: {"array1.array2.field": 23})
But I must specify right index in arrays. For one array can be used "$". And what can I do when needs to find in several arrays?
Thanks!

Related

Mongodb: push element to nested array if the condition is met

I have the following collection:
{
"_id": 11,
"outerArray": [
{ "_id" : 21,
"field": {
"innerArray" : [
1,
2,
3
]
}
},
{ "_id" : 22,
"field": {
"innerArray" : [
2,
3
]
}
},
{ "_id" : 23,
"field": {
"innerArray" : [
2
]
}
}
]
}
I need to go through all documents in collection and push to innerArray new element 4, if innerArray already contains element 1 or element 3
I tried to do it this way, and few others, similar to this one, but it didn't work as expected, it only pushes to innerArray of first element of outerArray
db.collection.updateMany(
{ "outerArray.field.innerArray": { $in: [ 1, 3 ] } },
{ $push: { "outerArray.$.field.innerArray": 4} }
)
How to make it push to all coresponding innerArrays?
Problem here is your missunderstanding a copule things.
When you do "outerArray.field.innerArray": { $in: [ 1, 3 ] } into your query, your are not getting only innerArray where has 1 or 3. You are gettings documents where exists these arrays.
So you are querying the entire document.
You have to use arrayFilter to update values when the filter is match.
So, If I've understood you correctly, the query you want is:
db.collection.update(
{}, //Empty object to find all documents
{
$push: { "outerArray.$[elem].field.innerArray": 4 }
},
{
"arrayFilters": [ { "elem.field.innerArray": { $in: [ 1, 3 ] } } ]
})
Example here
Note how the first object into update is empty. You have to put there the field to match the document (not the array, the document).
If you want to update only one document you have to fill first object (query object) with values you want, for example: {"_id": 11}.

(mongo) How could i get the documents that have a value in array along with size

I have a mongo collection with something like the below:
{
"_id" : ObjectId("59e013e83260c739f029ee21"),
"createdAt" : ISODate("2017-10-13T01:16:24.653+0000"),
"updatedAt" : ISODate("2017-11-11T17:13:52.956+0000"),
"age" : NumberInt(34),
"attributes" : [
{
"year" : "2017",
"contest" : [
{
"name" : "Category1",
"division" : "Department1"
},
{
"name" : "Category2",
"division" : "Department1"
}
]
},
{
"year" : "2016",
"contest" : [
{
"name" : "Category2",
"division" : "Department1"
}
]
},
{
"year" : "2015",
"contest" : [
{
"name" : "Category1",
"division" : "Department1"
}
]
}
],
"name" : {
"id" : NumberInt(9850214),
"first" : "john",
"last" : "afham"
}
}
now how could i get the number of documents who have contest with name category1 more than one time or more than 2 times ... and so on
I tried to use size and $gt but couldn't form a correct result
Assuming that a single contest will never contain the same name (e.g. "Category1") value more than once, here is what you can do.
The absence of any unwinds will result in improved performance in particular on big collections or data sets with loads of entries in your attributes arrays.
db.collection.aggregate({
$project: {
"numberOfOccurrences": {
$size: { // count the number of matching contest elements
$filter: { // get rid of all contest entries that do not contain at least one entry with name "Category1"
input: "$attributes",
cond: { $in: [ "Category1", "$$this.contest.name" ] }
}
}
}
}
}, {
$match: { // filter the number of documents
"numberOfOccurrences": {
$gt: 1 // put your desired min. number of matching contest entries here
}
}
}, {
$count: "numberOfDocuments" // count the number of matching documents
})
Try this on for size.
db.foo.aggregate([
// Start with breaking down attributes:
{$unwind: "$attributes"}
// Next, extract only name = Category1 from the contest array. This will yield
// an array of 0 or 1 because I am assuming that contest names WITHIN
// the contest array are unique. If found and we get an array of 1, turn that
// into a single doc instead of an array of a single doc by taking arrayElemAt 0.
// Otherwise, "x" is not set into the doc AT ALL. All other vars in the doc
// will go away after $project; if you want to keep them, change this to
// $addFields:
,{$project: {x: {$arrayElemAt: [ {$filter: {
input: "$attributes.contest",
as: "z",
cond: {$eq: [ "$$z.name", "Category1" ]}
}}, 0 ]}
}}
// We split up attributes before, creating multiple docs with the same _id. We
// must now "recombine" these _id (OP said he wants # of docs with name).
// We now have to capture all the single "x" that we created above; docs without
// Category1 will have NO "x" and we don't want to include them in the count.
// Also, we KNOW that name can only be Category 1 but division could vary, so
// let's capture that in the $push in case we might want it:
,{$group: {_id: "$_id", x: {$push: "$x.division"}}}
// One more pass to compute length of array:
,{$addFields: {len: {$size: "$x"}} }
// And lastly, the filter for one time or two times or n times:
,{$match: {len: {$gt: 2} }}
]);
First, we need to flatten the document by the attributes and contest fields. Then to group by the document initial _id and a contest names counting different contests along the way. Finally, we filter the result.
db.person.aggregate([
{ $unwind: "$attributes" },
{ $unwind: "$attributes.contest" },
{$group: {
_id: {initial_id: "$_id", contest: "$attributes.contest.name"},
count: {$sum: 1}
}
},
{$match: {$and: [{"_id.contest": "Category1"}, {"count": {$gt: 1}}]}}]);

MongoDB filter inner array of object

I have one document like this:
-document: users-
{
"name": "x", password: "x" recipes:
[{title: eggs, dificult: 1},{title: "pizza" dificult: 2}],
name: "y", password: "y" recipes: [{title: "oil", dificult: 2},{title: "eggandpotatoes" dificult: 2}]
}
I want to get all recipes filtering by title and dificult
I have tried some like this
db.users.find({$and: [{"recipes.title": /.*egg.*/},{"recipes.dificult": 2}]},
{"recipes.title": 1,"recipes.dificult": 1}).toArray();
this should return
{title: "eggandpotatoes" dificult: 2}
but return
{title: eggs, dificult: 1}
{title: "eggandpotatoes" dificult: 2}
I would like once the filter works, limit the result with start: 2 and end: 5
returning from the 2 result the next 5.
Thanks in advance.
You can use the $filter aggregation to filter the recipes and $slice to limit the results. It will give you everything you are looking expect but regex search which is not possible as of now.
db.users.aggregate([{
$project: {
_id: 0,
recipes: {
$slice: [{
$filter: {
input: "$recipes",
as: "recipe",
"cond": {
$and: [{
$eq: ["$$recipe.title", "eggandpotatoes"]
}, {
$eq: ["$$recipe.dificult", 2]
}]
}
}
}, 2, 3]
}
}
}]);
If you need to find out regular expression in title then use $elemMatch as below :
db.users.find({"recipes":{"$elemMatch":{"title": /.*egg.*/,"dificult": 2}}}, {"recipes.$":1})
One thing I mentioned here if your recipes array contains some thing like this
"recipes" : [ { "title" : "egg", "dificult" : 2 }, { "title" : "eggand", "dificult" : 2 } ]
then $ projection return only first matched array objcect in above case it will be
"recipes" : [ { "title" : "egg", "dificult" : 2 } ]
If you need this both array then go to #Sagar answer using aggregation

Can I sort elements of an array in mongo with a built in?

If I have this mongo document.
{
"_id" : ObjectId("52fd40e781ddcb34819ac21e"),
"name" : "fred",
"fruits_i_like" : [
"pear",
"apple",
"banana"
]
}
{
"_id" : ObjectId("52fd40fa81ddcb34819ac21f"),
"name" : "barney",
"fruits_i_like" : [
"pear",
"apple",
"peach"
]
}
can I sort the subkeys so that I get
{
"_id" : ObjectId("52fd40e781ddcb34819ac21e"),
"name" : "fred",
"fruits_i_like" : [
"apple",
"banana",
"pear"
]
}
{
"_id" : ObjectId("52fd40fa81ddcb34819ac21f"),
"name" : "barney",
"fruits_i_like" : [
"apple",
"peach",
"pear"
]
}
i.e - I don't really care about the ordering of the documents, but each time a document is printed in the find cursor, the list of fruits in the subkey should be sorted. I can do this with passing a custom javascript function to forEach() on the find() - but I wondered if there was a more built in way with the build in mongo features.
As you note operations such as forEach are iterators and you would have to do your sort actions on every item you retrieve. While this might be fine for some cases, there are a lot of reasons why this is not what you want. Particularly if you want to do any meaningful filtering of the content in inner arrays.
You can do a lot with array manipulation in the aggregation pipeline, so to return your results with the array items sorted:
db.collection.aggregate([
// Unwind the array elements into each document
{$unwind: "$fruits_i_like"},
// Sort on the array key
{$sort: { fruits_i_like: 1 }},
// Push the elements back into an array
{$group: {
_id: {_id: "$_id", name: "$name"},
fruits_i_like: {$push: "$fruits_i_like"}
}},
// Restore the original document form
{$project: { _id:0, _id: "$_id", name: "$_id.name", fruits_i_like: 1}}
])
So the use of $unwind denormalizes in a way so that you can do other operations with the inner elements. Also the aggregate command returns a cursor in latest drivers just like find, and will do so internally in future releases.
Of course if you want your documents to always have sorted array, look at the $sort modifier for use in update actions.

Get n-th element of an array in MongoDB

As part of my document in MongoDB I'm storing an array of objects. How can I query it for only the 4th element of the array for example? So I don't want the get the entire array out, just the 4th element.
Use $slice.
db.foo.find({ bar : "xyz" } , { my_array : { $slice : [n , 1] } } )
will retrieve the nth element of the array "my_array" of all documents in the foo collection where bar = "xyz".
Some other examples from the MongoDB documentation:
db.posts.find({}, {comments:{$slice: 5}}) // first 5 comments
db.posts.find({}, {comments:{$slice: -5}}) // last 5 comments
db.posts.find({}, {comments:{$slice: [20, 10]}}) // skip 20, limit 10
db.posts.find({}, {comments:{$slice: [-20, 10]}}) // 20 from end, limit 10
Which you can read here: http://www.mongodb.org/display/DOCS/Retrieving+a+Subset+of+Fields
You can use the $arrayElemAt operator new in MongoDB 3.2 to return the element at the specified array index.
Demo:
A collection named baskets contains documents that look like this:
{
"_id" : ObjectId("578f326f6db61a299a383c5a"),
"fruits" : [
"apple",
"mango",
"banana",
"apricot",
"cherry"
]
}
The following query return the element at index -2 (second element) in the "fruits" array.
db.baskets.aggregate(
[
{ "$project": { "matched": { "$arrayElemAt": [ "$fruits", 1 ] } } }
]
)
which produces
{
"_id" : ObjectId("578f326f6db61a299a383c5a"),
"matched" : "mango"
}
And the following query the element before the last element in the array; thus the element at index -2
db.baskets.aggregate(
[
{ "$project": { "matched": { "$arrayElemAt": [ "$fruits", -2 ] } } }
]
)
which yields:
{
"_id" : ObjectId("578f326f6db61a299a383c5a"),
"matched" : "apricot"
}
Another way to do this is to use the update array syntax. Here, contribs.1 sets the second element in the contribs array to have value ALGOL 58 (Taken from the manual page on update syntax)
db.bios.update(
{ _id: 1 },
{ $set: { 'contribs.1': 'ALGOL 58' } }
)