elastic_searchable 1.0.2 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -19,6 +19,15 @@ 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
+
22
31
  # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
23
32
  #
24
33
  # * Create a file at ~/.gitignore
@@ -30,10 +39,6 @@ test/*.sqlite3
30
39
  #
31
40
  # 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)
32
41
  #
33
- # For MacOS:
34
- #
35
- #.DS_Store
36
- #
37
42
  # For TextMate
38
43
  #*.tmproj
39
44
  #tmtags
@@ -42,6 +47,4 @@ test/*.sqlite3
42
47
  #*~
43
48
  #\#*
44
49
  #.\#*
45
- #
46
- # For vim:
47
- #*.swp
50
+
data/.rvmrc ADDED
@@ -0,0 +1,4 @@
1
+ rvm use ree-1.8.7-2011.03@rails3
2
+
3
+ # use rails2 gemset for testing as well
4
+ #ree-1.8.7-2011.03@rails2
data/README.rdoc CHANGED
@@ -27,15 +27,12 @@ Integrate the elasticsearch library into Rails.
27
27
  #defaults to localhost:9200
28
28
  ElasticSearchable.base_uri = 'server:9200'
29
29
 
30
- == Contributing to elastic_searchable
30
+ == Contributing
31
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
32
  * 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.
33
+ * Fix the issue
34
+ * Add unit tests
35
+ * Submit pull request on github
39
36
 
40
37
  == Copyright
41
38
 
@@ -14,10 +14,10 @@ Gem::Specification.new do |s|
14
14
 
15
15
  s.rubyforge_project = "elastic_searchable"
16
16
 
17
- s.add_runtime_dependency(%q<activerecord>, ["~> 3.0.0"])
17
+ s.add_runtime_dependency(%q<activerecord>, [">= 2.3.11"])
18
+ s.add_runtime_dependency(%q<will_paginate>, [">= 2.3.0"])
18
19
  s.add_runtime_dependency(%q<httparty>, ["~> 0.6.0"])
19
20
  s.add_runtime_dependency(%q<backgrounded>, ["~> 0.7.0"])
20
- s.add_runtime_dependency(%q<will_paginate>, [">= 2.3.15"])
21
21
  s.add_development_dependency(%q<shoulda>, [">= 0"])
22
22
  s.add_development_dependency(%q<mocha>, [">= 0"])
23
23
  s.add_development_dependency(%q<bundler>, [">= 0"])
@@ -25,7 +25,6 @@ Gem::Specification.new do |s|
25
25
  s.add_development_dependency(%q<sqlite3-ruby>, ["~> 1.3.2"])
26
26
  s.add_development_dependency(%q<ruby-debug>, [">= 0"])
27
27
 
28
-
29
28
  s.files = `git ls-files`.split("\n")
30
29
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
31
30
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
@@ -0,0 +1,72 @@
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 ActiveRecordExtensions
9
+ # Valid options:
10
+ # :index (optional) configure index to store data in. default to ElasticSearchable.default_index
11
+ # :type (optional) configue type to store data in. default to model table name
12
+ # :index_options (optional) configure index properties (ex: tokenizer)
13
+ # :mapping (optional) configure field properties for this model (ex: skip analyzer for field)
14
+ # :if (optional) reference symbol/proc condition to only index when condition is true
15
+ # :unless (optional) reference symbol/proc condition to skip indexing when condition is true
16
+ # :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)
17
+ #
18
+ # Available callbacks:
19
+ # after_index
20
+ # called after the object is indexed in elasticsearch
21
+ # (optional) :on => :create/:update can be used to only fire callback when object is created or updated
22
+ #
23
+ # after_percolate
24
+ # called after object is indexed in elasticsearch
25
+ # only fires if the update index call returns a non-empty set of registered percolations
26
+ # use the "percolations" instance method from within callback to inspect what percolations were returned
27
+ def elastic_searchable(options = {})
28
+ cattr_accessor :elastic_options
29
+ self.elastic_options = options.symbolize_keys
30
+
31
+ extend ElasticSearchable::Indexing::ClassMethods
32
+ extend ElasticSearchable::Queries
33
+
34
+ include ElasticSearchable::Indexing::InstanceMethods
35
+ include ElasticSearchable::Callbacks::InstanceMethods
36
+
37
+ backgrounded :update_index_on_create => ElasticSearchable::Callbacks.backgrounded_options, :update_index_on_update => ElasticSearchable::Callbacks.backgrounded_options
38
+ class << self
39
+ backgrounded :delete_id_from_index => ElasticSearchable::Callbacks.backgrounded_options
40
+ end
41
+
42
+ attr_accessor :index_lifecycle, :percolations
43
+ define_model_callbacks :index, :percolate, :only => :after
44
+ after_commit :update_index_on_create_backgrounded, :if => :should_index?, :unless => :elasticsearch_offline?, :on => :create
45
+ after_commit :update_index_on_update_backgrounded, :if => :should_index?, :unless => :elasticsearch_offline?, :on => :update
46
+ after_commit :delete_from_index, :unless => :elasticsearch_offline?, :on => :destroy
47
+
48
+ class_eval do
49
+ # retuns list of percolation matches found during indexing
50
+ def percolations
51
+ @percolations || []
52
+ end
53
+
54
+ class << self
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] << "self.index_lifecycle == :#{options[:on]}"
62
+ end
63
+ set_callback(:index, :after, *args, &block)
64
+ end
65
+
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ ActiveRecord::Base.send(:extend, ElasticSearchable::ActiveRecordExtensions)
@@ -1,3 +1,5 @@
1
+ require 'will_paginate'
2
+
1
3
  module ElasticSearchable
2
4
  module Indexing
3
5
  module ClassMethods
@@ -16,11 +18,12 @@ module ElasticSearchable
16
18
  end
17
19
 
18
20
  # create the index
19
- # http://www.elasticsearch.com/docs/elasticsearch/rest_api/admin/indices/create_index/
21
+ # http://www.elasticsearch.org/guide/reference/api/admin-indices-create-index.html
20
22
  def create_index
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
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, :body => options.to_json
24
27
  end
25
28
 
26
29
  # explicitly refresh the index, making all operations performed since the last refresh
@@ -41,6 +44,8 @@ module ElasticSearchable
41
44
  # http://www.elasticsearch.com/docs/elasticsearch/rest_api/delete/
42
45
  def delete_id_from_index(id)
43
46
  ElasticSearchable.request :delete, index_type_path(id)
47
+ rescue ElasticSearchable::ElasticError => e
48
+ ElasticSearchable.logger.warn e
44
49
  end
45
50
 
46
51
  # helper method to generate elasticsearch url for this object type
@@ -54,20 +59,22 @@ module ElasticSearchable
54
59
  end
55
60
 
56
61
  # reindex all records using bulk api
57
- # options:
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
62
  # see http://www.elasticsearch.org/guide/reference/api/bulk.html
63
+ # 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
67
+ #
68
+ # TODO: move this to AREL relation to remove the options scope param
62
69
  def reindex(options = {})
63
70
  self.update_index_mapping
64
- batch = options.delete(:batch) || 1
65
- options[:batch_size] ||= 1000
66
- options[:start] ||= (batch - 1) * options[:batch_size]
71
+ options.reverse_merge! :page => 1, :per_page => 1000, :total_entries => 1
67
72
  scope = options.delete(:scope) || self
68
- scope.find_in_batches(options) do |records|
69
- ElasticSearchable.logger.info "reindexing batch ##{batch}..."
70
- batch += 1
73
+
74
+ records = scope.paginate(options)
75
+ while records.any? do
76
+ ElasticSearchable.logger.debug "reindexing batch ##{records.current_page}..."
77
+
71
78
  actions = []
72
79
  records.each do |record|
73
80
  next unless record.should_index?
@@ -82,9 +89,12 @@ module ElasticSearchable
82
89
  begin
83
90
  ElasticSearchable.request(:put, '/_bulk', :body => "\n#{actions.join("\n")}\n") if actions.any?
84
91
  rescue ElasticError => e
85
- ElasticSearchable.logger.warn "Error indexing batch ##{batch}: #{e.message}"
92
+ ElasticSearchable.logger.warn "Error indexing batch ##{options[:page]}: #{e.message}"
86
93
  ElasticSearchable.logger.warn e
87
94
  end
95
+
96
+ options.merge! :page => (records.current_page + 1)
97
+ records = scope.paginate(options)
88
98
  end
89
99
  end
90
100
 
@@ -106,19 +116,19 @@ module ElasticSearchable
106
116
  query.merge! :percolate => "*" if _percolate_callbacks.any?
107
117
  response = ElasticSearchable.request :put, self.class.index_type_path(self.id), :query => query, :body => self.as_json_for_index.to_json
108
118
 
109
- @index_lifecycle = lifecycle ? lifecycle.to_sym : nil
119
+ self.index_lifecycle = lifecycle ? lifecycle.to_sym : nil
110
120
  _run_index_callbacks
111
121
 
112
- @percolations = response['matches'] || []
113
- _run_percolate_callbacks if @percolations.any?
122
+ self.percolations = response['matches'] || []
123
+ _run_percolate_callbacks if self.percolations.any?
114
124
  end
115
125
  # document to index in elasticsearch
116
126
  def as_json_for_index
117
- original_include_root_in_json = ::ActiveRecord::Base.include_root_in_json
118
- ::ActiveRecord::Base.include_root_in_json = false
127
+ original_include_root_in_json = self.class.include_root_in_json
128
+ self.class.include_root_in_json = false
119
129
  return self.as_json self.class.elastic_options[:json]
120
130
  ensure
121
- ::ActiveRecord::Base.include_root_in_json = original_include_root_in_json
131
+ self.class.include_root_in_json = original_include_root_in_json
122
132
  end
123
133
  def should_index?
124
134
  [self.class.elastic_options[:if]].flatten.compact.all? { |m| evaluate_elastic_condition(m) } &&
@@ -130,10 +140,14 @@ module ElasticSearchable
130
140
  # http://www.elasticsearch.org/blog/2011/02/08/percolator.html
131
141
  def percolate
132
142
  response = ElasticSearchable.request :get, self.class.index_type_path('_percolate'), :body => {:doc => self.as_json_for_index}.to_json
133
- response['matches']
143
+ self.percolations = response['matches'] || []
144
+ self.percolations
134
145
  end
135
146
 
136
147
  private
148
+ def elasticsearch_offline?
149
+ ElasticSearchable.offline?
150
+ end
137
151
  # ripped from activesupport
138
152
  def evaluate_elastic_condition(method)
139
153
  case method
@@ -1,4 +1,4 @@
1
1
  module ElasticSearchable
2
- VERSION = '1.0.2'
2
+ VERSION = '1.1.0'
3
3
  end
4
4
 
@@ -1,34 +1,31 @@
1
1
  require 'httparty'
2
2
  require 'logger'
3
- require 'elastic_searchable/active_record'
3
+ require 'elastic_searchable/active_record_extensions'
4
4
 
5
5
  module ElasticSearchable
6
+ DEFAULT_INDEX = 'elastic_searchable'
6
7
  include HTTParty
7
8
  format :json
8
9
  base_uri 'localhost:9200'
9
- #debug_output
10
10
 
11
11
  class ElasticError < StandardError; end
12
12
  class << self
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
- end
13
+ attr_accessor :logger, :default_index, :offline
22
14
 
23
- @@logger = Logger.new(STDOUT)
24
- @@logger.level = Logger::INFO
25
- def logger=(logger)
26
- @@logger = logger
15
+ # execute a block of work without reindexing objects
16
+ def offline(&block)
17
+ @offline = true
18
+ yield
19
+ ensure
20
+ @offline = false
27
21
  end
28
- def logger
29
- @@logger
22
+ def offline?
23
+ !!@offline
30
24
  end
31
- #perform a request to the elasticsearch server
25
+ # perform a request to the elasticsearch server
26
+ # configuration:
27
+ # ElasticSearchable.base_uri 'host:port' controls where to send request to
28
+ # ElasticSearchable.debug_output outputs all http traffic to console
32
29
  def request(method, url, options = {})
33
30
  response = self.send(method, url, options)
34
31
  logger.debug "elasticsearch request: #{method} #{url} #{"took #{response['took']}ms" if response['took']}"
@@ -46,4 +43,11 @@ module ElasticSearchable
46
43
  end
47
44
  end
48
45
 
49
- ActiveRecord::Base.send(:include, ElasticSearchable::ActiveRecord)
46
+ # configure default logger to standard out with info log level
47
+ ElasticSearchable.logger = Logger.new STDOUT
48
+ ElasticSearchable.logger.level = Logger::INFO
49
+
50
+ # configure default index to be elastic_searchable
51
+ # one index can hold many object 'types'
52
+ ElasticSearchable.default_index = ElasticSearchable::DEFAULT_INDEX
53
+
data/test/helper.rb CHANGED
@@ -10,15 +10,12 @@ 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
-
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'])
18
+ require 'setup_database'
22
19
 
23
20
  class Test::Unit::TestCase
24
21
  def delete_index
@@ -0,0 +1,48 @@
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
+ create_table :parents, :force => true do |t|
31
+ t.column :name, :string
32
+ t.column :description, :string
33
+ end
34
+ create_table :children, :force => true do |t|
35
+ t.column :name, :string
36
+ t.column :description, :string
37
+ t.belongs_to :parent
38
+ end
39
+ end
40
+
41
+ if WillPaginate.respond_to?(:enable_activerecord)
42
+ puts 'configuring will_paginate v2.x'
43
+ WillPaginate.enable_activerecord
44
+ else
45
+ puts 'configuring will_paginate v3.x'
46
+ require 'will_paginate/finders/active_record'
47
+ WillPaginate::Finders::ActiveRecord.enable!
48
+ end
@@ -1,40 +1,38 @@
1
1
  require File.join(File.dirname(__FILE__), 'helper')
2
2
 
3
3
  class TestElasticSearchable < Test::Unit::TestCase
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
4
+ def setup
5
+ delete_index
6
+ end
7
+ ElasticSearchable.debug_output
8
+ SINGLE_NODE_CLUSTER_CONFIG = {'number_of_replicas' => 0, 'number_of_shards' => 1}
9
+
10
+ context 'non elastic activerecord class' do
11
+ class Parent < ActiveRecord::Base
8
12
  end
9
- create_table :blogs, :force => true do |t|
10
- t.column :title, :string
11
- t.column :body, :string
13
+ setup do
14
+ @clazz = Parent
12
15
  end
13
- create_table :users, :force => true do |t|
14
- t.column :name, :string
16
+ should 'not respond to elastic_options' do
17
+ assert !@clazz.respond_to?(:elastic_options)
15
18
  end
16
- create_table :friends, :force => true do |t|
17
- t.column :name, :string
18
- t.column :favorite_color, :string
19
+ end
20
+ context 'instance of non-elastic_searchable activerecord class' do
21
+ class Parent < ActiveRecord::Base
19
22
  end
20
- create_table :books, :force => true do |t|
21
- t.column :title, :string
23
+ setup do
24
+ @instance = Parent.new
22
25
  end
23
- create_table :max_page_size_classes, :force => true do |t|
24
- t.column :name, :string
26
+ should 'not respond to percolations' do
27
+ assert !@instance.respond_to?(:percolations)
25
28
  end
26
29
  end
27
30
 
28
- def setup
29
- delete_index
30
- end
31
- def teardown
32
- delete_index
33
- end
34
31
  class Post < ActiveRecord::Base
35
- elastic_searchable :index_options => { "analysis.analyzer.default.tokenizer" => 'standard', "analysis.analyzer.default.filter" => ["standard", "lowercase", 'porterStem'] }
32
+ elastic_searchable :index_options => SINGLE_NODE_CLUSTER_CONFIG
36
33
  after_index :indexed
37
34
  after_index :indexed_on_create, :on => :create
35
+ after_index :indexed_on_update, :on => :update
38
36
  def indexed
39
37
  @indexed = true
40
38
  end
@@ -47,8 +45,14 @@ class TestElasticSearchable < Test::Unit::TestCase
47
45
  def indexed_on_create?
48
46
  @indexed_on_create
49
47
  end
48
+ def indexed_on_update
49
+ @indexed_on_update = true
50
+ end
51
+ def indexed_on_update?
52
+ @indexed_on_update
53
+ end
50
54
  end
51
- context 'Post class with default elastic_searchable config' do
55
+ context 'activerecord class with default elastic_searchable config' do
52
56
  setup do
53
57
  @clazz = Post
54
58
  end
@@ -58,9 +62,13 @@ class TestElasticSearchable < Test::Unit::TestCase
58
62
  should 'define elastic_options' do
59
63
  assert @clazz.elastic_options
60
64
  end
65
+ should 'respond to :percolations' do
66
+ assert @clazz.new.respond_to?(:percolations)
67
+ assert_equal [], @clazz.new.percolations
68
+ end
61
69
  end
62
70
 
63
- context 'ElasticSearchable.request with invalid url' do
71
+ context 'Model.request with invalid url' do
64
72
  should 'raise error' do
65
73
  assert_raises ElasticSearchable::ElasticError do
66
74
  ElasticSearchable.request :get, '/elastic_searchable/foobar/notfound'
@@ -68,36 +76,18 @@ class TestElasticSearchable < Test::Unit::TestCase
68
76
  end
69
77
  end
70
78
 
71
- context 'Post.create_index' do
79
+ context 'Model.create_index' do
72
80
  setup do
73
81
  Post.create_index
82
+ Post.refresh_index
74
83
  @status = ElasticSearchable.request :get, '/elastic_searchable/_status'
75
84
  end
76
85
  should 'have created index' do
77
86
  assert @status['ok']
78
87
  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
90
- end
91
-
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
97
- end
98
88
  end
99
89
 
100
- context 'Post.create' do
90
+ context 'Model.create' do
101
91
  setup do
102
92
  @post = Post.create :title => 'foo', :body => "bar"
103
93
  end
@@ -107,27 +97,78 @@ class TestElasticSearchable < Test::Unit::TestCase
107
97
  should 'have fired after_index_on_create callback' do
108
98
  assert @post.indexed_on_create?
109
99
  end
100
+ should 'not have fired after_index_on_update callback' do
101
+ assert !@post.indexed_on_update?
102
+ end
103
+ end
104
+
105
+ context 'Model.update' do
106
+ setup do
107
+ Post.create! :title => 'foo'
108
+ @post = Post.last
109
+ @post.update_attribute :title, 'bar'
110
+ end
111
+ should '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?
116
+ end
117
+ should 'have fired after_index_on_update callback' do
118
+ assert @post.indexed_on_update?
119
+ end
120
+ end
121
+
122
+ context 'Model.create within ElasticSearchable.offline block' do
123
+ setup do
124
+ Post.any_instance.expects(:update_index_on_create).never
125
+ ElasticSearchable.offline do
126
+ @post = Post.create :title => 'foo', :body => "bar"
127
+ end
128
+ end
129
+ should 'not have triggered indexing behavior' do end #see expectations
130
+ should 'not have fired after_index callback' do
131
+ assert !@post.indexed?
132
+ end
133
+ should 'not have fired after_index_on_create callback' do
134
+ assert !@post.indexed_on_create?
135
+ end
136
+ should 'not have fired after_index_on_update callback' do
137
+ assert !@post.indexed_on_update?
138
+ end
110
139
  end
111
140
 
112
141
  context 'with empty index when multiple database records' do
113
142
  setup do
143
+ Post.delete_all
114
144
  Post.create_index
115
145
  @first_post = Post.create :title => 'foo', :body => "first bar"
116
146
  @second_post = Post.create :title => 'foo', :body => "second bar"
117
- Post.clean_index
147
+ Post.delete_index
148
+ Post.create_index
118
149
  end
119
150
  should 'not raise error if error occurs reindexing model' do
120
151
  ElasticSearchable.expects(:request).raises(ElasticSearchable::ElasticError.new('faux error'))
121
- Post.reindex
152
+ assert_nothing_raised do
153
+ Post.reindex
154
+ end
122
155
  end
123
- context 'Post.reindex' do
156
+ should 'not raise error if destroying one instance' do
157
+ Logger.any_instance.expects(:warn)
158
+ assert_nothing_raised do
159
+ @first_post.destroy
160
+ end
161
+ end
162
+ context 'Model.reindex' do
124
163
  setup do
125
- Post.reindex
164
+ Post.reindex :per_page => 1, :scope => Post.scoped(:order => 'body desc')
126
165
  Post.refresh_index
127
166
  end
128
167
  should 'have reindexed both records' do
129
- ElasticSearchable.request :get, "/elastic_searchable/posts/#{@first_post.id}"
130
- ElasticSearchable.request :get, "/elastic_searchable/posts/#{@second_post.id}"
168
+ assert_nothing_raised do
169
+ ElasticSearchable.request :get, "/elastic_searchable/posts/#{@first_post.id}"
170
+ ElasticSearchable.request :get, "/elastic_searchable/posts/#{@second_post.id}"
171
+ end
131
172
  end
132
173
  end
133
174
  end
@@ -149,7 +190,7 @@ class TestElasticSearchable < Test::Unit::TestCase
149
190
  end
150
191
  should 'be paginated' do
151
192
  assert_equal 1, @results.current_page
152
- assert_equal 20, @results.per_page
193
+ assert_equal Post.per_page, @results.per_page
153
194
  assert_nil @results.previous_page
154
195
  assert_nil @results.next_page
155
196
  end
@@ -197,7 +238,7 @@ class TestElasticSearchable < Test::Unit::TestCase
197
238
 
198
239
 
199
240
  class Blog < ActiveRecord::Base
200
- elastic_searchable :if => proc {|b| b.should_index? }
241
+ elastic_searchable :if => proc {|b| b.should_index? }, :index_options => SINGLE_NODE_CLUSTER_CONFIG
201
242
  def should_index?
202
243
  false
203
244
  end
@@ -217,15 +258,32 @@ class TestElasticSearchable < Test::Unit::TestCase
217
258
  end
218
259
 
219
260
  class User < ActiveRecord::Base
220
- elastic_searchable :mapping => {:properties => {:name => {:type => :string, :index => :not_analyzed}}}
261
+ elastic_searchable :index_options => {
262
+ 'number_of_replicas' => 0,
263
+ 'number_of_shards' => 1,
264
+ "analysis.analyzer.default.tokenizer" => 'standard',
265
+ "analysis.analyzer.default.filter" => ["standard", "lowercase", 'porterStem']},
266
+ :mapping => {:properties => {:name => {:type => :string, :index => :not_analyzed}}}
221
267
  end
222
- context 'activerecord class with :mapping=>{}' do
268
+ context 'activerecord class with :index_options and :mapping' do
223
269
  context 'creating index' do
224
270
  setup do
225
271
  User.create_index
226
- @status = ElasticSearchable.request :get, '/elastic_searchable/users/_mapping'
272
+ end
273
+ should 'have used custom index_options' do
274
+ @status = ElasticSearchable.request :get, '/elastic_searchable/_status'
275
+ expected = {
276
+ "index.number_of_replicas" => "0",
277
+ "index.number_of_shards" => "1",
278
+ "index.analysis.analyzer.default.tokenizer" => "standard",
279
+ "index.analysis.analyzer.default.filter.0" => "standard",
280
+ "index.analysis.analyzer.default.filter.1" => "lowercase",
281
+ "index.analysis.analyzer.default.filter.2" => "porterStem"
282
+ }
283
+ assert_equal expected, @status['indices']['elastic_searchable']['settings'], @status.inspect
227
284
  end
228
285
  should 'have set mapping' do
286
+ @status = ElasticSearchable.request :get, '/elastic_searchable/users/_mapping'
229
287
  expected = {
230
288
  "users"=> {
231
289
  "properties"=> {
@@ -239,19 +297,27 @@ class TestElasticSearchable < Test::Unit::TestCase
239
297
  end
240
298
 
241
299
  class Friend < ActiveRecord::Base
242
- elastic_searchable :json => {:only => [:name]}
300
+ belongs_to :book
301
+ elastic_searchable :json => {:include => {:book => {:only => :title}}, :only => :name}, :index_options => SINGLE_NODE_CLUSTER_CONFIG
243
302
  end
244
- context 'activerecord class with optiona :json config' do
303
+ context 'activerecord class with optional :json config' do
245
304
  context 'creating index' do
246
305
  setup do
247
306
  Friend.create_index
248
- @friend = Friend.create! :name => 'bob', :favorite_color => 'red'
307
+ @book = Book.create! :isbn => '123abc', :title => 'another world'
308
+ @friend = Friend.new :name => 'bob', :favorite_color => 'red'
309
+ @friend.book = @book
310
+ @friend.save!
249
311
  Friend.refresh_index
250
312
  end
251
313
  should 'index json with configuration' do
252
314
  @response = ElasticSearchable.request :get, "/elastic_searchable/friends/#{@friend.id}"
315
+ # should not index:
316
+ # friend.favorite_color
317
+ # book.isbn
253
318
  expected = {
254
- "name" => 'bob' #favorite_color should not be indexed
319
+ "name" => 'bob',
320
+ 'book' => {'title' => 'another world'}
255
321
  }
256
322
  assert_equal expected, @response['_source'], @response.inspect
257
323
  end
@@ -263,7 +329,7 @@ class TestElasticSearchable < Test::Unit::TestCase
263
329
  ElasticSearchable.default_index = 'my_new_index'
264
330
  end
265
331
  teardown do
266
- ElasticSearchable.default_index = nil
332
+ ElasticSearchable.default_index = ElasticSearchable::DEFAULT_INDEX
267
333
  end
268
334
  should 'change default index' do
269
335
  assert_equal 'my_new_index', ElasticSearchable.default_index
@@ -318,7 +384,7 @@ class TestElasticSearchable < Test::Unit::TestCase
318
384
  end
319
385
 
320
386
  class MaxPageSizeClass < ActiveRecord::Base
321
- elastic_searchable
387
+ elastic_searchable :index_options => SINGLE_NODE_CLUSTER_CONFIG
322
388
  def self.max_per_page
323
389
  1
324
390
  end
metadata CHANGED
@@ -5,9 +5,9 @@ version: !ruby/object:Gem::Version
5
5
  prerelease:
6
6
  segments:
7
7
  - 1
8
+ - 1
8
9
  - 0
9
- - 2
10
- version: 1.0.2
10
+ version: 1.1.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Ryan Sonnek
@@ -15,8 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-03-30 00:00:00 -05:00
19
- default_executable:
18
+ date: 2011-04-15 00:00:00 Z
20
19
  dependencies:
21
20
  - !ruby/object:Gem::Dependency
22
21
  name: activerecord
@@ -24,62 +23,62 @@ dependencies:
24
23
  requirement: &id001 !ruby/object:Gem::Requirement
25
24
  none: false
26
25
  requirements:
27
- - - ~>
26
+ - - ">="
28
27
  - !ruby/object:Gem::Version
29
- hash: 7
28
+ hash: 21
30
29
  segments:
30
+ - 2
31
31
  - 3
32
- - 0
33
- - 0
34
- version: 3.0.0
32
+ - 11
33
+ version: 2.3.11
35
34
  type: :runtime
36
35
  version_requirements: *id001
37
36
  - !ruby/object:Gem::Dependency
38
- name: httparty
37
+ name: will_paginate
39
38
  prerelease: false
40
39
  requirement: &id002 !ruby/object:Gem::Requirement
41
40
  none: false
42
41
  requirements:
43
- - - ~>
42
+ - - ">="
44
43
  - !ruby/object:Gem::Version
45
- hash: 7
44
+ hash: 3
46
45
  segments:
46
+ - 2
47
+ - 3
47
48
  - 0
48
- - 6
49
- - 0
50
- version: 0.6.0
49
+ version: 2.3.0
51
50
  type: :runtime
52
51
  version_requirements: *id002
53
52
  - !ruby/object:Gem::Dependency
54
- name: backgrounded
53
+ name: httparty
55
54
  prerelease: false
56
55
  requirement: &id003 !ruby/object:Gem::Requirement
57
56
  none: false
58
57
  requirements:
59
58
  - - ~>
60
59
  - !ruby/object:Gem::Version
61
- hash: 3
60
+ hash: 7
62
61
  segments:
63
62
  - 0
64
- - 7
63
+ - 6
65
64
  - 0
66
- version: 0.7.0
65
+ version: 0.6.0
67
66
  type: :runtime
68
67
  version_requirements: *id003
69
68
  - !ruby/object:Gem::Dependency
70
- name: will_paginate
69
+ name: backgrounded
71
70
  prerelease: false
72
71
  requirement: &id004 !ruby/object:Gem::Requirement
73
72
  none: false
74
73
  requirements:
75
- - - ">="
74
+ - - ~>
76
75
  - !ruby/object:Gem::Version
77
- hash: 29
76
+ hash: 3
78
77
  segments:
79
- - 2
80
- - 3
81
- - 15
82
- version: 2.3.15
78
+ - 0
79
+ - 7
80
+ - 0
81
+ version: 0.7.0
83
82
  type: :runtime
84
83
  version_requirements: *id004
85
84
  - !ruby/object:Gem::Dependency
@@ -180,22 +179,22 @@ extra_rdoc_files: []
180
179
  files:
181
180
  - .document
182
181
  - .gitignore
182
+ - .rvmrc
183
183
  - Gemfile
184
184
  - LICENSE.txt
185
185
  - README.rdoc
186
186
  - Rakefile
187
- - VERSION
188
187
  - elastic_searchable.gemspec
189
188
  - lib/elastic_searchable.rb
190
- - lib/elastic_searchable/active_record.rb
189
+ - lib/elastic_searchable/active_record_extensions.rb
191
190
  - lib/elastic_searchable/callbacks.rb
192
191
  - lib/elastic_searchable/index.rb
193
192
  - lib/elastic_searchable/queries.rb
194
193
  - lib/elastic_searchable/version.rb
195
194
  - test/database.yml
196
195
  - test/helper.rb
196
+ - test/setup_database.rb
197
197
  - test/test_elastic_searchable.rb
198
- has_rdoc: true
199
198
  homepage: http://github.com/wireframe/elastic_searchable
200
199
  licenses: []
201
200
 
@@ -225,11 +224,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
225
224
  requirements: []
226
225
 
227
226
  rubyforge_project: elastic_searchable
228
- rubygems_version: 1.6.2
227
+ rubygems_version: 1.7.2
229
228
  signing_key:
230
229
  specification_version: 3
231
230
  summary: elastic search for activerecord
232
231
  test_files:
233
232
  - test/database.yml
234
233
  - test/helper.rb
234
+ - test/setup_database.rb
235
235
  - test/test_elastic_searchable.rb
data/VERSION DELETED
@@ -1 +0,0 @@
1
- 0.6.1
@@ -1,67 +0,0 @@
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