Permalink
Browse files

Released v1.0.2

  • Loading branch information...
Ben Johnson
Ben Johnson committed Sep 12, 2008
1 parent 4c13dd0 commit 07f8a72fefa39eeac59ca30c7fc02091d2f875da
View
@@ -1,4 +1,10 @@
-== 1.0.1 released 2008-09-08
+== 1.0.2 released 2008-09-12
+
+* Moved cached searchers out of the global namespace and into the Searchgasm::Cache namespce.
+* Various changes to improve performance. Added in benchmark reports in readme as well as a benchmarks directory.
+* Config.per_page works with new_search & new_search! only. Where as before it was only working if the search was protected.
+
+== 1.0.1 released 2008-09-11
* Cached "searchers" so when a new search object is instantiated it doesn't go through all of the meta programming and method creation. Helps a lot with performance. You will see the speed benefits after the first instantiation.
* Added in new options for page_links.
View
@@ -1,3 +1,6 @@
+benchmarks/benchmark.rb
+benchmarks/benchmark_helper.rb
+benchmarks/profile.rb
CHANGELOG.rdoc
examples/README.rdoc
init.rb
@@ -57,6 +60,7 @@ test/test_condition_base.rb
test/test_condition_types.rb
test/test_conditions_base.rb
test/test_conditions_protection.rb
+test/test_config.rb
test/test_helper.rb
test/test_search_base.rb
test/test_search_conditions.rb
View
@@ -55,23 +55,15 @@ In both examples, instead of using the "all" method you could use any search met
== The beauty of searchgasm, integration into rails
-Using Searchgasm in rails is the best part, because rails has all kinds of nifty methods to make dealing with ActiveRecord objects quick and easy, especially with forms. So let's take advantage of them! That's the idea behind this plugin. Searchgasm is searching, ordering, and pagination all rolled into one simple plugin. Take all of that pagination and searching cruft out of your models and let Searchgasm handle it. Check it out:
+Using Searchgasm in rails is the best part, because rails has all kinds of nifty methods to make dealing with ActiveRecord objects quick and easy, especially with forms. So let's take advantage of them! That's the idea behind this plugin. Searchgasm is searching, ordering, and pagination all rolled into one simple plugin. Take all of that pagination and searching cruft out of your models and controllers, and let Searchgasm handle it. Check it out:
# app/controllers/users_controller.rb
def index
@search = User.new_search(params[:search])
@users, @users_count = @search.all, @search.count
end
-Now your view. Things to note in this view:
-
-1. Passing a search object right into form\_for and fields\_for
-2. The built in conditions for each column and how you can traverse the relationships and set conditions on them
-3. The order_by_link helper
-4. The page_select and per_page_select helpers
-5. All of your search logic is in 1 spot: your view. Nice and DRY.
-
-Your view:
+Now your view:
# app/views/users/index.html.haml
- form_for @search do |f|
@@ -103,6 +95,14 @@ Your view:
- else
No users were found.
+Things to note in this view:
+
+1. Passing a search object right into form\_for and fields\_for
+2. The built in conditions for each column and how you can traverse the relationships and set conditions on them
+3. The order_by_link helper
+4. The page_select and per_page_select helpers
+5. All of your search logic is in 1 spot: your view. Nice and DRY.
+
<b>See my tutorial on this example: http://www.binarylogic.com/2008/9/7/tutorial-pagination-ordering-and-searching-with-searchgasm</b>
== Exhaustive Example w/ Object Based Searching (great for form_for or fields_for)
@@ -230,7 +230,7 @@ Not only can you use searchgasm when searching, but you can use it when setting
== Always use protection...against SQL injections
-If there is one thing we all know, it's to always use protection against SQL injections. That's why searchgasm protects you by default. The new\_search and new\_conditions methods are protected by default. This means that various checks are done to ensure it is not possible to perform any type of SQL injection. But this also limits how you can search, meaning you can't write raw SQL. If you want to be daring and search without protection, all that you have to do is add ! to the end of the methods: new\_search! and new\_conditions!.
+If there is one thing we all know, it's to always use protection against SQL injections. That's why searchgasm protects you by default. The new\_search and new\_conditions methods protect mass assignments by default (instantiation and search.options = {}). This means that various checks are done to ensure it is not possible to perform any type of SQL injection during mass assignments. But this also limits how you can search, meaning you can't write raw SQL. If you want to be daring and search without protection, all that you have to do is add ! to the end of the methods: new\_search! and new\_conditions!.
=== Protected from SQL injections
@@ -296,7 +296,7 @@ I want to use it, so let's add it:
class << self
# I pass you the column, you tell me what you want the method to be called.
# If you don't want to add this condition for that column, return nil
- # It defaults to "#{column.name}_sounds_like". So if thats what you want you don't even need to do this.
+ # It defaults to "#{column.name}_sounds_like" (using the class name). So if thats what you want you don't even need to do this.
def name_for_column(column)
super
end
@@ -309,7 +309,8 @@ I want to use it, so let's add it:
# You can return an array or a string. NOT a hash, because all of these conditions
# need to eventually get merged together. The array or string can be anything you would put in
- # the :conditions option for ActiveRecord::Base.find()
+ # the :conditions option for ActiveRecord::Base.find(). Also, for a list of methods / variables you can use check out
+ # Searchgasm::Condition::Base.
def to_conditions(value)
["#{quoted_table_name}.#{quoted_column_name} SOUNDS LIKE ?", value]
end
@@ -329,6 +330,21 @@ Pretty nifty, huh? You can create any condition ultimately creating any SQL you
I'm a big fan of understanding what I'm using, so here's a quick explanation: The design behind this plugin is pretty simple. The search object "sanitizes" down into the options passed into ActiveRecord::Base.find(). It serves as a transparent filter between you and ActiveRecord::Base.find(). This filter provides "enhancements" that get translated into options that ActiveRecord::Base.find() can understand. It doesn't dig into the ActiveRecord internals, it only uses what is publicly available. It jumps in and helps out <em>only</em> when needed, otherwise it sits back and lets ActiveRecord do all of the work. Between that and the extensive tests, this is a solid and fast plugin.
+== Performance / Benchmarking
+
+I ran searchgasm through some performance tests using ruby-prof. After working on it for a little while I improved performance quite a bit. Notice the "2nd instantiation" report. This is implementing caching and skips all dynamic method creation / meta programming. It resulted in code over 50 times faster.
+
+ user system total real
+ 1st instantiation: 0.000000 0.000000 0.000000 ( 0.005466)
+ 2nd instantiation: 0.000000 0.000000 0.000000 ( 0.000108)
+ Local ordering: 0.000000 0.000000 0.000000 ( 0.000265)
+ Advanced ordering: 0.000000 0.000000 0.000000 ( 0.000413)
+ Local conditions: 0.000000 0.000000 0.000000 ( 0.000241)
+ Advanced conditions: 0.000000 0.000000 0.000000 ( 0.000602)
+ Its complicated: 0.000000 0.000000 0.000000 ( 0.001017)
+
+I also included the benchmarking file in benchmarks/benchmark.rb to see for yourself.
+
== Reporting problems / bugs
http://binarylogic.lighthouseapp.com/projects/16601-searchgasm
View
@@ -0,0 +1,43 @@
+require File.dirname(__FILE__) + '/benchmark_helper.rb'
+
+times = 1
+
+Benchmark.bm(20) do |x|
+ x.report("1st instantiation:") { Account.new_search }
+ x.report("2nd instantiation:") { Account.new_search }
+
+ # Now that we see the benefits of caching, lets cache the rest of the classes and perform the rest of the tests,
+ # so that they are fair
+ User.new_search
+ Order.new_search
+
+ x.report("Local ordering:") do
+ times.times do
+ Account.new_search(:order_by => :name).sanitize
+ end
+ end
+
+ x.report("Advanced ordering:") do
+ times.times do
+ Account.new_search(:order_by => {:users => {:orders => :total}}).sanitize
+ end
+ end
+
+ x.report("Local conditions:") do
+ times.times do
+ Account.new_search(:conditions => {:name_like => "Binary"}).sanitize
+ end
+ end
+
+ x.report("Advanced conditions:") do
+ times.times do
+ Account.new_search(:conditions => {:users => {:orders => {:total_gt => 1}}}).sanitize
+ end
+ end
+
+ x.report("Its complicated:") do
+ times.times do
+ Account.new_search(:conditions => {:users => {:orders => {:total_gt => 1, :created_at_after => Time.now}, :first_name_like => "Ben"}, :name_begins_with => "Awesome"}, :per_page => 20, :page => 2, :order_by => {:users => {:orders => :total}}, :order_as => "ASC").sanitize
+ end
+ end
+end
@@ -0,0 +1,52 @@
+require "rubygems"
+require "benchmark"
+require "ruby-prof"
+require "activerecord"
+require File.dirname(__FILE__) + '/../test/libs/acts_as_tree'
+require File.dirname(__FILE__) + '/../lib/searchgasm'
+
+ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
+
+ActiveRecord::Schema.define(:version => 1) do
+ create_table :accounts do |t|
+ t.datetime :created_at
+ t.datetime :updated_at
+ t.string :name
+ t.boolean :active
+ end
+
+ create_table :users do |t|
+ t.datetime :created_at
+ t.datetime :updated_at
+ t.integer :account_id
+ t.integer :parent_id
+ t.string :first_name
+ t.string :last_name
+ t.boolean :active
+ t.text :bio
+ end
+
+ create_table :orders do |t|
+ t.datetime :created_at
+ t.datetime :updated_at
+ t.integer :user_id
+ t.float :total
+ t.text :description
+ t.binary :receipt
+ end
+end
+
+class Account < ActiveRecord::Base
+ has_many :users, :dependent => :destroy
+ has_many :orders, :through => :users
+end
+
+class User < ActiveRecord::Base
+ acts_as_tree
+ belongs_to :account
+ has_many :orders, :dependent => :destroy
+end
+
+class Order < ActiveRecord::Base
+ belongs_to :user
+end
View
@@ -0,0 +1,15 @@
+require File.dirname(__FILE__) + '/benchmark_helper.rb'
+require "ruby-prof"
+
+Account.new_search
+User.new_search
+Order.new_search
+
+RubyProf.start
+
+# Put profile code here
+
+result = RubyProf.stop
+
+printer = RubyProf::FlatPrinter.new(result)
+printer.print(STDOUT, 0)
View
@@ -1,3 +1,5 @@
+$:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
+
require "active_record"
require "active_support"
@@ -72,4 +74,8 @@ class Base
Base.register_condition("Searchgasm::Condition::#{condition.to_s.camelize}".constantize)
end
end
+
+ # The namespace I put all cached search classes.
+ module Cache
+ end
end
@@ -118,7 +118,9 @@ def accessible_conditions # :nodoc:
private
def sanitize_options_with_searchgasm(options = {})
return options unless Searchgasm::Search::Base.needed?(self, options)
- searchgasm_searcher(options).sanitize
+ search = searchgasm_searcher(options)
+ search.acting_as_filter = true
+ search.sanitize
end
def searchgasm_conditions(options = {})
@@ -22,7 +22,7 @@ class << self
# class << self
# # I pass you the column, you tell me what you want the method to be called.
# # If you don't want to add this condition for that column, return nil
- # # It defaults to "#{column.name}_sounds_like". So if thats what you want you don't even need to do this.
+ # # It defaults to "#{column.name}_sounds_like" (using the class name). So if thats what you want you don't even need to do this.
# def name_for_column(column)
# super
# end
@@ -35,7 +35,7 @@ class << self
#
# # You can return an array or a string. NOT a hash, because all of these conditions
# # need to eventually get merged together. The array or string can be anything you would put in
- # # the :conditions option for ActiveRecord::Base.find()
+ # # the :conditions option for ActiveRecord::Base.find(). Also, for a list of methods / variables you can use check out earchgasm::Condition::Base
# def to_conditions(value)
# ["#{quoted_table_name}.#{quoted_column_name} SOUNDS LIKE ?", value]
# end
@@ -75,24 +75,26 @@ def needed?(model_class, conditions) # :nodoc:
# Creates virtual classes for the class passed to it. This is a neccesity for keeping dynamically created method
# names specific to models. It provides caching and helps a lot with performance.
def create_virtual_class(model_class)
- class_search_name = "::#{model_class.name}Conditions"
+ class_search_name = "::Searchgasm::Cache::#{model_class.name}Conditions"
begin
- class_search_name.constantize
+ eval(class_search_name)
rescue NameError
eval <<-end_eval
- class #{class_search_name} < ::Searchgasm::Conditions::Base; end;
+ class #{class_search_name} < ::Searchgasm::Conditions::Base
+ def self.klass
+ #{model_class.name}
+ end
+
+ def klass
+ #{model_class.name}
+ end
+ end
+
+ #{class_search_name}
end_eval
-
- class_search_name.constantize
end
end
-
- # The class / model we are searching
- def klass
- # Can't cache this because thin and mongrel don't play nice with caching constants
- name.split("::").last.gsub(/Conditions$/, "").constantize
- end
end
def initialize(init_conditions = {})
@@ -123,10 +125,6 @@ def includes
i.blank? ? nil : (i.size == 1 ? i.first : i)
end
- def klass
- self.class.klass
- end
-
# Sanitizes the conditions down into conditions that ActiveRecord::Base.find can understand.
def sanitize
conditions = merge_conditions(*objects.collect { |object| object.sanitize })
@@ -244,7 +242,7 @@ def add_klass_conditions!
end
def assert_valid_conditions(conditions)
- conditions.stringify_keys.assert_valid_keys(self.class.condition_names + self.class.association_names)
+ conditions.stringify_keys.fast_assert_valid_keys(self.class.condition_names + self.class.association_names)
end
def associations
@@ -15,6 +15,12 @@ def deep_dup
new_hash
end
+
+ # assert_valid_keys was killing performance. Array.flatten was the culprit, so I rewrote this method, got a 35% performance increase
+ def fast_assert_valid_keys(valid_keys)
+ unknown_keys = keys - valid_keys
+ raise(ArgumentError, "Unknown key(s): #{unknown_keys.join(", ")}") unless unknown_keys.empty?
+ end
end
end
end
Oops, something went wrong.

0 comments on commit 07f8a72

Please sign in to comment.