Search code examples
ruby-on-railssunspotfaceted-search

Sunspot Multi Faceted Search


I am building an API that is using Sunspot to act as the search engine on the backend and implement a faceted search. I have the following model for how I am doing the many different facet types for all of my content:

Contents
  content_name
  searchable_fields

Facets
  facet_name

ContentFacets
  content_id
  facet_id

Where Content has_many facets though content_facets. I have managed to get everything set up to get this to work as a single faceted search - as the API it returns everything it needs to and when the front-end application sends back the necessary parameters it will drill down by that one facet, however, I can't get it to do multiple facets from there. Here is the search I have doing that (again, this is working):

#SearchController

facet_id = Facet.find_by_name(params[:commit]).id

@search = Content.search do
    fulltext params[:search]
    with(:facet_ids, facet_id) if params[:commit].present?
    facet(:facet_ids)
    paginate :page => 1, :per_page => 300
end

facets = @search.facet(:facet_ids).rows.map { |row| row.instance.name }

render :json => [facets, @search.results]

And then the Content Model for how it is configured:

searchable do
  text :searchable_fields
  integer :facet_ids, :multiple => true, :references => Facet
end

And lastly, here's how it is handled on the front-side application that is querying my API: (It would appear as if the previous parameters are being passed back as parameters in the route itself, while the :commit param from the submit_tag is what is determining the specific facet name)

<% if @facets %>
  <div id="facets">
  <h3>Given - <%= @search_params %></h3>
  <ul>
    <% @facets.each do |facet| %>
      <%= form_tag facet_path(@search_params), method: "POST" do %>
        <li><%= submit_tag facet %></li>
      <% end %>
    <% end %>
  </ul>
</div>

So my question is this: Given everything what would be the best way for me to make this into an actual multi faceted search, rather than just using the one facet and overwriting it every time a new one is selected?


Solution

  • With your particular problem, the following approach should work -

    In your view simply add this line above the submit_tag line

    <li><%= hidden_field_tag :existing_facets, @existing_facets %></li>
    

    Then before you make your call to the API have a chunk of code like so:

    @existing_facets = ""
    
            unless facet_params[:existing_facets].nil?
                @existing_facets =  "#{facet_params[:existing_facets]}+#{facet_params[:commit]}"
            end
    

    This will keep track of all of the facets that a User has selected for a specific query, while still keeping the original search parameters in there as well - these are being stored in order in a string for ease of use, separated by a + sign.

    So now on the API's side I would change your search code to the following:

    facets = gather_facets(params[:existing_facets], params[:commit])
    
    @search = Content.search do
        fulltext params[:search]
        facets.each do |facet|
            facet_id = Facet.find_by_name(facet).id
            with(:facet_ids, facet_id)
        end
        facet(:facet_ids)
        paginate :page => 1, :per_page => 300
    end
    
    facets = @search.facet(:facet_ids).rows.map { |row| row.instance.name }
    
    render :json => [facets, @search.results]
    
    def gather_facets(existing_str, new_facet)
      arr = existing_str.scan(/([^+]+)/).flatten << new_facet
    end
    

    This will keep track of all of the chosen facets, query them in order, and narrow itself down before the issue of iterating in a search query causes the API to slow down (assuming the facets are done properly :P)