"Field name duplication not allowed with modifiers" on update - mongodb

I get a "Field name duplication not allowed with modifiers" error while trying to update a field(s) in Mongo. An example:
> db.test.insert({test: "test1", array: [0]});
> var testFetch = db.test.findOne({test: "test1"});
> db.test.update(testFetch,
{$push: {array: 1}, //push element to end of key "array"
$pop: {array: -1} //pop element from the start of key "array"
});
Field name duplication not allowed with modifiers
Is there no way to perform this atomic operation? I don't want to be doing two separate updates for this.

There's an outstanding issue for this on Mongo's ticket system: http://jira.mongodb.org/browse/SERVER-1050
Looks like it's scheduled for this year. Your scenario is definitely a sensible scenario, but it's also tied to a bunch of edge cases. What if you $push and $pop on an empty array? What's expected? What do you want if you $push and $pull?
I don't want to be doing two separate updates for this.
I know that doing this really has "code smell", but is it a complete blocker for using this solution? Is the "double-update" going to completely destroy server performance?

Related

custom sort for a mongodb collection in meteor

I have this collection of products and i want to display a top 10 products based on a custom sort function
[{ _id: 1, title, tags:['a'], createdAt:ISODate("2016-01-28T00:00:00Z") } ,
{ _id: 2, title, tags:['d','a','e'], createdAt:ISODate("2016-01-24T00:00:00Z") }]
What i want to do is to sort it based on a "magic score" that can be calculated. For example, based on this formula: tag_count*5 - number_of_days_since_it_was_created.
If the first one is 1 day old, this makes the score:
[{_id:1 , score: 4}, {_id:2, score: 10}]
I have a few ideas on how i can achieve this, but i'm not sure how good they are, especially since i'm new to both mongo and meteor:
start an observer (Meteor.observe) and every time a document is
modified (or a new one created), recalculate the score and update it
on the collection itself. If i do this, i could just use $orderBy
where i need it.
after some reading i discovered that mongo aggregate or map_reduce
could help me achieve the same result, but as far as i found out,
meteor doesn't support it directly
sort the collection on the client side as an array, but using this
method i'm not sure how it will behave with pagination (considering that i subscribe to a limited number of documents)
Thank you for any information you can share with me!
Literal function sorting is just being implemented in meteor, so you should be able to do something like
Products.find({}, {sort: scoreComparator});
in an upcoming release.
You can use the transform property when creating collection. In this transform, store the magic operation as a function.
score=function(){
// return some score
};
transformer=function(product){
product.score=score;
// one could also use prototypal inheritance
};
Products=new Meteor.Collection('products',{transform:transformer});
Unfortunately, you cannot yet use the sort operator on virtual fields, because minimongo does not support it.
So the ultimate fall-back as you mentioned while nor the virtual field sorting nor the literate function sorting are supported in minimongo is client side sorting :
// Later, within some template
scoreComparator=function(prd1,prd2){
return prd1.score()-prd2.score();
}
Template.myTemplate.helpers({
products:function(){
return Products.find().fetch().sort(scoreComparator);
}
});
i'm not sure how it will behave with pagination (considering that i subscribe to a limited number of documents)
EDIT : the score will be computed among the subscribed documents, indeed.

MongoDB returning latest documents from each category in one find statement?

Let's say I have a status and a createdAt field and a few other fields like title and content.
The status field can take on open, closed, or pending values.
I basically want to return the 5 latest documents from each status value in one find statement. Is this possible? Also, can this be done without aggregate?
If so, how?
Try following
db.collectionName.find().sort({"_id":-1}).limit(5).pretty()
It return's latest five documents
Bad news: that's not possible with basic queries. For a simple reason: you can only limit on a complete result set. So even when you use a logic OR on the status field, the first twenty documents of your result set may well all be of status "closed". When you apply whatever limit on this result set, up to the first twenty documents will have the status "closed".
I am not even sure wether this can be done easily with aggregation, I have to think about that.
Markus right. Unfortunatelly, it's not possible to do it in one aggregation query due to the lack of slice operation in a project phase of an aggregation pipeline.
It means, that you could easily group documents by status like this:
db.collectionName.aggregate([
{ "$group": {
"_id": "$status",
"docs": { "$push": "$title" }
}}
]);
BUT there is not way to slice resulting sub-arrays during this aggregation. This feature was requested, but not implemented yet.
You could slice it after aggregation using a separate command. Or you always have an option to use Map-Reduce.
Another option is to iterate over statuses in your application and query documents by each status. I would go with this solution in your scenario, because you have just 3 different statuses. It means only 3 queries. It is not fatal(especially if you have index on status field).

MongoDB - no duplicate entries for a key

First of all, I've read this thread already and it didn't really help me on this particular problem. I'm also new to MongoDB.
I have a document in my db.songs collection:
{
"title" : "Ignorance"
"artist" : "Paramore"
"listeners" : ["John", "Bill", "Amber"]
}
I want enforce no duplicates on the users key, such that whenever I push "John" or an existing user, I get an error. Can I do this in mongo shell, and if so how can I configure my collection to employ this behavior?
Some example code that should give me a duplicate error (or some similar error):
db.songs.update({title:"Ignorance"}, {'$push':{listeners:"John"}})
Thank you in advance.
db.songs.ensureIndex({listeners:1},{unique:true})
Adding this index will not work. MongoDB will not ensure uniqueness within the subdocument using a unique index, instead it will do it collection wide. That is quite possibly why you are getting errors u8sing that.
Instead what you want to do is use something that will add the item to the "set" of items, that is where $addToSet ( http://docs.mongodb.org/manual/reference/operator/update/addToSet/ ) comes in.
Drop your index and use that operator and it should work.

Iterating over distinct items in one field in MongoDB

I have a very large collection (~7M items) in MongoDB, primarily consisting of documents with three fields.
I'd like to be able to iterate over all the unique values for one of the fields, in an expedient manner.
Currently, I'm querying for just that field, and then processing the returned results by iterating on the cursor for uniqueness. This works, but it's rather slow, and I suspect there must be a better way.
I know mongo has the db.collection.distinct() function, but this is limited by the maximum BSON size (16 MB), which my dataset exceeds.
Is there any way to iterate over something similar to the db.collection.distinct(), but using a cursor or some other method, so the record-size limit isn't as much of an issue?
I think maybe something like the map/reduce functionality would possibly be suited for this kind of thing, but I don't really understand the map-reduce paradigm in the first place, so I have no idea what I'm doing. The project I'm working on is partially to learn about working with different database tools, so I'm rather inexperienced.
I'm using PyMongo if it's relevant (I don't think it is). This should be mostly dependent on MongoDB alone.
Example:
For this dataset:
{"basePath" : "foo", "internalPath" : "Neque", "itemhash": "49f4c6804be2523e2a5e74b1ffbf7e05"}
{"basePath" : "foo", "internalPath" : "porro", "itemhash": "ffc8fd5ef8a4515a0b743d5f52b444bf"}
{"basePath" : "bar", "internalPath" : "quisquam", "itemhash": "cf34a8047defea9a51b4a75e9c28f9e7"}
{"basePath" : "baz", "internalPath" : "est", "itemhash": "c07bc6f51234205efcdeedb7153fdb04"}
{"basePath" : "foo", "internalPath" : "qui", "itemhash": "5aa8cfe2f0fe08ee8b796e70662bfb42"}
What I'd like to do is iterate over just the basePath field. For the above dataset, this means I'd iterate over foo, bar, and baz just once each.
I'm not sure if it's relevant, but the DB I have is structured so that while each field is not unique, the aggregate of all three is unique (this is enforced with an index).
The query and filter operation I'm currently using (note: I'm restricting the query to a subset of the items to reduce processing time):
self.log.info("Running path query")
itemCursor = self.dbInt.coll.find({"basePath": pathRE}, fields={'_id': False, 'internalPath': False, 'itemhash': False}, exhaust=True)
self.log.info("Query complete. Processing")
self.log.info("Query returned %d items", itemCursor.count())
self.log.info("Filtering returned items to require uniqueness.")
items = set()
for item in itemCursor:
# print item
items.add(item["basePath"])
self.log.info("total unique items = %s", len(items))
Running the same query with self.dbInt.coll.distinct("basePath") results in OperationFailure: command SON([('distinct', u'deduper_collection'), ('key', 'basePath')]) failed: exception: distinct too big, 16mb cap
Ok, here is the solution I wound up using. I'd add it as an answer, but I don't want to detract from the actual answers that got me here.
reStr = "^%s" % fqPathBase
pathRE = re.compile(reStr)
self.log.info("Running path query")
pipeline = [
{ "$match" :
{
"basePath" : pathRE
}
},
# Group the keys
{"$group":
{
"_id": "$basePath"
}
},
# Output to a collection "tmp_unique_coll"
{"$out": "tmp_unique_coll"}
]
itemCursor = self.dbInt.coll.aggregate(pipeline, allowDiskUse=True)
itemCursor = self.dbInt.db.tmp_unique_coll.find(exhaust=True)
self.log.info("Query complete. Processing")
self.log.info("Query returned %d items", itemCursor.count())
self.log.info("Filtering returned items to require uniqueness.")
items = set()
retItems = 0
for item in itemCursor:
retItems += 1
items.add(item["_id"])
self.log.info("Recieved items = %d", retItems)
self.log.info("total unique items = %s", len(items))
General performance compared to my previous solution is about 2X in terms of wall-clock time. On a query that returns 834273 items, with 11467 uniques:
Original method(retreive, stuff into a python set to enforce uniqueness):
real 0m22.538s
user 0m17.136s
sys 0m0.324s
Aggregate pipeline method :
real 0m9.881s
user 0m0.548s
sys 0m0.096s
So while the overall execution time is only ~2X better, the aggregation pipeline is massively more performant in terms of actual CPU time.
Update:
I revisited this project recently, and rewrote the DB layer to use a SQL database, and everything was much easier. A complex processing pipeline is now a simple SELECT DISTINCT(colName) WHERE xxx operation.
Realistically, MongoDB and NoSQL databases in general are vary much the wrong database type for what I'm trying to do here.
From the discussion points so far I'm going to take a stab at this. And I'm also noting that as of writing, the 2.6 release for MongoDB should be just around the corner, good weather permitting, so I am going to make some references there.
Oh and the FYI that didn't come up in chat, .distinct() is an entirely different animal that pre-dates the methods used in the responses here, and as such is subject to many limitations.
And this soltion is finally a solution for 2.6 up, or any current dev release over 2.5.3
The alternative for now is use mapReduce because the only restriction is the output size
Without going into the inner workings of distinct, I'm going to go on the presumption that aggregate is doing this more efficiently [and even more so in upcoming release].
db.collection.aggregate([
// Group the key and increment the count per match
{$group: { _id: "$basePath", count: {$sum: 1} }},
// Hey you can even sort it without breaking things
{$sort: { count: 1 }},
// Output to a collection "output"
{$out: "output"}
])
So we are using the $out pipeline stage to get the final result that is over 16MB into a collection of it's own. There you can do what you want with it.
As 2.6 is "just around the corner" there is one more tweak that can be added.
Use allowDiskUse from the runCommand form, where each stage can use disk and not be subject to memory restrictions.
The main point here, is that this is nearly live for production. And the performance will be better than the same operation in mapReduce. So go ahead and play. Install 2.5.5 for you own use now.
A MapReduce, in the current version of Mongo would avoid the problems of the results exceeding 16MB.
map = function() {
if(this['basePath']) {
emit(this['basePath'], 1);
}
// if basePath always exists you can just call the emit:
// emit(this.basePath);
};
reduce = function(key, values) {
return Array.sum(values);
};
For each document the basePath is emitted with a single value representing the count of that value. The reduce simply creates the sum of all the values. The resulting collection would have all unique values for basePath along with the total number of occurrences.
And, as you'll need to store the results to prevent an error using the out option which specifies a destination collection.
db.yourCollectionName.mapReduce(
map,
reduce,
{ out: "distinctMR" }
)
#Neil Lunn 's answer could be simplified:
field = 'basePath' # Field I want
db.collection.aggregate( [{'$project': {field: 1, '_id': 0}}])
$project filters fields for you. In particular, '_id': 0 filters out the _id field.
Result still too large? Batch it with $limit and $skip:
field = 'basePath' # Field I want
db.collection.aggregate( [{'$project': {field: 1, '_id': 0}}, {'$limit': X}, {'$skip': Y}])
I think the most scalable solution is to perform a query for each unique value. The queries must be executed one after the other, and each query will give you the "next" unique value based on the previous query result. The idea is that the query will return you one single document, that will contain the unique value that you are looking for. If you use the proper projection, mongo will just use the index loaded into memory without having to read from disk.
You can define this strategy using $gt operator in mongo, but you must take into account values like null or empty strings, and potentially discard them using the $ne or $nin operator. You can also extend this strategy using multiple keys, using operators like $gte for one key and $gt for the other.
This strategy should give you the distinct values of a string field in alphabetical order, or distinct numerical values sorted ascendingly.

How to set array length after updating it via $addToSet in mongodb?

Document structure looks like this,
{
blacklists:[] // elements should be unique
blacklistsLength:0 // length of blacklists
}
Adding sets of value to blacklists is easy.
db.posts.update({_id:...}, {$addtoSet:{blacklists:{$each:['peter', 'bob', 'steven']}}});
But How can I update blacklistLength at the same time to reflect the changes?
This is not possible. Either you have
Update the length seperately using a subsequent findAndModify
command or
You can do it per name and rewrite the query using a negation in
your criteria and $push rather than $addToSet (not necessarily
needed but a lot faster with large blacklists since addToSet is
always o(n) regardless of indexes) :
db.posts.update({_id:..., blacklists:{$ne:'peter'}}, {$push:{blacklists:{'peter'}},$inc:{blacklistsLength: 1}});
The latter being perfectly safe since the list and the length are adjusted atomically but obviously has slightly degraded performance. Since it also has the benefit of better overall performance due to the $push versus $addToSet performance issue on large arrays (and blacklists tend to become huge and remember that the $push version of the update uses an index on blacklist in the update criteria while $addToSet will NOT use an index during it's set scan) it is generally the best solution.
Would the following not work?
db.posts.update({_id:...}, {
$addtoSet:{blacklists:{$each:['peter', 'bob', 'steven']}},
$set: {blacklistsLength: ['peter', 'bob', 'steven'].length}
});
I had a similar problem, please see the discussion here: google groups mongo
As you can notice, following to this discussion, a bug was open:
Mongo Jira
As you upsert items into the database, simply query the item to see if it's in your embedded array. That way, you're avoiding pushing duplicate items, and only incrementing the counter as you add new items.
q = {'blacklists': {'$nin': ['blacklist_to_insert'] }}
u = {
'$push' : {'blacklists': { 'blacklist_to_insert' } },
'$inc' : {'total_blacklists': 1 }
}
o = { 'upsert' : true }
db.posts.update(q,u,o)