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

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

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

How do I match an array of sub-documents in MongoDB?

Match documents if a value in an array of sub-documents is greater than some value only if the same document contains a field that is equal to some value
I have a collection that contains documents with an array of sub-documents. This array of sub-documents contains a field that dictates whether or not I can filter the documents in the collection based on another field in the sub-document. This'll make more sense when you see an example of the document.
{
"_id":"ObjectId('XXX')",
"Data":{
"A":"",
"B":"-25.78562 ; 28.35629",
"C":"165"
},
"SubDocuments":[
{
"_id":"ObjectId('XXX')",
"Data":{
"Value":"XXX",
"DataFieldId":"B"
}
},
{
"_id":"ObjectId('XXX')",
"Data":{
"Value":"",
"DataFieldId":"A"
}
},
{
"_id":"ObjectId('XXX')",
"Data":{
"Value":"105",
"DataFieldId":"Z"
}
}
]
}
I only want to match documents that contain sub-documents with a DataFieldId that is equal to Z but also filter for Values that are greater than 105 only if Data Field Id is equal to Z.
Try as below:
db.collection.aggregate([
{
$project: {
_id:1,
Data:1,
filteredSubDocuments: {
$filter: {
input: "$SubDocuments",
as: "subDoc",
cond: {
$and: [
{ $eq: ["$$subDoc.Data.DataFieldId", "Z"] },
{ $gte: ["$$subDoc.Data.Value", 105] }
]
}
}
}
}
}
])
Resulted response will be:
{
"_id" : ObjectId("5cb09659952e3a179190d998"),
"Data" : {
"A" : "",
"B" : "-25.78562 ; 28.35629",
"C" : "165"
},
"filteredSubDocuments" : [
{
"_id" : "ObjectId('XXX')",
"Data" : {
"Value" : 105,
"DataFieldId" : "Z"
}
}
]
}
This can be done by using the $elemMatch operator on sub-documents, for details you can click on provided link. For your problem you can try below query by using $elemMatch which is match simpler than aggregation:
db.collectionName.find({
"SubDocuments": {
$elemMatch: {
"Data.DataFieldId": "Z" ,
"Data.Value" : {$gte: 105}
}
} })
Its working fine, I have verified it locally, one modification you required is that you have to put the value of SubDocuments.Data.Value as Number or Long as per your requirements.

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

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