9
votes

I'm trying to implement a sitewide search through the powerful Sunspot gem for Rails. This involves a search across multiple, very different models at once. What I WANT to do is use the faceting feature to allow the user to filter their search results on each model, or by default view all on the same page, interspersed with each other ordered by the :boost qualifier. Combining the faceting code from the Sunspot Railscast with the multiple model searching code from another Stackoverflow question (a variation upon the 'Multiple Type' code from the Sunspot documentation) gave me a solution that I'd think would work, but doesn't.

The multiple method search succeeds, but the facets always turn up null. My basic approach is to provide a virtual attribute on each model by the same name, :search_class, that is just the model's class name rendered into a string. I then try and use that as a facet. However, in the view logic the results of the facet (@search.facet(:search_class).rows) is always an empty array, including when @search.results returns many different models in the same query, despite each returned instance having a perfectly accessible Instance.search_class attribute.

I'm using Rails 3.1.0 and sunspot-rails 1.2.1.

What should I do to make this faceting code work?

Controller:

#searches_controller.rb
class SearchesController < ApplicationController

  def show
    @search = search(params[:q])
    @results = @search.results
  end

  protected
  def search(q)
    Sunspot.search Foo, Bar, CarlSagan do
      keywords q
      #provide faceting for "search class", a field representing a pretty version of the model name
      facet(:search_class)
      with(:search_class, params[:class]) if params[:class].present?
      paginate(:page => params[:page], :per_page => 30)
    end
  end

end

Models:

#Foo.rb
class Foo < ActiveRecord::Base
  searchable do
    text :full_name, :boost => 5
    text :about, :boost => 2
    #string for faceting
    string :search_class
  end

  #get model name, and, if 2+ words, make pretty
  def search_class
    self.class.name#.underscore.humanize.split(" ").each{|word| word.capitalize!}.join(" ")
  end
end

#Bar.rb
class Bar < ActiveRecord::Base
  searchable do
    text :full_name, :boost => 5
    text :about, :boost => 2
    #string for faceting
    string :search_class
  end

  #get model name, and, if 2+ words, make pretty
  def search_class
    self.class.name.underscore.humanize.split(" ").each{|word| word.capitalize!}.join(" ")
  end
end

#CarlSagan.rb
class CarlSagan < ActiveRecord::Base
  searchable do
    text :full_name, :boost => 5
    text :about, :boost => 2
    #string for faceting
    string :search_class
  end

  #get model name, and, if 2+ words, make pretty
  def search_class
    self.class.name#.underscore.humanize.split(" ").each{|word| word.capitalize!}.join(" ")
  end
end

View:

#searches/show.html.erb
<div id="search_results">
<% if @results.present? %> # If results exist, display them

            # If Railscasts-style facets are found, display and allow for filtering through params[:class]
    <% if @search.facet(:search_class).rows.count > 0 %>
        <div id="search_facets"> 
          <h3>Found:</h3>  
          <ul>  
            <% for row in @search.facet(:search_class).rows %>  
              <li>  
                <% if params[:class].blank? %>  
                  <%= row.count %>  <%= link_to row.value, :class => row.value %>
                <% else %>  
                  <strong><%= row.value %></strong> (<%= link_to "remove", :class => nil %>)  
                <% end %>  
              </li>  
            <% end %>  
          </ul>  
        </div>
    <% end %>


    <% @results.each do |s| %>
        <div id="search_result">
            <% if s.class.name=="Foo"%>
                <h5>Foo</h5>
                <p><%= link_to s.name, foo_path(s) %></p>
            <% elsif s.class.name=="Bar"%>
                <h5>Bar</h5>
                <p><%= link_to s.name, bar_path(s) %></p>
            <% elsif s.class.name=="CarlSagan"%>
                <h5>I LOVE YOU CARL SAGAN!</h5>
                <p><%= link_to s.name, carl_sagan_path(s.user) %></p>
            <% end %>
        </div>
    <% end %>

<% else %>
    <p>Your search returned no results.</p>
    <% end %>
</div>
1
Why would you use facets for this? Cant you just create a choice menu where all possible classes are listed with the number of results all have? Then when you call the search method, dynamically call the related class as argument for the search?Benjamin Udink ten Cate
Did you reindex your datas? And did you check if the datas has the class name in the sunspot files?Sebastien

1 Answers

2
votes

This

Sunspot.search(Foo, Bar){with(:about, 'a'); facet(:name)}

translates to the following in Solr

INFO: [] webapp=/solr path=/select params={facet=true&start=0&q=*:*&f.name_s.facet.mincount=1&facet.field=name_s&wt=ruby&fq=type:(Foo+OR+Bar)&fq=about_s:a&rows=30} hits=1 status=0 QTime=1 

You can find the exact Solr query in the solr/log/ solr_production.log file

if you notice facet(:name) translated to f.name_s.facet and not f.foo.facet and f.bar.facet. That is why it did not work as you expected.

The following will work but that needs one to create 3 dummy methods in each model. The idea is you need a separate facet line for each of the types.

Sunspot.search Foo, Bar, CarlSagan do
  keywords q
  #provide faceting for "search class", a field representing a pretty version of the model name
  facet(:foo)
  facet(:bar)
  facet(:carlsagan)
  with(:search_class, params[:class]) if params[:class].present?
  paginate(:page => params[:page], :per_page => 30)
end

Again, it is always better to look at the actual SOLR query log to debug the search issues. Sunspot makes many things magical but it has its limitations ;-)