= MetaSearch
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.
== NOTE
The successor to MetaSearch is {Ransack}[http://github.com/ernie/ransack]. It's got features
that MetaSearch doesn't, along with some API changes. I haven't had the time to dedicate to
making it bulletproof yet, so I'm releasing a 1.1.x branch of MetaSearch to help with migrations
to Rails 3.1.
This is intended to be a stopgap measure.
t's important to note that the long-term migration path for your apps should be toward
Ransack, which is written in a more sane manner that will make supporting new versions
of Rails much easier going forward.
== Getting Started
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, to install as a plugin:
rails plugin install git://github.com/ernie/meta_search.git
In your controller:
def index
@search = Article.search(params[:search])
@articles = @search.all # load all matching records
# @articles = @search.relation # Retrieve the relation, to lazy-load in view
# @articles = @search.paginate(:page => params[:page]) # Who doesn't love will_paginate?
end
In your view:
<%= form_for @search, :url => articles_path, :html => {:method => :get} do |f| %>
<%= f.label :title_contains %>
<%= f.text_field :title_contains %>
<%= f.label :comments_created_at_greater_than, 'With comments after' %>
<%= f.datetime_select :comments_created_at_greater_than, :include_blank => true %>
<%= f.submit %>
<% end %>
Options for the search method are documented at MetaSearch::Searches::ActiveRecord.
== "Wheres", and what they're good for
Wheres are how MetaSearch does its magic. Wheres have a name (and possible aliases) which are
appended to your model and association attributes. When you instantiate a MetaSearch::Builder
against a model (manually or by calling your model's +search+ method) the builder responds to
methods named for your model's attributes and associations, suffixed by the name of the Where.
These are the default Wheres, broken down by the types of ActiveRecord columns they can search
against:
=== All data types
* _equals_ (alias: _eq_) - Just as it sounds.
* _does_not_equal_ (aliases: _ne_, _noteq_) - The opposite of equals, oddly enough.
* _in_ - Takes an array, matches on equality with any of the items in the array.
* _not_in_ (aliases: _ni_, _notin_) - Like above, but negated.
* _is_null_ - The column has an SQL NULL value.
* _is_not_null_ - The column contains anything but NULL.
=== Strings
* _contains_ (aliases: _like_, _matches_) - Substring match.
* _does_not_contain_ (aliases: _nlike_, _nmatches_) - Negative substring match.
* _starts_with_ (alias: _sw_) - Match strings beginning with the entered term.
* _does_not_start_with_ (alias: _dnsw_) - The opposite of above.
* _ends_with_ (alias: _ew_) - Match strings ending with the entered term.
* _does_not_end_with_ (alias: _dnew_) - Negative of above.
=== Numbers, dates, and times
* _greater_than_ (alias: _gt_) - Greater than.
* _greater_than_or_equal_to_ (aliases: _gte_, _gteq_) - Greater than or equal to.
* _less_than_ (alias: _lt_) - Less than.
* _less_than_or_equal_to_ (aliases: _lte_, _lteq_) - Less than or equal to.
=== Booleans
* _is_true_ - Is true. Useful for a checkbox like "only show admin users".
* _is_false_ - The complement of _is_true_.
=== Non-boolean data types
* _is_present_ - As with _is_true_, useful with a checkbox. Not NULL or the empty string.
* _is_blank_ - Returns records with a value of NULL or the empty string in the column.
So, given a model like this...
class Article < ActiveRecord::Base
belongs_to :author
has_many :comments
has_many :moderations, :through => :comments
end
...you might end up with attributes like title_contains,
comments_title_starts_with, moderations_value_less_than,
author_name_equals, and so on.
Additionally, all of the above predicate types also have an _any and _all version, which
expects an array of the corresponding parameter type, and requires any or all of the
parameters to be a match, respectively. So:
Article.search :author_name_starts_with_any => ['Jim', 'Bob', 'Fred']
will match articles authored by Jimmy, Bobby, or Freddy, but not Winifred.
== Advanced usage
=== 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):
@search = current_user.projects.search(params[:search])
Or, you can build up any relation you like and call the search method on that object:
@projects_with_awesome_users_search =
Project.joins(:user).where(:users => {:awesome => true}).search(params[:search])
=== ORed conditions
If you'd like to match on one of several possible columns, you can do this:
<%= f.text_field :title_or_description_contains %>
<%= f.text_field :title_or_author_name_starts_with %>
Caveats:
* Only one match type is supported. You can't do
title_matches_or_description_starts_with for instance.
* If you're matching across associations, remember that the associated table will be
INNER JOINed, therefore limiting results to those that at least have a corresponding
record in the associated table.
=== 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 to 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 "travels" through 5 models total.
In the case of polymorphic belongs_to associations, things work a bit differently. Let's say
you have the following models:
class Article < ActiveRecord::Base
has_many :comments, :as => :commentable
end
class Post < ActiveRecord::Base
has_many :comments, :as => :commentable
end
class Comment < ActiveRecord::Base
belongs_to :commentable, :polymorphic => true
validates_presence_of :body
end
Your first instinct might be to set up a text field for :commentable_body_contains, but
you can't do this. MetaSearch would have no way to know which class lies on the other side
of the polymorphic association, so it wouldn't be able to join the correct tables.
Instead, you'll follow a convention Searchlogic users are already familiar with, using the
name of the polymorphic association, then the underscored class name (AwesomeClass becomes
awesome_class), then the delimiter "type", to tell MetaSearch anything that follows is an
attribute name. For example:
<%= f.text_field :commentable_article_type_body_contains %>
If you'd like to match on multiple types of polymorphic associations, you can join them
with \_or_, just like any other conditions:
<%= f.text_field :commentable_article_type_body_or_commentable_post_type_body_contains %>
It's not pretty, but it works. Alternately, consider creating a custom search method as
described below to save yourself some typing if you're creating a lot of these types of
search fields.
=== Adding a new Where
If none of the built-in search criteria work for you, you can add new Wheres. 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?)
}
See MetaSearch::Where for info on the 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.
=== multiparameter_field
The example Where above adds support for a "between" search, which 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 MetaSearch::Helpers::FormBuilder 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: