forked from binarylogic/searchlogic
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
87754bc
commit a9cbc43
Showing
16 changed files
with
990 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
Copyright (c) 2009 Binary Logic | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining | ||
a copy of this software and associated documentation files (the | ||
"Software"), to deal in the Software without restriction, including | ||
without limitation the rights to use, copy, modify, merge, publish, | ||
distribute, sublicense, and/or sell copies of the Software, and to | ||
permit persons to whom the Software is furnished to do so, subject to | ||
the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be | ||
included in all copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | ||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | ||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | ||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | ||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | ||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
= Searchlogic | ||
|
||
<b>Searchlogic has been <em>completely</em> 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 ~350 lines of code.</b> | ||
|
||
Searchlogic is a library that leverages named scopes to make searching in your application simple. | ||
|
||
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") | ||
|
||
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... | ||
|
||
== Named scopes for associations | ||
|
||
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. If you want to use the :include option, just specify it: | ||
|
||
User.orders_line_items_price_greater_than(20).all(:include => {:orders => :line_items}) | ||
|
||
Now all of Searchlogic's goodness fits nicely into your app and you can use it with your custom named scopes. Nice and clean. | ||
|
||
== Make searching in your application trivial | ||
|
||
The above is great, but what about tying all of this in with a search form in your application? Just do this... | ||
|
||
User.search(:username_like => "bjohnson", :age_less_than => 20) | ||
|
||
The above is equivalent to: | ||
|
||
User.username_like("bjohnson").age_less_than(20) | ||
|
||
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 | ||
|
||
== 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 searching edge case that you need, just create your own named scope. The sky is the limit. | ||
|
||
== 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]) | ||
|
||
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. | ||
|
||
== Copyright | ||
|
||
Copyright (c) 2009 {Ben Johnson of Binary Logic}[http://www.binarylogic.com], released under the MIT license |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
require 'rubygems' | ||
require 'rake' | ||
|
||
begin | ||
require 'jeweler' | ||
Jeweler::Tasks.new do |gem| | ||
gem.name = "search" | ||
gem.summary = %Q{TODO} | ||
gem.email = "bjohnson@binarylogic.com" | ||
gem.homepage = "http://github.com/binarylogic/search" | ||
gem.authors = ["binarylogic"] | ||
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings | ||
end | ||
|
||
rescue LoadError | ||
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler" | ||
end | ||
|
||
require 'spec/rake/spectask' | ||
Spec::Rake::SpecTask.new(:spec) do |spec| | ||
spec.libs << 'lib' << 'spec' | ||
spec.spec_files = FileList['spec/**/*_spec.rb'] | ||
end | ||
|
||
Spec::Rake::SpecTask.new(:rcov) do |spec| | ||
spec.libs << 'lib' << 'spec' | ||
spec.pattern = 'spec/**/*_spec.rb' | ||
spec.rcov = true | ||
end | ||
|
||
|
||
task :default => :spec | ||
|
||
require 'rake/rdoctask' | ||
Rake::RDocTask.new do |rdoc| | ||
if File.exist?('VERSION.yml') | ||
config = YAML.load(File.read('VERSION.yml')) | ||
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}" | ||
else | ||
version = "" | ||
end | ||
|
||
rdoc.rdoc_dir = 'rdoc' | ||
rdoc.title = "search #{version}" | ||
rdoc.rdoc_files.include('README*') | ||
rdoc.rdoc_files.include('lib/**/*.rb') | ||
end | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
require "searchlogic/named_scopes/conditions" | ||
require "searchlogic/named_scopes/ordering" | ||
require "searchlogic/named_scopes/associations" | ||
require "searchlogic/search" | ||
require "searchlogic/search_proxy" | ||
|
||
ActiveRecord::Base.extend(Searchlogic::NamedScopes::Conditions) | ||
ActiveRecord::Base.extend(Searchlogic::NamedScopes::Ordering) | ||
ActiveRecord::Base.extend(Searchlogic::NamedScopes::Associations) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
module Searchlogic | ||
module NamedScopes | ||
module Associations | ||
private | ||
def method_missing(name, *args, &block) | ||
if details = association_condition_details(name) | ||
create_association_condition(details[:association], details[:column], details[:condition], args) | ||
send(name, *args) | ||
elsif details = association_alias_condition_details(name) | ||
create_association_alias_condition(details[:association], details[:column], details[:condition], args) | ||
send(name, *args) | ||
elsif details = association_ordering_condition_details(name) | ||
create_association_ordering_condition(details[:association], details[:order_as], details[:column], args) | ||
send(name, *args) | ||
else | ||
super | ||
end | ||
end | ||
|
||
def association_condition_details(name) | ||
associations = reflect_on_all_associations.collect { |assoc| assoc.name } | ||
if name.to_s =~ /^(#{associations.join("|")})_(\w+)_(#{Conditions::PRIMARY_CONDITIONS.join("|")})$/ | ||
{:association => $1, :column => $2, :condition => $3} | ||
end | ||
end | ||
|
||
def create_association_condition(association_name, column, condition, args) | ||
named_scope("#{association_name}_#{column}_#{condition}", association_condition_options(association_name, "#{column}_#{condition}", args)) | ||
end | ||
|
||
def association_alias_condition_details(name) | ||
associations = reflect_on_all_associations.collect { |assoc| assoc.name } | ||
if name.to_s =~ /^(#{associations.join("|")})_(\w+)_(#{Conditions::ALIAS_CONDITIONS.join("|")})$/ | ||
{:association => $1, :column => $2, :condition => $3} | ||
end | ||
end | ||
|
||
def create_association_alias_condition(association, column, condition, args) | ||
primary_condition = primary_condition(condition) | ||
alias_name = "#{association}_#{column}_#{condition}" | ||
primary_name = "#{association}_#{column}_#{primary_condition}" | ||
send(primary_name, *args) # go back to method_missing and make sure we create the method | ||
(class << self; self; end).class_eval { alias_method alias_name, primary_name } | ||
end | ||
|
||
def association_ordering_condition_details(name) | ||
associations = reflect_on_all_associations.collect { |assoc| assoc.name } | ||
if name.to_s =~ /^(ascend|descend)_by_(#{associations.join("|")})_(\w+)$/ | ||
{:order_as => $1, :association => $2, :column => $3} | ||
end | ||
end | ||
|
||
def create_association_ordering_condition(association_name, order_as, column, args) | ||
named_scope("#{order_as}_by_#{association_name}_#{column}", association_condition_options(association_name, "#{order_as}_by_#{column}", args)) | ||
end | ||
|
||
def association_condition_options(association_name, association_condition, args) | ||
association = reflect_on_association(association_name.to_sym) | ||
scope = association.klass.send(association_condition, *args) | ||
arity = association.klass.named_scope_arity(association_condition) | ||
|
||
if !arity || arity == 0 | ||
# The underlying condition doesn't require any parameters, so let's just create a simple | ||
# named scope that is based on a hash. | ||
options = scope.proxy_options | ||
add_left_outer_join(options, association) | ||
options | ||
else | ||
# The underlying condition requires parameters, let's match the parameters it requires | ||
# and pass those onto the named scope. We can't use proxy_options because that returns the | ||
# result after a value has been passed. | ||
proc_args = [] | ||
arity.times { |i| proc_args << "arg#{i}"} | ||
eval <<-"end_eval" | ||
lambda { |#{proc_args.join(",")}| | ||
options = association.klass.named_scope_options(association_condition).call(#{proc_args.join(",")}) | ||
add_left_outer_join(options, association) | ||
options | ||
} | ||
end_eval | ||
end | ||
end | ||
|
||
# In a named scope you have 2 options for adding joins: :include and :joins. | ||
# | ||
# :include will execute multiple queries for each association and instantiate objects for each association. | ||
# This is not what we want when we are searching. The only other option left is :joins. We can pass the | ||
# name of the association directly, but AR creates an INNER JOIN. If we are ordering by an association's | ||
# attribute, and that association is optional, the records without an association will be omitted. Again, | ||
# not what we want. | ||
# | ||
# So the only option left is to use :joins with manually written SQL. We can still have AR generate this SQL | ||
# for us by leveraging it's join dependency classes. Instead of using the InnerJoinDependency we use the regular | ||
# JoinDependency which creates a LEFT OUTER JOIN, which is what we want. | ||
# | ||
# The code below was extracted out of AR's add_joins! method and then modified. | ||
def add_left_outer_join(options, association) | ||
join = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, association.name, nil).join_associations.collect { |assoc| assoc.association_join }.join | ||
options[:joins] ||= "" | ||
next if options[:joins].include?(join) | ||
options[:joins] = join.strip + (options[:joins].blank? ? "" : " #{options[:joins]}") | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.