elastic_searchable 2.0.1 → 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -28,4 +28,5 @@ Gem::Specification.new do |s|
28
28
  s.add_development_dependency(%q<pry>, ["0.9.6.2"])
29
29
  s.add_development_dependency(%q<shoulda>, ["2.11.3"])
30
30
  s.add_development_dependency(%q<mocha>, ["0.10.0"])
31
+ s.add_development_dependency(%q<pry>, ["0.9.9.3"])
31
32
  end
@@ -1,79 +1,282 @@
1
1
  require 'active_record'
2
2
  require 'backgrounded'
3
- require 'elastic_searchable/queries'
4
- require 'elastic_searchable/callbacks'
5
- require 'elastic_searchable/index'
6
3
  require 'elastic_searchable/paginator'
7
4
 
8
5
  module ElasticSearchable
9
6
  module ActiveRecordExtensions
10
- # Valid options:
11
- # :type (optional) configue type to store data in. default to model table name
12
- # :mapping (optional) configure field properties for this model (ex: skip analyzer for field)
13
- # :if (optional) reference symbol/proc condition to only index when condition is true
14
- # :unless (optional) reference symbol/proc condition to skip indexing when condition is true
15
- # :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)
16
- #
17
- # Available callbacks:
18
- # after_index
19
- # called after the object is indexed in elasticsearch
20
- # (optional) :on => :create/:update can be used to only fire callback when object is created or updated
21
- #
22
- # after_percolate
23
- # called after object is indexed in elasticsearch
24
- # only fires if the update index call returns a non-empty set of registered percolations
25
- # use the "percolations" instance method from within callback to inspect what percolations were returned
26
- def elastic_searchable(options = {})
27
- cattr_accessor :elastic_options
28
- self.elastic_options = options.symbolize_keys.merge(:unless => Array.wrap(options[:unless]).push(:elasticsearch_offline?))
29
-
30
- if self.elastic_options[:index_options]
31
- ActiveSupport::Deprecation.warn ":index_options has been deprecated. Use ElasticSearchable.index_settings instead.", caller
32
- end
33
- if self.elastic_options[:index]
34
- ActiveSupport::Deprecation.warn ":index has been deprecated. Use ElasticSearchable.index_name instead.", caller
35
- end
7
+ extend ActiveSupport::Concern
36
8
 
37
- extend ElasticSearchable::Indexing::ClassMethods
38
- extend ElasticSearchable::Queries
9
+ included do
10
+ define_model_callbacks :index, :percolate, :only => :after
11
+ end
39
12
 
40
- include ElasticSearchable::Indexing::InstanceMethods
41
- include ElasticSearchable::Callbacks::InstanceMethods
13
+ module ClassMethods
14
+ # Valid options:
15
+ # :type (optional) configue type to store data in. default to model table name
16
+ # :mapping (optional) configure field properties for this model (ex: skip analyzer for field)
17
+ # :if (optional) reference symbol/proc condition to only index when condition is true
18
+ # :unless (optional) reference symbol/proc condition to skip indexing when condition is true
19
+ # :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)
20
+ #
21
+ # after_percolate
22
+ # called after object is indexed in elasticsearch
23
+ # only fires if the update index call returns a non-empty set of registered percolations
24
+ # use the "percolations" instance method from within callback to inspect what percolations were returned
25
+ def elastic_searchable(options = {})
26
+ include ElasticSearchable::ActiveRecordExtensions::LocalMethods
42
27
 
43
- backgrounded :update_index_on_create => ElasticSearchable::Callbacks.backgrounded_options, :update_index_on_update => ElasticSearchable::Callbacks.backgrounded_options
44
- class << self
45
- backgrounded :delete_id_from_index => ElasticSearchable::Callbacks.backgrounded_options
46
- end
28
+ cattr_accessor :elastic_options
29
+ self.elastic_options = options.symbolize_keys.merge(:unless => Array.wrap(options[:unless]).push(:elasticsearch_offline?))
30
+ attr_reader :hit
31
+ attr_accessor :index_lifecycle
47
32
 
48
- attr_reader :hit # the hit json for this result
49
- attr_accessor :index_lifecycle, :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, :unless => :elasticsearch_offline?, :on => :destroy
54
-
55
- class_eval do
56
- # retuns list of percolation matches found during indexing
57
- def percolations
58
- @percolations || []
33
+ if self.elastic_options[:index_options]
34
+ ActiveSupport::Deprecation.warn ":index_options has been deprecated. Use ElasticSearchable.index_settings instead.", caller
35
+ end
36
+ if self.elastic_options[:index]
37
+ ActiveSupport::Deprecation.warn ":index has been deprecated. Use ElasticSearchable.index_name instead.", caller
59
38
  end
60
39
 
40
+ backgrounded :update_index_on_create => ElasticSearchable.backgrounded_options, :update_index_on_update => ElasticSearchable.backgrounded_options
61
41
  class << self
62
- # override default after_index callback definition to support :on option
63
- # see ActiveRecord::Transactions::ClassMethods#after_commit for example
64
- def after_index(*args, &block)
65
- options = args.last
66
- if options.is_a?(Hash) && options[:on]
67
- options[:if] = Array.wrap(options[:if])
68
- options[:if] << "self.index_lifecycle == :#{options[:on]}"
42
+ backgrounded :delete_id_from_index => ElasticSearchable.backgrounded_options
43
+ end
44
+
45
+ after_commit :update_index_on_create_backgrounded, :if => :should_index?, :on => :create
46
+ after_commit :update_index_on_update_backgrounded, :if => :should_index?, :on => :update
47
+ after_commit :delete_from_index, :unless => :elasticsearch_offline?, :on => :destroy
48
+ end
49
+ end
50
+
51
+ module LocalMethods
52
+ extend ActiveSupport::Concern
53
+
54
+ module ClassMethods
55
+ PER_PAGE_DEFAULT = 20
56
+
57
+ # Available callback method after indexing is complete
58
+ # called after the object is indexed in elasticsearch
59
+ # (optional) :on => :create/:update can be used to only fire callback when object is created or updated
60
+ # override default after_index callback definition to support :on option
61
+ # see ActiveRecord::Transactions::ClassMethods#after_commit for example
62
+ def after_index(*args, &block)
63
+ options = args.last
64
+ if options.is_a?(Hash) && options[:on]
65
+ options[:if] = Array.wrap(options[:if])
66
+ options[:if] << "self.index_lifecycle == :#{options[:on]}"
67
+ end
68
+ set_callback(:index, :after, *args, &block)
69
+ end
70
+
71
+ # default number of search results for this model
72
+ # can be overridden by implementing classes
73
+ def per_page
74
+ PER_PAGE_DEFAULT
75
+ end
76
+
77
+ # reindex all records using bulk api
78
+ # see http://www.elasticsearch.org/guide/reference/api/bulk.html
79
+ # options:
80
+ # :scope - scope to use for looking up records to reindex. defaults to self (all)
81
+ # :page - page/batch to begin indexing at. defaults to 1
82
+ # :per_page - number of records to index per batch. defaults to 1000
83
+ #
84
+ # TODO: move this to AREL relation to remove the options scope param
85
+ def reindex(options = {})
86
+ self.create_mapping
87
+ options.reverse_merge! :page => 1, :per_page => 1000
88
+ scope = options.delete(:scope) || self
89
+ page = options[:page]
90
+ per_page = options[:per_page]
91
+ records = scope.limit(per_page).offset(per_page * (page -1)).all
92
+ while records.any? do
93
+ ElasticSearchable.logger.debug "reindexing batch ##{page}..."
94
+ actions = []
95
+ records.each do |record|
96
+ next unless record.should_index?
97
+ begin
98
+ doc = ElasticSearchable.encode_json(record.as_json_for_index)
99
+ actions << ElasticSearchable.encode_json({:index => {'_index' => ElasticSearchable.index_name, '_type' => index_type, '_id' => record.id}})
100
+ actions << doc
101
+ rescue => e
102
+ ElasticSearchable.logger.warn "Unable to bulk index record: #{record.inspect} [#{e.message}]"
103
+ end
104
+ end
105
+ begin
106
+ ElasticSearchable.request(:put, '/_bulk', :body => "\n#{actions.join("\n")}\n") if actions.any?
107
+ rescue ElasticError => e
108
+ ElasticSearchable.logger.warn "Error indexing batch ##{page}: #{e.message}"
109
+ ElasticSearchable.logger.warn e
69
110
  end
70
- set_callback(:index, :after, *args, &block)
111
+
112
+ page += 1
113
+ records = scope.limit(per_page).offset(per_page* (page-1)).all
71
114
  end
115
+ end
116
+
117
+ # search returns a will_paginate collection of ActiveRecord objects for the search results
118
+ # supported options:
119
+ # :page - page of results to search for
120
+ # :per_page - number of results per page
121
+ #
122
+ # http://www.elasticsearch.com/docs/elasticsearch/rest_api/search/
123
+ def search(query, options = {})
124
+ page = (options.delete(:page) || 1).to_i
125
+ options[:fields] ||= '_id'
126
+ options[:size] ||= per_page_for_search(options)
127
+ options[:from] ||= options[:size] * (page - 1)
128
+ options[:query] ||= if query.is_a?(Hash)
129
+ query
130
+ else
131
+ {}.tap do |q|
132
+ q[:query_string] = { :query => query }
133
+ q[:query_string][:default_operator] = options.delete(:default_operator) if options.has_key?(:default_operator)
134
+ end
135
+ end
136
+ query = {}
137
+ case sort = options.delete(:sort)
138
+ when Array,Hash
139
+ options[:sort] = sort
140
+ when String
141
+ query[:sort] = sort
142
+ end
143
+
144
+ response = ElasticSearchable.request :get, index_mapping_path('_search'), :query => query, :json_body => options
145
+ hits = response['hits']
146
+ ids = hits['hits'].collect {|h| h['_id'].to_i }
147
+ results = self.find(ids).sort_by {|result| ids.index(result.id) }
148
+
149
+ results.each do |result|
150
+ result.instance_variable_set '@hit', hits['hits'][ids.index(result.id)]
151
+ end
152
+
153
+ ElasticSearchable::Paginator.handler.new(results, page, options[:size], hits['total'])
154
+ end
72
155
 
156
+ def index_type
157
+ self.elastic_options[:type] || self.table_name
158
+ end
159
+
160
+ # helper method to generate elasticsearch url for this object type
161
+ def index_mapping_path(action = nil)
162
+ ElasticSearchable.request_path [index_type, action].compact.join('/')
163
+ end
164
+
165
+ # delete all documents of this type in the index
166
+ # http://www.elasticsearch.com/docs/elasticsearch/rest_api/admin/indices/delete_mapping/
167
+ def delete_mapping
168
+ ElasticSearchable.request :delete, index_mapping_path
169
+ end
170
+
171
+ # configure the index for this type
172
+ # http://www.elasticsearch.com/docs/elasticsearch/rest_api/admin/indices/put_mapping/
173
+ def create_mapping
174
+ return unless self.elastic_options[:mapping]
175
+ ElasticSearchable.request :put, index_mapping_path('_mapping'), :json_body => {index_type => self.elastic_options[:mapping]}
176
+ end
177
+
178
+ # delete one record from the index
179
+ # http://www.elasticsearch.com/docs/elasticsearch/rest_api/delete/
180
+ def delete_id_from_index(id)
181
+ ElasticSearchable.request :delete, index_mapping_path(id)
182
+ rescue ElasticSearchable::ElasticError => e
183
+ ElasticSearchable.logger.warn e
184
+ end
185
+
186
+ private
187
+ # determine the number of search results per page
188
+ # supports will_paginate configuration by using:
189
+ # Model.per_page
190
+ # Model.max_per_page
191
+ def per_page_for_search(options = {})
192
+ per_page = (options.delete(:per_page) || self.per_page).to_i
193
+ per_page = [per_page, self.max_per_page].min if self.respond_to?(:max_per_page)
194
+ per_page
195
+ end
196
+ end
197
+
198
+ # retuns list of percolation matches found during indexing
199
+ # usable when the model is configured with an :after_index callback
200
+ def percolations
201
+ @percolations || []
202
+ end
203
+
204
+ # reindex the object in elasticsearch
205
+ # fires after_index callbacks after operation is complete
206
+ # see http://www.elasticsearch.org/guide/reference/api/index_.html
207
+ def reindex(lifecycle = nil)
208
+ query = {}
209
+ query[:percolate] = "*" if _percolate_callbacks.any?
210
+ response = ElasticSearchable.request :put, self.class.index_mapping_path(self.id), :query => query, :json_body => self.as_json_for_index
211
+
212
+ self.index_lifecycle = lifecycle ? lifecycle.to_sym : nil
213
+ _run_index_callbacks
214
+
215
+ @percolations = response['matches'] || []
216
+ _run_percolate_callbacks if @percolations.any?
217
+ end
218
+
219
+ # document to index in elasticsearch
220
+ # can be overridden by implementing class to customize the content
221
+ def as_json_for_index
222
+ original_include_root_in_json = self.class.include_root_in_json
223
+ self.class.include_root_in_json = false
224
+ return self.as_json self.class.elastic_options[:json]
225
+ ensure
226
+ self.class.include_root_in_json = original_include_root_in_json
227
+ end
228
+
229
+ # flag to tell if this instance should be indexed
230
+ def should_index?
231
+ [self.class.elastic_options[:if]].flatten.compact.all? { |m| evaluate_elastic_condition(m) } &&
232
+ ![self.class.elastic_options[:unless]].flatten.compact.any? { |m| evaluate_elastic_condition(m) }
233
+ end
234
+
235
+ # percolate this object to see what registered searches match
236
+ # can be done on transient/non-persisted objects!
237
+ # can be done automatically when indexing using :percolate => true config option
238
+ # http://www.elasticsearch.org/blog/2011/02/08/percolator.html
239
+ def percolate(percolator_query = nil)
240
+ body = {:doc => self.as_json_for_index}
241
+ body[:query] = percolator_query if percolator_query
242
+ response = ElasticSearchable.request :get, self.class.index_mapping_path('_percolate'), :json_body => body
243
+ @percolations = response['matches'] || []
244
+ end
245
+
246
+ private
247
+ def delete_from_index
248
+ self.class.delete_id_from_index_backgrounded self.id
249
+ end
250
+ def update_index_on_create
251
+ reindex :create
252
+ end
253
+ def update_index_on_update
254
+ reindex :update
255
+ end
256
+ def elasticsearch_offline?
257
+ ElasticSearchable.offline?
258
+ end
259
+ # ripped from activesupport
260
+ def evaluate_elastic_condition(method)
261
+ case method
262
+ when Symbol
263
+ self.send method
264
+ when String
265
+ eval(method, self.instance_eval { binding })
266
+ when Proc, Method
267
+ method.call
268
+ else
269
+ if method.respond_to?(kind)
270
+ method.send kind
271
+ else
272
+ raise ArgumentError,
273
+ "Callbacks must be a symbol denoting the method to call, a string to be evaluated, " +
274
+ "a block to be invoked, or an object responding to the callback method."
275
+ end
73
276
  end
74
277
  end
75
278
  end
76
279
  end
77
280
  end
78
281
 
79
- ActiveRecord::Base.send(:extend, ElasticSearchable::ActiveRecordExtensions)
282
+ ActiveRecord::Base.send(:include, ElasticSearchable::ActiveRecordExtensions)
@@ -1,3 +1,3 @@
1
1
  module ElasticSearchable
2
- VERSION = '2.0.1'
2
+ VERSION = '2.0.2'
3
3
  end
@@ -43,6 +43,11 @@ module ElasticSearchable
43
43
  response
44
44
  end
45
45
 
46
+ # options for automatic active record indexing
47
+ def backgrounded_options
48
+ {:queue => 'elasticsearch'}
49
+ end
50
+
46
51
  # escape lucene special characters
47
52
  def escape_query(string)
48
53
  string.to_s.gsub(/([\(\)\[\]\{\}\?\\\"!\^\+\-\*:~])/,'\\\\\1')
@@ -92,4 +97,3 @@ ElasticSearchable.logger.level = Logger::INFO
92
97
  # configure default index to be elastic_searchable
93
98
  # one index can hold many object 'types'
94
99
  ElasticSearchable.index_name = 'elastic_searchable'
95
-
@@ -388,6 +388,7 @@ class TestElasticSearchable < Test::Unit::TestCase
388
388
  end
389
389
  context "when index has configured percolation" do
390
390
  setup do
391
+ ElasticSearchable.request :delete, '/_percolator'
391
392
  ElasticSearchable.request :put, '/_percolator/elastic_searchable/myfilter', :json_body => {:query => {:query_string => {:query => 'foo' }}}
392
393
  ElasticSearchable.request :post, '/_percolator/_refresh'
393
394
  end
@@ -416,6 +417,7 @@ class TestElasticSearchable < Test::Unit::TestCase
416
417
  end
417
418
  context "with multiple percolators" do
418
419
  setup do
420
+ ElasticSearchable.request :delete, '/_percolator'
419
421
  ElasticSearchable.request :put, '/_percolator/elastic_searchable/greenfilter', :json_body => { :color => 'green', :query => {:query_string => {:query => 'foo' }}}
420
422
  ElasticSearchable.request :put, '/_percolator/elastic_searchable/bluefilter', :json_body => { :color => 'blue', :query => {:query_string => {:query => 'foo' }}}
421
423
  ElasticSearchable.request :post, '/_percolator/_refresh'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elastic_searchable
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.1
4
+ version: 2.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -13,7 +13,7 @@ date: 2012-04-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
16
- requirement: &2151868260 !ruby/object:Gem::Requirement
16
+ requirement: &2151881900 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 3.0.5
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *2151868260
24
+ version_requirements: *2151881900
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: httparty
27
- requirement: &2151867400 !ruby/object:Gem::Requirement
27
+ requirement: &2151881240 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: 0.6.0
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *2151867400
35
+ version_requirements: *2151881240
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: backgrounded
38
- requirement: &2151866680 !ruby/object:Gem::Requirement
38
+ requirement: &2151880600 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: 0.7.0
44
44
  type: :runtime
45
45
  prerelease: false
46
- version_requirements: *2151866680
46
+ version_requirements: *2151880600
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: multi_json
49
- requirement: &2151866000 !ruby/object:Gem::Requirement
49
+ requirement: &2151880000 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: 1.0.0
55
55
  type: :runtime
56
56
  prerelease: false
57
- version_requirements: *2151866000
57
+ version_requirements: *2151880000
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: rake
60
- requirement: &2151865340 !ruby/object:Gem::Requirement
60
+ requirement: &2151879320 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - =
@@ -65,10 +65,10 @@ dependencies:
65
65
  version: 0.9.2.2
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *2151865340
68
+ version_requirements: *2151879320
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: sqlite3
71
- requirement: &2151864760 !ruby/object:Gem::Requirement
71
+ requirement: &2151894960 !ruby/object:Gem::Requirement
72
72
  none: false
73
73
  requirements:
74
74
  - - =
@@ -76,10 +76,10 @@ dependencies:
76
76
  version: 1.3.4
77
77
  type: :development
78
78
  prerelease: false
79
- version_requirements: *2151864760
79
+ version_requirements: *2151894960
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: pry
82
- requirement: &2151864020 !ruby/object:Gem::Requirement
82
+ requirement: &2151894160 !ruby/object:Gem::Requirement
83
83
  none: false
84
84
  requirements:
85
85
  - - =
@@ -87,10 +87,10 @@ dependencies:
87
87
  version: 0.9.6.2
88
88
  type: :development
89
89
  prerelease: false
90
- version_requirements: *2151864020
90
+ version_requirements: *2151894160
91
91
  - !ruby/object:Gem::Dependency
92
92
  name: shoulda
93
- requirement: &2151863260 !ruby/object:Gem::Requirement
93
+ requirement: &2151893480 !ruby/object:Gem::Requirement
94
94
  none: false
95
95
  requirements:
96
96
  - - =
@@ -98,10 +98,10 @@ dependencies:
98
98
  version: 2.11.3
99
99
  type: :development
100
100
  prerelease: false
101
- version_requirements: *2151863260
101
+ version_requirements: *2151893480
102
102
  - !ruby/object:Gem::Dependency
103
103
  name: mocha
104
- requirement: &2151862580 !ruby/object:Gem::Requirement
104
+ requirement: &2151892820 !ruby/object:Gem::Requirement
105
105
  none: false
106
106
  requirements:
107
107
  - - =
@@ -109,7 +109,18 @@ dependencies:
109
109
  version: 0.10.0
110
110
  type: :development
111
111
  prerelease: false
112
- version_requirements: *2151862580
112
+ version_requirements: *2151892820
113
+ - !ruby/object:Gem::Dependency
114
+ name: pry
115
+ requirement: &2151892120 !ruby/object:Gem::Requirement
116
+ none: false
117
+ requirements:
118
+ - - =
119
+ - !ruby/object:Gem::Version
120
+ version: 0.9.9.3
121
+ type: :development
122
+ prerelease: false
123
+ version_requirements: *2151892120
113
124
  description: integrate the elastic search engine with rails
114
125
  email:
115
126
  - ryan@codecrate.com
@@ -128,12 +139,9 @@ files:
128
139
  - elastic_searchable.gemspec
129
140
  - lib/elastic_searchable.rb
130
141
  - lib/elastic_searchable/active_record_extensions.rb
131
- - lib/elastic_searchable/callbacks.rb
132
- - lib/elastic_searchable/index.rb
133
142
  - lib/elastic_searchable/pagination/kaminari.rb
134
143
  - lib/elastic_searchable/pagination/will_paginate.rb
135
144
  - lib/elastic_searchable/paginator.rb
136
- - lib/elastic_searchable/queries.rb
137
145
  - lib/elastic_searchable/version.rb
138
146
  - test/database.yml
139
147
  - test/helper.rb
@@ -153,7 +161,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
153
161
  version: '0'
154
162
  segments:
155
163
  - 0
156
- hash: 808032034448504633
164
+ hash: 2660925478965356473
157
165
  required_rubygems_version: !ruby/object:Gem::Requirement
158
166
  none: false
159
167
  requirements:
@@ -162,7 +170,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
162
170
  version: '0'
163
171
  segments:
164
172
  - 0
165
- hash: 808032034448504633
173
+ hash: 2660925478965356473
166
174
  requirements: []
167
175
  rubyforge_project: elastic_searchable
168
176
  rubygems_version: 1.8.15
@@ -1,22 +0,0 @@
1
- module ElasticSearchable
2
- module Callbacks
3
- class << self
4
- def backgrounded_options
5
- {:queue => 'elasticsearch'}
6
- end
7
- end
8
-
9
- module InstanceMethods
10
- private
11
- def delete_from_index
12
- self.class.delete_id_from_index_backgrounded self.id
13
- end
14
- def update_index_on_create
15
- reindex :create
16
- end
17
- def update_index_on_update
18
- reindex :update
19
- end
20
- end
21
- end
22
- end
@@ -1,140 +0,0 @@
1
- module ElasticSearchable
2
- module Indexing
3
- module ClassMethods
4
- # delete all documents of this type in the index
5
- # http://www.elasticsearch.com/docs/elasticsearch/rest_api/admin/indices/delete_mapping/
6
- def delete_mapping
7
- ElasticSearchable.request :delete, index_mapping_path
8
- end
9
-
10
- # configure the index for this type
11
- # http://www.elasticsearch.com/docs/elasticsearch/rest_api/admin/indices/put_mapping/
12
- def create_mapping
13
- return unless self.elastic_options[:mapping]
14
- ElasticSearchable.request :put, index_mapping_path('_mapping'), :json_body => {index_type => self.elastic_options[:mapping]}
15
- end
16
-
17
- # delete one record from the index
18
- # http://www.elasticsearch.com/docs/elasticsearch/rest_api/delete/
19
- def delete_id_from_index(id)
20
- ElasticSearchable.request :delete, index_mapping_path(id)
21
- rescue ElasticSearchable::ElasticError => e
22
- ElasticSearchable.logger.warn e
23
- end
24
-
25
- # helper method to generate elasticsearch url for this object type
26
- def index_mapping_path(action = nil)
27
- ElasticSearchable.request_path [index_type, action].compact.join('/')
28
- end
29
-
30
- # reindex all records using bulk api
31
- # see http://www.elasticsearch.org/guide/reference/api/bulk.html
32
- # options:
33
- # :scope - scope to use for looking up records to reindex. defaults to self (all)
34
- # :page - page/batch to begin indexing at. defaults to 1
35
- # :per_page - number of records to index per batch. defaults to 1000
36
- #
37
- # TODO: move this to AREL relation to remove the options scope param
38
- def reindex(options = {})
39
- self.create_mapping
40
- options.reverse_merge! :page => 1, :per_page => 1000
41
- scope = options.delete(:scope) || self
42
- page = options[:page]
43
- per_page = options[:per_page]
44
- records = scope.limit(per_page).offset(per_page * (page -1)).all
45
- while records.any? do
46
- ElasticSearchable.logger.debug "reindexing batch ##{page}..."
47
- actions = []
48
- records.each do |record|
49
- next unless record.should_index?
50
- begin
51
- doc = ElasticSearchable.encode_json(record.as_json_for_index)
52
- actions << ElasticSearchable.encode_json({:index => {'_index' => ElasticSearchable.index_name, '_type' => index_type, '_id' => record.id}})
53
- actions << doc
54
- rescue => e
55
- ElasticSearchable.logger.warn "Unable to bulk index record: #{record.inspect} [#{e.message}]"
56
- end
57
- end
58
- begin
59
- ElasticSearchable.request(:put, '/_bulk', :body => "\n#{actions.join("\n")}\n") if actions.any?
60
- rescue ElasticError => e
61
- ElasticSearchable.logger.warn "Error indexing batch ##{page}: #{e.message}"
62
- ElasticSearchable.logger.warn e
63
- end
64
-
65
- page += 1
66
- records = scope.limit(per_page).offset(per_page* (page-1)).all
67
- end
68
- end
69
-
70
- private
71
- def index_type
72
- self.elastic_options[:type] || self.table_name
73
- end
74
- end
75
-
76
- module InstanceMethods
77
- # reindex the object in elasticsearch
78
- # fires after_index callbacks after operation is complete
79
- # see http://www.elasticsearch.org/guide/reference/api/index_.html
80
- def reindex(lifecycle = nil)
81
- query = {}
82
- query[:percolate] = "*" if _percolate_callbacks.any?
83
- response = ElasticSearchable.request :put, self.class.index_mapping_path(self.id), :query => query, :json_body => self.as_json_for_index
84
-
85
- self.index_lifecycle = lifecycle ? lifecycle.to_sym : nil
86
- _run_index_callbacks
87
-
88
- self.percolations = response['matches'] || []
89
- _run_percolate_callbacks if self.percolations.any?
90
- end
91
- # document to index in elasticsearch
92
- def as_json_for_index
93
- original_include_root_in_json = self.class.include_root_in_json
94
- self.class.include_root_in_json = false
95
- return self.as_json self.class.elastic_options[:json]
96
- ensure
97
- self.class.include_root_in_json = original_include_root_in_json
98
- end
99
- def should_index?
100
- [self.class.elastic_options[:if]].flatten.compact.all? { |m| evaluate_elastic_condition(m) } &&
101
- ![self.class.elastic_options[:unless]].flatten.compact.any? { |m| evaluate_elastic_condition(m) }
102
- end
103
- # percolate this object to see what registered searches match
104
- # can be done on transient/non-persisted objects!
105
- # can be done automatically when indexing using :percolate => true config option
106
- # http://www.elasticsearch.org/blog/2011/02/08/percolator.html
107
- def percolate(percolator_query = nil)
108
- body = {:doc => self.as_json_for_index}
109
- body[:query] = percolator_query if percolator_query
110
- response = ElasticSearchable.request :get, self.class.index_mapping_path('_percolate'), :json_body => body
111
- self.percolations = response['matches'] || []
112
- self.percolations
113
- end
114
-
115
- private
116
- def elasticsearch_offline?
117
- ElasticSearchable.offline?
118
- end
119
- # ripped from activesupport
120
- def evaluate_elastic_condition(method)
121
- case method
122
- when Symbol
123
- self.send method
124
- when String
125
- eval(method, self.instance_eval { binding })
126
- when Proc, Method
127
- method.call
128
- else
129
- if method.respond_to?(kind)
130
- method.send kind
131
- else
132
- raise ArgumentError,
133
- "Callbacks must be a symbol denoting the method to call, a string to be evaluated, " +
134
- "a block to be invoked, or an object responding to the callback method."
135
- end
136
- end
137
- end
138
- end
139
- end
140
- end
@@ -1,59 +0,0 @@
1
- module ElasticSearchable
2
- module Queries
3
- PER_PAGE_DEFAULT = 20
4
-
5
- def per_page
6
- PER_PAGE_DEFAULT
7
- end
8
-
9
- # search returns a will_paginate collection of ActiveRecord objects for the search results
10
- # supported options:
11
- # :page - page of results to search for
12
- # :per_page - number of results per page
13
- #
14
- # http://www.elasticsearch.com/docs/elasticsearch/rest_api/search/
15
- def search(query, options = {})
16
- page = (options.delete(:page) || 1).to_i
17
- options[:fields] ||= '_id'
18
- options[:size] ||= per_page_for_search(options)
19
- options[:from] ||= options[:size] * (page - 1)
20
- options[:query] ||= if query.is_a?(Hash)
21
- query
22
- else
23
- {}.tap do |q|
24
- q[:query_string] = { :query => query }
25
- q[:query_string][:default_operator] = options.delete(:default_operator) if options.has_key?(:default_operator)
26
- end
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
-
36
- response = ElasticSearchable.request :get, index_mapping_path('_search'), :query => query, :json_body => options
37
- hits = response['hits']
38
- ids = hits['hits'].collect {|h| h['_id'].to_i }
39
- results = self.find(ids).sort_by {|result| ids.index(result.id) }
40
-
41
- results.each do |result|
42
- result.instance_variable_set '@hit', hits['hits'][ids.index(result.id)]
43
- end
44
-
45
- ElasticSearchable::Paginator.handler.new(results, page, options[:size], hits['total'])
46
- end
47
-
48
- private
49
- # determine the number of search results per page
50
- # supports will_paginate configuration by using:
51
- # Model.per_page
52
- # Model.max_per_page
53
- def per_page_for_search(options = {})
54
- per_page = (options.delete(:per_page) || (self.respond_to?(:per_page) ? self.per_page : nil) || ElasticSearchable::Queries::PER_PAGE_DEFAULT).to_i
55
- per_page = [per_page, self.max_per_page].min if self.respond_to?(:max_per_page)
56
- per_page
57
- end
58
- end
59
- end