metautonomo.us

Confounding URL typists since 2007.

MetaSearch

Description

MetaSearch is extensible searching for your form_for enjoyment. It “wraps” one of your ActiveRecord models, providing methods that allow you to build up search conditions against that model, and has a few extra form helpers to simplify sorting and supplying multiple parameters to your condition methods as well.

Installation

Installation is simple. In your Gemfile:

gem "meta_search"  # Last officially released gem
gem "meta_search", :git => "git://github.com/ernie/meta_search.git" # Track git repo

Or, if you prefer to install as a plugin:

rails plugin install git://github.com/ernie/meta_search.git

Basic Usage

In your controller:

  def index
     = Article.search(params[:search])
     = .all   # or  to lazy load in view
  end

In your view:

  <%= form_for  do |f| %>
    <%= f.label :title_contains %>
    <%= f.text_field :title_contains %><br />
    <%= f.label :comments_created_at_greater_than, 'With comments after' %>
    <%= f.datetime_select :comments_created_at_greater_than, :include_blank => true %><br />
    <!-- etc... -->
    <%= f.submit %>
  <% end %>

Default Where types and available options for the search method are listed in the RDocs.

Compound conditions (any/all)
All Where types automatically get an “any” and “all” variant. This has the same name and aliases as the original, but is suffixed with _any and _all, for an “OR” or “AND” search, respectively. So, if you want to provide the user with 5 different search boxes to enter possible article titles:

  <%= f.multiparameter_field :title_contains_any,
        *5.times.inject([]) {|a, b| a << {:field_type => :text_field}} +
        [:size => 10] %>

Multi-level associations
MetaSearch will allow you traverse your associations in one form, generating the necessary joins along the way. If you have the following models…

  class Company < ActiveRecord::Base
    has_many :developers
  end
 
  class Developer < ActiveRecord::Base
    belongs_to :company
    has_many :notes
  end

…you can do this in your form to search your companies by developers with certain notes:

  <%= f.text_field :developers_notes_note_contains %>

You can travel forward and back through the associations, so this would also work (though be entirely pointless in this case):

  <%= f.text_field :developers_notes_developer_company_name_contains %>

However, to prevent abuse, this is limited to associations of a total “depth” of 5 levels. This means that while starting from a Company model, as above, you could do Company -> :developers -> :notes -> :developer -> :company, which has gotten you right back where you started, but is 5 total tables.

Customizing

Narrowing the scope of a search
While the most common use case is to simply call Model.search(params[:search]), there may be times where you want to scope your search more tightly. For instance, only allowing users to search their own projects (assuming a current_user method returning the current user):

   = current_user.projects.search(params[:search])

Or, you can build up any relation you like and call the search method on that object:

   =
    Project.joins(:user).where(:users => {:awesome => true}).search(params[:search])

Adding a new Where:
If none of the built-in search criteria work for you, you can add a new Where (or 5). To do so, create an initializer (/config/initializers/meta_search.rb, for instance) and add lines like:

  MetaSearch::Where.add :between, :btw,
    :predicate => :in,
    :types => [:integer, :float, :decimal, :date, :datetime, :timestamp, :time],
    :formatter => Proc.new {|param| Range.new(param.first, param.last)},
    :validator => Proc.new {|param|
      param.is_a?(Array) && !(param[0].blank? || param[1].blank?)
    }

Again, see the RDocs for supported options.

Accessing custom search methods (and named scopes!)
MetaSearch can be given access to any class method on your model to extend its search capabilities. The only rule is that the method must return an ActiveRecord::Relation so that MetaSearch can continue to extend the search with other attributes. Conveniently, scopes (formerly “named scopes”) do this already.

Consider the following model:

  class Company < ActiveRecord::Base
    has_many :slackers, :class_name => "Developer", :conditions => {:slacker => true}
    scope :backwards_name, lambda {|name| where(:name => name.reverse)}
    scope :with_slackers_by_name_and_salary_range,
      lambda {|name, low, high|
        joins(:slackers).where(:developers => {:name => name, :salary => low..high})
      }
  end

To allow MetaSearch access to a model method, including a named scope, just use search_methods in the model:

  search_methods :backwards_name

This will allow you to add a text field named :backwards_name to your search form, and it will behave as you might expect.

In the case of the second scope, we have multiple parameters to pass in, of different types. We can pass the following to search_methods:

  search_methods :with_slackers_by_name_and_salary_range,
    :splat_param => true, :type => [:string, :integer, :integer]

MetaSearch needs us to tell it that we don’t want to keep the array supplied to it as-is, but “splat” it when passing it to the model method. Regarding :types: In this case, ActiveRecord would have been smart enough to handle the typecasting for us, but I wanted to demonstrate how we can tell MetaSearch that a given parameter is of a specific database “column type.” This is just a hint MetaSearch uses in the same way it does when casting “Where” params based on the DB column being searched. It’s also important so that things like dates get handled properly by FormBuilder.

View Helpers

multiparameter_field
The example “between” where we added in the earlier example requires an array with two parameters. These can be passed using Rails multiparameter attributes. To make life easier, MetaSearch adds a helper for this:

  <%= f.multiparameter_field :moderations_value_between,
      {:field_type => :text_field}, {:field_type => :text_field}, :size => 5 %>

multiparameter_field works pretty much like the other FormBuilder helpers, but it lets you sandwich a list of fields, each in hash format, between the attribute and the usual options hash. See the RDocs for more info.

checks and collection_checks
If you need to get an array into your where, and you don’t care about parameter order, you might choose to use a select or collection_select with multiple selection enabled, but everyone hates multiple selection boxes. MetaSearch adds a couple of additional helpers, checks and collection_checks to handle multiple selections in a more visually appealing manner. They can be called with or without a block. Without a block, you get an array of MetaSearch::Check objects to do with as you please.

With a block, each check is yielded to your template, like so:

  <h2>How many heads?</h2>
  <ul>
    <% f.checks :number_of_heads_in,
      [['One', 1], ['Two', 2], ['Three', 3]], :class => 'checkboxy' do |check| %>
      <li>
        <%= check.box %>
        <%= check.label %>
      </li>
    <% end %>
  </ul>

Again, full documentation is in the RDocs.

Sorting
If you’d like to sort by a specific column in your results (the attributes of the base model) or an association column then supply the meta_sort parameter in your form. The parameter takes the form column.direction where column is the column name or underscore-separated association_column combination, and direction is one of “asc” or “desc” for ascending or descending, respectively.

Normally, you won’t supply this parameter yourself, but instead will use the helper method sort_link in your views, like so:

  <%= sort_link , :title %>
  <%= sort_link , :author_name, "Author", {}, :class => "author" %>

The object is the instance of MetaSearch::Builder you received earlier in your controller. The other required parameter is the attribute name itself. Optionally, you can provide a string as a 3rd parameter to override the default link name, and then additional hashes for the standard options and html_options params to link_to.

All sort_link-generated links will have the CSS class “sort_link”, as well as a directional class (“ascending” or “descending”) if the link is for a currently sorted column, for your styling enjoyment.

This feature should hopefully help out those of you migrating from SearchLogic, and a thanks goes out to Ben Johnson for the HTML entities used for the up and down arrows, which provide a nice default look.

Security

Since search forms generate HTTP GET requests, the URLs invite tinkering, and you may have attributes or associations that you don’t want to expose to search from your application. So, if you’d like to allow only certain associations or attributes to be searched, you can do so inside your models:

  class Article < ActiveRecord::Base
    attr_searchable :some_public_data, :some_more_searchable_stuff
    assoc_searchable :search_this_association_why_dontcha
  end

If you’d rather blacklist attributes and associations rather than whitelist, use the attr_unsearchable and assoc_unsearchable method instead. If a whitelist is supplied, it takes precedence.

Excluded attributes on a model will be honored across associations, so if an Article has_many :comments and the Comment model looks something like this:

  class Comment < ActiveRecord::Base
    validates_presence_of :user_id, :body
    attr_unsearchable :user_id
  end

Then your call to Article.search will allow :comments_body_contains but not :comments_user_id_equals.

Contributing

There are several ways you can help MetaSearch continue to improve.

  • Use MetaSearch in your real-world projects and submit bug reports or feature suggestions.
  • Better yet, if you’re so inclined, fix the issue yourself and submit a patch! Or you can fork the project on GitHub and send me a pull request (please include tests!)
  • If you like MetaSearch, spread the word. More users == more eyes on code == more bugs getting found == more bugs getting fixed (hopefully!)
  • Lastly, if MetaSearch has saved you hours of development time on your latest Rails gig, and you’re feeling magnanimous, please consider making a donation to the project. I have spent hours of my personal time coding and supporting MetaSearch, and your donation would go a great way toward justifying that time spent to my loving wife. :)