Searchlogic
Searchlogic provides object based searching, common named scopes, and other useful tools.
Install / Use
/learn @binarylogic/SearchlogicREADME
= Searchlogic
Searchlogic makes using ActiveRecord named scopes easier and less repetitive. It helps keep your code DRY, clean, and simple.
The specs pass with ActiveRecord 2.3.9 - 2.3.14 on Ruby 1.9.3-p125, 1.9.2-p318, and 1.8.7-p358
== Helpful links
- <b>Documentation:</b> http://rdoc.info/projects/binarylogic/searchlogic
- <b>Repository:</b> http://github.com/binarylogic/searchlogic/tree/master
- <b>Issues:</b> http://github.com/binarylogic/searchlogic/issues
- <b>Google group:</b> http://groups.google.com/group/searchlogic
- <b>Railscast:</b> http://railscasts.com/episodes/176-searchlogic
<b>Before contacting me directly, please read:</b>
If you find a bug or a problem please post it in the issues section. 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
Install the gem from rubyforge:
sudo gem install searchlogic
Now just set it as a dependency in your project and you are ready to go.
You can also install this as a plugin:
script/plugin install git://github.com/binarylogic/searchlogic.git
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_equals(["bjohnson", "thunt"]) User.username_equals("a".."b") User.username_does_not_equal("bjohnson") User.username_begins_with("bjohnson") User.username_not_begin_with("bjohnson") User.username_like("bjohnson") User.username_not_like("bjohnson") User.username_ends_with("bjohnson") User.username_not_end_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_not_null User.username_blank
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).id_less_than(55) scope.all scope.first scope.count
etc...
For a complete list of conditions please see the constants in Searchlogic::NamedScopes::Conditions.
== Use condition aliases
Typing out 'greater_than_or_equal_to' is not fun. Instead Searchlogic provides various aliases for the conditions. For a complete list please see Searchlogic::NamedScopes::Conditions. But they are pretty straightforward:
User.username_is(10) # equals User.username_eq(10) # equals User.id_lt(10) # less than User.id_lte(10) # less than or equal to User.id_gt(10) # greater than User.id_gte(10) # greater than or equal to
etc...
== Search using scopes in associated classes
This is my favorite part of Searchlogic. You can dynamically call scopes on associated classes and Searchlogic will take care of creating the necessary joins for you. This is REALY nice for keeping your code DRY. The best way to explain this is to show you:
=== Searchlogic provided scopes
Let's take some basic scopes that Searchlogic provides for every model:
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
This is recursive, you can travel through your associations simply by typing it in the name of the method. Again these are just named scopes. You can chain them together, call methods off of them, etc.
=== Custom associated scopes
Also, these conditions aren't limited to the scopes Searchlogic provides. You can use your own scopes. Like this:
LineItem.named_scope :expensive, :conditions => "line_items.price > 500"
User.orders_line_items_expensive
As I stated above, Searchlogic will take care of creating the necessary joins for you. This is REALLY nice when trying to keep your code DRY, because if you wanted to use a scope like this in your User model you would have to copy over the conditions. Now you have 2 named scopes that are essentially doing the same thing. Why do that when you can dynamically access that scope using this feature?
=== Polymorphic associations
Polymorphic associations are tough because ActiveRecord doesn't support them with the :joins or :include options. Searchlogic checks for a specific syntax and takes care of this for you. Ex:
Audit.belongs_to :auditable, :polymorphic => true User.has_many :audits, :as => :auditable
Audit.auditable_user_type_username_equals("ben")
The above will take care of creating the inner join on the polymorphic association so that it only looks for type 'User'. On the surface it works the same as a non polymorphic association. The syntax difference being that you need to call the association and then specify the type:
[polymorphic association name]_[association type]_type
=== Uses :joins not :include
Another thing to note is that the joins created by Searchlogic do NOT use the :include option, making them <em>much</em> faster. Instead they leverage 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. Including objects into a query can be helpful with performance, especially when solving an N+1 query problem.
== Order your search
Just like the various conditions, Searchlogic gives you some very basic scopes for ordering your data:
User.ascend_by_id User.descend_by_id User.ascend_by_orders_line_items_price
etc...
== 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.
== Combine scopes with 'OR'
In the same fashion that Searchlogic provides a tool for accessing scopes in associated classes, it also provides a tool for combining scopes with 'OR'. As we all know, when scopes are combined they are joined with 'AND', but sometimes you need to combine scopes with 'OR'. Searchlogic solves this problem:
User.username_or_first_name_like("ben") => "username LIKE '%ben%' OR first_name like'%ben%'"
User.id_or_age_lt_or_username_or_first_name_begins_with(10) => "id < 10 OR age < 10 OR username LIKE 'ben%' OR first_name like'ben%'"
Notice you don't have to specify the explicit condition (like, gt, lt, begins with, etc.). You just need to eventually specify it. If you specify a column it will just use the next condition specified. So instead of:
User.username_like_or_first_name_like("ben")
You can do:
User.username_or_first_name_like("ben")
Again, these just map to named scopes. Use Searchlogic's dynamic scopes, use scopes on associations, use your own custom scopes. As long as it maps to a named scope it will join the conditions with 'OR'. There are no limitations.
== Create scope procedures
Sometimes you notice a pattern in your application where you are constantly combining certain named scopes. You want to keep the flexibility of being able to mix and match small named scopes, while at the same time being able to call a single scope for a common task. User searchlogic's scpe procedure:
User.scope_procedure :awesome, lambda { first_name_begins_with("ben").last_name_begins_with("johnson").website_equals("binarylogic.com") }
All that this is doing is creating a class level method, but what is nice about this method is that is more inline with your other named scopes. It also tells searchlogic that this method is 'safe' to use when using the search method. Ex:
User.search(:awesome => true)
Otherwise searchlogic will ignore the 'awesome' condition because there is no way to tell that its a valid scope. This is a security measure to keep users from passing in a scope with a named like 'destroy_all'.
== 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
