algoliasearch-rails 1.19.1 → 2.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- require 'algoliasearch'
1
+ require 'algolia'
10
2
 
3
+ require 'algoliasearch/version'
11
4
  require 'algoliasearch/utilities'
12
5
 
13
6
  if defined? Rails
@@ -54,33 +47,60 @@ module AlgoliaSearch
54
47
  end
55
48
 
56
49
  class IndexSettings
50
+ DEFAULT_BATCH_SIZE = 1000
57
51
 
58
52
  # AlgoliaSearch settings
59
- OPTIONS = [:minWordSizefor1Typo, :minWordSizefor2Typos, :typoTolerance,
60
- :hitsPerPage, :attributesToRetrieve,
61
- :attributesToHighlight, :attributesToSnippet, :attributesToIndex, :searchableAttributes,
62
- :highlightPreTag, :highlightPostTag,
63
- :ranking, :customRanking, :queryType, :attributesForFaceting,
64
- :separatorsToIndex, :optionalWords, :attributeForDistinct,
65
- :synonyms, :placeholders, :removeWordsIfNoResults, :replaceSynonymsInHighlight,
66
- :unretrievableAttributes, :disableTypoToleranceOnWords, :disableTypoToleranceOnAttributes, :altCorrections,
67
- :ignorePlurals, :maxValuesPerFacet, :distinct, :numericAttributesToIndex, :numericAttributesForFiltering,
68
- :allowTyposOnNumericTokens, :allowCompressionOfIntegerArray,
69
- :advancedSyntax, :disablePrefixOnAttributes, :disableTypoToleranceOnAttributes]
53
+ OPTIONS = [
54
+ # Attributes
55
+ :searchableAttributes, :attributesForFaceting, :unretrievableAttributes, :attributesToRetrieve,
56
+ # Ranking
57
+ :ranking, :customRanking, :relevancyStrictness, # Replicas are handled via `add_replica`
58
+ # Faceting
59
+ :maxValuesPerFacet, :sortFacetValuesBy,
60
+ # Highlighting / Snippeting
61
+ :attributesToHighlight, :attributesToSnippet, :highlightPreTag, :highlightPostTag,
62
+ :snippetEllipsisText, :restrictHighlightAndSnippetArrays,
63
+ # Pagination
64
+ :hitsPerPage, :paginationLimitedTo,
65
+ # Typo
66
+ :minWordSizefor1Typo, :minWordSizefor2Typos, :typoTolerance, :allowTyposOnNumericTokens,
67
+ :disableTypoToleranceOnAttributes, :disableTypoToleranceOnWords, :separatorsToIndex,
68
+ # Language
69
+ :ignorePlurals, :removeStopWords, :camelCaseAttributes, :decompoundedAttributes,
70
+ :keepDiacriticsOnCharacters, :queryLanguages, :indexLanguages,
71
+ # Query Rules
72
+ :enableRules,
73
+ # Query Strategy
74
+ :queryType, :removeWordsIfNoResults, :advancedSyntax, :optionalWords,
75
+ :disablePrefixOnAttributes, :disableExactOnAttributes, :exactOnSingleWordQuery, :alternativesAsExact,
76
+ # Performance
77
+ :numericAttributesForFiltering, :allowCompressionOfIntegerArray,
78
+ # Advanced
79
+ :attributeForDistinct, :distinct, :replaceSynonymsInHighlight, :minProximity, :responseFields,
80
+ :maxFacetHits,
81
+
82
+ # Rails-specific
83
+ :synonyms, :placeholders, :altCorrections,
84
+ ]
70
85
  OPTIONS.each do |k|
71
86
  define_method k do |v|
72
87
  instance_variable_set("@#{k}", v)
73
88
  end
74
89
  end
75
90
 
76
- def initialize(options, block)
91
+ def initialize(options, &block)
77
92
  @options = options
78
- instance_exec(&block) if block
93
+ instance_exec(&block) if block_given?
94
+ end
95
+
96
+ def use_serializer(serializer)
97
+ @serializer = serializer
98
+ # instance_variable_set("@serializer", serializer)
79
99
  end
80
100
 
81
101
  def attribute(*names, &block)
82
102
  raise ArgumentError.new('Cannot pass multiple attribute names if block given') if block_given? and names.length > 1
83
- raise ArgumentError.new('Cannot specify additional attributes on a replica index') if @options[:slave] || @options[:replica]
103
+ raise ArgumentError.new('Cannot specify additional attributes on a replica index') if @options[:replica]
84
104
  @attributes ||= {}
85
105
  names.flatten.each do |name|
86
106
  @attributes[name.to_s] = block_given? ? Proc.new { |o| o.instance_eval(&block) } : Proc.new { |o| o.send(name) }
@@ -90,7 +110,7 @@ module AlgoliaSearch
90
110
 
91
111
  def add_attribute(*names, &block)
92
112
  raise ArgumentError.new('Cannot pass multiple attribute names if block given') if block_given? and names.length > 1
93
- raise ArgumentError.new('Cannot specify additional attributes on a replica index') if @options[:slave] || @options[:replica]
113
+ raise ArgumentError.new('Cannot specify additional attributes on a replica index') if @options[:replica]
94
114
  @additional_attributes ||= {}
95
115
  names.each do |name|
96
116
  @additional_attributes[name.to_s] = block_given? ? Proc.new { |o| o.instance_eval(&block) } : Proc.new { |o| o.send(name) }
@@ -103,7 +123,7 @@ module AlgoliaSearch
103
123
  end
104
124
 
105
125
  def is_sequel?(object)
106
- defined?(::Sequel) && object.class < ::Sequel::Model
126
+ defined?(::Sequel) && defined?(::Sequel::Model) && object.class < ::Sequel::Model
107
127
  end
108
128
 
109
129
  def is_active_record?(object)
@@ -124,15 +144,7 @@ module AlgoliaSearch
124
144
  end
125
145
 
126
146
  def get_attribute_names(object)
127
- res = if @attributes.nil? || @attributes.length == 0
128
- get_default_attributes(object).keys
129
- else
130
- @attributes.keys
131
- end
132
-
133
- res += @additional_attributes.keys if @additional_attributes
134
-
135
- res
147
+ get_attributes(object).keys
136
148
  end
137
149
 
138
150
  def attributes_to_hash(attributes, object)
@@ -144,19 +156,27 @@ module AlgoliaSearch
144
156
  end
145
157
 
146
158
  def get_attributes(object)
147
- attributes = if @attributes.nil? || @attributes.length == 0
148
- get_default_attributes(object)
159
+ # If a serializer is set, we ignore attributes
160
+ # everything should be done via the serializer
161
+ if not @serializer.nil?
162
+ attributes = @serializer.new(object).attributes
149
163
  else
150
- if is_active_record?(object)
151
- object.class.unscoped do
152
- attributes_to_hash(@attributes, object)
153
- end
164
+ if @attributes.nil? || @attributes.length == 0
165
+ # no `attribute ...` have been configured, use the default attributes of the model
166
+ attributes = get_default_attributes(object)
154
167
  else
155
- attributes_to_hash(@attributes, object)
168
+ # at least 1 `attribute ...` has been configured, therefore use ONLY the one configured
169
+ if is_active_record?(object)
170
+ object.class.unscoped do
171
+ attributes = attributes_to_hash(@attributes, object)
172
+ end
173
+ else
174
+ attributes = attributes_to_hash(@attributes, object)
175
+ end
156
176
  end
157
177
  end
158
178
 
159
- attributes.merge!(attributes_to_hash(@additional_attributes, object))
179
+ attributes.merge!(attributes_to_hash(@additional_attributes, object)) if @additional_attributes
160
180
 
161
181
  if @options[:sanitize]
162
182
  sanitizer = begin
@@ -191,7 +211,7 @@ module AlgoliaSearch
191
211
  def encode_attributes(v)
192
212
  case v
193
213
  when String
194
- v.force_encoding('utf-8')
214
+ v.dup.force_encoding('utf-8')
195
215
  when Hash
196
216
  v.each { |key, value| v[key] = encode_attributes(value) }
197
217
  when Array
@@ -201,15 +221,15 @@ module AlgoliaSearch
201
221
  end
202
222
  end
203
223
 
204
- def geoloc(lat_attr, lng_attr)
205
- raise ArgumentError.new('Cannot specify additional attributes on a replica index') if @options[:slave] || @options[:replica]
224
+ def geoloc(lat_attr = nil, lng_attr = nil, &block)
225
+ raise ArgumentError.new('Cannot specify additional attributes on a replica index') if @options[:replica]
206
226
  add_attribute :_geoloc do |o|
207
- { :lat => o.send(lat_attr).to_f, :lng => o.send(lng_attr).to_f }
227
+ block_given? ? o.instance_eval(&block) : { :lat => o.send(lat_attr).to_f, :lng => o.send(lng_attr).to_f }
208
228
  end
209
229
  end
210
230
 
211
231
  def tags(*args, &block)
212
- raise ArgumentError.new('Cannot specify additional attributes on a replica index') if @options[:slave] || @options[:replica]
232
+ raise ArgumentError.new('Cannot specify additional attributes on a replica index') if @options[:replica]
213
233
  add_attribute :_tags do |o|
214
234
  v = block_given? ? o.instance_eval(&block) : args
215
235
  v.is_a?(Array) ? v : [v]
@@ -226,16 +246,12 @@ module AlgoliaSearch
226
246
  v = get_setting(k)
227
247
  settings[k] = v if !v.nil?
228
248
  end
229
- if !@options[:slave] && !@options[:replica]
230
- settings[:slaves] = additional_indexes.select { |opts, s| opts[:slave] }.map do |opts, s|
231
- name = opts[:index_name]
232
- name = "#{name}_#{Rails.env.to_s}" if opts[:per_environment]
233
- name
234
- end
235
- settings.delete(:slaves) if settings[:slaves].empty?
249
+
250
+ if !@options[:replica]
236
251
  settings[:replicas] = additional_indexes.select { |opts, s| opts[:replica] }.map do |opts, s|
237
252
  name = opts[:index_name]
238
253
  name = "#{name}_#{Rails.env.to_s}" if opts[:per_environment]
254
+ name = "virtual(#{name})" if opts[:virtual]
239
255
  name
240
256
  end
241
257
  settings.delete(:replicas) if settings[:replicas].empty?
@@ -244,27 +260,20 @@ module AlgoliaSearch
244
260
  end
245
261
 
246
262
  def add_index(index_name, options = {}, &block)
247
- raise ArgumentError.new('Cannot specify additional index on a replica index') if @options[:slave] || @options[:replica]
263
+ raise ArgumentError.new('Cannot specify additional index on a replica index') if @options[:replica]
248
264
  raise ArgumentError.new('No block given') if !block_given?
249
265
  raise ArgumentError.new('Options auto_index and auto_remove cannot be set on nested indexes') if options[:auto_index] || options[:auto_remove]
250
266
  @additional_indexes ||= {}
251
- 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] })
252
267
  options[:index_name] = index_name
253
- @additional_indexes[options] = IndexSettings.new(options, Proc.new)
268
+ @additional_indexes[options] = IndexSettings.new(options, &block)
254
269
  end
255
270
 
256
271
  def add_replica(index_name, options = {}, &block)
257
- raise ArgumentError.new('Cannot specify additional replicas on a replica index') if @options[:slave] || @options[:replica]
272
+ raise ArgumentError.new('Cannot specify additional replicas on a replica index') if @options[:replica]
258
273
  raise ArgumentError.new('No block given') if !block_given?
259
274
  add_index(index_name, options.merge({ :replica => true, :primary_settings => self }), &block)
260
275
  end
261
276
 
262
- def add_slave(index_name, options = {}, &block)
263
- raise ArgumentError.new('Cannot specify additional slaves on a slave index') if @options[:slave] || @options[:replica]
264
- raise ArgumentError.new('No block given') if !block_given?
265
- add_index(index_name, options.merge({ :slave => true, :primary_settings => self }), &block)
266
- end
267
-
268
277
  def additional_indexes
269
278
  @additional_indexes || {}
270
279
  end
@@ -282,11 +291,11 @@ module AlgoliaSearch
282
291
  # are correctly logged or thrown depending on the `raise_on_failure` option
283
292
  class SafeIndex
284
293
  def initialize(name, raise_on_failure)
285
- @index = ::Algolia::Index.new(name)
294
+ @index = AlgoliaSearch.client.init_index(name)
286
295
  @raise_on_failure = raise_on_failure.nil? || raise_on_failure
287
296
  end
288
297
 
289
- ::Algolia::Index.instance_methods(false).each do |m|
298
+ ::Algolia::Search::Index.instance_methods(false).each do |m|
290
299
  define_method(m) do |*args, &block|
291
300
  SafeIndex.log_or_throw(m, @raise_on_failure) do
292
301
  @index.send(m, *args, &block)
@@ -307,7 +316,7 @@ module AlgoliaSearch
307
316
  SafeIndex.log_or_throw(:get_settings, @raise_on_failure) do
308
317
  begin
309
318
  @index.get_settings(*args)
310
- rescue Algolia::AlgoliaError => e
319
+ rescue Algolia::AlgoliaHttpError => e
311
320
  return {} if e.code == 404 # not fatal
312
321
  raise e
313
322
  end
@@ -317,7 +326,7 @@ module AlgoliaSearch
317
326
  # expose move as well
318
327
  def self.move_index(old_name, new_name)
319
328
  SafeIndex.log_or_throw(:move_index, true) do
320
- ::Algolia.move_index(old_name, new_name)
329
+ AlgoliaSearch.client.move_index(old_name, new_name)
321
330
  end
322
331
  end
323
332
 
@@ -367,13 +376,13 @@ module AlgoliaSearch
367
376
  end
368
377
 
369
378
  def algoliasearch(options = {}, &block)
370
- self.algoliasearch_settings = IndexSettings.new(options, block_given? ? Proc.new : nil)
379
+ self.algoliasearch_settings = IndexSettings.new(options, &block)
371
380
  self.algoliasearch_options = { :type => algolia_full_const_get(model_name.to_s), :per_page => algoliasearch_settings.get_setting(:hitsPerPage) || 10, :page => 1 }.merge(options)
372
381
 
373
382
  attr_accessor :highlight_result, :snippet_result
374
383
 
375
384
  if options[:synchronous] == true
376
- if defined?(::Sequel) && self < Sequel::Model
385
+ if defined?(::Sequel) && defined?(::Sequel::Model) && self < Sequel::Model
377
386
  class_eval do
378
387
  copy_after_validation = instance_method(:after_validation)
379
388
  define_method(:after_validation) do |*args|
@@ -400,15 +409,14 @@ module AlgoliaSearch
400
409
  raise ArgumentError.new("Invalid `enqueue` option: #{options[:enqueue]}")
401
410
  end
402
411
  algoliasearch_options[:enqueue] = Proc.new do |record, remove|
403
- proc.call(record, remove) unless @algolia_without_auto_index_scope
412
+ proc.call(record, remove) unless algolia_without_auto_index_scope
404
413
  end
405
414
  end
406
415
  unless options[:auto_index] == false
407
- if defined?(::Sequel) && self < Sequel::Model
416
+ if defined?(::Sequel) && defined?(::Sequel::Model) && self < Sequel::Model
408
417
  class_eval do
409
418
  copy_after_validation = instance_method(:after_validation)
410
419
  copy_before_save = instance_method(:before_save)
411
- copy_after_commit = instance_method(:after_commit)
412
420
 
413
421
  define_method(:after_validation) do |*args|
414
422
  super(*args)
@@ -422,10 +430,23 @@ module AlgoliaSearch
422
430
  super(*args)
423
431
  end
424
432
 
425
- define_method(:after_commit) do |*args|
426
- super(*args)
427
- copy_after_commit.bind(self).call
428
- algolia_perform_index_tasks
433
+ sequel_version = Gem::Version.new(Sequel.version)
434
+ if sequel_version >= Gem::Version.new('4.0.0') && sequel_version < Gem::Version.new('5.0.0')
435
+ copy_after_commit = instance_method(:after_commit)
436
+ define_method(:after_commit) do |*args|
437
+ super(*args)
438
+ copy_after_commit.bind(self).call
439
+ algolia_perform_index_tasks
440
+ end
441
+ else
442
+ copy_after_save = instance_method(:after_save)
443
+ define_method(:after_save) do |*args|
444
+ super(*args)
445
+ copy_after_save.bind(self).call
446
+ self.db.after_commit do
447
+ algolia_perform_index_tasks
448
+ end
449
+ end
429
450
  end
430
451
  end
431
452
  else
@@ -439,7 +460,7 @@ module AlgoliaSearch
439
460
  end
440
461
  end
441
462
  unless options[:auto_remove] == false
442
- if defined?(::Sequel) && self < Sequel::Model
463
+ if defined?(::Sequel) && defined?(::Sequel::Model) && self < Sequel::Model
443
464
  class_eval do
444
465
  copy_after_destroy = instance_method(:after_destroy)
445
466
 
@@ -456,20 +477,28 @@ module AlgoliaSearch
456
477
  end
457
478
 
458
479
  def algolia_without_auto_index(&block)
459
- @algolia_without_auto_index_scope = true
480
+ self.algolia_without_auto_index_scope = true
460
481
  begin
461
482
  yield
462
483
  ensure
463
- @algolia_without_auto_index_scope = false
484
+ self.algolia_without_auto_index_scope = false
464
485
  end
465
486
  end
466
487
 
467
- def algolia_reindex!(batch_size = 1000, synchronous = false)
468
- return if @algolia_without_auto_index_scope
488
+ def algolia_without_auto_index_scope=(value)
489
+ Thread.current["algolia_without_auto_index_scope_for_#{self.model_name}"] = value
490
+ end
491
+
492
+ def algolia_without_auto_index_scope
493
+ Thread.current["algolia_without_auto_index_scope_for_#{self.model_name}"]
494
+ end
495
+
496
+ def algolia_reindex!(batch_size = AlgoliaSearch::IndexSettings::DEFAULT_BATCH_SIZE, synchronous = false)
497
+ return if algolia_without_auto_index_scope
469
498
  algolia_configurations.each do |options, settings|
470
499
  next if algolia_indexing_disabled?(options)
471
500
  index = algolia_ensure_init(options, settings)
472
- next if options[:slave] || options[:replica]
501
+ next if options[:replica]
473
502
  last_task = nil
474
503
 
475
504
  algolia_find_in_batches(batch_size) do |group|
@@ -489,38 +518,44 @@ module AlgoliaSearch
489
518
  end
490
519
  last_task = index.save_objects(objects)
491
520
  end
492
- index.wait_task(last_task["taskID"]) if last_task and (synchronous || options[:synchronous])
521
+ index.wait_task(last_task.raw_response["taskID"]) if last_task and (synchronous || options[:synchronous])
493
522
  end
494
523
  nil
495
524
  end
496
525
 
497
526
  # reindex whole database using a extra temporary index + move operation
498
- def algolia_reindex(batch_size = 1000, synchronous = false)
499
- return if @algolia_without_auto_index_scope
527
+ def algolia_reindex(batch_size = AlgoliaSearch::IndexSettings::DEFAULT_BATCH_SIZE, synchronous = false)
528
+ return if algolia_without_auto_index_scope
500
529
  algolia_configurations.each do |options, settings|
501
530
  next if algolia_indexing_disabled?(options)
502
- next if options[:slave] || options[:replica]
531
+ next if options[:replica]
503
532
 
504
533
  # fetch the master settings
505
534
  master_index = algolia_ensure_init(options, settings)
506
535
  master_settings = master_index.get_settings rescue {} # if master doesn't exist yet
536
+ master_exists = master_settings != {}
507
537
  master_settings.merge!(JSON.parse(settings.to_settings.to_json)) # convert symbols to strings
508
538
 
509
539
  # remove the replicas of the temporary index
510
- master_settings.delete :slaves
511
- master_settings.delete 'slaves'
512
540
  master_settings.delete :replicas
513
541
  master_settings.delete 'replicas'
514
542
 
515
543
  # init temporary index
516
- index_name = algolia_index_name(options)
517
- tmp_options = options.merge({ :index_name => "#{index_name}.tmp" })
544
+ src_index_name = algolia_index_name(options)
545
+ tmp_index_name = "#{src_index_name}.tmp"
546
+ tmp_options = options.merge({ :index_name => tmp_index_name })
518
547
  tmp_options.delete(:per_environment) # already included in the temporary index_name
519
548
  tmp_settings = settings.dup
520
- tmp_index = algolia_ensure_init(tmp_options, tmp_settings, master_settings)
549
+
550
+ if options[:check_settings] == false && master_exists
551
+ AlgoliaSearch.client.copy_index!(src_index_name, tmp_index_name, { scope: %w[settings synonyms rules] })
552
+ tmp_index = SafeIndex.new(tmp_index_name, !!options[:raise_on_failure])
553
+ else
554
+ tmp_index = algolia_ensure_init(tmp_options, tmp_settings, master_settings)
555
+ end
521
556
 
522
557
  algolia_find_in_batches(batch_size) do |group|
523
- if algolia_conditional_index?(tmp_options)
558
+ if algolia_conditional_index?(options)
524
559
  # select only indexable objects
525
560
  group = group.select { |o| algolia_indexable?(o, tmp_options) }
526
561
  end
@@ -528,35 +563,52 @@ module AlgoliaSearch
528
563
  tmp_index.save_objects(objects)
529
564
  end
530
565
 
531
- move_task = SafeIndex.move_index(tmp_index.name, index_name)
532
- master_index.wait_task(move_task["taskID"]) if synchronous || options[:synchronous]
566
+ move_task = SafeIndex.move_index(tmp_index.name, src_index_name)
567
+ master_index.wait_task(move_task.raw_response["taskID"]) if synchronous || options[:synchronous]
533
568
  end
534
569
  nil
535
570
  end
536
571
 
572
+ def algolia_set_settings(synchronous = false)
573
+ algolia_configurations.each do |options, settings|
574
+ if options[:primary_settings] && options[:inherit]
575
+ primary = options[:primary_settings].to_settings
576
+ primary.delete :replicas
577
+ primary.delete 'replicas'
578
+ final_settings = primary.merge(settings.to_settings)
579
+ else
580
+ final_settings = settings.to_settings
581
+ end
582
+
583
+ index = SafeIndex.new(algolia_index_name(options), true)
584
+ task = index.set_settings(final_settings)
585
+ index.wait_task(task.raw_response["taskID"]) if synchronous
586
+ end
587
+ end
588
+
537
589
  def algolia_index_objects(objects, synchronous = false)
538
590
  algolia_configurations.each do |options, settings|
539
591
  next if algolia_indexing_disabled?(options)
540
592
  index = algolia_ensure_init(options, settings)
541
- next if options[:slave] || options[:replica]
593
+ next if options[:replica]
542
594
  task = index.save_objects(objects.map { |o| settings.get_attributes(o).merge 'objectID' => algolia_object_id_of(o, options) })
543
- index.wait_task(task["taskID"]) if synchronous || options[:synchronous]
595
+ index.wait_task(task.raw_response["taskID"]) if synchronous || options[:synchronous]
544
596
  end
545
597
  end
546
598
 
547
599
  def algolia_index!(object, synchronous = false)
548
- return if @algolia_without_auto_index_scope
600
+ return if algolia_without_auto_index_scope
549
601
  algolia_configurations.each do |options, settings|
550
602
  next if algolia_indexing_disabled?(options)
551
603
  object_id = algolia_object_id_of(object, options)
552
604
  index = algolia_ensure_init(options, settings)
553
- next if options[:slave] || options[:replica]
605
+ next if options[:replica]
554
606
  if algolia_indexable?(object, options)
555
607
  raise ArgumentError.new("Cannot index a record with a blank objectID") if object_id.blank?
556
608
  if synchronous || options[:synchronous]
557
- index.add_object!(settings.get_attributes(object), object_id)
609
+ index.save_object!(settings.get_attributes(object).merge 'objectID' => algolia_object_id_of(object, options))
558
610
  else
559
- index.add_object(settings.get_attributes(object), object_id)
611
+ index.save_object(settings.get_attributes(object).merge 'objectID' => algolia_object_id_of(object, options))
560
612
  end
561
613
  elsif algolia_conditional_index?(options) && !object_id.blank?
562
614
  # remove non-indexable objects
@@ -571,13 +623,13 @@ module AlgoliaSearch
571
623
  end
572
624
 
573
625
  def algolia_remove_from_index!(object, synchronous = false)
574
- return if @algolia_without_auto_index_scope
626
+ return if algolia_without_auto_index_scope
575
627
  object_id = algolia_object_id_of(object)
576
628
  raise ArgumentError.new("Cannot index a record with a blank objectID") if object_id.blank?
577
629
  algolia_configurations.each do |options, settings|
578
630
  next if algolia_indexing_disabled?(options)
579
631
  index = algolia_ensure_init(options, settings)
580
- next if options[:slave] || options[:replica]
632
+ next if options[:replica]
581
633
  if synchronous || options[:synchronous]
582
634
  index.delete_object!(object_id)
583
635
  else
@@ -591,8 +643,8 @@ module AlgoliaSearch
591
643
  algolia_configurations.each do |options, settings|
592
644
  next if algolia_indexing_disabled?(options)
593
645
  index = algolia_ensure_init(options, settings)
594
- next if options[:slave] || options[:replica]
595
- synchronous || options[:synchronous] ? index.clear! : index.clear
646
+ next if options[:replica]
647
+ synchronous || options[:synchronous] ? index.clear_objects! : index.clear_objects
596
648
  @algolia_indexes[settings] = nil
597
649
  end
598
650
  nil
@@ -601,8 +653,6 @@ module AlgoliaSearch
601
653
  def algolia_raw_search(q, params = {})
602
654
  index_name = params.delete(:index) ||
603
655
  params.delete('index') ||
604
- params.delete(:slave) ||
605
- params.delete('slave') ||
606
656
  params.delete(:replica) ||
607
657
  params.delete('replica')
608
658
  index = algolia_index(index_name)
@@ -648,7 +698,7 @@ module AlgoliaSearch
648
698
  algolia_object_id_of(hit)
649
699
  end
650
700
  results = json['hits'].map do |hit|
651
- o = results_by_id[hit['objectID']]
701
+ o = results_by_id[hit['objectID'].to_s]
652
702
  if o
653
703
  o.highlight_result = hit['_highlightResult']
654
704
  o.snippet_result = hit['_snippetResult']
@@ -656,9 +706,9 @@ module AlgoliaSearch
656
706
  end
657
707
  end.compact
658
708
  # Algolia has a default limit of 1000 retrievable hits
659
- total_hits = json['nbHits'] < json['nbPages'] * json['hitsPerPage'] ?
660
- json['nbHits'] : json['nbPages'] * json['hitsPerPage']
661
- res = AlgoliaSearch::Pagination.create(results, total_hits, algoliasearch_options.merge({ :page => json['page'] + 1, :per_page => json['hitsPerPage'] }))
709
+ total_hits = json['nbHits'].to_i < json['nbPages'].to_i * json['hitsPerPage'].to_i ?
710
+ json['nbHits'].to_i: json['nbPages'].to_i * json['hitsPerPage'].to_i
711
+ res = AlgoliaSearch::Pagination.create(results, total_hits, algoliasearch_options.merge({ :page => json['page'].to_i + 1, :per_page => json['hitsPerPage'] }))
662
712
  res.extend(AdditionalMethods)
663
713
  res.send(:algolia_init_raw_answer, json)
664
714
  res
@@ -667,13 +717,11 @@ module AlgoliaSearch
667
717
  def algolia_search_for_facet_values(facet, text, params = {})
668
718
  index_name = params.delete(:index) ||
669
719
  params.delete('index') ||
670
- params.delete(:slave) ||
671
- params.delete('slave') ||
672
720
  params.delete(:replica) ||
673
721
  params.delete('replicas')
674
722
  index = algolia_index(index_name)
675
723
  query = Hash[params.map { |k, v| [k.to_s, v.to_s] }]
676
- index.search_facet(facet, text, query)['facetHits']
724
+ index.search_for_facet_values(facet, text, query)['facetHits']
677
725
  end
678
726
 
679
727
  # deprecated (renaming)
@@ -697,19 +745,22 @@ module AlgoliaSearch
697
745
  end
698
746
 
699
747
  def algolia_must_reindex?(object)
748
+ # use +algolia_dirty?+ method if implemented
749
+ return object.send(:algolia_dirty?) if (object.respond_to?(:algolia_dirty?))
750
+ # Loop over each index to see if a attribute used in records has changed
700
751
  algolia_configurations.each do |options, settings|
701
- next if options[:slave] || options[:replica]
752
+ next if algolia_indexing_disabled?(options)
753
+ next if options[:replica]
702
754
  return true if algolia_object_id_changed?(object, options)
703
755
  settings.get_attribute_names(object).each do |k|
704
- changed_method = "#{k}_changed?"
705
- return true if !object.respond_to?(changed_method) || object.send(changed_method)
756
+ return true if algolia_attribute_changed?(object, k)
757
+ # return true if !object.respond_to?(changed_method) || object.send(changed_method)
706
758
  end
707
759
  [options[:if], options[:unless]].each do |condition|
708
760
  case condition
709
761
  when nil
710
762
  when String, Symbol
711
- changed_method = "#{condition}_changed?"
712
- return true if !object.respond_to?(changed_method) || object.send(changed_method)
763
+ return true if algolia_attribute_changed?(object, condition)
713
764
  else
714
765
  # if the :if, :unless condition is a anything else,
715
766
  # we have no idea whether we should reindex or not
@@ -718,6 +769,7 @@ module AlgoliaSearch
718
769
  end
719
770
  end
720
771
  end
772
+ # By default, we don't reindex
721
773
  return false
722
774
  end
723
775
 
@@ -735,19 +787,21 @@ module AlgoliaSearch
735
787
 
736
788
  @algolia_indexes[settings] = SafeIndex.new(algolia_index_name(options), algoliasearch_options[:raise_on_failure])
737
789
 
738
- current_settings = @algolia_indexes[settings].get_settings rescue nil # if the index doesn't exist
739
-
740
790
  index_settings ||= settings.to_settings
741
791
  index_settings = options[:primary_settings].to_settings.merge(index_settings) if options[:inherit]
792
+ replicas = index_settings.delete(:replicas) ||
793
+ index_settings.delete('replicas')
794
+ index_settings[:replicas] = replicas unless replicas.nil? || options[:inherit]
795
+
796
+ options[:check_settings] = true if options[:check_settings].nil?
797
+
798
+ current_settings = if options[:check_settings] && !algolia_indexing_disabled?(options)
799
+ @algolia_indexes[settings].get_settings(:getVersion => 1) rescue nil # if the index doesn't exist
800
+ end
742
801
 
743
- if !algolia_indexing_disabled?(options) && (index_settings || algoliasearch_settings_changed?(current_settings, index_settings))
744
- used_slaves = !current_settings.nil? && !current_settings['slaves'].nil?
745
- replicas = index_settings.delete(:replicas) ||
746
- index_settings.delete('replicas') ||
747
- index_settings.delete(:slaves) ||
748
- index_settings.delete('slaves')
749
- index_settings[used_slaves ? :slaves : :replicas] = replicas unless replicas.nil? || options[:inherit]
750
- @algolia_indexes[settings].set_settings(index_settings)
802
+ if !algolia_indexing_disabled?(options) && options[:check_settings] && algoliasearch_settings_changed?(current_settings, index_settings)
803
+ set_settings_method = options[:synchronous] ? :set_settings! : :set_settings
804
+ @algolia_indexes[settings].send(set_settings_method, index_settings)
751
805
  end
752
806
 
753
807
  @algolia_indexes[settings]
@@ -783,8 +837,8 @@ module AlgoliaSearch
783
837
  end
784
838
 
785
839
  def algolia_object_id_changed?(o, options = nil)
786
- m = "#{algolia_object_id_method(options)}_changed?"
787
- o.respond_to?(m) ? o.send(m) : false
840
+ changed = algolia_attribute_changed?(o, algolia_object_id_method(options))
841
+ changed.nil? ? false : changed
788
842
  end
789
843
 
790
844
  def algoliasearch_settings_changed?(prev, current)
@@ -794,6 +848,8 @@ module AlgoliaSearch
794
848
  if v.is_a?(Array) and prev_v.is_a?(Array)
795
849
  # compare array of strings, avoiding symbols VS strings comparison
796
850
  return true if v.map { |x| x.to_s } != prev_v.map { |x| x.to_s }
851
+ elsif v.blank? # blank? check is needed to compare [] and null
852
+ return true unless prev_v.blank?
797
853
  else
798
854
  return true if prev_v != v
799
855
  end
@@ -862,7 +918,7 @@ module AlgoliaSearch
862
918
  def algolia_find_in_batches(batch_size, &block)
863
919
  if (defined?(::ActiveRecord) && ancestors.include?(::ActiveRecord::Base)) || respond_to?(:find_in_batches)
864
920
  find_in_batches(:batch_size => batch_size, &block)
865
- elsif defined?(::Sequel) && self < Sequel::Model
921
+ elsif defined?(::Sequel) && defined?(::Sequel::Model) && self < Sequel::Model
866
922
  dataset.extension(:pagination).each_page(batch_size, &block)
867
923
  else
868
924
  # don't worry, mongoid has its own underlying cursor/streaming mechanism
@@ -877,6 +933,50 @@ module AlgoliaSearch
877
933
  yield items unless items.empty?
878
934
  end
879
935
  end
936
+
937
+ def algolia_attribute_changed?(object, attr_name)
938
+ # if one of two method is implemented, we return its result
939
+ # true/false means whether it has changed or not
940
+ # +#{attr_name}_changed?+ always defined for automatic attributes but deprecated after Rails 5.2
941
+ # +will_save_change_to_#{attr_name}?+ should be use instead for Rails 5.2+, also defined for automatic attributes.
942
+ # If none of the method are defined, it's a dynamic attribute
943
+
944
+ method_name = "#{attr_name}_changed?"
945
+ if object.respond_to?(method_name)
946
+ # If +#{attr_name}_changed?+ respond we want to see if the method is user defined or if it's automatically
947
+ # defined by Rails.
948
+ # If it's user-defined, we call it.
949
+ # If it's automatic we check ActiveRecord version to see if this method is deprecated
950
+ # and try to call +will_save_change_to_#{attr_name}?+ instead.
951
+ # See: https://github.com/algolia/algoliasearch-rails/pull/338
952
+ # This feature is not compatible with Ruby 1.8
953
+ # In this case, we always call #{attr_name}_changed?
954
+ if Object.const_defined?(:RUBY_VERSION) && RUBY_VERSION.to_f < 1.9
955
+ return object.send(method_name)
956
+ end
957
+ unless automatic_changed_method?(object, method_name) && automatic_changed_method_deprecated?
958
+ return object.send(method_name)
959
+ end
960
+ end
961
+
962
+ if object.respond_to?("will_save_change_to_#{attr_name}?")
963
+ return object.send("will_save_change_to_#{attr_name}?")
964
+ end
965
+
966
+ # We don't know if the attribute has changed, so conservatively assume it has
967
+ true
968
+ end
969
+
970
+ def automatic_changed_method?(object, method_name)
971
+ raise ArgumentError.new("Method #{method_name} doesn't exist on #{object.class.name}") unless object.respond_to?(method_name)
972
+ file = object.method(method_name).source_location[0]
973
+ file.end_with?("active_model/attribute_methods.rb")
974
+ end
975
+
976
+ def automatic_changed_method_deprecated?
977
+ (defined?(::ActiveRecord) && ActiveRecord::VERSION::MAJOR >= 5 && ActiveRecord::VERSION::MINOR >= 1) ||
978
+ (defined?(::ActiveRecord) && ActiveRecord::VERSION::MAJOR > 5)
979
+ end
880
980
  end
881
981
 
882
982
  # these are the instance methods included
@@ -928,8 +1028,10 @@ module AlgoliaSearch
928
1028
  end
929
1029
 
930
1030
  def algolia_mark_must_reindex
931
- @algolia_must_reindex =
932
- if defined?(::Sequel) && is_a?(Sequel::Model)
1031
+ # algolia_must_reindex flag is reset after every commit as part. If we must reindex at any point in
1032
+ # a stransaction, keep flag set until it is explicitly unset
1033
+ @algolia_must_reindex ||=
1034
+ if defined?(::Sequel) && defined?(::Sequel::Model) && is_a?(Sequel::Model)
933
1035
  new? || self.class.algolia_must_reindex?(self)
934
1036
  else
935
1037
  new_record? || self.class.algolia_must_reindex?(self)