algoliasearch-rails 1.19.1 → 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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)