require 'algolia'
require 'algoliasearch/version'
require 'algoliasearch/utilities'
if defined? Rails
begin
require 'algoliasearch/railtie'
rescue LoadError
end
end
begin
require 'active_job'
rescue LoadError
# no queue support, fine
end
require 'logger'
module AlgoliaSearch
class NotConfigured < StandardError; end
class BadConfiguration < StandardError; end
class NoBlockGiven < StandardError; end
class MixedSlavesAndReplicas < StandardError; end
autoload :Configuration, 'algoliasearch/configuration'
extend Configuration
autoload :Pagination, 'algoliasearch/pagination'
class << self
attr_reader :included_in
def included(klass)
@included_in ||= []
@included_in << klass
@included_in.uniq!
klass.class_eval do
extend ClassMethods
include InstanceMethods
end
end
end
class IndexSettings
DEFAULT_BATCH_SIZE = 1000
# AlgoliaSearch settings
OPTIONS = [
# Attributes
:searchableAttributes, :attributesForFaceting, :unretrievableAttributes, :attributesToRetrieve,
# Ranking
:ranking, :customRanking, :relevancyStrictness, # Replicas are handled via `add_replica`
# Faceting
:maxValuesPerFacet, :sortFacetValuesBy,
# Highlighting / Snippeting
:attributesToHighlight, :attributesToSnippet, :highlightPreTag, :highlightPostTag,
:snippetEllipsisText, :restrictHighlightAndSnippetArrays,
# Pagination
:hitsPerPage, :paginationLimitedTo,
# Typo
:minWordSizefor1Typo, :minWordSizefor2Typos, :typoTolerance, :allowTyposOnNumericTokens,
:disableTypoToleranceOnAttributes, :disableTypoToleranceOnWords, :separatorsToIndex,
# Language
:ignorePlurals, :removeStopWords, :camelCaseAttributes, :decompoundedAttributes,
:keepDiacriticsOnCharacters, :queryLanguages, :indexLanguages,
# Query Rules
:enableRules,
# Query Strategy
:queryType, :removeWordsIfNoResults, :advancedSyntax, :optionalWords,
:disablePrefixOnAttributes, :disableExactOnAttributes, :exactOnSingleWordQuery, :alternativesAsExact,
# Performance
:numericAttributesForFiltering, :allowCompressionOfIntegerArray,
# Advanced
:attributeForDistinct, :distinct, :replaceSynonymsInHighlight, :minProximity, :responseFields,
:maxFacetHits,
# Rails-specific
:synonyms, :placeholders, :altCorrections,
]
OPTIONS.each do |k|
define_method k do |v|
instance_variable_set("@#{k}", v)
end
end
def initialize(options, &block)
@options = options
instance_exec(&block) if block_given?
end
def use_serializer(serializer)
@serializer = serializer
end
def attribute(*names, &block)
raise ArgumentError.new('Cannot pass multiple attribute names if block given') if block_given? and names.length > 1
raise ArgumentError.new('Cannot specify additional attributes on a replica index') if @options[:replica]
@attributes ||= {}
names.flatten.each do |name|
@attributes[name.to_s] = block_given? ? Proc.new { |o| o.instance_eval(&block) } : Proc.new { |o| o.send(name) }
end
end
alias :attributes :attribute
def add_attribute(*names, &block)
raise ArgumentError.new('Cannot pass multiple attribute names if block given') if block_given? and names.length > 1
raise ArgumentError.new('Cannot specify additional attributes on a replica index') if @options[:replica]
@additional_attributes ||= {}
names.each do |name|
@additional_attributes[name.to_s] = block_given? ? Proc.new { |o| o.instance_eval(&block) } : Proc.new { |o| o.send(name) }
end
end
alias :add_attributes :add_attribute
def is_mongoid?(object)
defined?(::Mongoid::Document) && object.class.include?(::Mongoid::Document)
end
def is_sequel?(object)
defined?(::Sequel) && defined?(::Sequel::Model) && object.class < ::Sequel::Model
end
def is_active_record?(object)
!is_mongoid?(object) && !is_sequel?(object)
end
def get_default_attributes(object)
if is_mongoid?(object)
# work-around mongoid 2.4's unscoped method, not accepting a block
object.attributes
elsif is_sequel?(object)
object.to_hash
else
object.class.unscoped do
object.attributes
end
end
end
def get_attribute_names(object)
get_attributes(object).keys
end
def attributes_to_hash(attributes, object)
if attributes
Hash[attributes.map { |name, value| [name.to_s, value.call(object) ] }]
else
{}
end
end
def get_attributes(object)
# If a serializer is set, we ignore attributes
# everything should be done via the serializer
if not @serializer.nil?
attributes = @serializer.new(object).attributes
else
if @attributes.nil? || @attributes.length == 0
# no `attribute ...` have been configured, use the default attributes of the model
attributes = get_default_attributes(object)
else
# at least 1 `attribute ...` has been configured, therefore use ONLY the one configured
if is_active_record?(object)
object.class.unscoped do
attributes = attributes_to_hash(@attributes, object)
end
else
attributes = attributes_to_hash(@attributes, object)
end
end
end
attributes.merge!(attributes_to_hash(@additional_attributes, object)) if @additional_attributes
attributes = sanitize_attributes(attributes, Rails::Html::FullSanitizer.new) if @options[:sanitize]
if @options[:force_utf8_encoding] && Object.const_defined?(:RUBY_VERSION) && RUBY_VERSION.to_f > 1.8
attributes = encode_attributes(attributes)
end
attributes
end
def sanitize_attributes(v, sanitizer)
case v
when String
sanitizer.sanitize(v)
when Hash
v.each { |key, value| v[key] = sanitize_attributes(value, sanitizer) }
when Array
v.map { |x| sanitize_attributes(x, sanitizer) }
else
v
end
end
def encode_attributes(v)
case v
when String
v.dup.force_encoding('utf-8')
when Hash
v.each { |key, value| v[key] = encode_attributes(value) }
when Array
v.map { |x| encode_attributes(x) }
else
v
end
end
def geoloc(lat_attr = nil, lng_attr = nil, &block)
raise ArgumentError.new('Cannot specify additional attributes on a replica index') if @options[:replica]
add_attribute :_geoloc do |o|
block_given? ? o.instance_eval(&block) : { :lat => o.send(lat_attr).to_f, :lng => o.send(lng_attr).to_f }
end
end
def tags(*args, &block)
raise ArgumentError.new('Cannot specify additional attributes on a replica index') if @options[:replica]
add_attribute :_tags do |o|
v = block_given? ? o.instance_eval(&block) : args
v.is_a?(Array) ? v : [v]
end
end
def get_setting(name)
instance_variable_get("@#{name}")
end
def to_settings
settings = to_hash
# Remove the synonyms setting since those need to be set separately
settings.delete(:synonyms)
settings.delete("synonyms")
Algolia::Search::IndexSettings.new(settings)
end
def to_hash
settings = {}
OPTIONS.each do |k|
v = get_setting(k)
settings[setting_name(k)] = v if !v.nil?
end
if !@options[:replica]
settings[:replicas] = additional_indexes.select { |opts, s| opts[:replica] }.map do |opts, s|
name = opts[:index_name]
name = "#{name}_#{Rails.env.to_s}" if opts[:per_environment]
name = "virtual(#{name})" if opts[:virtual]
name
end
settings.delete(:replicas) if settings[:replicas].empty?
end
settings
end
def setting_name(name)
name.to_s.gsub(/::/, '/').
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
gsub(/([a-z\d])([A-Z])/,'\1_\2').
tr("-", "_").
downcase
end
def add_index(index_name, options = {}, &block)
raise ArgumentError.new('Cannot specify additional index on a replica index') if @options[:replica]
raise ArgumentError.new('No block given') if !block_given?
raise ArgumentError.new('Options auto_index and auto_remove cannot be set on nested indexes') if options[:auto_index] || options[:auto_remove]
@additional_indexes ||= {}
options[:index_name] = index_name
@additional_indexes[options] = IndexSettings.new(options, &block)
end
def add_replica(index_name, options = {}, &block)
raise ArgumentError.new('Cannot specify additional replicas on a replica index') if @options[:replica]
raise ArgumentError.new('No block given') if !block_given?
add_index(index_name, options.merge({ :replica => true, :primary_settings => self }), &block)
end
def additional_indexes
@additional_indexes || {}
end
end
# Default queueing system
if defined?(::ActiveJob::Base)
# lazy load the ActiveJob class to ensure the
# queue is initialized before using it
# see https://github.com/algolia/algoliasearch-rails/issues/69
autoload :AlgoliaJob, 'algoliasearch/algolia_job'
end
# these are the class methods added when AlgoliaSearch is included
module ClassMethods
def self.extended(base)
class < algolia_full_const_get(model_name.to_s), :per_page => algoliasearch_settings.get_setting(:hitsPerPage) || 10, :page => 1 }.merge(options)
attr_accessor :highlight_result, :snippet_result
if options[:synchronous] == true
if defined?(::Sequel) && defined?(::Sequel::Model) && self < Sequel::Model
class_eval do
copy_after_validation = instance_method(:after_validation)
define_method(:after_validation) do |*args|
super(*args)
copy_after_validation.bind(self).call
algolia_mark_synchronous
end
end
else
after_validation :algolia_mark_synchronous if respond_to?(:after_validation)
end
end
if options[:enqueue]
raise ArgumentError.new("Cannot use a enqueue if the `synchronous` option if set") if options[:synchronous]
proc = if options[:enqueue] == true
Proc.new do |record, remove|
AlgoliaJob.perform_later(record, remove ? 'algolia_remove_from_index!' : 'algolia_index!')
end
elsif options[:enqueue].respond_to?(:call)
options[:enqueue]
elsif options[:enqueue].is_a?(Symbol)
Proc.new { |record, remove| self.send(options[:enqueue], record, remove) }
else
raise ArgumentError.new("Invalid `enqueue` option: #{options[:enqueue]}")
end
algoliasearch_options[:enqueue] = Proc.new do |record, remove|
proc.call(record, remove) unless algolia_without_auto_index_scope
end
end
unless options[:auto_index] == false
if defined?(::Sequel) && defined?(::Sequel::Model) && self < Sequel::Model
class_eval do
copy_after_validation = instance_method(:after_validation)
copy_before_save = instance_method(:before_save)
define_method(:after_validation) do |*args|
super(*args)
copy_after_validation.bind(self).call
algolia_mark_must_reindex
end
define_method(:before_save) do |*args|
copy_before_save.bind(self).call
algolia_mark_for_auto_indexing
super(*args)
end
sequel_version = Gem::Version.new(Sequel.version)
if sequel_version >= Gem::Version.new('4.0.0') && sequel_version < Gem::Version.new('5.0.0')
copy_after_commit = instance_method(:after_commit)
define_method(:after_commit) do |*args|
super(*args)
copy_after_commit.bind(self).call
algolia_perform_index_tasks
end
else
copy_after_save = instance_method(:after_save)
define_method(:after_save) do |*args|
super(*args)
copy_after_save.bind(self).call
self.db.after_commit do
algolia_perform_index_tasks
end
end
end
end
else
after_validation :algolia_mark_must_reindex if respond_to?(:after_validation)
before_save :algolia_mark_for_auto_indexing if respond_to?(:before_save)
if respond_to?(:after_commit)
after_commit :algolia_perform_index_tasks
elsif respond_to?(:after_save)
after_save :algolia_perform_index_tasks
end
end
end
unless options[:auto_remove] == false
if defined?(::Sequel) && defined?(::Sequel::Model) && self < Sequel::Model
class_eval do
copy_after_destroy = instance_method(:after_destroy)
define_method(:after_destroy) do |*args|
copy_after_destroy.bind(self).call
algolia_enqueue_remove_from_index!(algolia_synchronous?)
super(*args)
end
end
else
after_destroy { |searchable| searchable.algolia_enqueue_remove_from_index!(algolia_synchronous?) } if respond_to?(:after_destroy)
end
end
end
def algolia_without_auto_index(&block)
self.algolia_without_auto_index_scope = true
begin
yield
ensure
self.algolia_without_auto_index_scope = false
end
end
def algolia_without_auto_index_scope=(value)
Thread.current["algolia_without_auto_index_scope_for_#{self.model_name}"] = value
end
def algolia_without_auto_index_scope
Thread.current["algolia_without_auto_index_scope_for_#{self.model_name}"]
end
def algolia_reindex!(batch_size = AlgoliaSearch::IndexSettings::DEFAULT_BATCH_SIZE, synchronous = false)
return if algolia_without_auto_index_scope
algolia_configurations.each do |options, settings|
next if algolia_indexing_disabled?(options)
algolia_ensure_init(options, settings)
index_name = algolia_index_name(options)
next if options[:replica]
last_task = nil
algolia_find_in_batches(batch_size) do |group|
if algolia_conditional_index?(options)
# delete non-indexable objects
ids = group.select { |o| !algolia_indexable?(o, options) }.map { |o| algolia_object_id_of(o, options) }
AlgoliaSearch.client.delete_objects(index_name, ids.select { |id| !id.blank? })
# select only indexable objects
group = group.select { |o| algolia_indexable?(o, options) }
end
objects = group.map do |o|
attributes = settings.get_attributes(o)
unless attributes.class == Hash
attributes = attributes.to_hash
end
attributes.merge 'objectID' => algolia_object_id_of(o, options)
end
last_task = AlgoliaSearch.client.save_objects(index_name, objects).last.task_id
end
AlgoliaSearch.client.wait_for_task(index_name, last_task) if last_task and (synchronous || options[:synchronous])
end
nil
end
# reindex whole database using a extra temporary index + move operation
def algolia_reindex(batch_size = AlgoliaSearch::IndexSettings::DEFAULT_BATCH_SIZE, synchronous = false)
return if algolia_without_auto_index_scope
algolia_configurations.each do |options, settings|
next if algolia_indexing_disabled?(options)
next if options[:replica]
algolia_ensure_init(options, settings)
index_name = algolia_index_name(options)
# fetch the master settings
master_settings = AlgoliaSearch.client.get_settings(index_name).to_hash rescue {} # if master doesn't exist yet
master_exists = master_settings != {}
master_settings.merge!(settings.to_hash)
# remove the replicas of the temporary index
master_settings.delete :replicas
master_settings.delete 'replicas'
# init temporary index
tmp_index_name = "#{index_name}.tmp"
tmp_options = options.merge({ :index_name => tmp_index_name })
tmp_options.delete(:per_environment) # already included in the temporary index_name
tmp_settings = settings.dup
if options[:check_settings] == false && master_exists
task_id = AlgoliaSearch.client.operation_index(
index_name,
Algolia::Search::OperationIndexParams.new(operation: Algolia::Search::OperationType::COPY, destination: tmp_index_name, scope: %w[settings synonyms rules])
).task_id
AlgoliaSearch.client.wait_for_task(index_name, task_id)
end
algolia_find_in_batches(batch_size) do |group|
if algolia_conditional_index?(options)
# select only indexable objects
group = group.select { |o| algolia_indexable?(o, tmp_options) }
end
objects = group.map { |o| tmp_settings.get_attributes(o).merge 'objectID' => algolia_object_id_of(o, tmp_options) }
AlgoliaSearch.client.save_objects(tmp_index_name, objects)
end
task_id = AlgoliaSearch.client.operation_index(
tmp_index_name,
Algolia::Search::OperationIndexParams.new(operation: "move", destination: index_name)
).task_id
AlgoliaSearch.client.wait_for_task(index_name, task_id) if synchronous || options[:synchronous]
end
nil
end
def algolia_set_settings(synchronous = false)
algolia_configurations.each do |options, settings|
if options[:primary_settings] && options[:inherit]
primary = options[:primary_settings].to_settings.to_hash
primary.delete :replicas
primary.delete 'replicas'
final_settings = primary.merge(settings.to_settings.to_hash)
else
final_settings = settings.to_settings.to_hash
end
s = final_settings.map do |k, v|
[settings.setting_name(k), v]
end.to_h
synonyms = s.delete("synonyms") || s.delete(:synonyms)
unless synonyms.nil? || synonyms.empty?
resp = AlgoliaSearch.client.save_synonyms(index_name,synonyms.map {|s| Algolia::Search::SynonymHit.new({object_id: s.join("-"), synonyms: s, type: "synonym"}) } )
AlgoliaSearch.client.wait_for_task(index_name, resp.task_id) if synchronous || options[:synchronous]
end
resp = AlgoliaSearch.client.set_settings(index_name, Algolia::Search::IndexSettings.new(s))
AlgoliaSearch.client.wait_for_task(index_name, resp.task_id) if synchronous || options[:synchronous]
end
end
def algolia_index_objects(objects, synchronous = false)
algolia_configurations.each do |options, settings|
next if algolia_indexing_disabled?(options)
algolia_ensure_init(options, settings)
index_name = algolia_index_name(options)
next if options[:replica]
tasks = AlgoliaSearch.client.save_objects(index_name, objects.map { |o| settings.get_attributes(o).merge 'objectID' => algolia_object_id_of(o, options) })
tasks.each do |task|
AlgoliaSearch.client.wait_for_task(index_name, task.task_id) if synchronous || options[:synchronous]
end
end
end
def algolia_index!(object, synchronous = false)
return if algolia_without_auto_index_scope
algolia_configurations.each do |options, settings|
next if algolia_indexing_disabled?(options)
object_id = algolia_object_id_of(object, options)
index_name = algolia_index_name(options)
algolia_ensure_init(options, settings)
next if options[:replica]
if algolia_indexable?(object, options)
raise ArgumentError.new("Cannot index a record with a blank objectID") if object_id.blank?
resp = AlgoliaSearch.client.save_object(index_name, settings.get_attributes(object).merge({ 'objectID' => algolia_object_id_of(object, options) }))
if synchronous || options[:synchronous]
AlgoliaSearch.client.wait_for_task(index_name, resp.task_id)
end
elsif algolia_conditional_index?(options) && !object_id.blank?
# remove non-indexable objects
resp = AlgoliaSearch.client.delete_object(index_name, object_id)
if synchronous || options[:synchronous]
AlgoliaSearch.client.wait_for_task(index_name, resp.task_id)
end
end
end
nil
end
def algolia_remove_from_index!(object, synchronous = false)
return if algolia_without_auto_index_scope
object_id = algolia_object_id_of(object)
raise ArgumentError.new("Cannot index a record with a blank objectID") if object_id.blank?
algolia_configurations.each do |options, settings|
next if algolia_indexing_disabled?(options)
algolia_ensure_init(options, settings)
index_name = algolia_index_name(options)
next if options[:replica]
resp = AlgoliaSearch.client.delete_object(index_name, object_id)
if synchronous || options[:synchronous]
AlgoliaSearch.client.wait_for_task(index_name, resp.task_id)
end
end
nil
end
def algolia_clear_index!(synchronous = false)
algolia_configurations.each do |options, settings|
next if algolia_indexing_disabled?(options) || options[:replica]
algolia_ensure_init(options, settings)
index_name = algolia_index_name(options)
res = AlgoliaSearch.client.clear_objects(index_name)
if synchronous || options[:synchronous]
AlgoliaSearch.client.wait_for_task(index_name, res.task_id)
end
end
nil
end
def algolia_raw_search(q, params = {})
index_name_base = params.delete(:index) ||
params.delete('index') ||
params.delete(:replica) ||
params.delete('replica')
opts = algoliasearch_options
unless index_name_base.nil?
algolia_configurations.each do |o, s|
if o[:index_name].to_s == index_name_base.to_s
opts = o
ensure_algolia_index(index_name_base)
end
end
end
index_name = algolia_index_name(opts, index_name_base)
AlgoliaSearch.client.search_single_index(index_name,Hash[params.to_h.map { |k,v| [k.to_s, v.to_s] }].merge({query: q})).to_hash
end
module AdditionalMethods
def self.extended(base)
class < 0
end
json = algolia_raw_search(q, params)
hit_ids = json[:hits].map { |hit| hit[:objectID] }
if defined?(::Mongoid::Document) && self.include?(::Mongoid::Document)
condition_key = algolia_object_id_method.in
else
condition_key = algolia_object_id_method
end
results_by_id = algoliasearch_options[:type].where(condition_key => hit_ids).index_by do |hit|
algolia_object_id_of(hit)
end
results = json[:hits].map do |hit|
o = results_by_id[hit[:objectID].to_s]
if o
o.highlight_result = hit[:_highlightResult]
o.snippet_result = hit[:_snippetResult]
o
end
end.compact
# Algolia has a default limit of 1000 retrievable hits
total_hits = json[:nbHits].to_i < json[:nbPages].to_i * json[:hitsPerPage].to_i ?
json[:nbHits].to_i: json[:nbPages].to_i * json[:hitsPerPage].to_i
res = AlgoliaSearch::Pagination.create(results, total_hits, algoliasearch_options.merge({ :page => json[:page].to_i + 1, :per_page => json[:hitsPerPage] }))
res.extend(AdditionalMethods)
res.send(:algolia_init_raw_answer, json)
res
end
def algolia_search_for_facet_values(facet, text, params = {})
index_name = params.delete(:index) ||
params.delete('index') ||
params.delete(:replica) ||
params.delete('replicas')
index_name ||= algolia_index_name(algoliasearch_options)
req = Algolia::Search::SearchForFacetValuesRequest.new({facet_query: text, params: params.to_query})
AlgoliaSearch.client.search_for_facet_values(index_name, facet, req).facet_hits
end
# deprecated (renaming)
alias :algolia_search_facet :algolia_search_for_facet_values
def ensure_algolia_index(name = nil)
if name
algolia_configurations.each do |o, s|
return algolia_ensure_init(o, s) if o[:index_name].to_s == name.to_s
end
raise ArgumentError.new("Invalid index/replica name: #{name}")
end
algolia_ensure_init
end
def algolia_index_name(options = nil, index_name = nil)
options ||= algoliasearch_options
name = index_name || options[:index_name] || model_name.to_s.gsub('::', '_')
name = "#{name}_#{Rails.env.to_s}" if options[:per_environment]
name
end
def algolia_must_reindex?(object)
# use +algolia_dirty?+ method if implemented
return object.send(:algolia_dirty?) if (object.respond_to?(:algolia_dirty?))
# Loop over each index to see if a attribute used in records has changed
algolia_configurations.each do |options, settings|
next if algolia_indexing_disabled?(options)
next if options[:replica]
return true if algolia_object_id_changed?(object, options)
settings.get_attribute_names(object).each do |k|
return true if algolia_attribute_changed?(object, k, true)
end
[options[:if], options[:unless]].each do |condition|
case condition
when nil
when String, Symbol
return true if algolia_attribute_changed?(object, condition, true)
else
# if the :if, :unless condition is a anything else,
# we have no idea whether we should reindex or not
# let's always reindex then
return true
end
end
end
# By default, we don't reindex
return false
end
protected
def algolia_ensure_init(options = nil, settings = nil, index_settings_hash = nil)
raise ArgumentError.new('No `algoliasearch` block found in your model.') if algoliasearch_settings.nil?
@algolia_indexes_init ||= {}
options ||= algoliasearch_options
settings ||= algoliasearch_settings
return if @algolia_indexes_init[settings]
index_name = algolia_index_name(options)
index_settings_hash ||= settings.to_settings.to_hash
index_settings_hash = options[:primary_settings].to_settings.to_hash.merge(index_settings_hash) if options[:inherit]
replicas = index_settings_hash.delete(:replicas) || index_settings_hash.delete('replicas')
index_settings_hash[:replicas] = replicas unless replicas.nil? || options[:inherit]
options[:check_settings] = true if options[:check_settings].nil?
current_settings = if options[:check_settings] && !algolia_indexing_disabled?(options)
AlgoliaSearch.client.get_settings(index_name, {:getVersion => 1}).to_hash rescue nil # if the index doesn't exist
end
if !algolia_indexing_disabled?(options) && options[:check_settings] && algoliasearch_settings_changed?(current_settings, index_settings_hash)
s = index_settings_hash.map do |k, v|
[settings.setting_name(k), v]
end.to_h
synonyms = s.delete("synonyms") || s.delete(:synonyms)
unless synonyms.nil? || synonyms.empty?
resp = AlgoliaSearch.client.save_synonyms(index_name,synonyms.map {|s| Algolia::Search::SynonymHit.new({object_id: s.join("-"), synonyms: s, type: "synonym"}) } )
AlgoliaSearch.client.wait_for_task(index_name, resp.task_id) if options[:synchronous]
end
resp = AlgoliaSearch.client.set_settings(index_name, Algolia::Search::IndexSettings.new(s))
AlgoliaSearch.client.wait_for_task(index_name, resp.task_id) if options[:synchronous]
end
return
end
private
def algolia_configurations
raise ArgumentError.new('No `algoliasearch` block found in your model.') if algoliasearch_settings.nil?
if @configurations.nil?
@configurations = {}
@configurations[algoliasearch_options] = algoliasearch_settings
algoliasearch_settings.additional_indexes.each do |k,v|
@configurations[k] = v
if v.additional_indexes.any?
v.additional_indexes.each do |options, index|
@configurations[options] = index
end
end
end
end
@configurations
end
def algolia_object_id_method(options = nil)
options ||= algoliasearch_options
options[:id] || options[:object_id] || :id
end
def algolia_object_id_of(o, options = nil)
o.send(algolia_object_id_method(options)).to_s
end
def algolia_object_id_changed?(o, options = nil)
changed = algolia_attribute_changed?(o, algolia_object_id_method(options), false)
changed.nil? ? false : changed
end
def algoliasearch_settings_changed?(prev, current)
return true if prev.nil?
current.each do |k, v|
prev_v = prev[k.to_sym] || prev[k.to_s]
if v.is_a?(Array) and prev_v.is_a?(Array)
# compare array of strings, avoiding symbols VS strings comparison
return true if v.map { |x| x.to_s } != prev_v.map { |x| x.to_s }
elsif v.blank? # blank? check is needed to compare [] and null
return true unless prev_v.blank?
else
return true if prev_v != v
end
end
false
end
def algolia_full_const_get(name)
list = name.split('::')
list.shift if list.first.blank?
obj = Object.const_defined?(:RUBY_VERSION) && RUBY_VERSION.to_f < 1.9 ? Object : self
list.each do |x|
# This is required because const_get tries to look for constants in the
# ancestor chain, but we only want constants that are HERE
obj = obj.const_defined?(x) ? obj.const_get(x) : obj.const_missing(x)
end
obj
end
def algolia_conditional_index?(options = nil)
options ||= algoliasearch_options
options[:if].present? || options[:unless].present?
end
def algolia_indexable?(object, options = nil)
options ||= algoliasearch_options
if_passes = options[:if].blank? || algolia_constraint_passes?(object, options[:if])
unless_passes = options[:unless].blank? || !algolia_constraint_passes?(object, options[:unless])
if_passes && unless_passes
end
def algolia_constraint_passes?(object, constraint)
case constraint
when Symbol
object.send(constraint)
when String
object.send(constraint.to_sym)
when Enumerable
# All constraints must pass
constraint.all? { |inner_constraint| algolia_constraint_passes?(object, inner_constraint) }
else
if constraint.respond_to?(:call) # Proc
constraint.call(object)
else
raise ArgumentError, "Unknown constraint type: #{constraint} (#{constraint.class})"
end
end
end
def algolia_indexing_disabled?(options = nil)
options ||= algoliasearch_options
constraint = options[:disable_indexing] || options['disable_indexing']
case constraint
when nil
return false
when true, false
return constraint
when String, Symbol
return send(constraint)
else
return constraint.call if constraint.respond_to?(:call) # Proc
end
raise ArgumentError, "Unknown constraint type: #{constraint} (#{constraint.class})"
end
def algolia_find_in_batches(batch_size, &block)
if (defined?(::ActiveRecord) && ancestors.include?(::ActiveRecord::Base)) || respond_to?(:find_in_batches)
find_in_batches(:batch_size => batch_size, &block)
elsif defined?(::Sequel) && defined?(::Sequel::Model) && self < Sequel::Model
dataset.extension(:pagination).each_page(batch_size, &block)
else
# don't worry, mongoid has its own underlying cursor/streaming mechanism
items = []
all.each do |item|
items << item
if items.length % batch_size == 0
yield items
items = []
end
end
yield items unless items.empty?
end
end
def algolia_attribute_changed?(object, attr_name, default)
# if one of two method is implemented, we return its result
# true/false means whether it has changed or not
# +#{attr_name}_changed?+ always defined for automatic attributes but deprecated after Rails 5.2
# +will_save_change_to_#{attr_name}?+ should be use instead for Rails 5.2+, also defined for automatic attributes.
# If none of the method are defined, it's a dynamic attribute
method_name = "#{attr_name}_changed?"
if object.respond_to?(method_name)
# If +#{attr_name}_changed?+ respond we want to see if the method is user defined or if it's automatically
# defined by Rails.
# If it's user-defined, we call it.
# If it's automatic we check ActiveRecord version to see if this method is deprecated
# and try to call +will_save_change_to_#{attr_name}?+ instead.
# See: https://github.com/algolia/algoliasearch-rails/pull/338
# This feature is not compatible with Ruby 1.8
# In this case, we always call #{attr_name}_changed?
if Object.const_defined?(:RUBY_VERSION) && RUBY_VERSION.to_f < 1.9
return object.send(method_name)
end
unless automatic_changed_method?(object, method_name) && automatic_changed_method_deprecated?
return object.send(method_name)
end
end
if object.respond_to?("will_save_change_to_#{attr_name}?")
return object.send("will_save_change_to_#{attr_name}?")
end
# We don't know if the attribute has changed, so return the default passed
default
end
def automatic_changed_method?(object, method_name)
raise ArgumentError.new("Method #{method_name} doesn't exist on #{object.class.name}") unless object.respond_to?(method_name)
file = object.method(method_name).source_location[0]
file.end_with?("active_model/attribute_methods.rb")
end
def automatic_changed_method_deprecated?
(defined?(::ActiveRecord) && ActiveRecord::VERSION::MAJOR >= 5 && ActiveRecord::VERSION::MINOR >= 1) ||
(defined?(::ActiveRecord) && ActiveRecord::VERSION::MAJOR > 5)
end
end
# these are the instance methods included
module InstanceMethods
def self.included(base)
base.instance_eval do
alias_method :index!, :algolia_index! unless method_defined? :index!
alias_method :remove_from_index!, :algolia_remove_from_index! unless method_defined? :remove_from_index!
end
end
def algolia_index!(synchronous = false)
self.class.algolia_index!(self, synchronous || algolia_synchronous?)
end
def algolia_remove_from_index!(synchronous = false)
self.class.algolia_remove_from_index!(self, synchronous || algolia_synchronous?)
end
def algolia_enqueue_remove_from_index!(synchronous)
if algoliasearch_options[:enqueue]
algoliasearch_options[:enqueue].call(self, true) unless self.class.send(:algolia_indexing_disabled?, algoliasearch_options)
else
algolia_remove_from_index!(synchronous || algolia_synchronous?)
end
end
def algolia_enqueue_index!(synchronous)
if algoliasearch_options[:enqueue]
algoliasearch_options[:enqueue].call(self, false) unless self.class.send(:algolia_indexing_disabled?, algoliasearch_options)
else
algolia_index!(synchronous)
end
end
private
def algolia_synchronous?
@algolia_synchronous == true
end
def algolia_mark_synchronous
@algolia_synchronous = true
end
def algolia_mark_for_auto_indexing
@algolia_auto_indexing = true
end
def algolia_mark_must_reindex
# algolia_must_reindex flag is reset after every commit as part. If we must reindex at any point in
# a stransaction, keep flag set until it is explicitly unset
@algolia_must_reindex ||=
if defined?(::Sequel) && defined?(::Sequel::Model) && is_a?(Sequel::Model)
new? || self.class.algolia_must_reindex?(self)
else
new_record? || self.class.algolia_must_reindex?(self)
end
true
end
def algolia_perform_index_tasks
return if !@algolia_auto_indexing || @algolia_must_reindex == false
algolia_enqueue_index!(algolia_synchronous?)
remove_instance_variable(:@algolia_auto_indexing) if instance_variable_defined?(:@algolia_auto_indexing)
remove_instance_variable(:@algolia_synchronous) if instance_variable_defined?(:@algolia_synchronous)
remove_instance_variable(:@algolia_must_reindex) if instance_variable_defined?(:@algolia_must_reindex)
end
end
end