Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Searchlogic provides object based searching, common named scopes, and other useful named scope tools.

Fetching latest commit…

Cannot retrieve the latest commit at this time

README.rdoc

Searchlogic

Searchlogic has been completely rewritten for v2. It is much simpler and has taken an entirely new approach. To give you an idea, v1 had ~2300 lines of code, v2 has ~420 lines of code.

Attention v1 users. To start using v2 you must use the binarylogic-searchlogic gem. I have decided to start using github to host my gems. See below for installation instructions.

Searchlogic provides common named scopes and object based searching for ActiveRecord.

Helpful links

Before contacting me directly, please read:

If you find a bug or a problem please post it on lighthouse. If you need help with something, please use google groups. I check both regularly and get emails when anything happens, so that is the best place to get help. This also benefits other people in the future with the same questions / problems. Thank you.

Install & use

In your rails project:

# config/environment.rb
config.gem "binarylogic-searchlogic", 
  :lib     => 'searchlogic', 
  :source  => 'http://gems.github.com', 
  :version => '~> 2.0.0'

Then install the gem:

rake gems:install

That's it, you are ready to go. See below for usage examples.

Search using conditions on columns

Instead of explaining what Searchlogic can do, let me show you. Let's start at the top:

# We have the following model
User(id: integer, created_at: datetime, username: string, age: integer)

# Searchlogic gives you a bunch of named scopes for free:
User.username_equals("bjohnson")
User.username_does_not_equal("bjohnson")
User.username_begins_with("bjohnson")
User.username_like("bjohnson")
User.username_ends_with("bjohnson")
User.age_greater_than(20)
User.age_greater_than_or_equal_to(20)
User.age_less_than(20)
User.age_less_than_or_equal_to(20)
User.username_null
User.username_blank

# You can also order by columns
User.ascend_by_username
User.descend_by_username
User.order("ascend_by_username")

Any named scope Searchlogic creates is dynamic and created via method_missing. Meaning it will only create what you need. Also, keep in mind, these are just named scopes, you can chain them, call methods off of them, etc:

scope = User.username_like("bjohnson").age_greater_than(20).ascend_by_username
scope.all
scope.first
scope.count
# etc...

That's all pretty standard, but here's where Searchlogic starts to get interesting…

Search using conditions on associated columns

You also get named scopes for any of your associations:

# We have the following relationships
User.has_many :orders
Order.has_many :line_items
LineItem

# Set conditions on association columns
User.orders_total_greater_than(20)
User.orders_line_items_price_greater_than(20)

# Order by association columns
User.ascend_by_order_total
User.descend_by_orders_line_items_price

Again these are just named scopes. You can chain them together, call methods off of them, etc. What's great about these named scopes is that they do NOT use the :include option, making them much faster. Instead they create a LEFT OUTER JOIN and pass it to the :joins option, which is great for performance. To prove my point here is a quick benchmark from an application I am working on:

Benchmark.bm do |x|
  x.report { 10.times { Event.tickets_id_gt(10).all(:include => :tickets) } }
  x.report { 10.times { Event.tickets_id_gt(10).all } }
end
      user     system      total        real
 10.120000   0.170000  10.290000 ( 12.625521)
  2.630000   0.050000   2.680000 (  3.313754)

If you want to use the :include option, just specify it:

User.orders_line_items_price_greater_than(20).all(:include => {:orders => :line_items})

Obviously, only do this if you want to actually use the included objects.

Lastly, because we are using ActiveRecord, named scopes, combining conditions, and ordering based on associated columns, the decision was made to use left outer joins instead of inner joins. This allows us to be consistent, include optional associations when ordering, and avoid duplicate joins when eager loading associations. If we use an inner join and combine any of these things we will get an “ambiguous name” sql error for the table being joined twice. Just like anything, be mindful of the SQL being produced in your application. If you are joining beyond 4 or 5 levels deep then you might consider looking at the query and optimizing it. Part of that optimization may require the use of inner joins depending on the query.

Make searching and ordering data in your application trivial

The above is great, but what about tying all of this in with a search form in your application? What would be really nice is if we could use an object that represented a single search. Like this…

search = User.search(:username_like => "bjohnson", :age_less_than => 20)
search.all

The above is equivalent to:

User.username_like("bjohnson").age_less_than(20).all

You can set, read, and chain conditions off of your search too:

search.username_like                              => "bjohnson"
search.age_gt = 2                                 => 2
search.id_gt(10).email_begins_with("bjohnson")    => <#Searchlogic::Search...>
search.all                                        => An array of users
search.count                                      => integer
# .. etc

All that the search method does is chain named scopes together for you. What's so great about that? It keeps your controllers extremely simple:

class UsersController < ApplicationController
  def index
    @search = User.search(params[:search])
    @users = @search.all
  end
end

It doesn't get any simpler than that. Adding a search condition is as simple as adding a condition to your form. Remember all of those named scopes above? Just create fields with the same names:

- form_for @search do |f|
  = f.text_field :username_like
  = f.select :age_greater_than, (0..100)
  = f.text_field :orders_total_greater_than
  = f.submit

When a Searchlogic::Search object is passed to form_for it will add a hidden field for the “order” condition, to preserve the order of the data. If you want to order your search with a link, just specify the name of the column. Ex:

= order @search, :by => :age

This will create a link that alternates between calling “ascend_by_age” and “descend_by_age”. If you wanted to order your data by more than just a column, create your own named scopes: “ascend_by_*” and “descend_by_*”. The “order” helper is a very straight forward helper, checkout the docs for some of the options.

Use your existing named scopes

This is one of the big differences between Searchlogic v1 and v2. What about your existing named scopes? Let's say you have this:

User.named_scope :four_year_olds, :conditions => {:age => 4}

Again, these are all just named scopes, use it in the same way:

User.search(:four_year_olds => true, :username_like => "bjohnson")

Notice we pass true as the value. If a named scope does not accept any parameters (arity == 0) you can simply pass it true or false. If you pass false, the named scope will be ignored. If your named scope accepts a parameter, the value will be passed right to the named scope regardless of the value.

Now just throw it in your form:

- form_for @search do |f|
  = f.text_field :username_like
  = f.check_box :four_year_olds
  = f.submit

What's great about this is that you can do just about anything you want. If Searchlogic doesn't provide a named scope for that crazy edge case that you need, just create your own named scope. The sky is the limit.

Use any or all

Every condition you've seen in this readme also has 2 related conditions that you can use. Example:

User.username_like_any("bjohnson", "thunt")   # will return any users that have either of the strings in their username
User.username_like_all("bjohnson", "thunt")   # will return any users that have all of the strings in their username
User.username_like_any(["bjohnson", "thunt"]) # also accepts an array

This is great for checkbox filters, etc. Where you can pass an array right from your form to this condition.

Pagination (leverage will_paginate)

Instead of recreating the wheel with pagination, Searchlogic works great with will_paginate. All that Searchlogic is doing is creating named scopes, and will_paginate works great with named scopes:

User.username_like("bjohnson").age_less_than(20).paginate(:page => params[:page])
User.search(:username_like => "bjohnson", :age_less_than => 20).paginate(:page => params[:page])

If you don't like will_paginate, use another solution, or roll your own. Pagination really has nothing to do with searching, and the main goal for Searchlogic v2 was to keep it lean and simple. No reason to recreate the wheel and bloat the library.

Under the hood

Before I use a library in my application I like to glance at the source and try to at least understand the basics of how it works. If you are like me, a nice little explanation from the author is always helpful:

Searchlogic utilizes method_missing to create all of these named scopes. When it hits method_missing it creates a named scope to ensure it will never hit method missing for that named scope again. Sort of a caching mechanism. It works in the same fashion as ActiveRecord's “find_by_*” methods. This way only the named scopes you need are created and nothing more.

That's about it, the named scope options are pretty bare bones and created just like you would manually. The only big difference being the use of LEFT OUTER JOINS on associated conditions instead of INNER JOINS.

Credit

Thanks a lot to Tyler Hunt for helping plan, design, and start the project. He was a big help.

Copyright

Copyright © 2009 Ben Johnson of Binary Logic, released under the MIT license

Something went wrong with that request. Please try again.