When Ultrasphinx is used with polymorphic associations…

Published on Author Akhil BansalLeave a comment

Lets first consider simple has_many and belongs_to associations as:

class Article < ActiveRecord::Base
  has_many :comments
end
class Comment < ActiveRecord::Base
  belongs_to :article
end

Now if you wish to index the title of associated article with comment for searching, you just need to add ” is_indexed :fields => :body, :include => [{ :association_name => ‘article’, :field => ‘title’, :as=> ‘article_title’}] ”

So, your comment model will look like:

class Comment < ActiveRecord::Base
  belongs_to :article

  is_indexed :fields => :body,
             :include => [{ :association_name => 'article', :field => 'title', :as=> 'article_title'}]
end

Setup ultrasphinx by issuing:

rake ultrasphinx:bootstrap

Now at rails console try:

>> article = Article.create(:title => "This is the first article's title", :body => "Here comes first article's body.")
=> #<article id: 1, title: "This is the first article's title", tagline: nil, type: nil, body: "Here comes first article's body.", created_at: "2008-08-07 07:47:53", updated_at: "2008-08-07 07:47:53">
>> article.comments
=> []
>> article.comments<<comment.new(:body=>"first comment on first article. which says yahooo !!")
=> [#<comment id: 1, body: "first comment on first article. which says yahooo !...", article_id: 1, created_at: "2008-08-07 07:48:47", updated_at: "2008-08-07 07:48:47">]
>> Comment.find :all
=> [#<comment id: 1, body: "first comment on first article. which says yahooo !...", article_id: 1, created_at: "2008-08-07 07:48:47", updated_at: "2008-08-07 07:48:47">]
>> search = Ultrasphinx::Search.new(:query => "first article's title", :class_names => ['Comment']).run.results
=> [#<comment id: 1, body: "first comment on first article. which says yahooo !...", article_id: 1, created_at: "2008-08-07 07:48:47", updated_at: "2008-08-07 07:48:47">]

Simple, we have article and comment models with has_many belongs_to associations. Comments are indexed with their associated article title. So it can return comments if query matches with article’s titles.

Now, consider a case when we wish to change comment model to make it polymorphic. In that case our models will be look like:

 class Article < ActiveRecord::Base
   has_many :comments, :as => :commentable, :dependent => :destroy
 end
class Comment < ActiveRecord::Base
  belongs_to :commentable, :polymorphic => true
end

Check at rails if associations are working fine:

>> article = Article.create(:title => "First article title", :body => "here comes first artile's title")
=> #<article id: 1, title: "First article title", tagline: nil, type: nil, body: "here comes first artile's title", created_at: "2008-08-07 08:12:31", updated_at: "2008-08-07 08:12:31">
>> comment = Comment.new(:body => "hello there, I am a comment")
=> #<comment id: nil, body: "hello there, I am a comment", commentable_id: nil, commentable_type: nil, created_at: nil, updated_at: nil>
>> comment.commentable = article
=> #<article id: 1, title: "First article title", tagline: nil, type: nil, body: "here comes first artile's title", created_at: "2008-08-07 08:12:31", updated_at: "2008-08-07 08:12:31">
>> comment.save
=> true
>> comment.reload
=> #<comment id: 1, body: "hello there, I am a comment", commentable_id: 1, commentable_type: "Article", created_at: "2008-08-07 08:14:29", updated_at: "2008-08-07 08:14:29">
>> article.reload
=> #<article id: 1, title: "First article title", tagline: nil, type: nil, body: "here comes first artile's title", created_at: "2008-08-07 08:12:31", updated_at: "2008-08-07 08:12:31">
>> article.comments
=> [#<comment id: 1, body: "hello there, I am a comment", commentable_id: 1, commentable_type: "Article", created_at: "2008-08-07 08:14:29", updated_at: "2008-08-07 08:14:29">]
>> comment = Comment.new(:body => "hi there, this is second comment")
=> #<comment id: nil, body: "hi there, this is second comment", commentable_id: nil, commentable_type: nil, created_at: nil, updated_at: nil>
>> article.comments<< comment
=> [#<comment id: 1, body: "hello there, I am a comment", commentable_id: 1, commentable_type: "Article", created_at: "2008-08-07 08:14:29", updated_at: "2008-08-07 08:14:29">, #<comment id: 2, body: "hi there, this is second comment", commentable_id: 1, commentable_type: "Article", created_at: "2008-08-07 08:15:34", updated_at: "2008-08-07 08:15:34">]
>> article.comments
=> [#<comment id: 1, body: "hello there, I am a comment", commentable_id: 1, commentable_type: "Article", created_at: "2008-08-07 08:14:29", updated_at: "2008-08-07 08:14:29">, #<comment id: 2, body: "hi there, this is second comment", commentable_id: 1, commentable_type: "Article", created_at: "2008-08-07 08:15:34", updated_at: "2008-08-07 08:15:34">]
>> comment.commentable
=> #<article id: 1, title: "First article title", tagline: nil, type: nil, body: "here comes first artile's title", created_at: "2008-08-07 08:12:31", updated_at: "2008-08-07 08:12:31">

Now, the point is to index article title with comments. Here we can get associated article using ‘commentable’ i.e. comment.commentable.

So, change ultrasphinx is_indexed code in comment model accordingly:

class Comment < ActiveRecord::Base
  belongs_to :commentable, :polymorphic => true

  is_indexed :fields => :body,
            :include => [{ :association_name => 'commentable', :field => 'title', :as=> 'article_title'}]

end

Note that we have changed associan_name to ‘commentable’.
Next, when we reconfigure ultrasphinx using ” rake ultrasphinx:bootstrap”(since we have changed db schema), it starts throwing errors:

** Invoke ultrasphinx:bootstrap (first_time)
** Invoke ultrasphinx:_environment (first_time)
** Invoke environment (first_time)
** Execute environment
rake aborted!
uninitialized constant Commentable
/Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/dependencies.rb:278:in `load_missing_constant'
/Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/dependencies.rb:467:in `const_missing'
/Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/dependencies.rb:479:in `const_missing'
/Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/inflector.rb:283:in `constantize'
/Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/core_ext/string/inflections.rb:143:in `constantize'
/Users/akhilbansal/work/sti/vendor/plugins/ultrasphinx/lib/ultrasphinx/associations.rb:19:in `get_association_model'
/Users/akhilbansal/work/sti/vendor/plugins/ultrasphinx/lib/ultrasphinx/fields.rb:128:in `configure'
/Users/akhilbansal/work/sti/vendor/plugins/ultrasphinx/lib/ultrasphinx/fields.rb:124:in `each'
/Users/akhilbansal/work/sti/vendor/plugins/ultrasphinx/lib/ultrasphinx/fields.rb:124:in `configure'
/Users/akhilbansal/work/sti/vendor/plugins/ultrasphinx/lib/ultrasphinx/fields.rb:101:in `each'
/Users/akhilbansal/work/sti/vendor/plugins/ultrasphinx/lib/ultrasphinx/fields.rb:101:in `configure'
/Users/akhilbansal/work/sti/vendor/plugins/ultrasphinx/lib/ultrasphinx/configure.rb:32:in `load_constants'
/Users/akhilbansal/work/sti/vendor/plugins/ultrasphinx/lib/ultrasphinx/autoload.rb:8:in `after_initialize'
/Library/Ruby/Gems/1.8/gems/rails-2.1.0/lib/initializer.rb:148:in `process'
/Library/Ruby/Gems/1.8/gems/rails-2.1.0/lib/initializer.rb:93:in `send'
/Library/Ruby/Gems/1.8/gems/rails-2.1.0/lib/initializer.rb:93:in `run'
/Users/akhilbansal/work/sti/config/environment.rb:13
/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/rubygems/custom_require.rb:27:in `gem_original_require'
/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/rubygems/custom_require.rb:27:in `require'
/Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/dependencies.rb:509:in `require'
/Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/dependencies.rb:354:in `new_constants_in'
/Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/dependencies.rb:509:in `require'
/Library/Ruby/Gems/1.8/gems/rails-2.1.0/lib/tasks/misc.rake:3
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:546:in `call'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:546:in `execute'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:541:in `each'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:541:in `execute'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:508:in `invoke_with_call_chain'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:501:in `synchronize'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:501:in `invoke_with_call_chain'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:518:in `invoke_prerequisites'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:1183:in `each'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:1183:in `send'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:1183:in `each'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:515:in `invoke_prerequisites'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:507:in `invoke_with_call_chain'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:501:in `synchronize'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:501:in `invoke_with_call_chain'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:518:in `invoke_prerequisites'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:1183:in `each'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:1183:in `send'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:1183:in `each'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:515:in `invoke_prerequisites'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:507:in `invoke_with_call_chain'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:501:in `synchronize'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:501:in `invoke_with_call_chain'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:494:in `invoke'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:1931:in `invoke_task'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:1909:in `top_level'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:1909:in `each'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:1909:in `top_level'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:1948:in `standard_exception_handling'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:1903:in `top_level'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:1881:in `run'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:1948:in `standard_exception_handling'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake.rb:1878:in `run'
/Library/Ruby/Gems/1.8/gems/rake-0.8.1/bin/rake:31
/usr/bin/rake:19:in `load'
/usr/bin/rake:19

After spending some time on research, I was able to make it work by making some changes in comment model:

class Comment < ActiveRecord::Base
  belongs_to :commentable, :polymorphic => true

is_indexed :fields  => :body,
           :include => [{:class_name => 'Article',  :association_sql => 'left outer join articles on articles.id = comments.commentable_id and comments.commentable_type = "Article"', :field => 'title', :as=> 'article_title'}]
end

Lets check it on rails console:

>> search = Ultrasphinx::Search.new(:query => 'first article', :class_name => "Comment").run.results
=> [#<comment id: 1, body: "hello there, I am a comment", commentable_id: 1, commentable_type: "Article", created_at: "2008-08-07 08:14:29", updated_at: "2008-08-07 08:14:29">, #<comment id: 2, body: "hi there, this is second comment", commentable_id: 1, commentable_type: "Article", created_at: "2008-08-07 08:15:34", updated_at: "2008-08-07 08:15:34">]

In such cases we need to define class_name and association_sql in is_indexed statement instead of association_name.

Hope it helps…

Leave a Reply

Your email address will not be published. Required fields are marked *