Query to match a non-empty array with a certain value - mongodb

For example
> db.test.insert( { 'a':5 } )
> db.test.insert( { 'a': [5] } )
> db.test.find({ 'a':5})
{ "_id" : ObjectId("53e2b4366c9ef5cceb327e01"), "a" : 5 }
{ "_id" : ObjectId("53e2b43b6c9ef5cceb327e02"), "a" : [ 5 ] }
But, I only want to be able to match the first document.

The simplest way is to test for the presence of an array element using $exists and "dot notation":
db.test.find({ "a": 5, "a.0": { "$exists": false } })
Says find "a" equal to 5, but the first array element in "a" cannot exist.

Related

Insert new fields to document at given array index in MongoDB

I have the following document structure in a MongoDB collection :
{
"A" : [ {
"B" : [ { ... } ]
} ]
}
I'd like to update this to :
{
"A" : [ {
"B" : [ { ... } ],
"x" : [],
"y" : { ... }
} ]
}
In other words, I want the "x" and "y" fields to be added to the first element of the "A" array without loosing "B".
Ok as there is only one object in A array you could simply do as below :
Sample Collection Data :
{
"_id" : ObjectId("5e7c3cadc16b5679b4aeec26"),
A:[
{
B: [{ abc: 1 }]
}
]
}
Query :
/** Insert new fields into 'A' array's first object by index 0 */
db.collection.updateOne(
{ "_id" : ObjectId("5e7c3f77c16b5679b4af4caf") },
{ $set: { "A.0.x": [] , "A.0.y" : {abcInY :1 }} }
)
Output :
{
"_id" : ObjectId("5e7c3cadc16b5679b4aeec26"),
"A" : [
{
"B" : [
{
"abc" : 1
}
],
"x" : [],
"y" : {
"abcInY" : 1.0
}
}
]
}
Or Using positional operator $ :
db.collection.updateOne(
{ _id: ObjectId("5e7c3cadc16b5679b4aeec26") , 'A': {$exists : true}},
{ $set: { "A.$.x": [] , "A.$.y" : {abcInY :1 }} }
)
Note : Result will be the same, but functionally when positional operator is used fields x & y are inserted to first object of A array only when A field exists in that documents, if not this positional query would not insert anything (Optionally you can check A is an array condition as well if needed). But when you do updates using index 0 as like in first query if A doesn't exist in document then update would create an A field which is an object & insert fields inside it (Which might cause data inconsistency across docs with two types of A field) - Check below result of 1st query when A doesn't exists.
{
"_id" : ObjectId("5e7c3f77c16b5679b4af4caf"),
"noA" : 1,
"A" : {
"0" : {
"x" : [],
"y" : {
"abcInY" : 1.0
}
}
}
}
However, I think I was able to get anothe#whoami Thanks for the suggestion, I think your first solution should work. However, I think I was able to get another solution to this though I'm not sure if its better or worse (performance wise?) than what you have here. My solution is:
db.coll.update( { "_id" : ObjectId("5e7c4eb3a74cce7fd94a3fe7") }, [ { "$addFields" : { "A" : { "x" : [ 1, 2, 3 ], "y" : { "abc" } } } } ] )
The issue with this is that if "A" has more than one array entry then this will update all elements under "A" which is not something I want. Just out of curiosity is there a way of limiting this solution to only the first entry in "A"?

Sum different fields using aggregation in MongoDB

I have a document schema in a test collection like this:
{
"_id" : NumberLong("A unique number"),
"text" : {
"characters_map" : {
"a" : 4,
"f" : 3,
"b" : 6,
...
"o" : 3
}
...
}
...
}
I want to count the sum of each character in the whole collection. I tried using the aggregation framework, but it doesn't seem I do it correct.
I began by calculating each character separately ( i.e the 'a'), but with no luck. My best approach is:
> db.test.aggregate([
{ "$group" : {
_id : { a_count : "$text.characters_map.a" },
counter : { "$sum" : "$text.characters_map.a" }
}
}
])
with a_count representing the name of the id.
which results to:
{ "_id" : { "a_map" : 8 }, "counter" : 8 }
{ "_id" : { "a_map" : 5 }, "counter" : 5 }
{ "_id" : { "a_map" : 7 }, "counter" : 21 }
...
Not false entirely, but not what I want. These results mean I have 1 document with a field of: { "a" : 8, ... }, another 1 document with: { "a" : 5, ... } and 3 documents with: { "a" : 7, ... }.
Is there any way to group these fields together at "a" level? Is my approach completely wrong?
You have to change your structure to something like below.(Preferred)
[{
"text" : { "characters_map" : [{"k":"a", "v":4}, {"k":"b", "v":8}, {"k":"c", "v":5}, {"k":"d", "v":4 }] }
}]
You can use aggregation the way you tried in your post.
db.test.aggregate([
{$unwind:"$text.characters_map"},
{$group:{_id:"$text.characters_map.k", counter:{$sum:"$text.characters_map.v"}}}
])
This is the solution if you can't change your structure.
You can use 3.4.4 version and use $objectToArray & $arrayToObject to switch between dynamic keys and label value pair.
Stage 1: Convert the dynamic characters_map keys into label value pair.
Stage 2 & 3: $unwind & $group on the characters and sum their counts.
Stage 4 & 5: Group back the output from last grouping into array of key value pairs followed by $arrayToObject to convert the key value pairs into dynamic keys.
db.test.aggregate([
{$addFields: {"text.characters_map": {$objectToArray: "$text.characters_map"}}},
{$unwind:"$text.characters_map"},
{$group:{_id:"$text.characters_map.k", counter:{$sum:"$text.characters_map.v"}}},
{$group:{_id:null, arraykeyval:{$push:{k:"$_id", v:"$counter"}}}},
{$project:{result: {$arrayToObject:"$arraykeyval"}}}
])

mongodb findAndModify update element in array

There is an bson document:
{
"_id" : ObjectId("5718441f5116a60b08000b8c"),
"mails" : [
{
"id" : 2,
"a" : [
{
"a" : 1
},
{
"a" : 2
}
]
},
{
"id" : 1,
"a" : [
{
"a" : 1
},
{
"a" : 2
}
]
}
]
}
I need to return and clear the array "a" which belong to "mails.id == x" for given document. So I use findAndModify like:
db.mail.findAndModify({query: {"_id":ObjectId("5718441f5116a60b08000b8c")}, update: {$set:{"mails.$.a":[]}}, new: false, fields:{"mails":{$elemMatch:{"id":1}}}})
However this don't work. The problem is the $set should apply on one document in array rather than the whole document. So I need a projection to project it out.
If I left update to blank, it will return the desired part:
{
"_id" : ObjectId("5718441f5116a60b08000b8c"),
"mails" : [
{
"id" : 1,
"a" : [
{
"a" : 1
},
{
"a" : 2
}
]
}
]
}
But I don't know how to clear the array 'a' in 'mails'
You have to specify array element match in the query:
db.mail.findAndModify({query: {"_id":ObjectId("5718441f5116a60b08000b8c"), "mails":{$elemMatch:{"id":1}}}, update: {$set:{"mails.$.a":[]}}, new: false, fields:{"mails":{$elemMatch:{"id":1}}}})
mails.$ in you update matches the first matched element in the doc, so you have to match it in the query. Also, this query will update the doc, but it will return the old version, since you use new: false, if you want to get the updated version set it to true.

MongoDB, how to use document as the smallest unit to search the document in array?

Sorry for the title, but I really do not know how to make it clear. But I can show you.
Here I have insert two document
> db.test.find().pretty()
{
"_id" : ObjectId("557faa461ec825d473b21422"),
"c" : [
{
"a" : 3,
"b" : 7
}
]
}
{
"_id" : ObjectId("557faa4c1ec825d473b21423"),
"c" : [
{
"a" : 1,
"b" : 3
},
{
"a" : 5,
"b" : 9
}
]
}
>
I only want to select the first document with a value which is greater than 'a' and smaller than 'b', like '4'.
But when i search, i cannot get the result i want
> db.test.find({'c.a': {$lte: 4}, 'c.b': {$gte: 4}})
{ "_id" : ObjectId("557faa461ec825d473b21422"), "c" : [ { "a" : 3, "b" : 7 } ] }
{ "_id" : ObjectId("557faa4c1ec825d473b21423"), "c" : [ { "a" : 1, "b" : 3 }, { "a" : 5, "b" : 9 } ] }
>
Because '4' is greater than the '"a" : 1' and smaller than '"b" : 9' in the second document even it is not in the same document in the array, so the second one selected.
But I only want the first one selected.
I found this http://docs.mongodb.org/manual/reference/operator/query/elemMatch/#op._S_elemMatch, but it seems the example is not suitable for my situation.
You would want to
db.test.findOne({ c: {$elemMatch: {a: {$lte: 4}, b: {$gte: 4} } } })
With your query, you are searching for documents that have an object in the 'c' array that has a key 'a' with a value <= 4, and a key 'b' with a value >= 4.
The second record is return because c[0].a is <= 4, and c[1].b is >= 4.
Since you specified you wanted to select only the first document, you would want to do a findOne() instead of a find().
Use $elemMatch as below :
db.test.find({"c":{"$elemMatch":{"a":{"$lte":4},"b":{"$gte":4}}}})
Or
db.test.find({"c":{"$elemMatch":{"a":{"$lte":4},"b":{"$gte":4}}}},{"c.$":1})

mongodb $elemMatch

According to mongodb doc, syntax for $elemMatch would be,
t.find( { x : { $elemMatch : { a : 1, b : { $gt : 1 } } } } )
I have tried and it works fine.
The above means that, it can find if an object {a:1, b:'more than 1'} exist in the array x.
I have a requirement, where I need to figure out, if all the objects in an array exist in the database or not.
for example, let's say I have an array,
a=[{a:1, b:2},{a:3, b:4}, {a:5, b:6}]
and I need to find out if x contains all of them.
t.find( { x : { $elemMatch : { a : {$all:[1]}, b : {$all:[2]} } } } ) and it finds out all x containing {a:1, b:2}
But if I try, t.find( { x : { $elemMatch : { a : {$all:[1,3]}, b : {$all:[2,4]} } } } ), it fails. I know this is not correct.
Is there any way I can achieve this ?
Ideallt, it should be,
t.find( { x : { $elemMatch : {$all:[ {a:1, b:2}, {a:3, b:4}] } } )
I tried, it does not work.
t.find({$and:[{a:{$elemMatch:{a:1, b:2}}}, {a:{$elemMatch:{a:3, b:4}}}, {a:{$elemMatch:{a:5, b:6}}}]})
It isn't a particularly high performance option though.
You can not use elemMatch for this, but you can simply just create a query which checks whether a matches the whole array:
db.items.insert({ 'foo' : 1, 'a' : [{a:1, b:2},{a:3, b:4}, {a:5, b:6}]});
db.items.insert({ 'foo' : 1, 'a' : [{a:1, b:2},{a:3, b:4}, {a:8, b:7}]});
db.items.find({'a': [{a:1, b:2},{a:3, b:4}, {a:8, b:7}]});
{ "_id" : ObjectId("4f3391856e196eca5eaa7518"), "foo" : 1, "a" : [ { "a" : 1, "b" : 2 }, { "a" : 3, "b" : 4 }, { "a" : 8, "b" : 7 } ] }
However, for this to work the order of the elements in the array need to be the same for the document and the query. The following will not find anything:
db.items.find({'a': [{a:3, b:4},{a:1, b:2}, {a:8, b:7}]});
(Because {a:3, b:4} and {a:1, b:2} are swapped).
How about this:
db.items.find({x : { $all: [
{$elemMatch: {a: 1, b: 2}},
{$elemMatch: {a: 3, b: 4}},
{$elemMatch: {a: 5, b: 6}}
]}}
Check out the Mongo docs.
Also, note the docs warning:
In the current release, queries that use the $all operator must scan
all the documents that match the first element in the query array. As
a result, even with an index to support the query, the operation may
be long running, particularly when the first element in the array is
not very selective.