yc-algoliasearch-rails 2.1.4
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.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.rspec +1 -0
- data/CHANGELOG.MD +566 -0
- data/Gemfile +38 -0
- data/Gemfile.lock +213 -0
- data/LICENSE +21 -0
- data/README.md +1171 -0
- data/Rakefile +17 -0
- data/algoliasearch-rails.gemspec +95 -0
- data/lib/algoliasearch/algolia_job.rb +9 -0
- data/lib/algoliasearch/configuration.rb +30 -0
- data/lib/algoliasearch/pagination/kaminari.rb +40 -0
- data/lib/algoliasearch/pagination/will_paginate.rb +15 -0
- data/lib/algoliasearch/pagination.rb +19 -0
- data/lib/algoliasearch/railtie.rb +11 -0
- data/lib/algoliasearch/tasks/algoliasearch.rake +19 -0
- data/lib/algoliasearch/utilities.rb +48 -0
- data/lib/algoliasearch/version.rb +3 -0
- data/lib/algoliasearch-rails.rb +1083 -0
- data/spec/spec_helper.rb +52 -0
- data/spec/utilities_spec.rb +30 -0
- data/vendor/assets/javascripts/algolia/algoliasearch.angular.js +2678 -0
- data/vendor/assets/javascripts/algolia/algoliasearch.angular.min.js +7 -0
- data/vendor/assets/javascripts/algolia/algoliasearch.jquery.js +2678 -0
- data/vendor/assets/javascripts/algolia/algoliasearch.jquery.min.js +7 -0
- data/vendor/assets/javascripts/algolia/algoliasearch.js +2663 -0
- data/vendor/assets/javascripts/algolia/algoliasearch.min.js +7 -0
- data/vendor/assets/javascripts/algolia/bloodhound.js +727 -0
- data/vendor/assets/javascripts/algolia/bloodhound.min.js +7 -0
- data/vendor/assets/javascripts/algolia/typeahead.bundle.js +1782 -0
- data/vendor/assets/javascripts/algolia/typeahead.bundle.min.js +7 -0
- data/vendor/assets/javascripts/algolia/typeahead.jquery.js +1184 -0
- data/vendor/assets/javascripts/algolia/typeahead.jquery.min.js +7 -0
- data/vendor/assets/javascripts/algolia/v2/algoliasearch.angular.js +2678 -0
- data/vendor/assets/javascripts/algolia/v2/algoliasearch.angular.min.js +7 -0
- data/vendor/assets/javascripts/algolia/v2/algoliasearch.jquery.js +2678 -0
- data/vendor/assets/javascripts/algolia/v2/algoliasearch.jquery.min.js +7 -0
- data/vendor/assets/javascripts/algolia/v2/algoliasearch.js +2663 -0
- data/vendor/assets/javascripts/algolia/v2/algoliasearch.min.js +7 -0
- data/vendor/assets/javascripts/algolia/v3/algoliasearch.angular.js +6277 -0
- data/vendor/assets/javascripts/algolia/v3/algoliasearch.angular.min.js +3 -0
- data/vendor/assets/javascripts/algolia/v3/algoliasearch.jquery.js +6223 -0
- data/vendor/assets/javascripts/algolia/v3/algoliasearch.jquery.min.js +3 -0
- data/vendor/assets/javascripts/algolia/v3/algoliasearch.js +6070 -0
- data/vendor/assets/javascripts/algolia/v3/algoliasearch.min.js +3 -0
- metadata +174 -0
@@ -0,0 +1,1083 @@
|
|
1
|
+
require 'algolia'
|
2
|
+
|
3
|
+
require 'algoliasearch/version'
|
4
|
+
require 'algoliasearch/utilities'
|
5
|
+
|
6
|
+
if defined? Rails
|
7
|
+
begin
|
8
|
+
require 'algoliasearch/railtie'
|
9
|
+
rescue LoadError
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
begin
|
14
|
+
require 'active_job'
|
15
|
+
rescue LoadError
|
16
|
+
# no queue support, fine
|
17
|
+
end
|
18
|
+
|
19
|
+
require 'logger'
|
20
|
+
|
21
|
+
module AlgoliaSearch
|
22
|
+
|
23
|
+
class NotConfigured < StandardError; end
|
24
|
+
class BadConfiguration < StandardError; end
|
25
|
+
class NoBlockGiven < StandardError; end
|
26
|
+
class MixedSlavesAndReplicas < StandardError; end
|
27
|
+
|
28
|
+
autoload :Configuration, 'algoliasearch/configuration'
|
29
|
+
extend Configuration
|
30
|
+
|
31
|
+
autoload :Pagination, 'algoliasearch/pagination'
|
32
|
+
|
33
|
+
class << self
|
34
|
+
attr_reader :included_in
|
35
|
+
|
36
|
+
def included(klass)
|
37
|
+
@included_in ||= []
|
38
|
+
@included_in << klass
|
39
|
+
@included_in.uniq!
|
40
|
+
|
41
|
+
klass.class_eval do
|
42
|
+
extend ClassMethods
|
43
|
+
include InstanceMethods
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
class IndexSettings
|
50
|
+
DEFAULT_BATCH_SIZE = 1000
|
51
|
+
|
52
|
+
# AlgoliaSearch settings
|
53
|
+
OPTIONS = [
|
54
|
+
# Attributes
|
55
|
+
:searchableAttributes, :attributesForFaceting, :unretrievableAttributes, :attributesToRetrieve,
|
56
|
+
# Ranking
|
57
|
+
:ranking, :customRanking, # 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
|
+
]
|
85
|
+
OPTIONS.each do |k|
|
86
|
+
define_method k do |v|
|
87
|
+
instance_variable_set("@#{k}", v)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def initialize(options, &block)
|
92
|
+
@options = options
|
93
|
+
instance_exec(&block) if block_given?
|
94
|
+
end
|
95
|
+
|
96
|
+
def use_serializer(serializer)
|
97
|
+
@serializer = serializer
|
98
|
+
# instance_variable_set("@serializer", serializer)
|
99
|
+
end
|
100
|
+
|
101
|
+
def get_attribute_list
|
102
|
+
@attributes
|
103
|
+
end
|
104
|
+
|
105
|
+
def get_additional_attribute_list
|
106
|
+
@additional_attributes
|
107
|
+
end
|
108
|
+
|
109
|
+
def attribute(*names, &block)
|
110
|
+
raise ArgumentError.new('Cannot pass multiple attribute names if block given') if block_given? and names.length > 1
|
111
|
+
raise ArgumentError.new('Cannot specify additional attributes on a replica index') if @options[:replica]
|
112
|
+
@attributes ||= {}
|
113
|
+
names.flatten.each do |name|
|
114
|
+
@attributes[name.to_s] = block_given? ? Proc.new { |o| o.instance_eval(&block) } : Proc.new { |o| o.send(name) }
|
115
|
+
end
|
116
|
+
end
|
117
|
+
alias :attributes :attribute
|
118
|
+
|
119
|
+
def add_attribute(*names, &block)
|
120
|
+
raise ArgumentError.new('Cannot pass multiple attribute names if block given') if block_given? and names.length > 1
|
121
|
+
raise ArgumentError.new('Cannot specify additional attributes on a replica index') if @options[:replica]
|
122
|
+
@additional_attributes ||= {}
|
123
|
+
names.each do |name|
|
124
|
+
@additional_attributes[name.to_s] = block_given? ? Proc.new { |o| o.instance_eval(&block) } : Proc.new { |o| o.send(name) }
|
125
|
+
end
|
126
|
+
end
|
127
|
+
alias :add_attributes :add_attribute
|
128
|
+
|
129
|
+
def is_mongoid?(object)
|
130
|
+
defined?(::Mongoid::Document) && object.class.include?(::Mongoid::Document)
|
131
|
+
end
|
132
|
+
|
133
|
+
def is_sequel?(object)
|
134
|
+
defined?(::Sequel) && object.class < ::Sequel::Model
|
135
|
+
end
|
136
|
+
|
137
|
+
def is_active_record?(object)
|
138
|
+
!is_mongoid?(object) && !is_sequel?(object)
|
139
|
+
end
|
140
|
+
|
141
|
+
def get_default_attributes(object)
|
142
|
+
if is_mongoid?(object)
|
143
|
+
# work-around mongoid 2.4's unscoped method, not accepting a block
|
144
|
+
object.attributes
|
145
|
+
elsif is_sequel?(object)
|
146
|
+
object.to_hash
|
147
|
+
else
|
148
|
+
object.class.unscoped do
|
149
|
+
object.attributes
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def get_attribute_names(object)
|
155
|
+
get_attributes(object).keys
|
156
|
+
end
|
157
|
+
|
158
|
+
def attributes_to_hash(attributes, object)
|
159
|
+
if attributes
|
160
|
+
Hash[attributes.map { |name, value| [name.to_s, value.call(object) ] }]
|
161
|
+
else
|
162
|
+
{}
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def get_attributes(object)
|
167
|
+
# If a serializer is set, we ignore attributes
|
168
|
+
# everything should be done via the serializer
|
169
|
+
if not @serializer.nil?
|
170
|
+
attributes = @serializer.new(object).attributes
|
171
|
+
else
|
172
|
+
if @options[:primary_settings] && @options[:inherit_attributes]
|
173
|
+
attr = @options[:primary_settings].get_attribute_list
|
174
|
+
if attr.nil?
|
175
|
+
attr = @attributes
|
176
|
+
elsif not @attributes.nil?
|
177
|
+
attr.merge!(@attributes)
|
178
|
+
end
|
179
|
+
else
|
180
|
+
attr = @attributes
|
181
|
+
end
|
182
|
+
|
183
|
+
if attr.nil? || attr.length == 0
|
184
|
+
# no `attribute ...` have been configured, use the default attributes of the model
|
185
|
+
attributes = get_default_attributes(object)
|
186
|
+
else
|
187
|
+
# at least 1 `attribute ...` has been configured, therefore use ONLY the one configured
|
188
|
+
if is_active_record?(object)
|
189
|
+
object.class.unscoped do
|
190
|
+
attributes = attributes_to_hash(attr, object)
|
191
|
+
end
|
192
|
+
else
|
193
|
+
attributes = attributes_to_hash(attr, object)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
additional_attr = {}
|
199
|
+
if @options[:primary_settings] && @options[:inherit_attributes]
|
200
|
+
additional_attr.merge!(attributes_to_hash(@options[:primary_settings].get_additional_attribute_list, object))
|
201
|
+
end
|
202
|
+
additional_attr.merge!(attributes_to_hash(@additional_attributes, object)) if @additional_attributes
|
203
|
+
attributes.merge!(additional_attr)
|
204
|
+
|
205
|
+
if @options[:sanitize]
|
206
|
+
sanitizer = begin
|
207
|
+
::HTML::FullSanitizer.new
|
208
|
+
rescue NameError
|
209
|
+
# from rails 4.2
|
210
|
+
::Rails::Html::FullSanitizer.new
|
211
|
+
end
|
212
|
+
attributes = sanitize_attributes(attributes, sanitizer)
|
213
|
+
end
|
214
|
+
|
215
|
+
if @options[:force_utf8_encoding] && Object.const_defined?(:RUBY_VERSION) && RUBY_VERSION.to_f > 1.8
|
216
|
+
attributes = encode_attributes(attributes)
|
217
|
+
end
|
218
|
+
|
219
|
+
attributes
|
220
|
+
end
|
221
|
+
|
222
|
+
def sanitize_attributes(v, sanitizer)
|
223
|
+
case v
|
224
|
+
when String
|
225
|
+
sanitizer.sanitize(v)
|
226
|
+
when Hash
|
227
|
+
v.each { |key, value| v[key] = sanitize_attributes(value, sanitizer) }
|
228
|
+
when Array
|
229
|
+
v.map { |x| sanitize_attributes(x, sanitizer) }
|
230
|
+
else
|
231
|
+
v
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def encode_attributes(v)
|
236
|
+
case v
|
237
|
+
when String
|
238
|
+
v.force_encoding('utf-8')
|
239
|
+
when Hash
|
240
|
+
v.each { |key, value| v[key] = encode_attributes(value) }
|
241
|
+
when Array
|
242
|
+
v.map { |x| encode_attributes(x) }
|
243
|
+
else
|
244
|
+
v
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
def geoloc(lat_attr = nil, lng_attr = nil, &block)
|
249
|
+
raise ArgumentError.new('Cannot specify additional attributes on a replica index') if @options[:replica]
|
250
|
+
add_attribute :_geoloc do |o|
|
251
|
+
block_given? ? o.instance_eval(&block) : { :lat => o.send(lat_attr).to_f, :lng => o.send(lng_attr).to_f }
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def tags(*args, &block)
|
256
|
+
raise ArgumentError.new('Cannot specify additional attributes on a replica index') if @options[:replica]
|
257
|
+
add_attribute :_tags do |o|
|
258
|
+
v = block_given? ? o.instance_eval(&block) : args
|
259
|
+
v.is_a?(Array) ? v : [v]
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
def get_setting(name)
|
264
|
+
instance_variable_get("@#{name}")
|
265
|
+
end
|
266
|
+
|
267
|
+
def to_settings
|
268
|
+
settings = {}
|
269
|
+
OPTIONS.each do |k|
|
270
|
+
v = get_setting(k)
|
271
|
+
settings[k] = v if !v.nil?
|
272
|
+
end
|
273
|
+
|
274
|
+
if !@options[:replica]
|
275
|
+
settings[:replicas] = additional_indexes.select { |opts, s| opts[:replica] }.map do |opts, s|
|
276
|
+
name = opts[:index_name]
|
277
|
+
name = "#{name}_#{Rails.env.to_s}" if opts[:per_environment]
|
278
|
+
name = "virtual(#{name})" if opts[:virtual]
|
279
|
+
name
|
280
|
+
end
|
281
|
+
settings.delete(:replicas) if settings[:replicas].empty?
|
282
|
+
end
|
283
|
+
settings
|
284
|
+
end
|
285
|
+
|
286
|
+
def add_index(index_name, options = {}, &block)
|
287
|
+
raise ArgumentError.new('Cannot specify additional index on a replica index') if @options[:replica]
|
288
|
+
raise ArgumentError.new('No block given') if !block_given?
|
289
|
+
raise ArgumentError.new('Options auto_index and auto_remove cannot be set on nested indexes') if options[:auto_index] || options[:auto_remove]
|
290
|
+
@additional_indexes ||= {}
|
291
|
+
options[:index_name] = index_name
|
292
|
+
options.merge!({ :primary_settings => self }) if options[:inherit]
|
293
|
+
@additional_indexes[options] = IndexSettings.new(options, &block)
|
294
|
+
end
|
295
|
+
|
296
|
+
def add_replica(index_name, options = {}, &block)
|
297
|
+
raise ArgumentError.new('Cannot specify additional replicas on a replica index') if @options[:replica]
|
298
|
+
raise ArgumentError.new('No block given') if !block_given?
|
299
|
+
add_index(index_name, options.merge({ :replica => true }), &block)
|
300
|
+
end
|
301
|
+
|
302
|
+
def add_slave(index_name, options = {}, &block)
|
303
|
+
raise ArgumentError.new('Cannot specify additional slaves on a slave index') if @options[:slave] || @options[:replica]
|
304
|
+
raise ArgumentError.new('No block given') if !block_given?
|
305
|
+
add_index(index_name, options.merge({ :slave => true }), &block)
|
306
|
+
end
|
307
|
+
|
308
|
+
def additional_indexes
|
309
|
+
@additional_indexes || {}
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
# Default queueing system
|
314
|
+
if defined?(::ActiveJob::Base)
|
315
|
+
# lazy load the ActiveJob class to ensure the
|
316
|
+
# queue is initialized before using it
|
317
|
+
# see https://github.com/algolia/algoliasearch-rails/issues/69
|
318
|
+
autoload :AlgoliaJob, 'algoliasearch/algolia_job'
|
319
|
+
end
|
320
|
+
|
321
|
+
# this class wraps an Algolia::Index object ensuring all raised exceptions
|
322
|
+
# are correctly logged or thrown depending on the `raise_on_failure` option
|
323
|
+
class SafeIndex
|
324
|
+
def initialize(name, raise_on_failure)
|
325
|
+
@index = AlgoliaSearch.client.init_index(name)
|
326
|
+
@raise_on_failure = raise_on_failure.nil? || raise_on_failure
|
327
|
+
end
|
328
|
+
|
329
|
+
::Algolia::Search::Index.instance_methods(false).each do |m|
|
330
|
+
define_method(m) do |*args, &block|
|
331
|
+
SafeIndex.log_or_throw(m, @raise_on_failure) do
|
332
|
+
@index.send(m, *args, &block)
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
# special handling of wait_task to handle null task_id
|
338
|
+
def wait_task(task_id)
|
339
|
+
return if task_id.nil? && !@raise_on_failure # ok
|
340
|
+
SafeIndex.log_or_throw(:wait_task, @raise_on_failure) do
|
341
|
+
@index.wait_task(task_id)
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
# special handling of get_settings to avoid raising errors on 404
|
346
|
+
def get_settings(*args)
|
347
|
+
SafeIndex.log_or_throw(:get_settings, @raise_on_failure) do
|
348
|
+
begin
|
349
|
+
@index.get_settings(*args)
|
350
|
+
rescue Algolia::AlgoliaHttpError => e
|
351
|
+
return {} if e.code == 404 # not fatal
|
352
|
+
raise e
|
353
|
+
end
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
# expose move as well
|
358
|
+
def self.move_index(old_name, new_name)
|
359
|
+
SafeIndex.log_or_throw(:move_index, true) do
|
360
|
+
AlgoliaSearch.client.move_index(old_name, new_name)
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
private
|
365
|
+
def self.log_or_throw(method, raise_on_failure, &block)
|
366
|
+
begin
|
367
|
+
yield
|
368
|
+
rescue Algolia::AlgoliaError => e
|
369
|
+
raise e if raise_on_failure
|
370
|
+
# log the error
|
371
|
+
(Rails.logger || Logger.new(STDOUT)).error("[algoliasearch-rails] #{e.message}")
|
372
|
+
# return something
|
373
|
+
case method.to_s
|
374
|
+
when 'search'
|
375
|
+
# some attributes are required
|
376
|
+
{ 'hits' => [], 'hitsPerPage' => 0, 'page' => 0, 'facets' => {}, 'error' => e }
|
377
|
+
else
|
378
|
+
# empty answer
|
379
|
+
{ 'error' => e }
|
380
|
+
end
|
381
|
+
end
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
# these are the class methods added when AlgoliaSearch is included
|
386
|
+
module ClassMethods
|
387
|
+
|
388
|
+
def self.extended(base)
|
389
|
+
class <<base
|
390
|
+
alias_method :without_auto_index, :algolia_without_auto_index unless method_defined? :without_auto_index
|
391
|
+
alias_method :reindex!, :algolia_reindex! unless method_defined? :reindex!
|
392
|
+
alias_method :reindex, :algolia_reindex unless method_defined? :reindex
|
393
|
+
alias_method :index_objects, :algolia_index_objects unless method_defined? :index_objects
|
394
|
+
alias_method :index!, :algolia_index! unless method_defined? :index!
|
395
|
+
alias_method :remove_from_index!, :algolia_remove_from_index! unless method_defined? :remove_from_index!
|
396
|
+
alias_method :clear_index!, :algolia_clear_index! unless method_defined? :clear_index!
|
397
|
+
alias_method :search, :algolia_search unless method_defined? :search
|
398
|
+
alias_method :raw_search, :algolia_raw_search unless method_defined? :raw_search
|
399
|
+
alias_method :search_facet, :algolia_search_facet unless method_defined? :search_facet
|
400
|
+
alias_method :search_for_facet_values, :algolia_search_for_facet_values unless method_defined? :search_for_facet_values
|
401
|
+
alias_method :index, :algolia_index unless method_defined? :index
|
402
|
+
alias_method :index_name, :algolia_index_name unless method_defined? :index_name
|
403
|
+
alias_method :must_reindex?, :algolia_must_reindex? unless method_defined? :must_reindex?
|
404
|
+
end
|
405
|
+
|
406
|
+
base.cattr_accessor :algoliasearch_options, :algoliasearch_settings
|
407
|
+
end
|
408
|
+
|
409
|
+
def algoliasearch(options = {}, &block)
|
410
|
+
self.algoliasearch_settings = IndexSettings.new(options, &block)
|
411
|
+
self.algoliasearch_options = { :type => algolia_full_const_get(model_name.to_s), :per_page => algoliasearch_settings.get_setting(:hitsPerPage) || 10, :page => 1 }.merge(options)
|
412
|
+
|
413
|
+
attr_accessor :highlight_result, :snippet_result
|
414
|
+
|
415
|
+
if options[:synchronous] == true
|
416
|
+
if defined?(::Sequel) && self < Sequel::Model
|
417
|
+
class_eval do
|
418
|
+
copy_after_validation = instance_method(:after_validation)
|
419
|
+
define_method(:after_validation) do |*args|
|
420
|
+
super(*args)
|
421
|
+
copy_after_validation.bind(self).call
|
422
|
+
algolia_mark_synchronous
|
423
|
+
end
|
424
|
+
end
|
425
|
+
else
|
426
|
+
after_validation :algolia_mark_synchronous if respond_to?(:after_validation)
|
427
|
+
end
|
428
|
+
end
|
429
|
+
if options[:enqueue]
|
430
|
+
raise ArgumentError.new("Cannot use a enqueue if the `synchronous` option if set") if options[:synchronous]
|
431
|
+
proc = if options[:enqueue] == true
|
432
|
+
Proc.new do |record, remove|
|
433
|
+
AlgoliaJob.perform_later(record, remove ? 'algolia_remove_from_index!' : 'algolia_index!')
|
434
|
+
end
|
435
|
+
elsif options[:enqueue].respond_to?(:call)
|
436
|
+
options[:enqueue]
|
437
|
+
elsif options[:enqueue].is_a?(Symbol)
|
438
|
+
Proc.new { |record, remove| self.send(options[:enqueue], record, remove) }
|
439
|
+
else
|
440
|
+
raise ArgumentError.new("Invalid `enqueue` option: #{options[:enqueue]}")
|
441
|
+
end
|
442
|
+
algoliasearch_options[:enqueue] = Proc.new do |record, remove|
|
443
|
+
proc.call(record, remove) unless algolia_without_auto_index_scope
|
444
|
+
end
|
445
|
+
end
|
446
|
+
unless options[:auto_index] == false
|
447
|
+
if defined?(::Sequel) && self < Sequel::Model
|
448
|
+
class_eval do
|
449
|
+
copy_after_validation = instance_method(:after_validation)
|
450
|
+
copy_before_save = instance_method(:before_save)
|
451
|
+
|
452
|
+
define_method(:after_validation) do |*args|
|
453
|
+
super(*args)
|
454
|
+
copy_after_validation.bind(self).call
|
455
|
+
algolia_mark_must_reindex
|
456
|
+
end
|
457
|
+
|
458
|
+
define_method(:before_save) do |*args|
|
459
|
+
copy_before_save.bind(self).call
|
460
|
+
algolia_mark_for_auto_indexing
|
461
|
+
super(*args)
|
462
|
+
end
|
463
|
+
|
464
|
+
sequel_version = Gem::Version.new(Sequel.version)
|
465
|
+
if sequel_version >= Gem::Version.new('4.0.0') && sequel_version < Gem::Version.new('5.0.0')
|
466
|
+
copy_after_commit = instance_method(:after_commit)
|
467
|
+
define_method(:after_commit) do |*args|
|
468
|
+
super(*args)
|
469
|
+
copy_after_commit.bind(self).call
|
470
|
+
algolia_perform_index_tasks
|
471
|
+
end
|
472
|
+
else
|
473
|
+
copy_after_save = instance_method(:after_save)
|
474
|
+
define_method(:after_save) do |*args|
|
475
|
+
super(*args)
|
476
|
+
copy_after_save.bind(self).call
|
477
|
+
self.db.after_commit do
|
478
|
+
algolia_perform_index_tasks
|
479
|
+
end
|
480
|
+
end
|
481
|
+
end
|
482
|
+
end
|
483
|
+
else
|
484
|
+
after_validation :algolia_mark_must_reindex if respond_to?(:after_validation)
|
485
|
+
before_save :algolia_mark_for_auto_indexing if respond_to?(:before_save)
|
486
|
+
if respond_to?(:after_commit)
|
487
|
+
after_commit :algolia_perform_index_tasks
|
488
|
+
elsif respond_to?(:after_save)
|
489
|
+
after_save :algolia_perform_index_tasks
|
490
|
+
end
|
491
|
+
end
|
492
|
+
end
|
493
|
+
unless options[:auto_remove] == false
|
494
|
+
if defined?(::Sequel) && self < Sequel::Model
|
495
|
+
class_eval do
|
496
|
+
copy_after_destroy = instance_method(:after_destroy)
|
497
|
+
|
498
|
+
define_method(:after_destroy) do |*args|
|
499
|
+
copy_after_destroy.bind(self).call
|
500
|
+
algolia_enqueue_remove_from_index!(algolia_synchronous?)
|
501
|
+
super(*args)
|
502
|
+
end
|
503
|
+
end
|
504
|
+
else
|
505
|
+
after_destroy { |searchable| searchable.algolia_enqueue_remove_from_index!(algolia_synchronous?) } if respond_to?(:after_destroy)
|
506
|
+
end
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
def algolia_without_auto_index(&block)
|
511
|
+
self.algolia_without_auto_index_scope = true
|
512
|
+
begin
|
513
|
+
yield
|
514
|
+
ensure
|
515
|
+
self.algolia_without_auto_index_scope = false
|
516
|
+
end
|
517
|
+
end
|
518
|
+
|
519
|
+
def algolia_without_auto_index_scope=(value)
|
520
|
+
Thread.current["algolia_without_auto_index_scope_for_#{self.model_name}"] = value
|
521
|
+
end
|
522
|
+
|
523
|
+
def algolia_without_auto_index_scope
|
524
|
+
Thread.current["algolia_without_auto_index_scope_for_#{self.model_name}"]
|
525
|
+
end
|
526
|
+
|
527
|
+
def algolia_reindex!(batch_size = AlgoliaSearch::IndexSettings::DEFAULT_BATCH_SIZE, synchronous = false)
|
528
|
+
return if algolia_without_auto_index_scope
|
529
|
+
algolia_configurations.each do |options, settings|
|
530
|
+
next if algolia_indexing_disabled?(options)
|
531
|
+
index = algolia_ensure_init(options, settings)
|
532
|
+
next if options[:replica]
|
533
|
+
last_task = nil
|
534
|
+
|
535
|
+
algolia_find_in_batches(batch_size) do |group|
|
536
|
+
if algolia_conditional_index?(options)
|
537
|
+
# delete non-indexable objects
|
538
|
+
ids = group.select { |o| !algolia_indexable?(o, options) }.map { |o| algolia_object_id_of(o, options) }
|
539
|
+
index.delete_objects(ids.select { |id| !id.blank? })
|
540
|
+
# select only indexable objects
|
541
|
+
group = group.select { |o| algolia_indexable?(o, options) }
|
542
|
+
end
|
543
|
+
objects = group.map do |o|
|
544
|
+
attributes = settings.get_attributes(o)
|
545
|
+
unless attributes.class == Hash
|
546
|
+
attributes = attributes.to_hash
|
547
|
+
end
|
548
|
+
attributes.merge 'objectID' => algolia_object_id_of(o, options)
|
549
|
+
end
|
550
|
+
last_task = index.save_objects(objects)
|
551
|
+
end
|
552
|
+
index.wait_task(last_task.raw_response["taskID"]) if last_task and (synchronous || options[:synchronous])
|
553
|
+
end
|
554
|
+
nil
|
555
|
+
end
|
556
|
+
|
557
|
+
# reindex whole database using a temporary index + move operation
|
558
|
+
def algolia_reindex(batch_size = AlgoliaSearch::IndexSettings::DEFAULT_BATCH_SIZE, synchronous = false)
|
559
|
+
return if algolia_without_auto_index_scope
|
560
|
+
|
561
|
+
algolia_configurations.each do |options, settings|
|
562
|
+
|
563
|
+
next if algolia_indexing_disabled?(options)
|
564
|
+
|
565
|
+
if options[:primary_settings] && options[:inherit_settings]
|
566
|
+
final_settings_map = options[:primary_settings].to_settings
|
567
|
+
final_settings_map.merge!(settings.to_settings)
|
568
|
+
else
|
569
|
+
final_settings_map = settings.to_settings
|
570
|
+
end
|
571
|
+
|
572
|
+
# Always remove the replicas because the index is either:
|
573
|
+
# - Temporary index (never set replicas to tmp index you're going to move)
|
574
|
+
# - A replica itself so it cannot have replicas
|
575
|
+
final_settings_map.delete :slaves
|
576
|
+
final_settings_map.delete :replicas
|
577
|
+
|
578
|
+
# For replica, we set_settings in case they are different or if they have changed
|
579
|
+
if options[:slave] || options[:replica]
|
580
|
+
replicaIndex = SafeIndex.new(algolia_index_name(options), algoliasearch_options[:raise_on_failure])
|
581
|
+
replicaIndex.set_settings final_settings_map
|
582
|
+
next
|
583
|
+
end
|
584
|
+
|
585
|
+
src_index_name = algolia_index_name(options)
|
586
|
+
|
587
|
+
# init temporary index
|
588
|
+
::Algolia::copy_index!(src_index_name, "#{src_index_name}.tmp", %w(settings synonyms rules))
|
589
|
+
tmp_index = SafeIndex.new("#{src_index_name}.tmp", !!options[:raise_on_failure])
|
590
|
+
tmp_index.set_settings final_settings_map
|
591
|
+
|
592
|
+
algolia_find_in_batches(batch_size) do |group|
|
593
|
+
if algolia_conditional_index?(options)
|
594
|
+
# select only indexable objects
|
595
|
+
group = group.select { |o| algolia_indexable?(o, options) }
|
596
|
+
end
|
597
|
+
objects = group.map { |o| settings.get_attributes(o).merge 'objectID' => algolia_object_id_of(o, options) }
|
598
|
+
tmp_index.save_objects(objects)
|
599
|
+
end
|
600
|
+
|
601
|
+
move_task = SafeIndex.move_index(tmp_index.name, src_index_name)
|
602
|
+
# wait if synchronous
|
603
|
+
SafeIndex.new(src_index_name, !!options[:raise_on_failure]).wait_task(move_task["taskID"]) if synchronous || options[:synchronous]
|
604
|
+
end
|
605
|
+
nil
|
606
|
+
end
|
607
|
+
|
608
|
+
def algolia_set_settings(synchronous = false)
|
609
|
+
algolia_configurations.each do |options, settings|
|
610
|
+
if options[:primary_settings] && (options[:inherit] || options[:inherit_settings])
|
611
|
+
primary = options[:primary_settings].to_settings
|
612
|
+
primary.delete :replicas
|
613
|
+
primary.delete 'replicas'
|
614
|
+
final_settings = primary.merge(settings.to_settings)
|
615
|
+
else
|
616
|
+
final_settings = settings.to_settings
|
617
|
+
end
|
618
|
+
|
619
|
+
index = SafeIndex.new(algolia_index_name(options), true)
|
620
|
+
task = index.set_settings(final_settings)
|
621
|
+
index.wait_task(task.raw_response["taskID"]) if synchronous
|
622
|
+
end
|
623
|
+
end
|
624
|
+
|
625
|
+
def algolia_index_objects(objects, synchronous = false)
|
626
|
+
algolia_configurations.each do |options, settings|
|
627
|
+
next if algolia_indexing_disabled?(options)
|
628
|
+
index = algolia_ensure_init(options, settings)
|
629
|
+
next if options[:replica]
|
630
|
+
task = index.save_objects(objects.map { |o| settings.get_attributes(o).merge 'objectID' => algolia_object_id_of(o, options) })
|
631
|
+
index.wait_task(task.raw_response["taskID"]) if synchronous || options[:synchronous]
|
632
|
+
end
|
633
|
+
end
|
634
|
+
|
635
|
+
def algolia_index!(object, synchronous = false)
|
636
|
+
return if algolia_without_auto_index_scope
|
637
|
+
algolia_configurations.each do |options, settings|
|
638
|
+
next if algolia_indexing_disabled?(options)
|
639
|
+
object_id = algolia_object_id_of(object, options)
|
640
|
+
index = algolia_ensure_init(options, settings)
|
641
|
+
next if options[:replica]
|
642
|
+
if algolia_indexable?(object, options)
|
643
|
+
raise ArgumentError.new("Cannot index a record with a blank objectID") if object_id.blank?
|
644
|
+
if synchronous || options[:synchronous]
|
645
|
+
index.save_object!(settings.get_attributes(object).merge 'objectID' => algolia_object_id_of(object, options))
|
646
|
+
else
|
647
|
+
index.save_object(settings.get_attributes(object).merge 'objectID' => algolia_object_id_of(object, options))
|
648
|
+
end
|
649
|
+
elsif algolia_conditional_index?(options) && !object_id.blank?
|
650
|
+
# remove non-indexable objects
|
651
|
+
if synchronous || options[:synchronous]
|
652
|
+
index.delete_object!(object_id)
|
653
|
+
else
|
654
|
+
index.delete_object(object_id)
|
655
|
+
end
|
656
|
+
end
|
657
|
+
end
|
658
|
+
nil
|
659
|
+
end
|
660
|
+
|
661
|
+
def algolia_remove_from_index!(object, synchronous = false)
|
662
|
+
return if algolia_without_auto_index_scope
|
663
|
+
object_id = algolia_object_id_of(object)
|
664
|
+
raise ArgumentError.new("Cannot index a record with a blank objectID") if object_id.blank?
|
665
|
+
algolia_configurations.each do |options, settings|
|
666
|
+
next if algolia_indexing_disabled?(options)
|
667
|
+
index = algolia_ensure_init(options, settings)
|
668
|
+
next if options[:replica]
|
669
|
+
if synchronous || options[:synchronous]
|
670
|
+
index.delete_object!(object_id)
|
671
|
+
else
|
672
|
+
index.delete_object(object_id)
|
673
|
+
end
|
674
|
+
end
|
675
|
+
nil
|
676
|
+
end
|
677
|
+
|
678
|
+
def algolia_clear_index!(synchronous = false)
|
679
|
+
algolia_configurations.each do |options, settings|
|
680
|
+
next if algolia_indexing_disabled?(options)
|
681
|
+
index = algolia_ensure_init(options, settings)
|
682
|
+
next if options[:replica]
|
683
|
+
synchronous || options[:synchronous] ? index.clear_objects! : index.clear_objects
|
684
|
+
@algolia_indexes[settings] = nil
|
685
|
+
end
|
686
|
+
nil
|
687
|
+
end
|
688
|
+
|
689
|
+
def algolia_raw_search(q, params = {})
|
690
|
+
index_name = params.delete(:index) ||
|
691
|
+
params.delete('index') ||
|
692
|
+
params.delete(:replica) ||
|
693
|
+
params.delete('replica')
|
694
|
+
index = algolia_index(index_name)
|
695
|
+
index.search(q, Hash[params.map { |k,v| [k.to_s, v.to_s] }])
|
696
|
+
end
|
697
|
+
|
698
|
+
module AdditionalMethods
|
699
|
+
def self.extended(base)
|
700
|
+
class <<base
|
701
|
+
alias_method :raw_answer, :algolia_raw_answer unless method_defined? :raw_answer
|
702
|
+
alias_method :facets, :algolia_facets unless method_defined? :facets
|
703
|
+
end
|
704
|
+
end
|
705
|
+
|
706
|
+
def algolia_raw_answer
|
707
|
+
@algolia_json
|
708
|
+
end
|
709
|
+
|
710
|
+
def algolia_facets
|
711
|
+
@algolia_json['facets']
|
712
|
+
end
|
713
|
+
|
714
|
+
private
|
715
|
+
def algolia_init_raw_answer(json)
|
716
|
+
@algolia_json = json
|
717
|
+
end
|
718
|
+
end
|
719
|
+
|
720
|
+
def algolia_search(q, params = {})
|
721
|
+
if AlgoliaSearch.configuration[:pagination_backend]
|
722
|
+
# kaminari and will_paginate start pagination at 1, Algolia starts at 0
|
723
|
+
params[:page] = (params.delete('page') || params.delete(:page)).to_i
|
724
|
+
params[:page] -= 1 if params[:page].to_i > 0
|
725
|
+
end
|
726
|
+
json = algolia_raw_search(q, params)
|
727
|
+
hit_ids = json['hits'].map { |hit| hit['objectID'] }
|
728
|
+
if defined?(::Mongoid::Document) && self.include?(::Mongoid::Document)
|
729
|
+
condition_key = algolia_object_id_method.in
|
730
|
+
else
|
731
|
+
condition_key = algolia_object_id_method
|
732
|
+
end
|
733
|
+
results_by_id = algoliasearch_options[:type].where(condition_key => hit_ids).index_by do |hit|
|
734
|
+
algolia_object_id_of(hit)
|
735
|
+
end
|
736
|
+
results = json['hits'].map do |hit|
|
737
|
+
o = results_by_id[hit['objectID'].to_s]
|
738
|
+
if o
|
739
|
+
o.highlight_result = hit['_highlightResult']
|
740
|
+
o.snippet_result = hit['_snippetResult']
|
741
|
+
o
|
742
|
+
end
|
743
|
+
end.compact
|
744
|
+
# Algolia has a default limit of 1000 retrievable hits
|
745
|
+
total_hits = json['nbHits'].to_i < json['nbPages'].to_i * json['hitsPerPage'].to_i ?
|
746
|
+
json['nbHits'].to_i: json['nbPages'].to_i * json['hitsPerPage'].to_i
|
747
|
+
res = AlgoliaSearch::Pagination.create(results, total_hits, algoliasearch_options.merge({ :page => json['page'].to_i + 1, :per_page => json['hitsPerPage'] }))
|
748
|
+
res.extend(AdditionalMethods)
|
749
|
+
res.send(:algolia_init_raw_answer, json)
|
750
|
+
res
|
751
|
+
end
|
752
|
+
|
753
|
+
def algolia_search_for_facet_values(facet, text, params = {})
|
754
|
+
index_name = params.delete(:index) ||
|
755
|
+
params.delete('index') ||
|
756
|
+
params.delete(:replica) ||
|
757
|
+
params.delete('replicas')
|
758
|
+
index = algolia_index(index_name)
|
759
|
+
query = Hash[params.map { |k, v| [k.to_s, v.to_s] }]
|
760
|
+
index.search_for_facet_values(facet, text, query)['facetHits']
|
761
|
+
end
|
762
|
+
|
763
|
+
# deprecated (renaming)
|
764
|
+
alias :algolia_search_facet :algolia_search_for_facet_values
|
765
|
+
|
766
|
+
def algolia_index(name = nil)
|
767
|
+
if name
|
768
|
+
algolia_configurations.each do |o, s|
|
769
|
+
return algolia_ensure_init(o, s) if o[:index_name].to_s == name.to_s
|
770
|
+
end
|
771
|
+
raise ArgumentError.new("Invalid index/replica name: #{name}")
|
772
|
+
end
|
773
|
+
algolia_ensure_init
|
774
|
+
end
|
775
|
+
|
776
|
+
def algolia_index_name(options = nil)
|
777
|
+
options ||= algoliasearch_options
|
778
|
+
name = options[:index_name] || model_name.to_s.gsub('::', '_')
|
779
|
+
name = "#{name}_#{Rails.env.to_s}" if options[:per_environment]
|
780
|
+
name
|
781
|
+
end
|
782
|
+
|
783
|
+
def algolia_must_reindex?(object)
|
784
|
+
# use +algolia_dirty?+ method if implemented
|
785
|
+
return object.send(:algolia_dirty?) if (object.respond_to?(:algolia_dirty?))
|
786
|
+
# Loop over each index to see if a attribute used in records has changed
|
787
|
+
algolia_configurations.each do |options, settings|
|
788
|
+
next if algolia_indexing_disabled?(options)
|
789
|
+
next if options[:replica]
|
790
|
+
return true if algolia_object_id_changed?(object, options)
|
791
|
+
settings.get_attribute_names(object).each do |k|
|
792
|
+
return true if algolia_attribute_changed?(object, k)
|
793
|
+
# return true if !object.respond_to?(changed_method) || object.send(changed_method)
|
794
|
+
end
|
795
|
+
[options[:if], options[:unless]].each do |condition|
|
796
|
+
case condition
|
797
|
+
when nil
|
798
|
+
when String, Symbol
|
799
|
+
return true if algolia_attribute_changed?(object, condition)
|
800
|
+
else
|
801
|
+
# if the :if, :unless condition is a anything else,
|
802
|
+
# we have no idea whether we should reindex or not
|
803
|
+
# let's always reindex then
|
804
|
+
return true
|
805
|
+
end
|
806
|
+
end
|
807
|
+
end
|
808
|
+
# By default, we don't reindex
|
809
|
+
return false
|
810
|
+
end
|
811
|
+
|
812
|
+
protected
|
813
|
+
|
814
|
+
def algolia_ensure_init(options = nil, settings = nil, index_settings = nil)
|
815
|
+
raise ArgumentError.new('No `algoliasearch` block found in your model.') if algoliasearch_settings.nil?
|
816
|
+
|
817
|
+
@algolia_indexes ||= {}
|
818
|
+
|
819
|
+
options ||= algoliasearch_options
|
820
|
+
settings ||= algoliasearch_settings
|
821
|
+
|
822
|
+
return @algolia_indexes[settings] if @algolia_indexes[settings]
|
823
|
+
|
824
|
+
@algolia_indexes[settings] = SafeIndex.new(algolia_index_name(options), algoliasearch_options[:raise_on_failure])
|
825
|
+
|
826
|
+
index_settings ||= settings.to_settings
|
827
|
+
index_settings = options[:primary_settings].to_settings.merge(index_settings) if options[:inherit]
|
828
|
+
|
829
|
+
options[:check_settings] = true if options[:check_settings].nil?
|
830
|
+
|
831
|
+
current_settings = if options[:check_settings]
|
832
|
+
@algolia_indexes[settings].get_settings(:getVersion => 1) rescue nil # if the index doesn't exist
|
833
|
+
end
|
834
|
+
|
835
|
+
if !algolia_indexing_disabled?(options) && options[:check_settings] && algoliasearch_settings_changed?(current_settings, index_settings)
|
836
|
+
replicas = index_settings.delete(:replicas) ||
|
837
|
+
index_settings.delete('replicas')
|
838
|
+
index_settings[:replicas] = replicas unless replicas.nil? || options[:inherit]
|
839
|
+
@algolia_indexes[settings].set_settings!(index_settings)
|
840
|
+
end
|
841
|
+
|
842
|
+
@algolia_indexes[settings]
|
843
|
+
end
|
844
|
+
|
845
|
+
private
|
846
|
+
|
847
|
+
def algolia_configurations
|
848
|
+
raise ArgumentError.new('No `algoliasearch` block found in your model.') if algoliasearch_settings.nil?
|
849
|
+
if @configurations.nil?
|
850
|
+
@configurations = {}
|
851
|
+
@configurations[algoliasearch_options] = algoliasearch_settings
|
852
|
+
algoliasearch_settings.additional_indexes.each do |k,v|
|
853
|
+
@configurations[k] = v
|
854
|
+
|
855
|
+
if v.additional_indexes.any?
|
856
|
+
v.additional_indexes.each do |options, index|
|
857
|
+
@configurations[options] = index
|
858
|
+
end
|
859
|
+
end
|
860
|
+
end
|
861
|
+
end
|
862
|
+
@configurations
|
863
|
+
end
|
864
|
+
|
865
|
+
def algolia_object_id_method(options = nil)
|
866
|
+
options ||= algoliasearch_options
|
867
|
+
options[:id] || options[:object_id] || :id
|
868
|
+
end
|
869
|
+
|
870
|
+
def algolia_object_id_of(o, options = nil)
|
871
|
+
o.send(algolia_object_id_method(options)).to_s
|
872
|
+
end
|
873
|
+
|
874
|
+
def algolia_object_id_changed?(o, options = nil)
|
875
|
+
changed = algolia_attribute_changed?(o, algolia_object_id_method(options))
|
876
|
+
changed.nil? ? false : changed
|
877
|
+
end
|
878
|
+
|
879
|
+
def algoliasearch_settings_changed?(prev, current)
|
880
|
+
return true if prev.nil?
|
881
|
+
current.each do |k, v|
|
882
|
+
prev_v = prev[k.to_s]
|
883
|
+
if v.is_a?(Array) and prev_v.is_a?(Array)
|
884
|
+
# compare array of strings, avoiding symbols VS strings comparison
|
885
|
+
return true if v.map { |x| x.to_s } != prev_v.map { |x| x.to_s }
|
886
|
+
else
|
887
|
+
return true if prev_v != v
|
888
|
+
end
|
889
|
+
end
|
890
|
+
false
|
891
|
+
end
|
892
|
+
|
893
|
+
def algolia_full_const_get(name)
|
894
|
+
list = name.split('::')
|
895
|
+
list.shift if list.first.blank?
|
896
|
+
obj = Object.const_defined?(:RUBY_VERSION) && RUBY_VERSION.to_f < 1.9 ? Object : self
|
897
|
+
list.each do |x|
|
898
|
+
# This is required because const_get tries to look for constants in the
|
899
|
+
# ancestor chain, but we only want constants that are HERE
|
900
|
+
obj = obj.const_defined?(x) ? obj.const_get(x) : obj.const_missing(x)
|
901
|
+
end
|
902
|
+
obj
|
903
|
+
end
|
904
|
+
|
905
|
+
def algolia_conditional_index?(options = nil)
|
906
|
+
options ||= algoliasearch_options
|
907
|
+
options[:if].present? || options[:unless].present?
|
908
|
+
end
|
909
|
+
|
910
|
+
def algolia_indexable?(object, options = nil)
|
911
|
+
options ||= algoliasearch_options
|
912
|
+
if_passes = options[:if].blank? || algolia_constraint_passes?(object, options[:if])
|
913
|
+
unless_passes = options[:unless].blank? || !algolia_constraint_passes?(object, options[:unless])
|
914
|
+
if_passes && unless_passes
|
915
|
+
end
|
916
|
+
|
917
|
+
def algolia_constraint_passes?(object, constraint)
|
918
|
+
case constraint
|
919
|
+
when Symbol
|
920
|
+
object.send(constraint)
|
921
|
+
when String
|
922
|
+
object.send(constraint.to_sym)
|
923
|
+
when Enumerable
|
924
|
+
# All constraints must pass
|
925
|
+
constraint.all? { |inner_constraint| algolia_constraint_passes?(object, inner_constraint) }
|
926
|
+
else
|
927
|
+
if constraint.respond_to?(:call) # Proc
|
928
|
+
constraint.call(object)
|
929
|
+
else
|
930
|
+
raise ArgumentError, "Unknown constraint type: #{constraint} (#{constraint.class})"
|
931
|
+
end
|
932
|
+
end
|
933
|
+
end
|
934
|
+
|
935
|
+
def algolia_indexing_disabled?(options = nil)
|
936
|
+
options ||= algoliasearch_options
|
937
|
+
constraint = options[:disable_indexing] || options['disable_indexing']
|
938
|
+
case constraint
|
939
|
+
when nil
|
940
|
+
return false
|
941
|
+
when true, false
|
942
|
+
return constraint
|
943
|
+
when String, Symbol
|
944
|
+
return send(constraint)
|
945
|
+
else
|
946
|
+
return constraint.call if constraint.respond_to?(:call) # Proc
|
947
|
+
end
|
948
|
+
raise ArgumentError, "Unknown constraint type: #{constraint} (#{constraint.class})"
|
949
|
+
end
|
950
|
+
|
951
|
+
def algolia_find_in_batches(batch_size, &block)
|
952
|
+
if (defined?(::ActiveRecord) && ancestors.include?(::ActiveRecord::Base)) || respond_to?(:find_in_batches)
|
953
|
+
find_in_batches(:batch_size => batch_size, &block)
|
954
|
+
elsif defined?(::Sequel) && self < Sequel::Model
|
955
|
+
dataset.extension(:pagination).each_page(batch_size, &block)
|
956
|
+
else
|
957
|
+
# don't worry, mongoid has its own underlying cursor/streaming mechanism
|
958
|
+
items = []
|
959
|
+
all.each do |item|
|
960
|
+
items << item
|
961
|
+
if items.length % batch_size == 0
|
962
|
+
yield items
|
963
|
+
items = []
|
964
|
+
end
|
965
|
+
end
|
966
|
+
yield items unless items.empty?
|
967
|
+
end
|
968
|
+
end
|
969
|
+
|
970
|
+
def algolia_attribute_changed?(object, attr_name)
|
971
|
+
# if one of two method is implemented, we return its result
|
972
|
+
# true/false means whether it has changed or not
|
973
|
+
# +#{attr_name}_changed?+ always defined for automatic attributes but deprecated after Rails 5.2
|
974
|
+
# +will_save_change_to_#{attr_name}?+ should be use instead for Rails 5.2+, also defined for automatic attributes.
|
975
|
+
# If none of the method are defined, it's a dynamic attribute
|
976
|
+
|
977
|
+
method_name = "#{attr_name}_changed?"
|
978
|
+
if object.respond_to?(method_name)
|
979
|
+
# If +#{attr_name}_changed?+ respond we want to see if the method is user defined or if it's automatically
|
980
|
+
# defined by Rails.
|
981
|
+
# If it's user-defined, we call it.
|
982
|
+
# If it's automatic we check ActiveRecord version to see if this method is deprecated
|
983
|
+
# and try to call +will_save_change_to_#{attr_name}?+ instead.
|
984
|
+
# See: https://github.com/algolia/algoliasearch-rails/pull/338
|
985
|
+
# This feature is not compatible with Ruby 1.8
|
986
|
+
# In this case, we always call #{attr_name}_changed?
|
987
|
+
if Object.const_defined?(:RUBY_VERSION) && RUBY_VERSION.to_f < 1.9
|
988
|
+
return object.send(method_name)
|
989
|
+
end
|
990
|
+
unless automatic_changed_method?(object, method_name) && automatic_changed_method_deprecated?
|
991
|
+
return object.send(method_name)
|
992
|
+
end
|
993
|
+
end
|
994
|
+
|
995
|
+
if object.respond_to?("will_save_change_to_#{attr_name}?")
|
996
|
+
return object.send("will_save_change_to_#{attr_name}?")
|
997
|
+
end
|
998
|
+
|
999
|
+
# We don't know if the attribute has changed, so conservatively assume it has
|
1000
|
+
true
|
1001
|
+
end
|
1002
|
+
|
1003
|
+
def automatic_changed_method?(object, method_name)
|
1004
|
+
raise ArgumentError.new("Method #{method_name} doesn't exist on #{object.class.name}") unless object.respond_to?(method_name)
|
1005
|
+
file = object.method(method_name).source_location[0]
|
1006
|
+
file.end_with?("active_model/attribute_methods.rb")
|
1007
|
+
end
|
1008
|
+
|
1009
|
+
def automatic_changed_method_deprecated?
|
1010
|
+
(defined?(::ActiveRecord) && ActiveRecord::VERSION::MAJOR >= 5 && ActiveRecord::VERSION::MINOR >= 1) ||
|
1011
|
+
(defined?(::ActiveRecord) && ActiveRecord::VERSION::MAJOR > 5)
|
1012
|
+
end
|
1013
|
+
end
|
1014
|
+
|
1015
|
+
# these are the instance methods included
|
1016
|
+
module InstanceMethods
|
1017
|
+
|
1018
|
+
def self.included(base)
|
1019
|
+
base.instance_eval do
|
1020
|
+
alias_method :index!, :algolia_index! unless method_defined? :index!
|
1021
|
+
alias_method :remove_from_index!, :algolia_remove_from_index! unless method_defined? :remove_from_index!
|
1022
|
+
end
|
1023
|
+
end
|
1024
|
+
|
1025
|
+
def algolia_index!(synchronous = false)
|
1026
|
+
self.class.algolia_index!(self, synchronous || algolia_synchronous?)
|
1027
|
+
end
|
1028
|
+
|
1029
|
+
def algolia_remove_from_index!(synchronous = false)
|
1030
|
+
self.class.algolia_remove_from_index!(self, synchronous || algolia_synchronous?)
|
1031
|
+
end
|
1032
|
+
|
1033
|
+
def algolia_enqueue_remove_from_index!(synchronous)
|
1034
|
+
if algoliasearch_options[:enqueue]
|
1035
|
+
algoliasearch_options[:enqueue].call(self, true) unless self.class.send(:algolia_indexing_disabled?, algoliasearch_options)
|
1036
|
+
else
|
1037
|
+
algolia_remove_from_index!(synchronous || algolia_synchronous?)
|
1038
|
+
end
|
1039
|
+
end
|
1040
|
+
|
1041
|
+
def algolia_enqueue_index!(synchronous)
|
1042
|
+
if algoliasearch_options[:enqueue]
|
1043
|
+
algoliasearch_options[:enqueue].call(self, false) unless self.class.send(:algolia_indexing_disabled?, algoliasearch_options)
|
1044
|
+
else
|
1045
|
+
algolia_index!(synchronous)
|
1046
|
+
end
|
1047
|
+
end
|
1048
|
+
|
1049
|
+
private
|
1050
|
+
|
1051
|
+
def algolia_synchronous?
|
1052
|
+
@algolia_synchronous == true
|
1053
|
+
end
|
1054
|
+
|
1055
|
+
def algolia_mark_synchronous
|
1056
|
+
@algolia_synchronous = true
|
1057
|
+
end
|
1058
|
+
|
1059
|
+
def algolia_mark_for_auto_indexing
|
1060
|
+
@algolia_auto_indexing = true
|
1061
|
+
end
|
1062
|
+
|
1063
|
+
def algolia_mark_must_reindex
|
1064
|
+
# algolia_must_reindex flag is reset after every commit as part. If we must reindex at any point in
|
1065
|
+
# a stransaction, keep flag set until it is explicitly unset
|
1066
|
+
@algolia_must_reindex ||=
|
1067
|
+
if defined?(::Sequel) && is_a?(Sequel::Model)
|
1068
|
+
new? || self.class.algolia_must_reindex?(self)
|
1069
|
+
else
|
1070
|
+
new_record? || self.class.algolia_must_reindex?(self)
|
1071
|
+
end
|
1072
|
+
true
|
1073
|
+
end
|
1074
|
+
|
1075
|
+
def algolia_perform_index_tasks
|
1076
|
+
return if !@algolia_auto_indexing || @algolia_must_reindex == false
|
1077
|
+
algolia_enqueue_index!(algolia_synchronous?)
|
1078
|
+
remove_instance_variable(:@algolia_auto_indexing) if instance_variable_defined?(:@algolia_auto_indexing)
|
1079
|
+
remove_instance_variable(:@algolia_synchronous) if instance_variable_defined?(:@algolia_synchronous)
|
1080
|
+
remove_instance_variable(:@algolia_must_reindex) if instance_variable_defined?(:@algolia_must_reindex)
|
1081
|
+
end
|
1082
|
+
end
|
1083
|
+
end
|