algoliasearch-rails 1.16.3 → 1.25.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +56 -42
  3. data/{ChangeLog → CHANGELOG.MD} +163 -1
  4. data/Gemfile +25 -6
  5. data/Gemfile.lock +144 -325
  6. data/LICENSE +6 -7
  7. data/README.md +448 -306
  8. data/algoliasearch-rails.gemspec +10 -8
  9. data/lib/algoliasearch-rails.rb +285 -117
  10. data/lib/algoliasearch/configuration.rb +3 -1
  11. data/lib/algoliasearch/tasks/algoliasearch.rake +6 -2
  12. data/lib/algoliasearch/utilities.rb +29 -3
  13. data/lib/algoliasearch/version.rb +3 -0
  14. data/spec/spec_helper.rb +26 -4
  15. data/vendor/assets/javascripts/algolia/algoliasearch.angular.js +23 -12
  16. data/vendor/assets/javascripts/algolia/algoliasearch.angular.min.js +2 -2
  17. data/vendor/assets/javascripts/algolia/algoliasearch.jquery.js +23 -12
  18. data/vendor/assets/javascripts/algolia/algoliasearch.jquery.min.js +2 -2
  19. data/vendor/assets/javascripts/algolia/algoliasearch.js +22 -12
  20. data/vendor/assets/javascripts/algolia/algoliasearch.min.js +2 -2
  21. data/vendor/assets/javascripts/algolia/v2/algoliasearch.angular.js +23 -12
  22. data/vendor/assets/javascripts/algolia/v2/algoliasearch.angular.min.js +2 -2
  23. data/vendor/assets/javascripts/algolia/v2/algoliasearch.jquery.js +23 -12
  24. data/vendor/assets/javascripts/algolia/v2/algoliasearch.jquery.min.js +2 -2
  25. data/vendor/assets/javascripts/algolia/v2/algoliasearch.js +22 -12
  26. data/vendor/assets/javascripts/algolia/v2/algoliasearch.min.js +2 -2
  27. data/vendor/assets/javascripts/algolia/v3/algoliasearch.angular.js +2020 -1301
  28. data/vendor/assets/javascripts/algolia/v3/algoliasearch.angular.min.js +3 -3
  29. data/vendor/assets/javascripts/algolia/v3/algoliasearch.jquery.js +2019 -1300
  30. data/vendor/assets/javascripts/algolia/v3/algoliasearch.jquery.min.js +3 -3
  31. data/vendor/assets/javascripts/algolia/v3/algoliasearch.js +2003 -1284
  32. data/vendor/assets/javascripts/algolia/v3/algoliasearch.min.js +3 -3
  33. metadata +19 -15
  34. data/VERSION +0 -1
@@ -1,10 +1,12 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
 
3
- VERSION = File.read(File.join(File.dirname(__FILE__), 'VERSION')).strip
3
+ require File.join(File.dirname(__FILE__), 'lib', 'algoliasearch', 'version')
4
+
5
+ require 'date'
4
6
 
5
7
  Gem::Specification.new do |s|
6
8
  s.name = "algoliasearch-rails"
7
- s.version = VERSION
9
+ s.version = AlgoliaSearch::VERSION
8
10
 
9
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
10
12
  s.authors = ["Algolia"]
@@ -12,7 +14,7 @@ Gem::Specification.new do |s|
12
14
  s.description = "AlgoliaSearch integration to your favorite ORM"
13
15
  s.email = "contact@algolia.com"
14
16
  s.extra_rdoc_files = [
15
- "ChangeLog",
17
+ "CHANGELOG.MD",
16
18
  "LICENSE",
17
19
  "README.md"
18
20
  ]
@@ -20,13 +22,12 @@ Gem::Specification.new do |s|
20
22
  ".document",
21
23
  ".rspec",
22
24
  ".travis.yml",
23
- "ChangeLog",
25
+ "CHANGELOG.MD",
24
26
  "Gemfile",
25
27
  "Gemfile.lock",
26
28
  "LICENSE",
27
29
  "README.md",
28
30
  "Rakefile",
29
- "VERSION",
30
31
  "algoliasearch-rails.gemspec",
31
32
  "lib/algoliasearch-rails.rb",
32
33
  "lib/algoliasearch/algolia_job.rb",
@@ -37,6 +38,7 @@ Gem::Specification.new do |s|
37
38
  "lib/algoliasearch/railtie.rb",
38
39
  "lib/algoliasearch/tasks/algoliasearch.rake",
39
40
  "lib/algoliasearch/utilities.rb",
41
+ "lib/algoliasearch/version.rb",
40
42
  "spec/spec_helper.rb",
41
43
  "spec/utilities_spec.rb",
42
44
  "vendor/assets/javascripts/algolia/algoliasearch.angular.js",
@@ -77,7 +79,7 @@ Gem::Specification.new do |s|
77
79
 
78
80
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
79
81
  s.add_runtime_dependency(%q<json>, [">= 1.5.1"])
80
- s.add_runtime_dependency(%q<algoliasearch>, ["~> 1.12.1"])
82
+ s.add_runtime_dependency(%q<algoliasearch>, [">= 1.26.0", "< 2.0.0"])
81
83
  s.add_development_dependency(%q<will_paginate>, [">= 2.3.15"])
82
84
  s.add_development_dependency(%q<kaminari>, [">= 0"])
83
85
  s.add_development_dependency "travis"
@@ -85,11 +87,11 @@ Gem::Specification.new do |s|
85
87
  s.add_development_dependency "rdoc"
86
88
  else
87
89
  s.add_dependency(%q<json>, [">= 1.5.1"])
88
- s.add_dependency(%q<algoliasearch>, ["~> 1.12.1"])
90
+ s.add_dependency(%q<algoliasearch>, [">= 1.26.0", "< 2.0.0"])
89
91
  end
90
92
  else
91
93
  s.add_dependency(%q<json>, [">= 1.5.1"])
92
- s.add_dependency(%q<algoliasearch>, ["~> 1.12.1"])
94
+ s.add_dependency(%q<algoliasearch>, [">= 1.26.0", "< 2.0.0"])
93
95
  end
94
96
  end
95
97
 
@@ -1,13 +1,6 @@
1
- begin
2
- require "rubygems"
3
- require "bundler"
4
-
5
- Bundler.setup :default
6
- rescue => e
7
- puts "AlgoliaSearch: #{e.message}"
8
- end
9
1
  require 'algoliasearch'
10
2
 
3
+ require 'algoliasearch/version'
11
4
  require 'algoliasearch/utilities'
12
5
 
13
6
  if defined? Rails
@@ -30,6 +23,7 @@ module AlgoliaSearch
30
23
  class NotConfigured < StandardError; end
31
24
  class BadConfiguration < StandardError; end
32
25
  class NoBlockGiven < StandardError; end
26
+ class MixedSlavesAndReplicas < StandardError; end
33
27
 
34
28
  autoload :Configuration, 'algoliasearch/configuration'
35
29
  extend Configuration
@@ -53,33 +47,62 @@ module AlgoliaSearch
53
47
  end
54
48
 
55
49
  class IndexSettings
50
+ DEFAULT_BATCH_SIZE = 1000
56
51
 
57
52
  # AlgoliaSearch settings
58
- OPTIONS = [:minWordSizefor1Typo, :minWordSizefor2Typos, :typoTolerance,
59
- :hitsPerPage, :attributesToRetrieve,
60
- :attributesToHighlight, :attributesToSnippet, :attributesToIndex,
61
- :highlightPreTag, :highlightPostTag,
62
- :ranking, :customRanking, :queryType, :attributesForFaceting,
63
- :separatorsToIndex, :optionalWords, :attributeForDistinct,
64
- :synonyms, :placeholders, :removeWordsIfNoResults, :replaceSynonymsInHighlight,
65
- :unretrievableAttributes, :disableTypoToleranceOnWords, :disableTypoToleranceOnAttributes, :altCorrections,
66
- :ignorePlurals, :maxValuesPerFacet, :distinct, :numericAttributesToIndex,
67
- :allowTyposOnNumericTokens, :allowCompressionOfIntegerArray,
68
- :advancedSyntax]
53
+ OPTIONS = [
54
+ # Attributes
55
+ :searchableAttributes, :attributesForFaceting, :unretrievableAttributes, :attributesToRetrieve,
56
+ :attributesToIndex, #Legacy name of searchableAttributes
57
+ # Ranking
58
+ :ranking, :customRanking, # Replicas are handled via `add_replica`
59
+ # Faceting
60
+ :maxValuesPerFacet, :sortFacetValuesBy,
61
+ # Highlighting / Snippeting
62
+ :attributesToHighlight, :attributesToSnippet, :highlightPreTag, :highlightPostTag,
63
+ :snippetEllipsisText, :restrictHighlightAndSnippetArrays,
64
+ # Pagination
65
+ :hitsPerPage, :paginationLimitedTo,
66
+ # Typo
67
+ :minWordSizefor1Typo, :minWordSizefor2Typos, :typoTolerance, :allowTyposOnNumericTokens,
68
+ :disableTypoToleranceOnAttributes, :disableTypoToleranceOnWords, :separatorsToIndex,
69
+ # Language
70
+ :ignorePlurals, :removeStopWords, :camelCaseAttributes, :decompoundedAttributes,
71
+ :keepDiacriticsOnCharacters, :queryLanguages, :indexLanguages,
72
+ # Query Rules
73
+ :enableRules,
74
+ # Query Strategy
75
+ :queryType, :removeWordsIfNoResults, :advancedSyntax, :optionalWords,
76
+ :disablePrefixOnAttributes, :disableExactOnAttributes, :exactOnSingleWordQuery, :alternativesAsExact,
77
+ # Performance
78
+ :numericAttributesForFiltering, :allowCompressionOfIntegerArray,
79
+ :numericAttributesToIndex, # Legacy name of numericAttributesForFiltering
80
+ # Advanced
81
+ :attributeForDistinct, :distinct, :replaceSynonymsInHighlight, :minProximity, :responseFields,
82
+ :maxFacetHits,
83
+
84
+ # Rails-specific
85
+ :synonyms, :placeholders, :altCorrections,
86
+ ]
69
87
  OPTIONS.each do |k|
70
88
  define_method k do |v|
71
89
  instance_variable_set("@#{k}", v)
72
90
  end
73
91
  end
74
92
 
75
- def initialize(options, block)
93
+ def initialize(options, &block)
76
94
  @options = options
77
- instance_exec(&block) if block
95
+ instance_exec(&block) if block_given?
96
+ end
97
+
98
+ def use_serializer(serializer)
99
+ @serializer = serializer
100
+ # instance_variable_set("@serializer", serializer)
78
101
  end
79
102
 
80
103
  def attribute(*names, &block)
81
104
  raise ArgumentError.new('Cannot pass multiple attribute names if block given') if block_given? and names.length > 1
82
- raise ArgumentError.new('Cannot specify additional attributes on a slave index') if @options[:slave]
105
+ raise ArgumentError.new('Cannot specify additional attributes on a replica index') if @options[:slave] || @options[:replica]
83
106
  @attributes ||= {}
84
107
  names.flatten.each do |name|
85
108
  @attributes[name.to_s] = block_given? ? Proc.new { |o| o.instance_eval(&block) } : Proc.new { |o| o.send(name) }
@@ -89,7 +112,7 @@ module AlgoliaSearch
89
112
 
90
113
  def add_attribute(*names, &block)
91
114
  raise ArgumentError.new('Cannot pass multiple attribute names if block given') if block_given? and names.length > 1
92
- raise ArgumentError.new('Cannot specify additional attributes on a slave index') if @options[:slave]
115
+ raise ArgumentError.new('Cannot specify additional attributes on a replica index') if @options[:slave] || @options[:replica]
93
116
  @additional_attributes ||= {}
94
117
  names.each do |name|
95
118
  @additional_attributes[name.to_s] = block_given? ? Proc.new { |o| o.instance_eval(&block) } : Proc.new { |o| o.send(name) }
@@ -123,15 +146,7 @@ module AlgoliaSearch
123
146
  end
124
147
 
125
148
  def get_attribute_names(object)
126
- res = if @attributes.nil? || @attributes.length == 0
127
- get_default_attributes(object).keys
128
- else
129
- @attributes.keys
130
- end
131
-
132
- res += @additional_attributes.keys if @additional_attributes
133
-
134
- res
149
+ get_attributes(object).keys
135
150
  end
136
151
 
137
152
  def attributes_to_hash(attributes, object)
@@ -143,19 +158,27 @@ module AlgoliaSearch
143
158
  end
144
159
 
145
160
  def get_attributes(object)
146
- attributes = if @attributes.nil? || @attributes.length == 0
147
- get_default_attributes(object)
161
+ # If a serializer is set, we ignore attributes
162
+ # everything should be done via the serializer
163
+ if not @serializer.nil?
164
+ attributes = @serializer.new(object).attributes
148
165
  else
149
- if is_active_record?(object)
150
- object.class.unscoped do
151
- attributes_to_hash(@attributes, object)
152
- end
166
+ if @attributes.nil? || @attributes.length == 0
167
+ # no `attribute ...` have been configured, use the default attributes of the model
168
+ attributes = get_default_attributes(object)
153
169
  else
154
- attributes_to_hash(@attributes, object)
170
+ # at least 1 `attribute ...` has been configured, therefore use ONLY the one configured
171
+ if is_active_record?(object)
172
+ object.class.unscoped do
173
+ attributes = attributes_to_hash(@attributes, object)
174
+ end
175
+ else
176
+ attributes = attributes_to_hash(@attributes, object)
177
+ end
155
178
  end
156
179
  end
157
180
 
158
- attributes.merge!(attributes_to_hash(@additional_attributes, object))
181
+ attributes.merge!(attributes_to_hash(@additional_attributes, object)) if @additional_attributes
159
182
 
160
183
  if @options[:sanitize]
161
184
  sanitizer = begin
@@ -200,15 +223,15 @@ module AlgoliaSearch
200
223
  end
201
224
  end
202
225
 
203
- def geoloc(lat_attr, lng_attr)
204
- raise ArgumentError.new('Cannot specify additional attributes on a slave index') if @options[:slave]
226
+ def geoloc(lat_attr = nil, lng_attr = nil, &block)
227
+ raise ArgumentError.new('Cannot specify additional attributes on a replica index') if @options[:slave] || @options[:replica]
205
228
  add_attribute :_geoloc do |o|
206
- { :lat => o.send(lat_attr).to_f, :lng => o.send(lng_attr).to_f }
229
+ block_given? ? o.instance_eval(&block) : { :lat => o.send(lat_attr).to_f, :lng => o.send(lng_attr).to_f }
207
230
  end
208
231
  end
209
232
 
210
233
  def tags(*args, &block)
211
- raise ArgumentError.new('Cannot specify additional attributes on a slave index') if @options[:slave]
234
+ raise ArgumentError.new('Cannot specify additional attributes on a replica index') if @options[:slave] || @options[:replica]
212
235
  add_attribute :_tags do |o|
213
236
  v = block_given? ? o.instance_eval(&block) : args
214
237
  v.is_a?(Array) ? v : [v]
@@ -225,27 +248,43 @@ module AlgoliaSearch
225
248
  v = get_setting(k)
226
249
  settings[k] = v if !v.nil?
227
250
  end
228
- settings[:slaves] = additional_indexes.select { |options, s| options[:slave] }.map do |options, s|
229
- name = options[:index_name]
230
- name = "#{name}_#{Rails.env.to_s}" if options[:per_environment]
231
- name
232
- end if !@options[:slave]
251
+ if !@options[:slave] && !@options[:replica]
252
+ settings[:slaves] = additional_indexes.select { |opts, s| opts[:slave] }.map do |opts, s|
253
+ name = opts[:index_name]
254
+ name = "#{name}_#{Rails.env.to_s}" if opts[:per_environment]
255
+ name
256
+ end
257
+ settings.delete(:slaves) if settings[:slaves].empty?
258
+ settings[:replicas] = additional_indexes.select { |opts, s| opts[:replica] }.map do |opts, s|
259
+ name = opts[:index_name]
260
+ name = "#{name}_#{Rails.env.to_s}" if opts[:per_environment]
261
+ name
262
+ end
263
+ settings.delete(:replicas) if settings[:replicas].empty?
264
+ end
233
265
  settings
234
266
  end
235
267
 
236
268
  def add_index(index_name, options = {}, &block)
237
- raise ArgumentError.new('Cannot specify additional index on a slave index') if @options[:slave]
269
+ raise ArgumentError.new('Cannot specify additional index on a replica index') if @options[:slave] || @options[:replica]
238
270
  raise ArgumentError.new('No block given') if !block_given?
239
271
  raise ArgumentError.new('Options auto_index and auto_remove cannot be set on nested indexes') if options[:auto_index] || options[:auto_remove]
240
- options[:index_name] = index_name
241
272
  @additional_indexes ||= {}
242
- @additional_indexes[options] = IndexSettings.new(options, Proc.new)
273
+ raise MixedSlavesAndReplicas.new('Cannot mix slaves and replicas in the same configuration (add_slave is deprecated)') if (options[:slave] && @additional_indexes.any? { |opts, _| opts[:replica] }) || (options[:replica] && @additional_indexes.any? { |opts, _| opts[:slave] })
274
+ options[:index_name] = index_name
275
+ @additional_indexes[options] = IndexSettings.new(options, &block)
276
+ end
277
+
278
+ def add_replica(index_name, options = {}, &block)
279
+ raise ArgumentError.new('Cannot specify additional replicas on a replica index') if @options[:slave] || @options[:replica]
280
+ raise ArgumentError.new('No block given') if !block_given?
281
+ add_index(index_name, options.merge({ :replica => true, :primary_settings => self }), &block)
243
282
  end
244
283
 
245
284
  def add_slave(index_name, options = {}, &block)
246
- raise ArgumentError.new('Cannot specify additional slaves on a slave index') if @options[:slave]
285
+ raise ArgumentError.new('Cannot specify additional slaves on a slave index') if @options[:slave] || @options[:replica]
247
286
  raise ArgumentError.new('No block given') if !block_given?
248
- add_index(index_name, options.merge({ :slave => true }), &block)
287
+ add_index(index_name, options.merge({ :slave => true, :primary_settings => self }), &block)
249
288
  end
250
289
 
251
290
  def additional_indexes
@@ -271,7 +310,7 @@ module AlgoliaSearch
271
310
 
272
311
  ::Algolia::Index.instance_methods(false).each do |m|
273
312
  define_method(m) do |*args, &block|
274
- SafeIndex.log_or_throw(m) do
313
+ SafeIndex.log_or_throw(m, @raise_on_failure) do
275
314
  @index.send(m, *args, &block)
276
315
  end
277
316
  end
@@ -280,18 +319,18 @@ module AlgoliaSearch
280
319
  # special handling of wait_task to handle null task_id
281
320
  def wait_task(task_id)
282
321
  return if task_id.nil? && !@raise_on_failure # ok
283
- SafeIndex.log_or_throw(:wait_task) do
322
+ SafeIndex.log_or_throw(:wait_task, @raise_on_failure) do
284
323
  @index.wait_task(task_id)
285
324
  end
286
325
  end
287
326
 
288
327
  # special handling of get_settings to avoid raising errors on 404
289
328
  def get_settings(*args)
290
- SafeIndex.log_or_throw(:move_index) do
329
+ SafeIndex.log_or_throw(:get_settings, @raise_on_failure) do
291
330
  begin
292
331
  @index.get_settings(*args)
293
332
  rescue Algolia::AlgoliaError => e
294
- return {} if e.status == 404 # not fatal
333
+ return {} if e.code == 404 # not fatal
295
334
  raise e
296
335
  end
297
336
  end
@@ -299,17 +338,17 @@ module AlgoliaSearch
299
338
 
300
339
  # expose move as well
301
340
  def self.move_index(old_name, new_name)
302
- SafeIndex.log_or_throw(:move_index) do
341
+ SafeIndex.log_or_throw(:move_index, true) do
303
342
  ::Algolia.move_index(old_name, new_name)
304
343
  end
305
344
  end
306
345
 
307
346
  private
308
- def self.log_or_throw(method, &block)
347
+ def self.log_or_throw(method, raise_on_failure, &block)
309
348
  begin
310
349
  yield
311
350
  rescue Algolia::AlgoliaError => e
312
- raise e if @raise_on_failure
351
+ raise e if raise_on_failure
313
352
  # log the error
314
353
  (Rails.logger || Logger.new(STDOUT)).error("[algoliasearch-rails] #{e.message}")
315
354
  # return something
@@ -350,7 +389,7 @@ module AlgoliaSearch
350
389
  end
351
390
 
352
391
  def algoliasearch(options = {}, &block)
353
- self.algoliasearch_settings = IndexSettings.new(options, block_given? ? Proc.new : nil)
392
+ self.algoliasearch_settings = IndexSettings.new(options, &block)
354
393
  self.algoliasearch_options = { :type => algolia_full_const_get(model_name.to_s), :per_page => algoliasearch_settings.get_setting(:hitsPerPage) || 10, :page => 1 }.merge(options)
355
394
 
356
395
  attr_accessor :highlight_result, :snippet_result
@@ -383,7 +422,7 @@ module AlgoliaSearch
383
422
  raise ArgumentError.new("Invalid `enqueue` option: #{options[:enqueue]}")
384
423
  end
385
424
  algoliasearch_options[:enqueue] = Proc.new do |record, remove|
386
- proc.call(record, remove) unless @algolia_without_auto_index_scope
425
+ proc.call(record, remove) unless algolia_without_auto_index_scope
387
426
  end
388
427
  end
389
428
  unless options[:auto_index] == false
@@ -391,7 +430,6 @@ module AlgoliaSearch
391
430
  class_eval do
392
431
  copy_after_validation = instance_method(:after_validation)
393
432
  copy_before_save = instance_method(:before_save)
394
- copy_after_commit = instance_method(:after_commit)
395
433
 
396
434
  define_method(:after_validation) do |*args|
397
435
  super(*args)
@@ -405,10 +443,23 @@ module AlgoliaSearch
405
443
  super(*args)
406
444
  end
407
445
 
408
- define_method(:after_commit) do |*args|
409
- super(*args)
410
- copy_after_commit.bind(self).call
411
- algolia_perform_index_tasks
446
+ sequel_version = Gem::Version.new(Sequel.version)
447
+ if sequel_version >= Gem::Version.new('4.0.0') && sequel_version < Gem::Version.new('5.0.0')
448
+ copy_after_commit = instance_method(:after_commit)
449
+ define_method(:after_commit) do |*args|
450
+ super(*args)
451
+ copy_after_commit.bind(self).call
452
+ algolia_perform_index_tasks
453
+ end
454
+ else
455
+ copy_after_save = instance_method(:after_save)
456
+ define_method(:after_save) do |*args|
457
+ super(*args)
458
+ copy_after_save.bind(self).call
459
+ self.db.after_commit do
460
+ algolia_perform_index_tasks
461
+ end
462
+ end
412
463
  end
413
464
  end
414
465
  else
@@ -439,20 +490,28 @@ module AlgoliaSearch
439
490
  end
440
491
 
441
492
  def algolia_without_auto_index(&block)
442
- @algolia_without_auto_index_scope = true
493
+ self.algolia_without_auto_index_scope = true
443
494
  begin
444
495
  yield
445
496
  ensure
446
- @algolia_without_auto_index_scope = false
497
+ self.algolia_without_auto_index_scope = false
447
498
  end
448
499
  end
449
500
 
450
- def algolia_reindex!(batch_size = 1000, synchronous = false)
451
- return if @algolia_without_auto_index_scope
501
+ def algolia_without_auto_index_scope=(value)
502
+ Thread.current["algolia_without_auto_index_scope_for_#{self.model_name}"] = value
503
+ end
504
+
505
+ def algolia_without_auto_index_scope
506
+ Thread.current["algolia_without_auto_index_scope_for_#{self.model_name}"]
507
+ end
508
+
509
+ def algolia_reindex!(batch_size = AlgoliaSearch::IndexSettings::DEFAULT_BATCH_SIZE, synchronous = false)
510
+ return if algolia_without_auto_index_scope
452
511
  algolia_configurations.each do |options, settings|
453
512
  next if algolia_indexing_disabled?(options)
454
513
  index = algolia_ensure_init(options, settings)
455
- next if options[:slave]
514
+ next if options[:slave] || options[:replica]
456
515
  last_task = nil
457
516
 
458
517
  algolia_find_in_batches(batch_size) do |group|
@@ -472,36 +531,45 @@ module AlgoliaSearch
472
531
  end
473
532
  last_task = index.save_objects(objects)
474
533
  end
475
- index.wait_task(last_task["taskID"]) if last_task and synchronous == true
534
+ index.wait_task(last_task["taskID"]) if last_task and (synchronous || options[:synchronous])
476
535
  end
477
536
  nil
478
537
  end
479
538
 
480
539
  # reindex whole database using a extra temporary index + move operation
481
- def algolia_reindex(batch_size = 1000, synchronous = false)
482
- return if @algolia_without_auto_index_scope
540
+ def algolia_reindex(batch_size = AlgoliaSearch::IndexSettings::DEFAULT_BATCH_SIZE, synchronous = false)
541
+ return if algolia_without_auto_index_scope
483
542
  algolia_configurations.each do |options, settings|
484
543
  next if algolia_indexing_disabled?(options)
485
- next if options[:slave]
544
+ next if options[:slave] || options[:replica]
486
545
 
487
546
  # fetch the master settings
488
547
  master_index = algolia_ensure_init(options, settings)
489
548
  master_settings = master_index.get_settings rescue {} # if master doesn't exist yet
490
549
  master_settings.merge!(JSON.parse(settings.to_settings.to_json)) # convert symbols to strings
491
550
 
492
- # remove the slaves of the temporary index
551
+ # remove the replicas of the temporary index
493
552
  master_settings.delete :slaves
494
553
  master_settings.delete 'slaves'
554
+ master_settings.delete :replicas
555
+ master_settings.delete 'replicas'
495
556
 
496
557
  # init temporary index
497
- index_name = algolia_index_name(options)
498
- tmp_options = options.merge({ :index_name => "#{index_name}.tmp" })
558
+ src_index_name = algolia_index_name(options)
559
+ tmp_index_name = "#{src_index_name}.tmp"
560
+ tmp_options = options.merge({ :index_name => tmp_index_name })
499
561
  tmp_options.delete(:per_environment) # already included in the temporary index_name
500
562
  tmp_settings = settings.dup
501
- tmp_index = algolia_ensure_init(tmp_options, tmp_settings, master_settings)
502
563
 
503
- algolia_find_in_batches(batch_size) do |group|
504
- if algolia_conditional_index?(tmp_options)
564
+ if options[:check_settings] == false
565
+ ::Algolia::copy_index!(src_index_name, tmp_index_name, %w(settings synonyms rules))
566
+ tmp_index = SafeIndex.new(tmp_index_name, !!options[:raise_on_failure])
567
+ else
568
+ tmp_index = algolia_ensure_init(tmp_options, tmp_settings, master_settings)
569
+ end
570
+
571
+ algolia_find_in_batches(batch_size) do |group|
572
+ if algolia_conditional_index?(options)
505
573
  # select only indexable objects
506
574
  group = group.select { |o| algolia_indexable?(o, tmp_options) }
507
575
  end
@@ -509,39 +577,58 @@ module AlgoliaSearch
509
577
  tmp_index.save_objects(objects)
510
578
  end
511
579
 
512
- move_task = SafeIndex.move_index(tmp_index.name, index_name)
513
- tmp_index.wait_task(move_task["taskID"]) if synchronous == true
580
+ move_task = SafeIndex.move_index(tmp_index.name, src_index_name)
581
+ master_index.wait_task(move_task["taskID"]) if synchronous || options[:synchronous]
514
582
  end
515
583
  nil
516
584
  end
517
585
 
586
+ def algolia_set_settings(synchronous = false)
587
+ algolia_configurations.each do |options, settings|
588
+ if options[:primary_settings] && options[:inherit]
589
+ primary = options[:primary_settings].to_settings
590
+ primary.delete :slaves
591
+ primary.delete 'slaves'
592
+ primary.delete :replicas
593
+ primary.delete 'replicas'
594
+ final_settings = primary.merge(settings.to_settings)
595
+ else
596
+ final_settings = settings.to_settings
597
+ end
598
+
599
+ index = SafeIndex.new(algolia_index_name(options), true)
600
+ task = index.set_settings(final_settings)
601
+ index.wait_task(task["taskID"]) if synchronous
602
+ end
603
+ end
604
+
518
605
  def algolia_index_objects(objects, synchronous = false)
519
606
  algolia_configurations.each do |options, settings|
520
607
  next if algolia_indexing_disabled?(options)
521
608
  index = algolia_ensure_init(options, settings)
522
- next if options[:slave]
609
+ next if options[:slave] || options[:replica]
523
610
  task = index.save_objects(objects.map { |o| settings.get_attributes(o).merge 'objectID' => algolia_object_id_of(o, options) })
524
- index.wait_task(task["taskID"]) if synchronous == true
611
+ index.wait_task(task["taskID"]) if synchronous || options[:synchronous]
525
612
  end
526
613
  end
527
614
 
528
615
  def algolia_index!(object, synchronous = false)
529
- return if @algolia_without_auto_index_scope
616
+ return if algolia_without_auto_index_scope
530
617
  algolia_configurations.each do |options, settings|
531
618
  next if algolia_indexing_disabled?(options)
532
619
  object_id = algolia_object_id_of(object, options)
533
620
  index = algolia_ensure_init(options, settings)
534
- next if options[:slave]
621
+ next if options[:slave] || options[:replica]
535
622
  if algolia_indexable?(object, options)
536
623
  raise ArgumentError.new("Cannot index a record with a blank objectID") if object_id.blank?
537
- if synchronous
624
+ if synchronous || options[:synchronous]
538
625
  index.add_object!(settings.get_attributes(object), object_id)
539
626
  else
540
627
  index.add_object(settings.get_attributes(object), object_id)
541
628
  end
542
629
  elsif algolia_conditional_index?(options) && !object_id.blank?
543
630
  # remove non-indexable objects
544
- if synchronous
631
+ if synchronous || options[:synchronous]
545
632
  index.delete_object!(object_id)
546
633
  else
547
634
  index.delete_object(object_id)
@@ -552,14 +639,14 @@ module AlgoliaSearch
552
639
  end
553
640
 
554
641
  def algolia_remove_from_index!(object, synchronous = false)
555
- return if @algolia_without_auto_index_scope
642
+ return if algolia_without_auto_index_scope
556
643
  object_id = algolia_object_id_of(object)
557
644
  raise ArgumentError.new("Cannot index a record with a blank objectID") if object_id.blank?
558
645
  algolia_configurations.each do |options, settings|
559
646
  next if algolia_indexing_disabled?(options)
560
647
  index = algolia_ensure_init(options, settings)
561
- next if options[:slave]
562
- if synchronous
648
+ next if options[:slave] || options[:replica]
649
+ if synchronous || options[:synchronous]
563
650
  index.delete_object!(object_id)
564
651
  else
565
652
  index.delete_object(object_id)
@@ -572,15 +659,20 @@ module AlgoliaSearch
572
659
  algolia_configurations.each do |options, settings|
573
660
  next if algolia_indexing_disabled?(options)
574
661
  index = algolia_ensure_init(options, settings)
575
- next if options[:slave]
576
- synchronous ? index.clear! : index.clear
662
+ next if options[:slave] || options[:replica]
663
+ synchronous || options[:synchronous] ? index.clear! : index.clear
577
664
  @algolia_indexes[settings] = nil
578
665
  end
579
666
  nil
580
667
  end
581
668
 
582
669
  def algolia_raw_search(q, params = {})
583
- index_name = params.delete(:index) || params.delete('index') || params.delete(:slave) || params.delete('slave')
670
+ index_name = params.delete(:index) ||
671
+ params.delete('index') ||
672
+ params.delete(:slave) ||
673
+ params.delete('slave') ||
674
+ params.delete(:replica) ||
675
+ params.delete('replica')
584
676
  index = algolia_index(index_name)
585
677
  index.search(q, Hash[params.map { |k,v| [k.to_s, v.to_s] }])
586
678
  end
@@ -624,7 +716,7 @@ module AlgoliaSearch
624
716
  algolia_object_id_of(hit)
625
717
  end
626
718
  results = json['hits'].map do |hit|
627
- o = results_by_id[hit['objectID']]
719
+ o = results_by_id[hit['objectID'].to_s]
628
720
  if o
629
721
  o.highlight_result = hit['_highlightResult']
630
722
  o.snippet_result = hit['_snippetResult']
@@ -632,16 +724,21 @@ module AlgoliaSearch
632
724
  end
633
725
  end.compact
634
726
  # Algolia has a default limit of 1000 retrievable hits
635
- total_hits = json['nbHits'] < json['nbPages'] * json['hitsPerPage'] ?
636
- json['nbHits'] : json['nbPages'] * json['hitsPerPage']
637
- res = AlgoliaSearch::Pagination.create(results, total_hits, algoliasearch_options.merge({ :page => json['page'] + 1, :per_page => json['hitsPerPage'] }))
727
+ total_hits = json['nbHits'].to_i < json['nbPages'].to_i * json['hitsPerPage'].to_i ?
728
+ json['nbHits'].to_i: json['nbPages'].to_i * json['hitsPerPage'].to_i
729
+ res = AlgoliaSearch::Pagination.create(results, total_hits, algoliasearch_options.merge({ :page => json['page'].to_i + 1, :per_page => json['hitsPerPage'] }))
638
730
  res.extend(AdditionalMethods)
639
731
  res.send(:algolia_init_raw_answer, json)
640
732
  res
641
733
  end
642
734
 
643
735
  def algolia_search_for_facet_values(facet, text, params = {})
644
- index_name = params.delete(:index) || params.delete('index') || params.delete(:slave) || params.delete('slave')
736
+ index_name = params.delete(:index) ||
737
+ params.delete('index') ||
738
+ params.delete(:slave) ||
739
+ params.delete('slave') ||
740
+ params.delete(:replica) ||
741
+ params.delete('replicas')
645
742
  index = algolia_index(index_name)
646
743
  query = Hash[params.map { |k, v| [k.to_s, v.to_s] }]
647
744
  index.search_facet(facet, text, query)['facetHits']
@@ -655,7 +752,7 @@ module AlgoliaSearch
655
752
  algolia_configurations.each do |o, s|
656
753
  return algolia_ensure_init(o, s) if o[:index_name].to_s == name.to_s
657
754
  end
658
- raise ArgumentError.new("Invalid index/slave name: #{name}")
755
+ raise ArgumentError.new("Invalid index/replica name: #{name}")
659
756
  end
660
757
  algolia_ensure_init
661
758
  end
@@ -668,23 +765,31 @@ module AlgoliaSearch
668
765
  end
669
766
 
670
767
  def algolia_must_reindex?(object)
768
+ # use +algolia_dirty?+ method if implemented
769
+ return object.send(:algolia_dirty?) if (object.respond_to?(:algolia_dirty?))
770
+ # Loop over each index to see if a attribute used in records has changed
671
771
  algolia_configurations.each do |options, settings|
672
- next if options[:slave]
772
+ next if algolia_indexing_disabled?(options)
773
+ next if options[:slave] || options[:replica]
673
774
  return true if algolia_object_id_changed?(object, options)
674
775
  settings.get_attribute_names(object).each do |k|
675
- changed_method = "#{k}_changed?"
676
- return true if !object.respond_to?(changed_method) || object.send(changed_method)
776
+ return true if algolia_attribute_changed?(object, k)
777
+ # return true if !object.respond_to?(changed_method) || object.send(changed_method)
677
778
  end
678
779
  [options[:if], options[:unless]].each do |condition|
679
780
  case condition
680
781
  when nil
681
782
  when String, Symbol
682
- changed_method = "#{condition}_changed?"
683
- return true if !object.respond_to?(changed_method) || object.send(changed_method)
783
+ return true if algolia_attribute_changed?(object, condition)
684
784
  else
785
+ # if the :if, :unless condition is a anything else,
786
+ # we have no idea whether we should reindex or not
787
+ # let's always reindex then
788
+ return true
685
789
  end
686
790
  end
687
791
  end
792
+ # By default, we don't reindex
688
793
  return false
689
794
  end
690
795
 
@@ -692,16 +797,33 @@ module AlgoliaSearch
692
797
 
693
798
  def algolia_ensure_init(options = nil, settings = nil, index_settings = nil)
694
799
  raise ArgumentError.new('No `algoliasearch` block found in your model.') if algoliasearch_settings.nil?
800
+
695
801
  @algolia_indexes ||= {}
802
+
696
803
  options ||= algoliasearch_options
697
804
  settings ||= algoliasearch_settings
805
+
698
806
  return @algolia_indexes[settings] if @algolia_indexes[settings]
807
+
699
808
  @algolia_indexes[settings] = SafeIndex.new(algolia_index_name(options), algoliasearch_options[:raise_on_failure])
700
- current_settings = @algolia_indexes[settings].get_settings rescue nil # if the index doesn't exist
701
- if !algolia_indexing_disabled?(options) && (index_settings || algoliasearch_settings_changed?(current_settings, settings.to_settings))
702
- index_settings ||= settings.to_settings
809
+
810
+ current_settings = @algolia_indexes[settings].get_settings(:getVersion => 1) rescue nil # if the index doesn't exist
811
+
812
+ index_settings ||= settings.to_settings
813
+ index_settings = options[:primary_settings].to_settings.merge(index_settings) if options[:inherit]
814
+
815
+ options[:check_settings] = true if options[:check_settings].nil?
816
+
817
+ if !algolia_indexing_disabled?(options) && options[:check_settings] && algoliasearch_settings_changed?(current_settings, index_settings)
818
+ used_slaves = !current_settings.nil? && !current_settings['slaves'].nil?
819
+ replicas = index_settings.delete(:replicas) ||
820
+ index_settings.delete('replicas') ||
821
+ index_settings.delete(:slaves) ||
822
+ index_settings.delete('slaves')
823
+ index_settings[used_slaves ? :slaves : :replicas] = replicas unless replicas.nil? || options[:inherit]
703
824
  @algolia_indexes[settings].set_settings(index_settings)
704
825
  end
826
+
705
827
  @algolia_indexes[settings]
706
828
  end
707
829
 
@@ -735,8 +857,8 @@ module AlgoliaSearch
735
857
  end
736
858
 
737
859
  def algolia_object_id_changed?(o, options = nil)
738
- m = "#{algolia_object_id_method(options)}_changed?"
739
- o.respond_to?(m) ? o.send(m) : false
860
+ changed = algolia_attribute_changed?(o, algolia_object_id_method(options))
861
+ changed.nil? ? false : changed
740
862
  end
741
863
 
742
864
  def algoliasearch_settings_changed?(prev, current)
@@ -829,6 +951,50 @@ module AlgoliaSearch
829
951
  yield items unless items.empty?
830
952
  end
831
953
  end
954
+
955
+ def algolia_attribute_changed?(object, attr_name)
956
+ # if one of two method is implemented, we return its result
957
+ # true/false means whether it has changed or not
958
+ # +#{attr_name}_changed?+ always defined for automatic attributes but deprecated after Rails 5.2
959
+ # +will_save_change_to_#{attr_name}?+ should be use instead for Rails 5.2+, also defined for automatic attributes.
960
+ # If none of the method are defined, it's a dynamic attribute
961
+
962
+ method_name = "#{attr_name}_changed?"
963
+ if object.respond_to?(method_name)
964
+ # If +#{attr_name}_changed?+ respond we want to see if the method is user defined or if it's automatically
965
+ # defined by Rails.
966
+ # If it's user-defined, we call it.
967
+ # If it's automatic we check ActiveRecord version to see if this method is deprecated
968
+ # and try to call +will_save_change_to_#{attr_name}?+ instead.
969
+ # See: https://github.com/algolia/algoliasearch-rails/pull/338
970
+ # This feature is not compatible with Ruby 1.8
971
+ # In this case, we always call #{attr_name}_changed?
972
+ if Object.const_defined?(:RUBY_VERSION) && RUBY_VERSION.to_f < 1.9
973
+ return object.send(method_name)
974
+ end
975
+ unless automatic_changed_method?(object, method_name) && automatic_changed_method_deprecated?
976
+ return object.send(method_name)
977
+ end
978
+ end
979
+
980
+ if object.respond_to?("will_save_change_to_#{attr_name}?")
981
+ return object.send("will_save_change_to_#{attr_name}?")
982
+ end
983
+
984
+ # We don't know if the attribute has changed, so conservatively assume it has
985
+ true
986
+ end
987
+
988
+ def automatic_changed_method?(object, method_name)
989
+ raise ArgumentError.new("Method #{method_name} doesn't exist on #{object.class.name}") unless object.respond_to?(method_name)
990
+ file = object.method(method_name).source_location[0]
991
+ file.end_with?("active_model/attribute_methods.rb")
992
+ end
993
+
994
+ def automatic_changed_method_deprecated?
995
+ (defined?(::ActiveRecord) && ActiveRecord::VERSION::MAJOR >= 5 && ActiveRecord::VERSION::MINOR >= 1) ||
996
+ (defined?(::ActiveRecord) && ActiveRecord::VERSION::MAJOR > 5)
997
+ end
832
998
  end
833
999
 
834
1000
  # these are the instance methods included
@@ -851,15 +1017,15 @@ module AlgoliaSearch
851
1017
 
852
1018
  def algolia_enqueue_remove_from_index!(synchronous)
853
1019
  if algoliasearch_options[:enqueue]
854
- algoliasearch_options[:enqueue].call(self, true)
1020
+ algoliasearch_options[:enqueue].call(self, true) unless self.class.send(:algolia_indexing_disabled?, algoliasearch_options)
855
1021
  else
856
- algolia_remove_from_index!(synchronous)
1022
+ algolia_remove_from_index!(synchronous || algolia_synchronous?)
857
1023
  end
858
1024
  end
859
1025
 
860
1026
  def algolia_enqueue_index!(synchronous)
861
1027
  if algoliasearch_options[:enqueue]
862
- algoliasearch_options[:enqueue].call(self, false)
1028
+ algoliasearch_options[:enqueue].call(self, false) unless self.class.send(:algolia_indexing_disabled?, algoliasearch_options)
863
1029
  else
864
1030
  algolia_index!(synchronous)
865
1031
  end
@@ -880,7 +1046,9 @@ module AlgoliaSearch
880
1046
  end
881
1047
 
882
1048
  def algolia_mark_must_reindex
883
- @algolia_must_reindex =
1049
+ # algolia_must_reindex flag is reset after every commit as part. If we must reindex at any point in
1050
+ # a stransaction, keep flag set until it is explicitly unset
1051
+ @algolia_must_reindex ||=
884
1052
  if defined?(::Sequel) && is_a?(Sequel::Model)
885
1053
  new? || self.class.algolia_must_reindex?(self)
886
1054
  else