How to avoid mounting SQL by concatenating strings?
Yeah, that does feel kinda wrong. But OR queries in Rails are tricky.

There are two things here: One is creating the query, the other is parsing the params.

I'd probably start1 by preparing/parsing the params so building the query is cleaner. I'd add a method - or several - to the controller, rather than the model, to do this. Both model and controller are involved in this, but I'd rather not pass params to the model. Things like checking the string value of the telecommute param seems like a job for the controller, whereas the model's search method should only have to deal with a boolean. For instance, you might want to call Job.search from code, where it'd be silly to use string values.

There's some cleanup you can do here too. For instance, I'd strongly consider adding constants or methods for the work_types array and the location keys.

I'd also be more consistent; in one place you use %w(...) to make an array of (string) keys, and in another you use [...] to make an array of (symbol) keys.

Here's an example, if you were to just use one method:

def search_params
  search_terms = {}

  # put the "AND" terms in the hash
  search_terms[:telecommute] = params['telecommute'] == 'true'

  Job.LOCATION_KEYS.each_with_object(search_terms) do |key, memo|
    memo[key] = params[key] if key.present?
  end

  # put the "OR" terms in a nested hash
  Job.WORK_TYPES.each_with_object(search_terms) do |key, memo|
    if params[key].present?
      memo[:work_types] ||= {}
      memo[:work_types][key] = true
    end
  end

  search_terms
end


It ain't pretty, but at least it's all in one place.

As for the searching itself, you can use regular ActiveRecord's query interface without descending to Arel:

def self.search(hash = {})
  # we'll be modifying the hash, so work on a dup
  # (you'll first want to check if it's empty, though!)
  terms = hash.dup

  # grab the work types
  work_types = terms.delete(:work_types)

  # add the AND clauses
  query = self.where(terms)

  # add the OR clauses (if any)
  if work_types
    subquery = self.where(work_types).where_values.inject(:or) # magic!
    query = query.where(subquery)
  end

  query
end


I get this:

Job.search(telecommute: true, country: "US", work_types: { full_time: true, freelance: true }).to_sql
# =&gt; "SELECT "jobs".* FROM "jobs"  WHERE "jobs"."telecommute" = 't' AND "jobs"."country" = 'us' AND (("jobs"."full_time" = 't' OR "jobs"."freelance" = 't'))"




1 Edit: Actually, that's not where I'd start if I were writing this from scratch. It is however where I started my rewrite, but that's because it was already there. Otherwise, I'd start by defining the search method as I'd want it to look and work - without worrying about the params -, and then worry about how to make the params fit.