Querying multiple models with different attributes using Sunspot
Asked Answered
S

2

7

I'm using Sunspot to index and search several models in a Rails project and I need to limit results based on the models' HABTM associations with a Department model. This is because users may not have permission to see records in all departments so results from those departments shouldn't be returned.

Here are the important parts of two of the models:

class Message < ActiveRecord::Base
  has_many :comments, dependent: :destroy
  has_and_belongs_to_many :departments

  searchable do
    text :title, :body
    text :comments do
      comments.map(&:body)
    end
    date :created_at
    integer :department_ids, using: :department_ids, references: Department, multiple: true
  end
end

class Document < ActiveRecord::Base
  has_and_belongs_to_many :departments

  searchable do
    text :name
    date :created_at
    integer :department_ids, using: :department_ids, references: Department, multiple: true
  end
end

And here's the search controller code:

class SearchController < ApplicationController
  def index
    # These arrays are created here for the sake of this example
    document_permitted_departments = [1, 2, 3]
    message_permitted_departments = [3, 4]

    search = Sunspot.search Document, Message do
      # This obviously doesn't work
      with(:department_ids, document_permitted_departments)
      with(:department_ids, message_permitted_departments)
      fulltext params[:q]
      paginate page: params[:page], per_page: SEARCH_RESULTS_PER_PAGE
      order_by :created_at, :desc
    end
    @results = search.results
    @number_of_results = search.total

    respond_to do |format|
      format.js
      format.html
    end
  end
end

The problem is that a user may be able to read documents in Department A and Department B but they should only see messages in Department B.

Is there a way to apply the with scope to a specific model in a multi-model search? Or is there another way of doing this that I'm missing?

Shy answered 16/2, 2014 at 12:21 Comment(0)
S
11

After a lot more Googling and some trial and error I finally figured it out. Here's a heavily annotated version of the code I ended up with:

class SearchController < ApplicationController
  before_filter :authenticate_user!

  def index
    # These arrays are created here for the sake of this example
    # Push 0 on to the end because empty arrays break the `with :department_ids` scopes below
    document_permitted_departments = [1, 2, 3].push(0)
    message_permitted_departments = [3, 4].push(0)

    search = Sunspot.search Document, Message do
      any_of do # Return anything that matches any of the scopes in this block
        all_of do # Return only those results that match these scopes
          with :class, Document # This limits scopes in this block to Document results
          with :department_ids, document_permitted_departments
        end

        all_of do # Return only those results that match these scopes
          with :class, Message # This limits scopes in this block to Message results
          with :department_ids, message_permitted_departments
        end
      end

      fulltext params[:q]
      paginate page: params[:page], per_page: SEARCH_RESULTS_PER_PAGE
      order_by :created_at, :desc
    end
    @results = search.results
    @number_of_results = search.total

    respond_to do |format|
      format.js # index.js.erb
      format.html # index.html.erb
    end
  end
end
Shy answered 18/2, 2014 at 21:39 Comment(1)
I'd be interested in how to do highlights across varied field names and varied models using this syntax :-)Scholem
S
0

Adding to Simon answer,

I have found one more sceneario. Where u want to apply condition in one model and match other with fulltext only.

My first try was this

  def search_all(options)
    Sunspot.search(Post, Tag) do
      any_of do
        all_of do
          with :class, Post
          with(:status, :published)
        end
      end

      fulltext options[:search]
      group(:class) { limit 30 }
    end
  end

This was giving me only Post but not Tag, somehow status: :published is applied on Tag and no results coming for Tag. I tried some other variations too.

Finally I figured out one solution.

Sunspot.search(Post, Tag) do
  any_of do
    all_of do
      with :class, Post
      with(:status, :published)
    end

    # This might look weiered to put all_of without any filter, However
    # without the next all_of block, status: :published is applied in tags and no results appear for tags.
    # Meaning any_of is ignored if there is one nested block and Post all_of is applied for entire filter.
    all_of do
      with :class, Tag
    end
  end

  fulltext options[:search]
  group(:class) { limit 30 }
end

This solution worked. May be This might have some other solution. Still I m happy with this.

Surname answered 12/7, 2016 at 3:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.