Removing the minimum element of a particular attribute type in MongoDB - mongodb

I have the following schemea in MongoDB:
{
"_id" : 100,
"name" : "John Doe",
"scores" : [
{
"type" : "exam",
"score" : 334.45023
},
{
"type" : "quiz",
"score" : 11.78273309957772
},
{
"type" : "homework",
"score" : 6.676176060654615
},
{
"type" : "homework",
"score" : 35.8740349954354
}
]
}
I am looking for a way to remove the homework with the least score. I have found a related answer here but, it doesn't help much as I need to find out only the 'homework' with th least score and remove it.
I am using MongoDB along with the PyMongo Driver.

you need to add match like:
myresults = scores.aggregate( [ { "$unwind": "$scores" }, { '$match': {'scores.type': "homework" } }, { "$group": { '_id':'$_id' , 'minitem': { '$min': "$scores.score" } } } ] )
for result in myresults['result']:
scores.update( { '_id': result['_id'] }, { '$pull': { 'scores': { 'score': result['minitem'] } } } )

I tried using native mongodb commands and it works.
Use the below 2 commands to make it work.
1)
cursor = db.students.aggregate([{ "$unwind": "$scores" }, { "$match": { "scores.type": "homework"}},{ "$group": {'_id': '$_id', 'minitem': {'$min':"$scores.score"}}}]), null
2)
cursor.forEach(function(coll) {db.students.update({'_id': coll._id}, {'$pull': {'scores': {'score': coll.minitem}}})})

Here is a solution using Python:
students = db.students
cursor = students.find({})
for doc in cursor:
hw_scores = []
for item in doc["scores"]:
if item["type"] == "homework":
hw_scores.append(item["score"])
hw_scores.sort()
hw_min = hw_scores[0]
students.update({"_id": doc["_id"]},
{"$pull":{"scores":{"score":hw_min}}})

I don't think it's possible using native mongodb commands. I think the best way of doing it would be to write a javascript function to drop the lowest score and run it on the server; this would have the advantage of being automic, so that a list couldn't be updated while you were removing from it, keeping things consistent.
Here's some documentation: http://docs.mongodb.org/manual/applications/server-side-javascript/

I followed the third answer here. Removing the minimum element of a particular attribute type in MongoDB
var MongoClient = require('mongodb').MongoClient;
MongoClient.connect('mongodb://localhost:27017/school', function(err, db) {
if(err) throw err;
var students = db.collection('students');
cursor = students.aggregate(
[{ "$unwind": "$scores" },
{ "$match": { "scores.type": "homework"}},
{ "$group": {'_id': '$_id', 'minitem': {
'$min': "$scores.score"
}
}
}
]);
cursor.forEach(
function(coll) {
students.update({
'_id': coll._id
}, {
'$pull': {
'scores': {
'score': coll.minitem
}
}
})
});
});

Related

how to lower case the value of unique:true keys in mongodb?

I have created a mongodb and by mistake have entered duplicate values in the form of capital and small case letters.
I have made the index unique. MongoDB is case sensitive and hence, considered the capital letter and small letter as different values.
Now my problem is the database have got around 32 GB. and I came across this issue. Kindly help me.
Here is the sample:
db.tt.createIndex({'email':1},{unique:true})
> db.tt.find().pretty()
{
"_id" : ObjectId("591d706c0ef9acde11d7af66"),
"email" : "g#gmail.com",
"src" : [
{
"acc" : "ln"
},
{
"acc" : "drb"
}
]
}
{
"_id" : ObjectId("591d70740ef9acde11d7af68"),
"email" : "G#gmail.com",
"src" : [
{
"acc" : "ln"
},
{
"acc" : "drb"
},
{
"acc" : "dd"
}
]
}
How I can make the email as lowercase and assign the src values to the original one. Kindly help me.
you can achive this using $toLower aggregation operator like this :
db.tt.aggregate([
{
$project:{
email:{
$toLower:"$email"
},
src:1
}
},
{
$unwind:"$src"
},
{
$group:{
_id:"$email",
src:{
$addToSet:"$src"
}
}
},
{
$project:{
_id:0,
email:"$_id",
src:1
}
},
{
$out:"anotherCollection"
}
])
$addToSet allow to keep oly one distinct occurence of src items
this will write this document to a new collection named anotherCollection:
{ "email" : "g#gmail.com", "src" : [ { "acc" : "dd" }, { "acc" : "drb" }, { "acc" : "ln" } ] }
Note that with $out, you can averwrite directly your tt collection, however before doing this make sure to understand what your doing because all previous data will be lost
The most efficient way I can think of to merge the data is run an aggregation and loop the result to write back to the collection in bulk operations:
var ops = [];
db.tt.aggregate([
{ "$unwind": "$src" },
{ "$group": {
"_id": { "$toLower": "$email" },
"src": { "$addToSet": "$src" },
"ids": { "$addToSet": "$_id" }
}}
]).forEach(doc => {
var id = doc.ids.shift();
ops = [
...ops,
{
"deleteMany": {
"filter": { "_id": { "$in": doc.ids } }
}
},
{
"updateOne": {
"filter": { "_id": id },
"update": {
"$set": { "email": doc._id },
"$addToSet": { "src": { "$each": doc.src } }
}
}
},
];
if ( ops.length >= 500 ) {
db.tt.bulkWrite(ops);
ops = [];
}
});
if ( ops.length > 0 )
db.tt.bulkWrite(ops);
In steps, that's $unwind the array items so they can be merged via $addToSet, under a $group on using $toLower on the email value. You also want to keep the set of unique source document ids.
In the loop you shift the first _id value off of doc.ids and update that document with the lowercase email and the revised "src" set. Using $addToSet here makes the operation write safe with any other updates that might occur to the document.
Then the other operation in the loop deletes the other documents that shared the same converted case email, so there are no duplicates. Actually do that one first. The default "ordered" operations make sure this is fine.
And do it in the shell, since it's a one-off operation and is really just as simple as listing as shown.

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

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.

select only subdocuments or arrays

{
"_id":{
"oid":"4f33bf69873dbc73a7d21dc3"
},
"country":"IND",
"states":[{
"name":"orissa",
"direction":"east",
"population":41947358,
"districts":[{
"name":"puri",
"headquarter":"puri",
"population":1498604
},
{
"name":"khordha",
"headquarter":"bhubaneswar",
"population":1874405
}
]
},
{
"name":"andhra pradesh",
"direction":"south",
"population":84665533,
"districts":[{
"name":"rangareddi",
"headquarter":"hyderabad",
"population":3506670
},
{
"name":"vishakhapatnam",
"headquarter":"vishakhapatnam",
"population":3789823
}
]
}
]
}
In above collection(i.e countries) i have only one document , and i want to fetch the details about a particular state (lets say "country.states.name" : "orissa" ) ,But i want my result as here under instead of entire document .Is there a way in Mogo...
{
"name": "orissa",
"direction": "east",
"population": 41947358,
"districts": [
{
"name": "puri",
"headquarter": "puri",
"population": 1498604
},
{
"name": "khordha",
"headquarter": "bhubaneswar",
"population": 1874405
}
]
}
Thanks
Tried this:
db.countries.aggregate(
{
"$project": {
"state": "$states",
"_id": 0
}
},
{
"$unwind": "$state"
},
{
"$group": {
"_id": "$state.name",
"state": {
"$first": "$state"
}
}
},
{
"$match": {
"_id": "orissa"
}
}
);
And got:
{
"result" : [
{
"_id" : "orissa",
"state" : {
"name" : "orissa",
"direction" : "east",
"population" : 41947358,
"districts" : [
{
"name" : "puri",
"headquarter" : "puri",
"population" : 1498604
},
{
"name" : "khordha",
"headquarter" : "bhubaneswar",
"population" : 1874405
}
]
}
}
],
"ok" : 1
You can't do it right now, but you will be able to with $unwind in the aggregation framework. You can try it now with the experimental 2.1 branch, the stable version will come out in 2.2, probably in a few months.
Any query in mongodb always return root document.
There is only one way for you to load one sub document with parent via $slice if you know ordinal number of state in nested array:
// skip ordinalNumberOfState -1, limit 1
db.countries.find({_id: 1}, {states:{$slice: [ordinalNumber -1 , 1]}})
$slice work in default order (as documents was inserted in nested array).
Also if you don't need fields from a country you can include only _id and states in result:
db.countries.find({_id: 1}, {states:{$slice: [ordinalNumber -1 , 1]}, _id: 1})
Then result document will looks like this one:
{
"_id":{
"oid":"4f33bf69873dbc73a7d21dc3"
},
"states":[{
"name":"orissa",
"direction":"east",
"population":41947358,
"districts":[{
"name":"puri",
"headquarter":"puri",
"population":1498604
},
{
"name":"khordha",
"headquarter":"bhubaneswar",
"population":1874405
}
]
}]
}
db.countries.find({ "states": { "$elemMatch": { "name": orissa }}},{"country" : 1, "states.$": 1 })
If you don't want to use aggregate, you can do it pretty easily at the application layer using underscore (included by default):
var country = Groops.findOne({"property":value);
var state _.where(country, {"state":statename});
This will give you the entire state record that matches statename. Very convenient.