Hi I am using retire and elasticsearch in a rails project to index my users and an additional model special_codes. My goal is to add an index to my Users model so that I when I search on Users the new index(special_code) will provide hits.
class User < ActiveRecord::Base
include Tire::Model::Search
include Tire::Model::Callbacks
has_one :specialcode
after_create :generate_specialcode
mapping do
indexes :email, :type => 'string'
indexes :specialcode do
indexes :code, :type => 'string'
end
end
def to_indexed_json
to_json( include: { specialcode: {only: [:code]} })
end
private
def generate_specialcode
Specialcode.create(code: 'derp', user_id: self.id)
self.tire.update_index #not really needed(see after_save), just here for example.
end
end
class Specialcode < ActiveRecord::Base
include Tire::Model::Search
include Tire::Model::Callbacks
belongs_to :user
after_save :update_user_index
private
def update_user_index
self.user.tire.update_index
end
end
I'd like to see User.search('derp') bring back a user hit because I've indexed the special code in with the user. I've tried quite a bit with mappings and updating indexes without getting any hits. The example above hopefully provides a base to work from. Thanks
Update
I found the solution with the help of Bruce. I've added mapping, to_indexed_json, and update_user_index to the code above.
I suppose you have to_indexed_json and mappings in User model already? Try use after_touch callback to update index in user.
Related
I have two relevant models here: InventoryItem and Store.
class InventoryItem < ActiveRecord::Base
belongs_to :store
include Tire::Model::Search
include Tire::Model::Callbacks
def self.search(params)
tire.search(load: true, :per_page => 40) do
query { string params[:query], default_operator: "AND" } if params[:query].present?
end
end
end
class Store < ActiveRecord::Base
geocoded_by :address
after_validation :geocode, :if => lambda{ |obj| obj.address_changed? }
has_many :inventory_items
end
I want to search inventory_items from stores that are nearby the current user (User is also geocoded using geocoder gem). The approach I'm considering is to scope the model somehow, but I'm unsure of how to implement. Here's how I find nearby stores in my controllers typically:
#stores = Store.near([current_user.latitude, current_user.longitude], current_user.distance_preference, :order => :distance)
Is there a way to do a :local scope on my InventoryItem model somehow based on whether its associated store is nearby the current user? Alternatively, is there a way to accomplish my goal with elasticsearch? Thanks in advance!
There are a few ways you could accomplish this. I think the easiest way to start would be something like this:
class Store < ActiveRecord::Base
scope :for_user, -> {|user| near([user.latitude, user.longitude], user.distance_preference, :order => :distance)}
end
Which would allow you to call
Store.for_user(whatever_user)
On the Inventory, I would also try to find by stores:
class InventoryItem < ActiveRecord::Base
scope :for_stores, -> {|stores| where(store_id: stores.map(&:id))}
end
I am not familiar with your search engine there, but if it does active record scope nesting, you should be able to do something like:
InventoryItem.for_stores(Store.for_user(whatever_user)).search(...)
I have simple Rails app with a Post model that belongs to a creator (which is a User object) and I want to find posts that match the creator's first_name.
class Post < ActiveRecord::Base
include Tire::Model::Search
include Tire::Model::Callbacks
belongs_to :creator, polymorphic: true
mapping do
indexes :id, type: 'integer'
indexes :title, boost: 10
indexes :creator do
indexes :first_name
indexes :service_type
end
end
end
When I run search, there are no results:
Post.tire.search(load: true, page: params[:page], per_page: params[:per_page]) do
query do
match 'creator.first_name', 'Matt'
end
end
Even when I've run rake environment tire:import:all FORCE=true and verified that a Post exists with a creator that has a first name of 'Matt' in the database.
Is there something I am missing? Thanks!
UPDATE:
Adding the following to Post.rb did the trick:
def to_indexed_json
as_json.merge(
creator: creator.as_indexed_json
).to_json
end
Apparently, you need to specify the association in the to_indexed_json to make it work.
If one first build their models with a belong_to and has_many association and then realized they need to move to a embedded_in and embeds_many association, how would one do this without invalidating thousands of records? Need to migrate them somehow.
I am not so sure my solution is right or not. This is something you might try to accomplish it.
Suppose You have models - like this
#User Model
class User
include Mongoid::Document
has_many :books
end
#Book Model
class Book
include Mongoid::Document
field :title
belongs_to :user
end
At first step I will create another model that is similar to the Book model above but it's embedded instead of referenced.
#EmbedBook Model
class EmbedBook
include Mongoid::Document
field :title
embedded_in :user
end
#User Model (Update with EmbedBook Model)
class User
include Mongoid::Document
embeds_many :embed_books
has_many :books
end
Then create a Mongoid Migration with something like this for the above example
class ReferenceToEmbed < Mongoid::Migration
def up
User.all.each do |user|
user.books.each do |book|
embed_book = user.embed_books.new
embed_book.title = book.title
embed_book.save
book.destroy
end
end
end
def down
# I am not so sure How to reverse this migration so I am skipping it here
end
end
After running the migration. From here you can see that reference books are embedded, but the name for the embedded model is EmbedBook and model Book is still there
So the next step would be to make model book as embed instead.
class Book
include Mongoid::Document
embedded_in :user
field :title
end
class User
include Mongoid::Document
embeds_many :books
embeds_many :embed_books
end
So the next would be to migrate embedbook type to book type
class EmbedBookToBook < Mongoid::Migration
def up
User.all.each do |user|
user.embed_books.each do |embed_book|
book = user.books.new
book.title = embed_book.title
book.save
embed_book.destroy
end
end
def down
# I am skipping this portion. Since I am not so sure how to migrate back.
end
end
Now If you see Book is changed from referenced to embedded.
You can remove EmbedBook model to make the changing complete.
This is just the suggestion. Try this on your development before trying on production. Since, I think there might be something wrong in my suggestion.
10gen has a couple of articles on data modeling which could be useful:
Data Modeling Considerations for MongoDB Applications
Embedded One-to-Many Relationships
Referenced One-to-Many Relationships
MongoDB Data Modeling and Rails
Remember that there are two limitations in MongoDB when it comes to embedding:
the document size-limit is 16MB - this implies a max number of embedded documents, even if you just embed their object-id
if you ever want to search across all embedded documents from the top-level, then don't embed, but use referenced documents instead!
Try these steps:
In User model leave the has_many :books relation, and add the
embedded relation with a different name to not override the books
method.
class User
include Mongoid::Document
has_many :books
embeds_many :embedded_books, :class_name => "Book"
end
Now if you call the embedded_books method from a User instance
mongoid should return an empty array.
Without adding any embedded relation to Book model, write your own
migration script:
class Book
include Mongoid::Document
field :title, type: String
field :price, type: Integer
belongs_to :user
def self.migrate
attributes_to_migrate = ["title","price"] # Use strings not symbols,
# we keep only what we need.
# We skip :user_id field because
# is a field related to belongs_to association.
Book.all.each do |book|
attrs = book.attributes.slice(*attributes_to_migrate)
user = book.user // through belong_to association
user.embedded_book.create!(attrs)
end
end
end
Calling Book.migrate you should have all the Books copied inside each user who was
associated with belongs_to relation.
Now you can remove the has_many and belongs_to relations, and
finally switch to clean embedded solution.
class User
include Mongoid::Document
embeds_many :books
end
class Book
include Mongoid::Document
field :title, type: String
field :price, type: Integer
embedded_in :user
end
I have not tested this solution, but theoretically should work, let me know.
I have a much shorter concise answer:
Let's assume that you have the same models:
#User Model
class User
include Mongoid::Document
has_many :books
end
#Book Model
class Book
include Mongoid::Document
field :title
belongs_to :user
end
So change it to embeds:
#User Model
class User
include Mongoid::Document
embeds_many :books
end
#Book Model
class Book
include Mongoid::Document
field :title
embedded_in :user
end
And generate a mongoid migration like this:
class EmbedBooks < Mongoid::Migration
##attributes_to_migrate = [:title]
def self.up
Book.unscoped.where(:user_id.ne => nil).all.each do |book|
user = User.find book[:user_id]
if user
attrs = book.attributes.slice(*##attributes_to_migrate)
user.books.create! attrs
end
end
end
def self.down
User.unscoped.all.each do |user|
user.books.each do |book|
attrs = ##attributes_to_migrate.reduce({}) do |sym,attr|
sym[attr] = book[attr]
sym
end
attrs[:user] = user
Book.find_or_create_by(**attrs)
end
end
end
end
This works because when you query from class level, it is looking for the top level collection (which still exists even if you change your relations), and the book[:user_id] is a trick to access the document attribute instead of autogenerated methods which also exists as you have not done anything to delete them.
So there you have it, a simple migration from relational to embedded
Here i have Review module
class Review
include Mongoid::Document
include Mongoid::Timestamps
belongs_to :job
has_many :options, :class_name => "Option"
accepts_nested_attributes_for :options, allow_destroy: true
end
Also Option Model
class Option
include Mongoid::Document
include Mongoid::Timestamps
field :name, type: String
field :comment, type: String
belongs_to :review,:class_name => "Review"
end
Now in my Employee:: Review controller
def show
#employee_review = Employee::Review.find(params[:id])
#employee = Employee::Employee.find(#employee_review.employee)
#employee_id = #employee.id
#job_title = #employee.job.id
#review = Review.find_by(job_id: #job_title)
end
Here #review = Review.find_by(job_id: #job_title)
This code rarely works.
I checked out there id passed in #job_title everytime But it does not find the data inside Review model everytime. But when i use Reviews controller to check data then it works automatically in Employee:: Review controller to find that data related to given job_id .
What could be the reason of not finding data in Review model through this controller?
But sometime it finds.
What happens if you go the other way around?
#review = Job.find(#job_title).reviews
I have following classes in a Rails app:
class Post
include Mongoid::Document
include Sunspot::Mongoid
belongs_to :user
searchable do
text :name
string :user_id
end
end
class Post::Image < Post
searchable do
text :location
time :taken_at
end
end
class Post::Link < Post
searchable do
text :href
end
end
As far as I know, calling following should just work.
Post.search do
fulltext('something')
with(:user_id, user.id)
end
But it does not. It returns an empty result. Calling Post::Link.search does also not work:
# => Sunspot::UnrecognizedFieldError: No field configured for Post::Link with name 'user_id'
Any suggestions?