Single Table Inheritance with Sunspot/Solr and Rails - ruby-on-rails

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?

Related

How to index model association in Tire(retire)

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.

Rails 3 eager loading of deep nested association

I am building a public activity stream which contains a stream of the following:
User posted 3 minutes ago
User starred a post
I am using the public_activity gem for achieving this.
My question is whether there is a way to use the includes for a polymorpic function.
The code that i am running currently is as follows:
#app/models/post.rb
class Post < ActiveRecord::Base
include PublicActivity::Common
attr_accessible :content, :visibility
validates_presence_of :content
belongs_to :postable, :polymorphic => true
has_many :stars
end
#app/models/star.rb
class Star < ActiveRecord::Base
include PublicActivity::Common
validate :duplicate_star
belongs_to :user
belongs_to :post
private
def duplicate_star
errors.add(:base, "Post already starred") if Star.exists?(:user_id => self.user, :post_id => self.post)
end
end
#app/controllers/users_controller.rb
class UsersController < ApplicationController
def index
#post = Post.new
#activities = PublicActivity::Activity.order("id desc").all.includes(:trackable, :owner)
end
end
The trackable can be a post or a star.
I have render functions for displaying both.
The problem is, if I try to output something like {user} starred {postcontent}, it does it this way:
activity.trackable.post.content
So this results in multiple queries, each time it finds a post.
How do I tackle this problem/situation?
Thanks in advance.
Are you not able to use the standard eager loading syntax?
#activities = PublicActivity::Activity.order("id desc").includes(:owner, :trackable => {:post => :content})

How to migrate from belongs_to, to embedded_in in Mongoid?

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

Filter on parent object attribute in ActiveAdmin

I'd like to be able to filter an object based on an attribute of it's parent:
class Call < ActiveRecord::Base
belongs_to :user
end
class User < ActiveRecord::Base
has_many :calls
end
I'd like to be able to do this:
ActiveAdmin.register Call do
filter :user
end
and have it filter on user.name, rather than present a select of all users. Can this be done?
Denis's solution almost worked for me. I just needed to add the filter type. For example:
ActiveAdmin.register Call do
filter :user_name, :as => :string
end
Try this:
ActiveAdmin.register Call do
filter :user_name
end
Since ActiveAdmin uses meta_search for filters their doc is very helpful: https://github.com/ernie/meta_search
In the next release of ActiveAdmin (I work with 1.0.0.pre) you can use Ransack methods. So, let say you have an Article, which belongs_to User.
You will have the following admin/article.rb file
ActiveAdmin.register Article do
controller do
def scoped_collection
Article.includes(:user)
end
end
index do
column :id
column :created_at
column :title
column("Author", sortable: 'users.first_name') { |item| link_to item.user.full_name, user_path(item.user) }
actions
end
filter :user_first_name_cont, :as => :string
filter :user_last_name_cont, :as => :string
end
Here, user_first_name_cont is ransack method which filters on associated user first_name and 'cont' means contains.
You can use nested resources from InheritedResource which is used by ActiveAdmin, so your list is automatically filtered by the parent.
ActiveAdmin.register User do
# this is the parent resource
end
ActiveAdmin.register Call do
belongs_to :user # nested below resource user
end
You can then use rake routes to see the new nested routes, generated by ActiveAdmin :) Hope it helps
I'd say it hugely depends on the types of associations you have between your models - it took me hours to figure this out.
Example
class User < ActiveRecord::Base
belongs_to :user
end
class Email < ActiveRecord::Base
has_one :email
end
To filter users based on their emails, you do this (don't forget the as: :string part as it gives you access to Ransack search methods such as contains and the likes)
ActiveAdmin.register User do
filter :user_email, :as => :string
end

SunSpot Geosearching through assocation

class Office < ActiveRecord::Base
has_many :users
searchable do
text :name
location :coordinates do
Sunspot::Util::Coordinates.new(latitude, longitude)
end
end
end
class User < ActiveRecord::Base
belongs_to :office
searchable do
text :name, :default_boost => 2
text :description
end
end
With this kind of a setup, how can I search using SunSpot (on Solr) on Rails for a user within a given lat/long? For example, I want to be able to do this:
#search = User.search() do
fulltext(params[:q])
with(:coordinates).near(#lat, #long, :precision => 5)
end
The following works just fine:
#search = Office.search() do
fulltext(params[:q])
with(:coordinates).near(#lat, #long, :precision => 5)
end
What is the best way to accomplish this given that the lat/long for each User really lives in the Office class?
The office association should be in scope inside your User's searchable block.
Given that, here's what I would start with (untested, off the top of my head, etc):
class User < ActiveRecord::Base
belongs_to :office
searchable do
text :name, :default_boost => 2
text :description
location :coordinates do
Sunspot::Util::Coordinates.new(office.latitude, office.longitude)
end
end
end
Fetching values for associated objects in a block like this is actually a pretty common pattern for handling denormalization with Sunspot.

Resources