elastic_searchable 0.7.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -19,15 +19,6 @@ pkg
19
19
  test/*.log
20
20
  test/*.sqlite3
21
21
 
22
- # For vim:
23
- *.swp
24
-
25
- # For MacOS:
26
- .DS_Store
27
-
28
- # git files
29
- *.orig
30
-
31
22
  # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
32
23
  #
33
24
  # * Create a file at ~/.gitignore
@@ -39,6 +30,10 @@ test/*.sqlite3
39
30
  #
40
31
  # Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line)
41
32
  #
33
+ # For MacOS:
34
+ #
35
+ #.DS_Store
36
+ #
42
37
  # For TextMate
43
38
  #*.tmproj
44
39
  #tmtags
@@ -47,4 +42,6 @@ test/*.sqlite3
47
42
  #*~
48
43
  #\#*
49
44
  #.\#*
50
-
45
+ #
46
+ # For vim:
47
+ #*.swp
data/LICENSE.txt CHANGED
@@ -1,22 +1,20 @@
1
- The MIT License
1
+ Copyright (c) 2011 Ryan Sonnek
2
2
 
3
- Copyright (c) 2011 Socialcast, Inc
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
4
10
 
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in
13
- all copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
- THE SOFTWARE.
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
22
13
 
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,44 @@
1
+ = elastic_searchable
2
+
3
+ Integrate the elasticsearch library into Rails.
4
+
5
+ == Usage
6
+ class Blog < ActiveRecord::Base
7
+ elastic_searchable
8
+ end
9
+
10
+ results = Blog.search 'foo'
11
+
12
+ == Features
13
+
14
+ * fast. fast! FAST! 30% faster than rubberband on average.
15
+ * active record callbacks automatically keep search index up to date as your data changes
16
+ * out of the box background indexing of data using backgrounded. Don't lock up a foreground process waiting on a background job!
17
+ * integrates with will_paginate library for easy pagination of search results
18
+
19
+ == Installation
20
+ #Gemfile
21
+ gem 'elastic_searchable'
22
+
23
+ == Configuration
24
+
25
+ #config/initializers/elastic_searchable.rb
26
+ #customize elasticsearch host
27
+ #defaults to localhost:9200
28
+ ElasticSearchable.base_uri = 'server:9200'
29
+
30
+ == Contributing to elastic_searchable
31
+
32
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
33
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
34
+ * Fork the project
35
+ * Start a feature/bugfix branch
36
+ * Commit and push until you are happy with your contribution
37
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
38
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
39
+
40
+ == Copyright
41
+
42
+ Copyright (c) 2011 Ryan Sonnek. See LICENSE.txt for
43
+ further details.
44
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.6.1
@@ -14,18 +14,18 @@ Gem::Specification.new do |s|
14
14
 
15
15
  s.rubyforge_project = "elastic_searchable"
16
16
 
17
- s.add_runtime_dependency(%q<activerecord>, ["~> 2.3.5"])
17
+ s.add_runtime_dependency(%q<activerecord>, ["~> 3.0.0"])
18
18
  s.add_runtime_dependency(%q<httparty>, ["~> 0.6.0"])
19
19
  s.add_runtime_dependency(%q<backgrounded>, ["~> 0.7.0"])
20
20
  s.add_runtime_dependency(%q<will_paginate>, ["~> 2.3.15"])
21
- s.add_runtime_dependency(%q<larsklevan-after_commit>, ["~> 1.0.5"])
22
21
  s.add_development_dependency(%q<shoulda>, [">= 0"])
23
22
  s.add_development_dependency(%q<mocha>, [">= 0"])
24
- s.add_development_dependency(%q<jeweler>, ["~> 1.5.2"])
23
+ s.add_development_dependency(%q<bundler>, [">= 0"])
25
24
  s.add_development_dependency(%q<rcov>, [">= 0"])
26
25
  s.add_development_dependency(%q<sqlite3-ruby>, ["~> 1.3.2"])
27
26
  s.add_development_dependency(%q<ruby-debug>, [">= 0"])
28
27
 
28
+
29
29
  s.files = `git ls-files`.split("\n")
30
30
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
31
31
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
@@ -0,0 +1,67 @@
1
+ require 'active_record'
2
+ require 'backgrounded'
3
+ require 'elastic_searchable/queries'
4
+ require 'elastic_searchable/callbacks'
5
+ require 'elastic_searchable/index'
6
+
7
+ module ElasticSearchable
8
+ module ActiveRecord
9
+ def self.included(base)
10
+ base.send :extend, ClassMethods
11
+ end
12
+
13
+ module ClassMethods
14
+ attr_accessor :elastic_options
15
+
16
+ # Valid options:
17
+ # :index (optional) configure index to store data in. default to ElasticSearchable.default_index
18
+ # :type (optional) configue type to store data in. default to model table name
19
+ # :index_options (optional) configure index properties (ex: tokenizer)
20
+ # :mapping (optional) configure field properties for this model (ex: skip analyzer for field)
21
+ # :if (optional) reference symbol/proc condition to only index when condition is true
22
+ # :unless (optional) reference symbol/proc condition to skip indexing when condition is true
23
+ # :json (optional) configure the json document to be indexed (see http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html#method-i-as_json for available options)
24
+ #
25
+ # Available callbacks:
26
+ # after_index
27
+ # called after the object is indexed in elasticsearch
28
+ # (optional) :on => :create/:update can be used to only fire callback when object is created or updated
29
+ #
30
+ # after_percolate
31
+ # called after object is indexed in elasticsearch
32
+ # only fires if the update index call returns a non-empty set of registered percolations
33
+ # use the "percolations" instance method from within callback to inspect what percolations were returned
34
+ def elastic_searchable(options = {})
35
+ options.symbolize_keys!
36
+ self.elastic_options = options
37
+
38
+ extend ElasticSearchable::Indexing::ClassMethods
39
+ extend ElasticSearchable::Queries
40
+
41
+ include ElasticSearchable::Indexing::InstanceMethods
42
+ include ElasticSearchable::Callbacks::InstanceMethods
43
+
44
+ backgrounded :update_index_on_create => ElasticSearchable::Callbacks.backgrounded_options, :update_index_on_update => ElasticSearchable::Callbacks.backgrounded_options
45
+ class << self
46
+ backgrounded :delete_id_from_index => ElasticSearchable::Callbacks.backgrounded_options
47
+ end
48
+
49
+ attr_accessor :percolations
50
+ define_model_callbacks :index, :percolate, :only => :after
51
+ after_commit :update_index_on_create_backgrounded, :if => :should_index?, :on => :create
52
+ after_commit :update_index_on_update_backgrounded, :if => :should_index?, :on => :update
53
+ after_commit :delete_from_index, :on => :destroy
54
+ end
55
+ # override default after_index callback definition to support :on option
56
+ # see ActiveRecord::Transactions::ClassMethods#after_commit for example
57
+ def after_index(*args, &block)
58
+ options = args.last
59
+ if options.is_a?(Hash) && options[:on]
60
+ options[:if] = Array.wrap(options[:if])
61
+ options[:if] << "@index_lifecycle == :#{options[:on]}"
62
+ end
63
+ set_callback(:index, :after, *args, &block)
64
+ end
65
+ end
66
+ end
67
+ end
@@ -1,5 +1,3 @@
1
- require 'will_paginate'
2
-
3
1
  module ElasticSearchable
4
2
  module Indexing
5
3
  module ClassMethods
@@ -13,17 +11,16 @@ module ElasticSearchable
13
11
  # http://www.elasticsearch.com/docs/elasticsearch/rest_api/admin/indices/put_mapping/
14
12
  def update_index_mapping
15
13
  if mapping = self.elastic_options[:mapping]
16
- ElasticSearchable.request :put, index_type_path('_mapping'), :json_body => {index_type => mapping}
14
+ ElasticSearchable.request :put, index_type_path('_mapping'), :body => {index_type => mapping}.to_json
17
15
  end
18
16
  end
19
17
 
20
18
  # create the index
21
- # http://www.elasticsearch.org/guide/reference/api/admin-indices-create-index.html
19
+ # http://www.elasticsearch.com/docs/elasticsearch/rest_api/admin/indices/create_index/
22
20
  def create_index
23
- options = {}
24
- options.merge! :settings => self.elastic_options[:index_options] if self.elastic_options[:index_options]
25
- options.merge! :mappings => {index_type => self.elastic_options[:mapping]} if self.elastic_options[:mapping]
26
- ElasticSearchable.request :put, index_path, :json_body => options
21
+ options = self.elastic_options[:index_options] ? self.elastic_options[:index_options].to_json : ''
22
+ ElasticSearchable.request :put, index_path, :body => options
23
+ self.update_index_mapping
27
24
  end
28
25
 
29
26
  # explicitly refresh the index, making all operations performed since the last refresh
@@ -44,8 +41,6 @@ module ElasticSearchable
44
41
  # http://www.elasticsearch.com/docs/elasticsearch/rest_api/delete/
45
42
  def delete_id_from_index(id)
46
43
  ElasticSearchable.request :delete, index_type_path(id)
47
- rescue ElasticSearchable::ElasticError => e
48
- ElasticSearchable.logger.warn e
49
44
  end
50
45
 
51
46
  # helper method to generate elasticsearch url for this object type
@@ -59,26 +54,26 @@ module ElasticSearchable
59
54
  end
60
55
 
61
56
  # reindex all records using bulk api
62
- # see http://www.elasticsearch.org/guide/reference/api/bulk.html
63
57
  # options:
64
- # :scope - scope to use for looking up records to reindex. defaults to self (all)
65
- # :page - page/batch to begin indexing at. defaults to 1
66
- # :per_page - number of records to index per batch. defaults to 1000
58
+ # :scope - scope the find_in_batches to only a subset of records
59
+ # :batch - counter to start indexing at
60
+ # :include - passed to find_in_batches to hydrate objects
61
+ # see http://www.elasticsearch.org/guide/reference/api/bulk.html
67
62
  def reindex(options = {})
68
63
  self.update_index_mapping
69
- options.reverse_merge! :page => 1, :per_page => 1000, :total_entries => 1
64
+ batch = options.delete(:batch) || 1
65
+ options[:batch_size] ||= 1000
66
+ options[:start] ||= (batch - 1) * options[:batch_size]
70
67
  scope = options.delete(:scope) || self
71
-
72
- records = scope.paginate(options)
73
- while records.any? do
74
- ElasticSearchable.logger.debug "reindexing batch ##{records.current_page}..."
75
-
68
+ scope.find_in_batches(options) do |records|
69
+ ElasticSearchable.logger.info "reindexing batch ##{batch}..."
70
+ batch += 1
76
71
  actions = []
77
72
  records.each do |record|
78
73
  next unless record.should_index?
79
74
  begin
80
- doc = ElasticSearchable.encode_json(record.as_json_for_index)
81
- actions << ElasticSearchable.encode_json({:index => {'_index' => index_name, '_type' => index_type, '_id' => record.id}})
75
+ doc = record.as_json_for_index.to_json
76
+ actions << {:index => {'_index' => index_name, '_type' => index_type, '_id' => record.id}}.to_json
82
77
  actions << doc
83
78
  rescue => e
84
79
  ElasticSearchable.logger.warn "Unable to bulk index record: #{record.inspect} [#{e.message}]"
@@ -87,12 +82,9 @@ module ElasticSearchable
87
82
  begin
88
83
  ElasticSearchable.request(:put, '/_bulk', :body => "\n#{actions.join("\n")}\n") if actions.any?
89
84
  rescue ElasticError => e
90
- ElasticSearchable.logger.warn "Error indexing batch ##{options[:page]}: #{e.message}"
85
+ ElasticSearchable.logger.warn "Error indexing batch ##{batch}: #{e.message}"
91
86
  ElasticSearchable.logger.warn e
92
87
  end
93
-
94
- options.merge! :page => (options[:page] + 1)
95
- records = scope.paginate(options)
96
88
  end
97
89
  end
98
90
 
@@ -111,20 +103,22 @@ module ElasticSearchable
111
103
  # see http://www.elasticsearch.org/guide/reference/api/index_.html
112
104
  def reindex(lifecycle = nil)
113
105
  query = {}
114
- query.merge! :percolate => "*" if self.class.elastic_options[:percolate]
115
- response = ElasticSearchable.request :put, self.class.index_type_path(self.id), :query => query, :json_body => self.as_json_for_index
106
+ query.merge! :percolate => "*" if _percolate_callbacks.any?
107
+ response = ElasticSearchable.request :put, self.class.index_type_path(self.id), :query => query, :body => self.as_json_for_index.to_json
116
108
 
117
- self.run_callbacks("after_index_on_#{lifecycle}".to_sym) if lifecycle
118
- self.run_callbacks(:after_index)
109
+ @index_lifecycle = lifecycle ? lifecycle.to_sym : nil
110
+ _run_index_callbacks
119
111
 
120
- if percolate_callback = self.class.elastic_options[:percolate]
121
- matches = response['matches']
122
- self.send percolate_callback, matches if matches.any?
123
- end
112
+ @percolations = response['matches']
113
+ _run_percolate_callbacks if @percolations.any?
124
114
  end
125
115
  # document to index in elasticsearch
126
116
  def as_json_for_index
127
- self.as_json self.class.elastic_options[:json]
117
+ original_include_root_in_json = ::ActiveRecord::Base.include_root_in_json
118
+ ::ActiveRecord::Base.include_root_in_json = false
119
+ return self.as_json self.class.elastic_options[:json]
120
+ ensure
121
+ ::ActiveRecord::Base.include_root_in_json = original_include_root_in_json
128
122
  end
129
123
  def should_index?
130
124
  [self.class.elastic_options[:if]].flatten.compact.all? { |m| evaluate_elastic_condition(m) } &&
@@ -135,14 +129,11 @@ module ElasticSearchable
135
129
  # can be done automatically when indexing using :percolate => true config option
136
130
  # http://www.elasticsearch.org/blog/2011/02/08/percolator.html
137
131
  def percolate
138
- response = ElasticSearchable.request :get, self.class.index_type_path('_percolate'), :json_body => {:doc => self.as_json_for_index}
132
+ response = ElasticSearchable.request :get, self.class.index_type_path('_percolate'), :body => {:doc => self.as_json_for_index}.to_json
139
133
  response['matches']
140
134
  end
141
135
 
142
136
  private
143
- def elasticsearch_offline?
144
- ElasticSearchable.offline?
145
- end
146
137
  # ripped from activesupport
147
138
  def evaluate_elastic_condition(method)
148
139
  case method
@@ -13,27 +13,11 @@ module ElasticSearchable
13
13
  def search(query, options = {})
14
14
  page = (options.delete(:page) || 1).to_i
15
15
  options[:fields] ||= '_id'
16
+ options[:q] ||= query
16
17
  options[:size] ||= per_page_for_search(options)
17
18
  options[:from] ||= options[:size] * (page - 1)
18
- if query.is_a?(Hash)
19
- options[:query] = query
20
- else
21
- options[:query] = {
22
- :query_string => {
23
- :query => query,
24
- :default_operator => options.delete(:default_operator)
25
- }
26
- }
27
- end
28
- query = {}
29
- case sort = options.delete(:sort)
30
- when Array,Hash
31
- options[:sort] = sort
32
- when String
33
- query[:sort] = sort
34
- end
35
19
 
36
- response = ElasticSearchable.request :get, index_type_path('_search'), :query => query, :json_body => options
20
+ response = ElasticSearchable.request :get, index_type_path('_search'), :query => options
37
21
  hits = response['hits']
38
22
  ids = hits['hits'].collect {|h| h['_id'].to_i }
39
23
  results = self.find(ids).sort_by {|result| ids.index(result.id) }
@@ -1,4 +1,4 @@
1
1
  module ElasticSearchable
2
- VERSION = '0.7.3'
2
+ VERSION = '1.0.0'
3
3
  end
4
4
 
@@ -1,40 +1,35 @@
1
1
  require 'httparty'
2
2
  require 'logger'
3
- require 'elastic_searchable/active_record_extensions'
3
+ require 'elastic_searchable/active_record'
4
4
 
5
5
  module ElasticSearchable
6
- DEFAULT_INDEX = 'elastic_searchable'
7
6
  include HTTParty
8
7
  format :json
9
8
  base_uri 'localhost:9200'
9
+ #debug_output
10
10
 
11
11
  class ElasticError < StandardError; end
12
12
  class << self
13
- attr_accessor :logger, :default_index, :offline
14
-
15
- # execute a block of work without reindexing objects
16
- def offline(&block)
17
- @offline = true
18
- yield
19
- ensure
20
- @offline = false
13
+ # setup the default index to use
14
+ # one index can hold many object 'types'
15
+ @@default_index = nil
16
+ def default_index=(index)
17
+ @@default_index = index
18
+ end
19
+ def default_index
20
+ @@default_index || 'elastic_searchable'
21
21
  end
22
- def offline?
23
- !!@offline
22
+
23
+ @@logger = Logger.new(STDOUT)
24
+ @@logger.level = Logger::INFO
25
+ def logger=(logger)
26
+ @@logger = logger
24
27
  end
25
- # encapsulate encoding hash into json string
26
- # support Yajl encoder if installed
27
- def encode_json(options = {})
28
- defined?(Yajl) ? Yajl::Encoder.encode(options) : ActiveSupport::JSON.encode(options)
28
+ def logger
29
+ @@logger
29
30
  end
30
- # perform a request to the elasticsearch server
31
- # configuration:
32
- # ElasticSearchable.base_uri 'host:port' controls where to send request to
33
- # ElasticSearchable.debug_output outputs all http traffic to console
31
+ #perform a request to the elasticsearch server
34
32
  def request(method, url, options = {})
35
- options.merge! :headers => {'Content-Type' => 'application/json'}
36
- options.merge! :body => ElasticSearchable.encode_json(options[:json_body]) if options[:json_body]
37
-
38
33
  response = self.send(method, url, options)
39
34
  logger.debug "elasticsearch request: #{method} #{url} #{"took #{response['took']}ms" if response['took']}"
40
35
  validate_response response
@@ -51,11 +46,4 @@ module ElasticSearchable
51
46
  end
52
47
  end
53
48
 
54
- # configure default logger to standard out with info log level
55
- ElasticSearchable.logger = Logger.new STDOUT
56
- ElasticSearchable.logger.level = Logger::INFO
57
-
58
- # configure default index to be elastic_searchable
59
- # one index can hold many object 'types'
60
- ElasticSearchable.default_index = ElasticSearchable::DEFAULT_INDEX
61
-
49
+ ActiveRecord::Base.send(:include, ElasticSearchable::ActiveRecord)
data/test/helper.rb CHANGED
@@ -10,12 +10,15 @@ end
10
10
  require 'test/unit'
11
11
  require 'shoulda'
12
12
  require 'mocha'
13
- require 'ruby-debug'
13
+ require "ruby-debug"
14
14
 
15
15
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
16
16
  $LOAD_PATH.unshift(File.dirname(__FILE__))
17
17
  require 'elastic_searchable'
18
- require 'setup_database'
18
+
19
+ config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
20
+ ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
21
+ ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite'])
19
22
 
20
23
  class Test::Unit::TestCase
21
24
  def delete_index
@@ -1,24 +1,40 @@
1
1
  require File.join(File.dirname(__FILE__), 'helper')
2
2
 
3
3
  class TestElasticSearchable < Test::Unit::TestCase
4
- def setup
5
- delete_index
6
- end
7
- ElasticSearchable.debug_output
8
-
9
- context 'non elastic activerecord class' do
10
- class Cat < ActiveRecord::Base
4
+ ActiveRecord::Schema.define(:version => 1) do
5
+ create_table :posts, :force => true do |t|
6
+ t.column :title, :string
7
+ t.column :body, :string
8
+ end
9
+ create_table :blogs, :force => true do |t|
10
+ t.column :title, :string
11
+ t.column :body, :string
11
12
  end
12
- should 'not respond to elastic_options' do
13
- assert !Cat.respond_to?(:elastic_options)
13
+ create_table :users, :force => true do |t|
14
+ t.column :name, :string
15
+ end
16
+ create_table :friends, :force => true do |t|
17
+ t.column :name, :string
18
+ t.column :favorite_color, :string
19
+ end
20
+ create_table :books, :force => true do |t|
21
+ t.column :title, :string
22
+ end
23
+ create_table :max_page_size_classes, :force => true do |t|
24
+ t.column :name, :string
14
25
  end
15
26
  end
16
27
 
28
+ def setup
29
+ delete_index
30
+ end
31
+ def teardown
32
+ delete_index
33
+ end
17
34
  class Post < ActiveRecord::Base
18
- elastic_searchable :index_options => {'number_of_replicas' => 0, 'number_of_shards' => 1}
35
+ elastic_searchable :index_options => { "analysis.analyzer.default.tokenizer" => 'standard', "analysis.analyzer.default.filter" => ["standard", "lowercase", 'porterStem'] }
19
36
  after_index :indexed
20
- after_index_on_create :indexed_on_create
21
- after_index_on_update :indexed_on_update
37
+ after_index :indexed_on_create, :on => :create
22
38
  def indexed
23
39
  @indexed = true
24
40
  end
@@ -31,14 +47,8 @@ class TestElasticSearchable < Test::Unit::TestCase
31
47
  def indexed_on_create?
32
48
  @indexed_on_create
33
49
  end
34
- def indexed_on_update
35
- @indexed_on_update = true
36
- end
37
- def indexed_on_update?
38
- @indexed_on_update
39
- end
40
50
  end
41
- context 'activerecord class with default elastic_searchable config' do
51
+ context 'Post class with default elastic_searchable config' do
42
52
  setup do
43
53
  @clazz = Post
44
54
  end
@@ -50,7 +60,7 @@ class TestElasticSearchable < Test::Unit::TestCase
50
60
  end
51
61
  end
52
62
 
53
- context 'Model.request with invalid url' do
63
+ context 'ElasticSearchable.request with invalid url' do
54
64
  should 'raise error' do
55
65
  assert_raises ElasticSearchable::ElasticError do
56
66
  ElasticSearchable.request :get, '/elastic_searchable/foobar/notfound'
@@ -58,95 +68,66 @@ class TestElasticSearchable < Test::Unit::TestCase
58
68
  end
59
69
  end
60
70
 
61
- context 'Model.create_index' do
71
+ context 'Post.create_index' do
62
72
  setup do
63
73
  Post.create_index
64
- Post.refresh_index
65
74
  @status = ElasticSearchable.request :get, '/elastic_searchable/_status'
66
75
  end
67
76
  should 'have created index' do
68
77
  assert @status['ok']
69
78
  end
79
+ should 'have used custom index_options' do
80
+ expected = {
81
+ "index.number_of_replicas" => "1",
82
+ "index.number_of_shards" => "5",
83
+ "index.analysis.analyzer.default.tokenizer" => "standard",
84
+ "index.analysis.analyzer.default.filter.0" => "standard",
85
+ "index.analysis.analyzer.default.filter.1" => "lowercase",
86
+ "index.analysis.analyzer.default.filter.2" => "porterStem"
87
+ }
88
+ assert_equal expected, @status['indices']['elastic_searchable']['settings'], @status.inspect
89
+ end
70
90
  end
71
91
 
72
- context 'Model.create' do
73
- setup do
74
- @post = Post.create :title => 'foo', :body => "bar"
75
- end
76
- should 'have fired after_index callback' do
77
- assert @post.indexed?
78
- end
79
- should 'have fired after_index_on_create callback' do
80
- assert @post.indexed_on_create?
81
- end
82
- should 'not have fired after_index_on_update callback' do
83
- assert !@post.indexed_on_update?
92
+ context 'deleting object that does not exist in search index' do
93
+ should 'raise error' do
94
+ assert_raises ElasticSearchable::ElasticError do
95
+ Post.delete_id_from_index 123
96
+ end
84
97
  end
85
98
  end
86
99
 
87
- context 'Model.update' do
100
+ context 'Post.create' do
88
101
  setup do
89
- Post.create :title => 'foo', :body => 'bar'
90
- @post = Post.last
91
- @post.title = 'baz'
92
- @post.save
102
+ @post = Post.create :title => 'foo', :body => "bar"
93
103
  end
94
104
  should 'have fired after_index callback' do
95
105
  assert @post.indexed?
96
106
  end
97
- should 'not have fired after_index_on_create callback' do
98
- assert !@post.indexed_on_create?
99
- end
100
- should 'have fired after_index_on_update callback' do
101
- assert @post.indexed_on_update?
102
- end
103
- end
104
-
105
- context 'Model.create within ElasticSearchable.offline block' do
106
- setup do
107
- ElasticSearchable.offline do
108
- @post = Post.create :title => 'foo', :body => "bar"
109
- end
110
- end
111
- should 'not have fired after_index callback' do
112
- assert !@post.indexed?
113
- end
114
- should 'not have fired after_index_on_create callback' do
115
- assert !@post.indexed_on_create?
107
+ should 'have fired after_index_on_create callback' do
108
+ assert @post.indexed_on_create?
116
109
  end
117
110
  end
118
111
 
119
112
  context 'with empty index when multiple database records' do
120
113
  setup do
121
- Post.delete_all
122
114
  Post.create_index
123
115
  @first_post = Post.create :title => 'foo', :body => "first bar"
124
116
  @second_post = Post.create :title => 'foo', :body => "second bar"
125
- Post.delete_index
126
- Post.create_index
117
+ Post.clean_index
127
118
  end
128
119
  should 'not raise error if error occurs reindexing model' do
129
120
  ElasticSearchable.expects(:request).raises(ElasticSearchable::ElasticError.new('faux error'))
130
- assert_nothing_raised do
131
- Post.reindex
132
- end
133
- end
134
- should 'not raise error if destroying one instance' do
135
- Logger.any_instance.expects(:warn)
136
- assert_nothing_raised do
137
- @first_post.destroy
138
- end
121
+ Post.reindex
139
122
  end
140
- context 'Model.reindex' do
123
+ context 'Post.reindex' do
141
124
  setup do
142
- Post.reindex :per_page => 1, :scope => Post.scoped(:order => 'body desc')
125
+ Post.reindex
143
126
  Post.refresh_index
144
127
  end
145
128
  should 'have reindexed both records' do
146
- assert_nothing_raised do
147
- ElasticSearchable.request :get, "/elastic_searchable/posts/#{@first_post.id}"
148
- ElasticSearchable.request :get, "/elastic_searchable/posts/#{@second_post.id}"
149
- end
129
+ ElasticSearchable.request :get, "/elastic_searchable/posts/#{@first_post.id}"
130
+ ElasticSearchable.request :get, "/elastic_searchable/posts/#{@second_post.id}"
150
131
  end
151
132
  end
152
133
  end
@@ -168,33 +149,11 @@ class TestElasticSearchable < Test::Unit::TestCase
168
149
  end
169
150
  should 'be paginated' do
170
151
  assert_equal 1, @results.current_page
171
- assert_equal Post.per_page, @results.per_page
152
+ assert_equal 20, @results.per_page
172
153
  assert_nil @results.previous_page
173
154
  assert_nil @results.next_page
174
155
  end
175
156
  end
176
-
177
- context 'searching for results using a query Hash' do
178
- setup do
179
- @results = Post.search({
180
- :filtered => {
181
- :query => {
182
- :term => {:title => 'foo'},
183
- },
184
- :filter => {
185
- :or => [
186
- {:term => {:body => 'second'}},
187
- {:term => {:body => 'third'}}
188
- ]
189
- }
190
- }
191
- })
192
- end
193
- should 'find only the object which ' do
194
- assert_does_not_contain @results, @first_post
195
- assert_contains @results, @second_post
196
- end
197
- end
198
157
 
199
158
  context 'searching for second page using will_paginate params' do
200
159
  setup do
@@ -223,16 +182,6 @@ class TestElasticSearchable < Test::Unit::TestCase
223
182
  assert_equal @first_post, @results.last
224
183
  end
225
184
  end
226
-
227
- context 'advanced sort options' do
228
- setup do
229
- @results = Post.search 'foo', :sort => [{:id => 'desc'}]
230
- end
231
- should 'sort results correctly' do
232
- assert_equal @second_post, @results.first
233
- assert_equal @first_post, @results.last
234
- end
235
- end
236
185
 
237
186
  context 'destroying one object' do
238
187
  setup do
@@ -248,7 +197,7 @@ class TestElasticSearchable < Test::Unit::TestCase
248
197
 
249
198
 
250
199
  class Blog < ActiveRecord::Base
251
- elastic_searchable :if => proc {|b| b.should_index? }, :index_options => {'number_of_replicas' => 0, 'number_of_shards' => 1}
200
+ elastic_searchable :if => proc {|b| b.should_index? }
252
201
  def should_index?
253
202
  false
254
203
  end
@@ -268,32 +217,15 @@ class TestElasticSearchable < Test::Unit::TestCase
268
217
  end
269
218
 
270
219
  class User < ActiveRecord::Base
271
- elastic_searchable :index_options => {
272
- 'number_of_replicas' => 0,
273
- 'number_of_shards' => 1,
274
- "analysis.analyzer.default.tokenizer" => 'standard',
275
- "analysis.analyzer.default.filter" => ["standard", "lowercase", 'porterStem']},
276
- :mapping => {:properties => {:name => {:type => :string, :index => :not_analyzed}}}
220
+ elastic_searchable :mapping => {:properties => {:name => {:type => :string, :index => :not_analyzed}}}
277
221
  end
278
- context 'activerecord class with :index_options and :mapping' do
222
+ context 'activerecord class with :mapping=>{}' do
279
223
  context 'creating index' do
280
224
  setup do
281
225
  User.create_index
282
- end
283
- should 'have used custom index_options' do
284
- @status = ElasticSearchable.request :get, '/elastic_searchable/_status'
285
- expected = {
286
- "index.number_of_replicas" => "0",
287
- "index.number_of_shards" => "1",
288
- "index.analysis.analyzer.default.tokenizer" => "standard",
289
- "index.analysis.analyzer.default.filter.0" => "standard",
290
- "index.analysis.analyzer.default.filter.1" => "lowercase",
291
- "index.analysis.analyzer.default.filter.2" => "porterStem"
292
- }
293
- assert_equal expected, @status['indices']['elastic_searchable']['settings'], @status.inspect
226
+ @status = ElasticSearchable.request :get, '/elastic_searchable/users/_mapping'
294
227
  end
295
228
  should 'have set mapping' do
296
- @status = ElasticSearchable.request :get, '/elastic_searchable/users/_mapping'
297
229
  expected = {
298
230
  "users"=> {
299
231
  "properties"=> {
@@ -307,27 +239,19 @@ class TestElasticSearchable < Test::Unit::TestCase
307
239
  end
308
240
 
309
241
  class Friend < ActiveRecord::Base
310
- belongs_to :book
311
- elastic_searchable :json => {:include => {:book => {:only => :title}}, :only => :name}, :index_options => {'number_of_replicas' => 0, 'number_of_shards' => 1}
242
+ elastic_searchable :json => {:only => [:name]}
312
243
  end
313
- context 'activerecord class with optional :json config' do
244
+ context 'activerecord class with optiona :json config' do
314
245
  context 'creating index' do
315
246
  setup do
316
247
  Friend.create_index
317
- @book = Book.create! :isbn => '123abc', :title => 'another world'
318
- @friend = Friend.new :name => 'bob', :favorite_color => 'red'
319
- @friend.book = @book
320
- @friend.save!
248
+ @friend = Friend.create! :name => 'bob', :favorite_color => 'red'
321
249
  Friend.refresh_index
322
250
  end
323
251
  should 'index json with configuration' do
324
252
  @response = ElasticSearchable.request :get, "/elastic_searchable/friends/#{@friend.id}"
325
- # should not index:
326
- # friend.favorite_color
327
- # book.isbn
328
253
  expected = {
329
- "name" => 'bob',
330
- 'book' => {'title' => 'another world'}
254
+ "name" => 'bob' #favorite_color should not be indexed
331
255
  }
332
256
  assert_equal expected, @response['_source'], @response.inspect
333
257
  end
@@ -339,7 +263,7 @@ class TestElasticSearchable < Test::Unit::TestCase
339
263
  ElasticSearchable.default_index = 'my_new_index'
340
264
  end
341
265
  teardown do
342
- ElasticSearchable.default_index = ElasticSearchable::DEFAULT_INDEX
266
+ ElasticSearchable.default_index = nil
343
267
  end
344
268
  should 'change default index' do
345
269
  assert_equal 'my_new_index', ElasticSearchable.default_index
@@ -347,24 +271,32 @@ class TestElasticSearchable < Test::Unit::TestCase
347
271
  end
348
272
 
349
273
  class Book < ActiveRecord::Base
350
- elastic_searchable :percolate => :on_percolated
351
- def on_percolated(percolated)
352
- @percolated = percolated
274
+ elastic_searchable
275
+ after_percolate :on_percolated
276
+ def on_percolated
277
+ @percolated = percolations
353
278
  end
354
279
  def percolated
355
280
  @percolated
356
281
  end
357
282
  end
358
- context 'Book class with percolate=true' do
283
+ context 'Book class with after_percolate callback' do
359
284
  context 'with created index' do
360
285
  setup do
361
286
  Book.create_index
362
287
  end
363
288
  context "when index has configured percolation" do
364
289
  setup do
365
- ElasticSearchable.request :put, '/_percolator/elastic_searchable/myfilter', :json_body => {:query => {:query_string => {:query => 'foo' }}}
290
+ ElasticSearchable.request :put, '/_percolator/elastic_searchable/myfilter', :body => {:query => {:query_string => {:query => 'foo' }}}.to_json
366
291
  ElasticSearchable.request :post, '/_percolator/_refresh'
367
292
  end
293
+ context 'creating an object that does not match the percolation' do
294
+ setup do
295
+ Book.any_instance.expects(:on_percolated).never
296
+ @book = Book.create! :title => 'bar'
297
+ end
298
+ should 'not percolate the record' do end #see expectations
299
+ end
368
300
  context 'creating an object that matches the percolation' do
369
301
  setup do
370
302
  @book = Book.create :title => "foo"
@@ -386,7 +318,7 @@ class TestElasticSearchable < Test::Unit::TestCase
386
318
  end
387
319
 
388
320
  class MaxPageSizeClass < ActiveRecord::Base
389
- elastic_searchable :index_options => {'number_of_replicas' => 0, 'number_of_shards' => 1}
321
+ elastic_searchable
390
322
  def self.max_per_page
391
323
  1
392
324
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elastic_searchable
3
3
  version: !ruby/object:Gem::Version
4
- hash: 5
4
+ hash: 23
5
5
  prerelease:
6
6
  segments:
7
+ - 1
7
8
  - 0
8
- - 7
9
- - 3
10
- version: 0.7.3
9
+ - 0
10
+ version: 1.0.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Ryan Sonnek
@@ -15,7 +15,8 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-05-04 00:00:00 Z
18
+ date: 2011-03-30 00:00:00 -05:00
19
+ default_executable:
19
20
  dependencies:
20
21
  - !ruby/object:Gem::Dependency
21
22
  name: activerecord
@@ -25,12 +26,12 @@ dependencies:
25
26
  requirements:
26
27
  - - ~>
27
28
  - !ruby/object:Gem::Version
28
- hash: 9
29
+ hash: 7
29
30
  segments:
30
- - 2
31
31
  - 3
32
- - 5
33
- version: 2.3.5
32
+ - 0
33
+ - 0
34
+ version: 3.0.0
34
35
  type: :runtime
35
36
  version_requirements: *id001
36
37
  - !ruby/object:Gem::Dependency
@@ -82,23 +83,21 @@ dependencies:
82
83
  type: :runtime
83
84
  version_requirements: *id004
84
85
  - !ruby/object:Gem::Dependency
85
- name: larsklevan-after_commit
86
+ name: shoulda
86
87
  prerelease: false
87
88
  requirement: &id005 !ruby/object:Gem::Requirement
88
89
  none: false
89
90
  requirements:
90
- - - ~>
91
+ - - ">="
91
92
  - !ruby/object:Gem::Version
92
- hash: 29
93
+ hash: 3
93
94
  segments:
94
- - 1
95
95
  - 0
96
- - 5
97
- version: 1.0.5
98
- type: :runtime
96
+ version: "0"
97
+ type: :development
99
98
  version_requirements: *id005
100
99
  - !ruby/object:Gem::Dependency
101
- name: shoulda
100
+ name: mocha
102
101
  prerelease: false
103
102
  requirement: &id006 !ruby/object:Gem::Requirement
104
103
  none: false
@@ -112,7 +111,7 @@ dependencies:
112
111
  type: :development
113
112
  version_requirements: *id006
114
113
  - !ruby/object:Gem::Dependency
115
- name: mocha
114
+ name: bundler
116
115
  prerelease: false
117
116
  requirement: &id007 !ruby/object:Gem::Requirement
118
117
  none: false
@@ -125,26 +124,10 @@ dependencies:
125
124
  version: "0"
126
125
  type: :development
127
126
  version_requirements: *id007
128
- - !ruby/object:Gem::Dependency
129
- name: jeweler
130
- prerelease: false
131
- requirement: &id008 !ruby/object:Gem::Requirement
132
- none: false
133
- requirements:
134
- - - ~>
135
- - !ruby/object:Gem::Version
136
- hash: 7
137
- segments:
138
- - 1
139
- - 5
140
- - 2
141
- version: 1.5.2
142
- type: :development
143
- version_requirements: *id008
144
127
  - !ruby/object:Gem::Dependency
145
128
  name: rcov
146
129
  prerelease: false
147
- requirement: &id009 !ruby/object:Gem::Requirement
130
+ requirement: &id008 !ruby/object:Gem::Requirement
148
131
  none: false
149
132
  requirements:
150
133
  - - ">="
@@ -154,11 +137,11 @@ dependencies:
154
137
  - 0
155
138
  version: "0"
156
139
  type: :development
157
- version_requirements: *id009
140
+ version_requirements: *id008
158
141
  - !ruby/object:Gem::Dependency
159
142
  name: sqlite3-ruby
160
143
  prerelease: false
161
- requirement: &id010 !ruby/object:Gem::Requirement
144
+ requirement: &id009 !ruby/object:Gem::Requirement
162
145
  none: false
163
146
  requirements:
164
147
  - - ~>
@@ -170,11 +153,11 @@ dependencies:
170
153
  - 2
171
154
  version: 1.3.2
172
155
  type: :development
173
- version_requirements: *id010
156
+ version_requirements: *id009
174
157
  - !ruby/object:Gem::Dependency
175
158
  name: ruby-debug
176
159
  prerelease: false
177
- requirement: &id011 !ruby/object:Gem::Requirement
160
+ requirement: &id010 !ruby/object:Gem::Requirement
178
161
  none: false
179
162
  requirements:
180
163
  - - ">="
@@ -184,7 +167,7 @@ dependencies:
184
167
  - 0
185
168
  version: "0"
186
169
  type: :development
187
- version_requirements: *id011
170
+ version_requirements: *id010
188
171
  description: integrate the elastic search engine with rails
189
172
  email:
190
173
  - ryan@codecrate.com
@@ -197,22 +180,22 @@ extra_rdoc_files: []
197
180
  files:
198
181
  - .document
199
182
  - .gitignore
200
- - CONTRIBUTORS.txt
201
183
  - Gemfile
202
184
  - LICENSE.txt
203
- - README.md
185
+ - README.rdoc
204
186
  - Rakefile
187
+ - VERSION
205
188
  - elastic_searchable.gemspec
206
189
  - lib/elastic_searchable.rb
207
- - lib/elastic_searchable/active_record_extensions.rb
190
+ - lib/elastic_searchable/active_record.rb
208
191
  - lib/elastic_searchable/callbacks.rb
209
192
  - lib/elastic_searchable/index.rb
210
193
  - lib/elastic_searchable/queries.rb
211
194
  - lib/elastic_searchable/version.rb
212
195
  - test/database.yml
213
196
  - test/helper.rb
214
- - test/setup_database.rb
215
197
  - test/test_elastic_searchable.rb
198
+ has_rdoc: true
216
199
  homepage: http://github.com/wireframe/elastic_searchable
217
200
  licenses: []
218
201
 
@@ -242,12 +225,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
242
225
  requirements: []
243
226
 
244
227
  rubyforge_project: elastic_searchable
245
- rubygems_version: 1.7.2
228
+ rubygems_version: 1.6.2
246
229
  signing_key:
247
230
  specification_version: 3
248
231
  summary: elastic search for activerecord
249
232
  test_files:
250
233
  - test/database.yml
251
234
  - test/helper.rb
252
- - test/setup_database.rb
253
235
  - test/test_elastic_searchable.rb
data/CONTRIBUTORS.txt DELETED
@@ -1,6 +0,0 @@
1
- Ryan Sonnek - Original Author
2
-
3
-
4
- Complete list of contributors:
5
- https://github.com/socialcast/elastic_searchable/contributors
6
-
data/README.md DELETED
@@ -1,51 +0,0 @@
1
- # elastic_searchable
2
-
3
- Integrate the elasticsearch library into Rails.
4
-
5
- ## Usage
6
-
7
- ```ruby
8
- class Blog < ActiveRecord::Base
9
- elastic_searchable
10
- end
11
-
12
- results = Blog.search 'foo'
13
- ```
14
-
15
- ## Features
16
-
17
- * fast. fast! FAST! 30% faster than rubberband on average.
18
- * active record callbacks automatically keep search index up to date as your data changes
19
- * out of the box background indexing of data using backgrounded. Don't lock up a foreground process waiting on a background job!
20
- * integrates with will_paginate library for easy pagination of search results
21
-
22
- ## Installation
23
-
24
- ```ruby
25
- # Bundler Gemfile
26
- gem 'elastic_searchable'
27
- ```
28
-
29
- ## Configuration
30
-
31
- ```ruby
32
- # config/initializers/elastic_searchable.rb
33
- # (optional) customize elasticsearch host
34
- # default is localhost:9200
35
- ElasticSearchable.base_uri = 'server:9200'
36
- ```
37
-
38
- ## Contributing
39
-
40
- * Fork the project
41
- * Fix the issue
42
- * Add unit tests
43
- * Submit pull request on github
44
-
45
- See CONTRIBUTORS.txt for list of project contributors
46
-
47
- ## Copyright
48
-
49
- Copyright (c) 2011 Socialcast, Inc.
50
- See LICENSE.txt for further details.
51
-
@@ -1,41 +0,0 @@
1
- require 'active_record'
2
- require 'after_commit'
3
- require 'backgrounded'
4
- require 'elastic_searchable/queries'
5
- require 'elastic_searchable/callbacks'
6
- require 'elastic_searchable/index'
7
-
8
- module ElasticSearchable
9
- module ActiveRecordExtensions
10
- # Valid options:
11
- # :index (optional) configure index to store data in. default to ElasticSearchable.default_index
12
- # :type (optional) configue type to store data in. default to model table name
13
- # :index_options (optional) configure index properties (ex: tokenizer)
14
- # :mapping (optional) configure field properties for this model (ex: skip analyzer for field)
15
- # :if (optional) reference symbol/proc condition to only index when condition is true
16
- # :unless (optional) reference symbol/proc condition to skip indexing when condition is true
17
- # :json (optional) configure the json document to be indexed (see http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html#method-i-as_json for available options)
18
- def elastic_searchable(options = {})
19
- cattr_accessor :elastic_options
20
- self.elastic_options = options.symbolize_keys.merge(:unless => Array.wrap(options[:unless]).push(:elasticsearch_offline?))
21
-
22
- extend ElasticSearchable::Indexing::ClassMethods
23
- extend ElasticSearchable::Queries
24
-
25
- include ElasticSearchable::Indexing::InstanceMethods
26
- include ElasticSearchable::Callbacks::InstanceMethods
27
-
28
- backgrounded :update_index_on_create => ElasticSearchable::Callbacks.backgrounded_options, :update_index_on_update => ElasticSearchable::Callbacks.backgrounded_options
29
- class << self
30
- backgrounded :delete_id_from_index => ElasticSearchable::Callbacks.backgrounded_options
31
- end
32
-
33
- define_callbacks :after_index_on_create, :after_index_on_update, :after_index
34
- after_commit_on_create :update_index_on_create_backgrounded, :if => :should_index?
35
- after_commit_on_update :update_index_on_update_backgrounded, :if => :should_index?
36
- after_commit_on_destroy :delete_from_index
37
- end
38
- end
39
- end
40
-
41
- ActiveRecord::Base.send(:extend, ElasticSearchable::ActiveRecordExtensions)
@@ -1,33 +0,0 @@
1
- config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
2
- ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
3
- ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite'])
4
-
5
- ActiveRecord::Schema.define(:version => 1) do
6
- create_table :posts, :force => true do |t|
7
- t.column :title, :string
8
- t.column :body, :string
9
- t.column :name, :string
10
- end
11
- create_table :blogs, :force => true do |t|
12
- t.column :title, :string
13
- t.column :body, :string
14
- end
15
- create_table :users, :force => true do |t|
16
- t.column :name, :string
17
- end
18
- create_table :friends, :force => true do |t|
19
- t.column :name, :string
20
- t.column :favorite_color, :string
21
- t.belongs_to :book
22
- end
23
- create_table :books, :force => true do |t|
24
- t.column :title, :string
25
- t.column :isbn, :string
26
- end
27
- create_table :max_page_size_classes, :force => true do |t|
28
- t.column :name, :string
29
- end
30
- end
31
-
32
- WillPaginate.enable_activerecord
33
-