I have a model Event that is connected to MongoDB using Mongoid:
class Event
include Mongoid::Document
include Mongoid::Timestamps
field :user_name, type: String
field :action, type: String
field :ip_address, type: String
scope :recent, -> { where(:created_at.gte => 1.month.ago) }
end
Usually when I use ActiveRecord, I can do something like this to group results:
#action_counts = Event.group('action').where(:user_name =>"my_name").recent.count
And I get results with the following format:
{"action_1"=>46, "action_2"=>36, "action_3"=>41, "action_4"=>40, "action_5"=>37}
What is the best way to do the same thing with Mongoid?
Thanks in advance
I think you'll have to use map/reduce to do that. Look at this SO question for more details:
Mongoid Group By or MongoDb group by in rails
Otherwise, you can simply use the group_by method from Enumerable. Less efficient, but it should do the trick unless you have hundreds of thousands documents.
EDIT: Example of using map/reduce in this case
I'm not really familiar with it but by reading the docs and playing around I couldn't reproduce the exact same hash you want but try this:
def self.count_and_group_by_action
map = %Q{
function() {
key = this.action;
value = {count: 1};
emit(key, value);
# emit a new document {"_id" => "action", "value" => {count: 1}}
# for each input document our scope is applied to
}
}
# the idea now is to "flatten" the emitted documents that
# have the same key. Good, but we need to do something with the values
reduce = %Q{
function(key, values) {
var reducedValue = {count: 0};
# we prepare a reducedValue
# we then loop through the values associated to the same key,
# in this case, the 'action' name
values.forEach(function(value) {
reducedValue.count += value.count; # we increment the reducedValue - thx captain obvious
});
# and return the 'reduced' value for that key,
# an 'aggregate' of all the values associated to the same key
return reducedValue;
}
}
self.map_reduce(map, reduce).out(inline: true)
# we apply the map_reduce functions
# inline: true is because we don't need to store the results in a collection
# we just need a hash
end
So when you call:
Event.where(:user_name =>"my_name").recent.count_and_group_by_action
It should return something like:
[{ "_id" => "action1", "value" => { "count" => 20 }}, { "_id" => "action2" , "value" => { "count" => 10 }}]
Disclaimer: I'm no mongodb nor mongoid specialist, I've based my example on what I could find in the referenced SO question and Mongodb/Mongoid documentation online, any suggestion to make this better would be appreciated.
Resources:
http://docs.mongodb.org/manual/core/map-reduce/
http://mongoid.org/en/mongoid/docs/querying.html#map_reduce
Mongoid Group By or MongoDb group by in rails
Related
I use Ruby on Rails and Postgres 9.5.
In the database I have a table called events.
I want to find all events based on particular value in the data field.
events.data has the following possible json value:
{ 'transition' => { 'from' => 'bacon', 'to' => 'ham' } }
How can I build a query that will find an event with data => transition => from bacon?
Assuming that your model is called Event, you can do it like this:
Event.where("data -> 'transition' ->> ? = ?", 'from', 'bacon')
Here is jsonb operators reference.
This query will return all events where data.transition.from is equal to bacon.
To DRY your queries, you can add it to the repository, e.g.:
# app/repositories/event_repository.rb
module EventRepository
extend ActiveSupport::Concern
included do
scope :where_transition, ->(field, value) { where("data -> 'transition' ->> ? = ?", field, value) }
end
end
After that include it in your model:
include EventRepository
Then you can use it like this:
Event.where_transition('from', 'bacon')
Event.where_transition('to', 'ham')
Each user has one address.
class User
include Mongoid::Document
has_one :address
end
class Address
include Mongoid::Document
belongs_to :user
field :street_name, type:String
end
u = User.find(...)
u.address.update(street_name: 'Main St')
If we have a User without an Address, this will fail.
So, is there a good (built-in) way to do u.address.update_or_initialize_with?
Mongoid 5
I am not familiar with ruby. But I think I understand the problem. Your schema might looks like this.
user = {
_id : user1234,
address: address789
}
address = {
_id: address789,
street_name: ""
user: user1234
}
//in mongodb(javascript), you can get/update address of user this way
u = User.find({_id: user1234})
u.address //address789
db.address.update({user: u.address}, {street_name: "new_street name"})
//but since the address has not been created, the variable u does not even have property address.
u.address = undefined
Perhaps you can try to just create and attached it manually like this:
#create an address document, to get _id of this address
address = address.insert({street_name: "something"});
#link or attached it to u.address
u.update({address: address._id})
I had this problem recently. There is a built in way but it differs from active records' #find_or_initialize_by or #find_or_create_by method.
In my case, I needed to bulk insert records and update or create if not found, but I believe the same technique can be used even if you are not bulk inserting.
# returns an array of query hashes:
def update_command(users)
updates = []
users.each do |user|
updates << { 'q' => {'user_id' => user._id},
'u' => {'address' => 'address'},
'multi' => false,
'upsert' => true }
end
{ update: Address.collection_name.to_s, updates: updates, ordered: false }
end
def bulk_update(users)
client = Mongoid.default_client
command = bulk_command(users)
client.command command
client.close
end
since your not bulk updating, assuming you have a foreign key field called user_id in your Address collection. You might be able to:
Address.collection.update({ 'q' => {'user_id' => user._id},
'u' => {'address' => 'address'},
'multi' => false,
'upsert' => true }
which will match against the user_id, update the given fields when found (address in this case) or create a new one when not found.
For this to work, there is 1 last crucial step though.
You must add an index to your Address collection with a special flag.
The field you are querying on (user_id in this case)
must be indexed with a flag of either { unique: true }
or { sparse: true }. the unique flag will raise an error
if you have 2 or more nil user_id fields. The sparse option wont.
Use that if you think you may have nil values.
access your mongo db through the terminal
show dbs
use your_db_name
check if the addresses collection already has the index you are looking for
db.addresses.getIndexes()
if it already has an index on user_id, you may want to remove it
db.addresses.dropIndex( { user_id: 1} )
and create it again with the following flag:
db.addresses.createIndex( { user_id: 1}, { sparse: true } )
https://docs.mongodb.com/manual/reference/method/db.collection.update/
EDIT #1
There seems to have changes in Mongoid 5.. instead of User.collection.update you can use User.collection.update_one
https://docs.mongodb.com/manual/reference/method/db.collection.updateOne/
The docs show you need a filter rather than a query as first argument but they seem to be the same..
Address.collection.update_one( { user_id: user_id },
'$set' => { "address": 'the_address', upsert: true} )
PS:
If you only write { "address": 'the_address' } as your update clause without including an update operator such as $set, the whole document will get overwritten rather than updating just the address field.
EDIT#2
About why you may want to index with unique or sparse
If you look at the upsert section in the link bellow, you will see:
To avoid multiple upserts, ensure that the filter fields are uniquely
indexed.
https://docs.mongodb.com/manual/reference/method/db.collection.updateOne/
I'm using the elasticsearch-rails gem and the elasticsearch-model gem and writing a query that happens to be really huge just because of the way the gem accepts queries.
The query itself isn't very long, but it's the filters that are very, very long, and I need to pass variables in to filter out the results correctly. Here is an example:
def search_for(input, question_id, tag_id)
query = {
:query => {
:filtered => {
:query => {
:match => {
:content => input
}
},
:filter => {
:bool => {
:must => [
{
# another nested bool with should
},
{
# another nested bool with must for question_id
},
{
# another nested bool with must for tag_id
}
]
}
}
}
}
}
User.search(query) # provided by elasticsearch-model gem
end
For brevity's sake, I've omitted the other nested bools, but as you can imagine, this can get quite long quite fast.
Does anyone have any ideas on how to store this? I was thinking of a yml file, but it seems wrong especially because I need to pass in question_id and tag_id. Any other ideas?
If anyone is familiar with those gems and knows whether the gem's search method accepts other formats, I'd like to know that, too. Looks to me that it just wants something that can turn into a hash.
I think using a method is fine. I would separate the searching from the query:
def query_for(input, question_id, tag_id)
query = {
:query => {
...
end
search query_for(input, question_id, tag_id)
Also, I see that this search functionality is in the User model, but I wonder if it is belongs there. Would it make more sense to have a Search or Query model?
I currently have a Mongoid model in a Ruby on Rails application as such:
class Listen
include Mongoid::Document
field :song_title, type: String
field :song_artist, type: String
field :loc, :type => Array
field :listened_at, type: Time, default: -> { Time.now }
index(
[[:loc, Mongo::GEO2D]], background: true
)
end
When I try to query the collection for example
listens = Listen.where(:loc => {"$within" => {"$centerSphere" => [location, (radius.fdiv(6371))]}})
I am returned the error (locations have been blanked out, the X's are not returned)
Mongo::OperationFailure (can't find special index: 2d for: { loc: { $within: { $centerSphere: [ [ XX.XXXXXXX, X.XXXXXXX ], 0.0001569612305760477 ] } } }):
I know I can create the indexed through a rake task such as rake db:mongoid:create_indexes but I don't want to have to do this every time a model is created. Is there any way for the model to create this automatically on insert to the collection?
Nope there is no way.
You must create indexes (not just Geo) once, to use it.
Need some help with how to use atomic modifiers on an embedded document.
To illustrate, let's assume I've got a collection that looks like this.
Posts Collection
{
"_id" : ObjectId("blah"),
"title" : "Some title",
"comments" : [
{
"_id" : ObjectId("bleh"),
"text" : "Some comment text",
"score" : 0,
"voters" : []
}
]
}
What I'm looking to do with MongoMapper/MongoDB is perform an atomic update on a specific comment within a post document.
Something like:
class Comment
include MongoMapper::EmbeddedDocument
# Other stuff...
# For the current comment that doesn't have the current user voting, increment the vote score and add that user to the voters array so they can't vote again
def upvote!(user_id)
collection.update({"comments._id" => post_id, "comments.voters" => {"$ne" => user_id}},
{"$inc" => {"comments.score" => 1}, "$push" => {"comments.voters" => user_id}})
end
end
That's basically what I have now and it isn't working at all (nothing gets updated). Ideally, I'd also want to reload the document / embedded document but it seems as though there may not be a way to do this using MongoMapper's embedded document. Any ideas as to what I'm doing wrong?
Got this working for anyone that's interested. Two things I was missing
Using $elemMatch to search objects within an array that need to satisfy two conditions (such as _id = "" AND voters DOES NOT contain the user_id)
Using the $ operator on the $inc and $push operations to ensure I'm modifying the specific object that's referenced by my query.
def upvote!(user_id)
# Use the Ruby Mongo driver to make a direct call to collection.update
collection.update(
{
'meanings' => {
'$elemMatch' => {
'_id' => self.id,
'voters' => {'$ne' => user_id}
}
}
},
{
'$inc' => { 'meanings.$.votes' => 1 },
'$push' => { 'meanings.$.voters' => user_id }
})
end