Retrieving the length of a list in MongoDB - mongodb

I'm trying to do a mongo query where I get the length of an array in each document, without retrieving the full contents of the list. Ideally, this would be a projection option along these lines:
db.log.find({},{entries:{$length: 1}})
but this isn't supported. Maybe this is possible in an elegant way with the new aggregation framework? What I've come up with is this:
db.log.find({},{"entries.length": 1})
Which returns results like this:
{ "_id" : ObjectId("50d2fb07e64cfa55431de693"), "entries" : [ { }, { }, { }, { }, { }, { }, { }, { }, { }, { }, { }, { }, { }, { }, { }, { }, { }, { } ] }
This is ugly but basically serves my needs since I can count the length of this list without the network weight of getting the full contents. But I have no idea why this works. What is this query actually doing?

Now, I could think in two approachs:
1) Using aggregation framework:
db.log.aggregate([ { $unwind : "$entries" }, { $group : { _id : "$_id", entries : {$sum:1} } } ]);
2) Or you can add a field to the document that holds the entries count. So, each time that you push a new value to entries array, you must increment the counter. The update will be like this:
db.log.update({ _id : 123 }, { $push : { entries : 'value' }, $inc : { entriesCount : 1 } })
Clearly, you have a trade-off here: the aggregation framework is too expensive for this simple operation. But adding a field to document, every update should increment the counter.
IMHO, the counter looks more reasonable, though it looks a workaround.

According to the mongodb documentation:
You can use as well $size:
db.log.aggregate([{$project:{'_id':1, 'count':{$size: "$entriesCount"}}}]);

Related

Return all fields MongoDB Aggregate

I tried searching on here but couldn't really find what I need. I have documents like this:
{
appletype:Granny,
color:Green,
datePicked:2015-01-26,
dateRipe:2015-01-24,
numPicked:3
},
{
appletype:Granny,
color:Green,
datePicked:2015-01-01,
dateRipe:2014-12-28,
numPicked:6
}
I would like to return only those apples picked latest, will all fields. I want my query to return me the first document only essentially. When I try to do:
db.collection.aggregate([
{ $match : { "appletype" : "Granny" } },
{ $sort : { "datePicked" : 1 } },
{ $group : { "_id" : { "appletype" : "$appletype" },
"datePicked" : { $max : "$datePicked" } },
])
It does return me all the apples picked latest, however with only appletype:Granny and datePicked:2015-01-26. I need the remaining fields. I tries using $project and adding all the fields, but it didn't get me what I needed. Also, when I added the other fields to the group, since datePicked is unique, it returned both records.
How can I go about returning all fields, for only the latest datePicked?
Thanks!
From your description, it sounds like you want one document for each of the types of apple in your collection and showing the document with the most recent datePicked value.
Here is an aggregate query for that:
db.collection.aggregate([
{ $sort: { "datePicked": -1 },
{ $group: { _id: "$appletype", color: { $first: "$color" }, datePicked: { $first: "$datePicked" }, dateRipe: { $first: "$dateRipe" }, numPicked: { $first: "$numPicked" } } },
{ $project: { _id: 0, color: 1, datePicked: 1, dateRipe: 1, numPicked: 1, appletype: "$_id" } }
])
But then based on the aggregate query you've written, it looks like you're trying to get this:
db.collection.find({appletype: "Granny"}).sort({datePicked: -1}).limit(1);

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

way to update multiple documents with different values

I have the following documents:
[{
"_id":1,
"name":"john",
"position":1
},
{"_id":2,
"name":"bob",
"position":2
},
{"_id":3,
"name":"tom",
"position":3
}]
In the UI a user can change position of items(eg moving Bob to first position, john gets position 2, tom - position 3).
Is there any way to update all positions in all documents at once?
You can not update two documents at once with a MongoDB query. You will always have to do that in two queries. You can of course set a value of a field to the same value, or increment with the same number, but you can not do two distinct updates in MongoDB with the same query.
You can use db.collection.bulkWrite() to perform multiple operations in bulk. It has been available since 3.2.
It is possible to perform operations out of order to increase performance.
From mongodb 4.2 you can do using pipeline in update using $set operator
there are many ways possible now due to many operators in aggregation pipeline though I am providing one of them
exports.updateDisplayOrder = async keyValPairArr => {
try {
let data = await ContestModel.collection.update(
{ _id: { $in: keyValPairArr.map(o => o.id) } },
[{
$set: {
displayOrder: {
$let: {
vars: { obj: { $arrayElemAt: [{ $filter: { input: keyValPairArr, as: "kvpa", cond: { $eq: ["$$kvpa.id", "$_id"] } } }, 0] } },
in:"$$obj.displayOrder"
}
}
}
}],
{ runValidators: true, multi: true }
)
return data;
} catch (error) {
throw error;
}
}
example key val pair is: [{"id":"5e7643d436963c21f14582ee","displayOrder":9}, {"id":"5e7643e736963c21f14582ef","displayOrder":4}]
Since MongoDB 4.2 update can accept aggregation pipeline as second argument, allowing modification of multiple documents based on their data.
See https://docs.mongodb.com/manual/reference/method/db.collection.update/#modify-a-field-using-the-values-of-the-other-fields-in-the-document
Excerpt from documentation:
Modify a Field Using the Values of the Other Fields in the Document
Create a members collection with the following documents:
db.members.insertMany([
{ "_id" : 1, "member" : "abc123", "status" : "A", "points" : 2, "misc1" : "note to self: confirm status", "misc2" : "Need to activate", "lastUpdate" : ISODate("2019-01-01T00:00:00Z") },
{ "_id" : 2, "member" : "xyz123", "status" : "A", "points" : 60, "misc1" : "reminder: ping me at 100pts", "misc2" : "Some random comment", "lastUpdate" : ISODate("2019-01-01T00:00:00Z") }
])
Assume that instead of separate misc1 and misc2 fields, you want to gather these into a new comments field. The following update operation uses an aggregation pipeline to:
add the new comments field and set the lastUpdate field.
remove the misc1 and misc2 fields for all documents in the collection.
db.members.update(
{ },
[
{ $set: { status: "Modified", comments: [ "$misc1", "$misc2" ], lastUpdate: "$$NOW" } },
{ $unset: [ "misc1", "misc2" ] }
],
{ multi: true }
)
Suppose after updating your position your array will looks like
const objectToUpdate = [{
"_id":1,
"name":"john",
"position":2
},
{
"_id":2,
"name":"bob",
"position":1
},
{
"_id":3,
"name":"tom",
"position":3
}].map( eachObj => {
return {
updateOne: {
filter: { _id: eachObj._id },
update: { name: eachObj.name, position: eachObj.position }
}
}
})
YourModelName.bulkWrite(objectToUpdate,
{ ordered: false }
).then((result) => {
console.log(result);
}).catch(err=>{
console.log(err.result.result.writeErrors[0].err.op.q);
})
It will update all position with different value.
Note : I have used here ordered : false for better performance.

Aggregation in MongoDB, using unwind

I need to aggregate all tags from records like this:
https://gist.github.com/sbassi/5642925
(there are 2 sample records in this snippet) and sort them by size (first the tag that appears with more frequency). But I don't want to take into account data that have specific "user_id" (lets say, 2,3,6 and 12).
Here is my try (just the aggregation, without filtering and sorting):
db.user_library.aggregate( { $unwind : "$annotations.data.tags" }, {
$group : { _id : "$annotations.data.tags" ,totalTag : { $sum : 1 } } }
)
And I got:
{ "result" : [ ], "ok" : 1 }
Right now you can't unwind an array that is nested inside another array. See SERVER-6436
Consider structuring the data differently, having an array field with all tags for that document or possibly unwinding annotations and then unwinding annotations.data.tags in a stacked unwind like this:
db.user_library.aggregate([
{ $project: { 'annotations.data.tags': 1 } },
{ $unwind: '$annotations' },
{ $unwind: '$annotations.data.tags' },
{ $group: { _id: '$annotations.data.tags', totalTag: { $sum: 1 } } }
])

Filter based on root document property

This is the documents structure:
{
'_id' : ObjectId('56be1b51a0f4c8591f37f62a'),
'name': 'Bob',
'sub_users': [{'_id' : ObjectId('56be1b51a0f4c8591f37f62a')}]
}
{
'_id' : ObjectId('56be1b51a0f4c8591f37f62b'),
'name': 'Alice',
'sub_users': [{'_id' : ObjectId('56be1b51a0f4c8591f37f62a')}]
}
The sub_users array is used basically to link accounts, in the example Alice is Bob's manager since she has him as a sub_user. Bob has his own id in the sub_users array and this is wrong (no one really is his own boss).
I want to find all the Bobs, it feels like a simple query but I can't find the way to do it, or to even to google it properly, tried this (probably knowing it wouldn't work);
db.users.aggregate([
{ $group: { _id: '_id' } },
{ $match: { sub_users: { $elemMatch: { _id: '$$ROOT._id' } } } }
])
And it didn't worked, so the question is; how to find a document whose nested documents have the same value as the root element (for a certain field)?
To get there I'm using compare expression - please see example below:
db.users.aggregate([{
$unwind : "$sub_users"
}, //have all ids on same level
{
$project : {
_id : 1,
name : 1,
sameId : {
$cmp : ["$_id", "$sub_users._id"]
},
}
}, {
$match : {
sameId : 0
}
}
])