Mongoid strange query result - mongodb

I'm using MongoDB through Mongoid with Rails 3 and observe this strange behavior when doing query in rails console:
> Table.where(:field => {"$exists" => true}).count
=> 3735
> Table.where(:field => {"$exists" => true}, :field => {"$ne" => ""}).count
=> 14878 # wtf???
> Table.where(:field => {"$exists" => true}, :field => "").count
=> 0 # at least it's not negative
> Table.where(:field => {"$exists" => false}).count
=> 11143
Since 11143 + 3735 = 14878, I assume that where(:field => {"$exists" => true}, :field => {"$ne" => ""}) also counts those records in which :field is not present (because nil != ""?). However, I believed conditions listed in #where would be joined with and, so it should match only those records where :field is not empty string AND is present.

You say "However, I believed conditions listed in #where would be joined with 'and'," but this is not correct. The conditions are a hash, and you have a collision on the key :field. Ruby silently uses the last value.
Please review the documentation for selection in Mongoid http://mongoid.org/en/origin/docs/selection.html, and use #and for a proper 'and' conjunction. Note that you can #inspect your query and examine the returned Criteria object. For example:
puts Table.where(:field => {"$exists" => true}, :field => {"$ne" => ""}).inspect
Hope that this helps.

Related

Find query with and operator in PHP

Hi i am working on backend of web application & want to find the documents from mongodb database that contain key active_status with value set to both 1 & 2. With mongodb PHP i am confused of how to find with both parameters in single query.
My query was this:
$mongoDb = MongoDbConnector::getCollection("endusers");
$endUserData = $mongoDb->find(array('active_status' => 1, '$and' => array('active_status' => 2)));
I have to fetch the users whose active_status should be 1 & 2. The above query doesnt seems to work. What is the right one for that?
Thanks on advance for quick response.
You have $and the wrong way around. Both arguments need to be included:
$endUserData = $mongoDb->find(array(
'$and' => array(
array( 'active_status' => 1 )
array( 'active_status' => 2 )
)
));
And since that would only make sense when looking for both elements within an array element, then you should instead use $all, which is shorter syntax:
$endUserData = $mongoDb->find(array(
'active_status' => array( '$all' => array(1,2) )
));
I should add that unless you intend to match a document like this:
{ "active_status" => [1,2] }
The you do not in fact want $and at all, but rather you want $or or better yet $in for multiple possible values on the same field:
$endUserData = $mongoDb->find(array(
'active_status' => array( '$in' => array(1,2) )
));
This matches documents like this:
{ "active_status": 1 },
{ "active_status": 2 }

How to keep orders in MongoDB?

In my MongoDB document I have object like this
[_id] => MongoId Object (
[$id] => 52a46b44aabacb5c218b4567
)
[results] => Array (
[http://google.com] => Array (
[position] => 1
[data] => 42672
)
[http://bing.com] => Array (
[position] => 2
[data] => 9423
)
[http://yandex.com] => Array (
[position] => 3
[data] => 5513
)
)
I would like to change data parameter in "bing.com" from 9423 to for instance 300. Moreover, I have to keep order of the sites. It have to looks like this
[_id] => MongoId Object (
[$id] => 52a46b44aabacb5c218b4567
)
[results] => Array (
[http://google.com] => Array (
[position] => 1
[data] => 42672
)
[http://bing.com] => Array (
[position] => 2
[data] => 300
)
[http://yandex.com] => Array (
[position] => 3
[data] => 5513
)
)
Is this achievable in Mongo?
The reordering of fields issue has been fixed as of MongoDB v2.5.2 (2.6 release). Having said that one way you can avoid the issue completely is having results as an array instead of a (sub)document. Also note you should not use "." as part of the key name either.
With 2.4, with the following you will see there is reodering in the case of _id=1 (subdocument) but not in the case of _id=2 (array).
$document = array("_id" => 1, "results" => array('http://google.com' => array('position' => 1, 'data' => 42672),
'http://bing.com' => array('position' => 2, 'data' => 9423),
'http://yandex.com' => array('position' => 3, 'data' => 5513)));
$coll->insert($document);
$document = array("_id" => 2, "results" => array(array('site' => 'http://google.com', 'data' => 42672),
array('site' => 'http://bing.com', 'data' => 9423),
array('site' => 'http://yandex.com', 'data' => 5513)));
$coll->insert($document);
$coll->update(array("_id" => 1), array('$set'=>array("results.http://bing.com.data"=>300)));
$coll->update(array("_id" => 2, 'results.site' => 'http://bing.com'), array('$set'=>array('results.$.data'=>300)));
I've included examples below using the mongo shell for clarity, but the PHP equivalent should be straightforward to work out.
I notice you originally modelled your list of sites as an embedded document, however the order of fields within an embedded document is currently not guaranteed to be preserved so you should instead use an array.
Additionally, you cannot use field names with embedded dots (.) in MongoDB so you should not plan to store urls as field names (see: Field name restrictions).
In order to find an element in an array you need to search by a value (not a field name) so your schema should look more like:
{
_id: ObjectId("52a46b44aabacb5c218b4567"),
results: [
{
site: 'http://google.com',
position: 1,
data: 42762
},
{
site: 'http://bing.com',
position: 2,
data: 9423
},
{
site: 'http://yandex.com',
position: 3,
data: 5513
}
]
}
Assuming the array site elements are unique, you can use the positional operator $ to find and update the matching embedded document in place.
For example, to perform your update of the "bing.com" data value:
db.sites.update(
// Match criteria
{
_id:ObjectId("52a46b44aabacb5c218b4567"),
'results.site':'http://bing.com'
},
// Update
{ $set: {
'results.$.data': 300 }
}
)
In MongoDB 2.4+ you have the option of pushing to a sorted array which could also be a useful approach to maintaining your array in sorted order when you add new entries.
It's worth noting that if you plan to store many (i.e. thousands) of items in an array this can impose a significant performance penalty due to document growth and the complexity of updating large arrays.
I am pretty sure that (as every other DBMS) you can't and should't rely on records orders.
Instead I would advice you to add index (on position, i.e. db.people.ensureIndex( { position: 1 } )) and query your record sorted by that field, i. e.: db.collection.find().sort( { position: 1 } )

Use a collection on mongoid that does not have a model

I need to run a collection on Mongoid but this collection does not have a model, so i cant use it like: Project.collection.map_reduce( ..., :query => scoped.selector)
Does anyone know how could i do this?
Thanks
Here's an answer/example that works for Mongoid 3.1.5 / Moped 1.5.1, hope that it helps.
Please respond with your version info if you want something more specific to your environment.
Note that since you don't have a Mongoid model,
you loose the facilities of ODM level and have to drop down to the Moped driver level.
test/unit/session_test.rb
require 'test_helper'
require 'pp'
class SessionTest < ActiveSupport::TestCase
def setup
#session = Mongoid.default_session
#collection_name = 'project'
#collection = #session[#collection_name]
#collection.drop
end
test "collection map-reduce without model" do
puts "\nMongoid::VERSION:#{Mongoid::VERSION}\nMoped::VERSION:#{Moped::VERSION}"
docs = [
{'name' => 'Charlie', 'gender' => 'M', 'age' => 11},
{'name' => 'Lucy', 'gender' => 'F', 'age' => 13},
{'name' => 'Sally', 'gender' => 'F', 'age' => 15},
{'name' => 'Linus', 'gender' => 'M', 'age' => 11},
{'name' => 'Snoopy'},
]
#collection.insert(docs)
assert_equal docs.size, #collection.find.to_a.size
pp #session.command(
:mapReduce => #collection_name,
:map => 'function(){ emit(this.gender, this.age); }',
:reduce => 'function(key, values){ return Array.sum(values)/values.length; }',
:out => { :inline => 1 },
:query => { 'age' => { '$exists' => true } }
)['results']
end
end
$ rake test
Run options:
# Running tests:
[1/1] SessionTest#test_collection_map-reduce_without_model
Mongoid::VERSION:3.1.5
Moped::VERSION:1.5.1
[{"_id"=>"F", "value"=>14.0}, {"_id"=>"M", "value"=>11.0}]
Finished tests in 0.111151s, 8.9968 tests/s, 8.9968 assertions/s.
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips

How to rename a column in a collection in Mongodb using MongoMapper?

Mongodb gives an option for renaming a column name as follows
db.collection.update({},{'$rename'=> {'old_name' => 'new_name'}}, false,true)
Is it possible for using MongoMapper to do the same? The documentation doesn't specify anything.
I also tried getting the Mongodb connection handle from MongoMapper as
connection = MongoMapper.connection
db = MongoMapper.database
collection = db.collection('collection_name')
collection .update(....)
and doing the same query but it doesn't work.
MongoMapper uses the 10gen Ruby driver, and MongoMapper::Document provides access to the underlying driver objects.
The following working test shows that you can use Model.collection.update to do what you want, to rename a field for the model 'Model'. Make sure to use the :multi => true option to update if you want to update/rename more than a single document.
app/models/model.rb
class Model
include MongoMapper::Document
end
test/unit/model_test.rb
require 'test_helper'
class ModelTest < ActiveSupport::TestCase
def setup
Model.remove
end
test "rename" do
puts "Model.collection.class: #{Model.collection.class}"
puts "Model.database.class: #{Model.database.class}"
Model.create( 'old_name' => 'name value 0', 'another_key' => 'another value 0' )
Model.create( 'old_name' => 'name value 1', 'another_key' => 'another value 1' )
assert_equal(2, Model.where( 'old_name' => { '$exists' => true } ).count)
Model.collection.update( {}, { '$rename' => { 'old_name' => 'new_name' } }, :multi => true )
assert_equal(0, Model.where( 'old_name' => { '$exists' => true } ).count)
p Model.all
end
end
$ rake test
Rack::File headers parameter replaces cache_control after Rack 1.5.
Run options:
# Running tests:
Model.collection.class: Mongo::Collection
Model.database.class: Mongo::DB
[DEPRECATED] The 'safe' write concern option has been deprecated in favor of 'w'.
[#<Model _id: BSON::ObjectId('5101809d7f11ba1256000001'), another_key: "another value 0", new_name: "name value 0">, #<Model _id: BSON::ObjectId('5101809d7f11ba1256000002'), another_key: "another value 1", new_name: "name value 1">]
.
Finished tests in 0.012344s, 81.0110 tests/s, 162.0220 assertions/s.
1 tests, 2 assertions, 0 failures, 0 errors, 0 skips

OR operator in Mongo 'update' $criteria

I want to check for an existing record by matching either title or url fields. If either one matches, update that record. Otherwise, insert.
How do write the following properly (using Mongoid in Ruby):
articles.update(
{ **:title => story.title OR :url => story.url** },
{ :title => story.title, :url => story.url, :source => story.source, :last_updated => Time.now },
{ :upsert => true } )
Thanks!
You need do the request and update it like :
'''
articles.any_of({:title => xxx}, {:url => yyyy}).update( :foo => 'bar')
'''