begin
require "rubygems"
require "bundler"
Bundler.setup :default
rescue => e
puts "AlgoliaSearch: #{e.message}"
end
require 'algoliasearch'
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
# AlgoliaSearch settings
OPTIONS = [:minWordSizefor1Typo, :minWordSizefor2Typos, :typoTolerance,
:hitsPerPage, :attributesToRetrieve,
:attributesToHighlight, :attributesToSnippet, :attributesToIndex, :searchableAttributes,
:highlightPreTag, :highlightPostTag,
:ranking, :customRanking, :queryType, :attributesForFaceting,
:separatorsToIndex, :optionalWords, :attributeForDistinct,
:synonyms, :placeholders, :removeWordsIfNoResults, :replaceSynonymsInHighlight,
:unretrievableAttributes, :disableTypoToleranceOnWords, :disableTypoToleranceOnAttributes, :altCorrections,
:ignorePlurals, :maxValuesPerFacet, :distinct, :numericAttributesToIndex, :numericAttributesForFiltering,
:allowTyposOnNumericTokens, :allowCompressionOfIntegerArray,
:advancedSyntax]
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
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[:slave] || @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[:slave] || @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) && 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)
res = if @attributes.nil? || @attributes.length == 0
get_default_attributes(object).keys
else
@attributes.keys
end
res += @additional_attributes.keys if @additional_attributes
res
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)
attributes = if @attributes.nil? || @attributes.length == 0
get_default_attributes(object)
else
if is_active_record?(object)
object.class.unscoped do
attributes_to_hash(@attributes, object)
end
else
attributes_to_hash(@attributes, object)
end
end
attributes.merge!(attributes_to_hash(@additional_attributes, object))
if @options[:sanitize]
sanitizer = begin
::HTML::FullSanitizer.new
rescue NameError
# from rails 4.2
::Rails::Html::FullSanitizer.new
end
attributes = sanitize_attributes(attributes, sanitizer)
end
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.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, lng_attr)
raise ArgumentError.new('Cannot specify additional attributes on a replica index') if @options[:slave] || @options[:replica]
add_attribute :_geoloc do |o|
{ :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[:slave] || @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 = {}
OPTIONS.each do |k|
v = get_setting(k)
settings[k] = v if !v.nil?
end
if !@options[:slave] && !@options[:replica]
settings[:slaves] = additional_indexes.select { |opts, s| opts[:slave] }.map do |opts, s|
name = opts[:index_name]
name = "#{name}_#{Rails.env.to_s}" if opts[:per_environment]
name
end
settings.delete(:slaves) if settings[:slaves].empty?
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
end
settings.delete(:replicas) if settings[:replicas].empty?
end
settings
end
def add_index(index_name, options = {}, &block)
raise ArgumentError.new('Cannot specify additional index on a replica index') if @options[:slave] || @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 ||= {}
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] })
options[:index_name] = index_name
@additional_indexes[options] = IndexSettings.new(options, Proc.new)
end
def add_replica(index_name, options = {}, &block)
raise ArgumentError.new('Cannot specify additional replicas on a replica index') if @options[:slave] || @options[:replica]
raise ArgumentError.new('No block given') if !block_given?
add_index(index_name, options.merge({ :replica => true }), &block)
end
def add_slave(index_name, options = {}, &block)
raise ArgumentError.new('Cannot specify additional slaves on a slave index') if @options[:slave] || @options[:replica]
raise ArgumentError.new('No block given') if !block_given?
add_index(index_name, options.merge({ :slave => true }), &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
# this class wraps an Algolia::Index object ensuring all raised exceptions
# are correctly logged or thrown depending on the `raise_on_failure` option
class SafeIndex
def initialize(name, raise_on_failure)
@index = ::Algolia::Index.new(name)
@raise_on_failure = raise_on_failure.nil? || raise_on_failure
end
::Algolia::Index.instance_methods(false).each do |m|
define_method(m) do |*args, &block|
SafeIndex.log_or_throw(m) do
@index.send(m, *args, &block)
end
end
end
# special handling of wait_task to handle null task_id
def wait_task(task_id)
return if task_id.nil? && !@raise_on_failure # ok
SafeIndex.log_or_throw(:wait_task) do
@index.wait_task(task_id)
end
end
# special handling of get_settings to avoid raising errors on 404
def get_settings(*args)
SafeIndex.log_or_throw(:get_settings) do
begin
@index.get_settings(*args)
rescue Algolia::AlgoliaError => e
return {} if e.status == 404 # not fatal
raise e
end
end
end
# expose move as well
def self.move_index(old_name, new_name)
SafeIndex.log_or_throw(:move_index) do
::Algolia.move_index(old_name, new_name)
end
end
private
def self.log_or_throw(method, &block)
begin
yield
rescue Algolia::AlgoliaError => e
raise e if @raise_on_failure
# log the error
(Rails.logger || Logger.new(STDOUT)).error("[algoliasearch-rails] #{e.message}")
# return something
case method.to_s
when 'search'
# some attributes are required
{ 'hits' => [], 'hitsPerPage' => 0, 'page' => 0, 'facets' => {}, 'error' => e }
else
# empty answer
{ 'error' => e }
end
end
end
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) && 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) && self < Sequel::Model
class_eval do
copy_after_validation = instance_method(:after_validation)
copy_before_save = instance_method(:before_save)
copy_after_commit = instance_method(:after_commit)
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
define_method(:after_commit) do |*args|
super(*args)
copy_after_commit.bind(self).call
algolia_perform_index_tasks
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) && 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)
@algolia_without_auto_index_scope = true
begin
yield
ensure
@algolia_without_auto_index_scope = false
end
end
def algolia_reindex!(batch_size = 1000, synchronous = false)
return if @algolia_without_auto_index_scope
algolia_configurations.each do |options, settings|
next if algolia_indexing_disabled?(options)
index = algolia_ensure_init(options, settings)
next if options[:slave] || 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) }
index.delete_objects(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 = index.save_objects(objects)
end
index.wait_task(last_task["taskID"]) 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 = 1000, synchronous = false)
return if @algolia_without_auto_index_scope
algolia_configurations.each do |options, settings|
next if algolia_indexing_disabled?(options)
next if options[:slave] || options[:replica]
# fetch the master settings
master_index = algolia_ensure_init(options, settings)
master_settings = master_index.get_settings rescue {} # if master doesn't exist yet
master_settings.merge!(JSON.parse(settings.to_settings.to_json)) # convert symbols to strings
# remove the replicas of the temporary index
master_settings.delete :slaves
master_settings.delete 'slaves'
master_settings.delete :replicas
master_settings.delete 'replicas'
# init temporary index
index_name = algolia_index_name(options)
tmp_options = options.merge({ :index_name => "#{index_name}.tmp" })
tmp_options.delete(:per_environment) # already included in the temporary index_name
tmp_settings = settings.dup
tmp_index = algolia_ensure_init(tmp_options, tmp_settings, master_settings)
algolia_find_in_batches(batch_size) do |group|
if algolia_conditional_index?(tmp_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) }
tmp_index.save_objects(objects)
end
move_task = SafeIndex.move_index(tmp_index.name, index_name)
master_index.wait_task(move_task["taskID"]) if synchronous || options[:synchronous]
end
nil
end
def algolia_index_objects(objects, synchronous = false)
algolia_configurations.each do |options, settings|
next if algolia_indexing_disabled?(options)
index = algolia_ensure_init(options, settings)
next if options[:slave] || options[:replica]
task = index.save_objects(objects.map { |o| settings.get_attributes(o).merge 'objectID' => algolia_object_id_of(o, options) })
index.wait_task(task["taskID"]) if synchronous || options[:synchronous]
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 = algolia_ensure_init(options, settings)
next if options[:slave] || options[:replica]
if algolia_indexable?(object, options)
raise ArgumentError.new("Cannot index a record with a blank objectID") if object_id.blank?
if synchronous || options[:synchronous]
index.add_object!(settings.get_attributes(object), object_id)
else
index.add_object(settings.get_attributes(object), object_id)
end
elsif algolia_conditional_index?(options) && !object_id.blank?
# remove non-indexable objects
if synchronous || options[:synchronous]
index.delete_object!(object_id)
else
index.delete_object(object_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)
index = algolia_ensure_init(options, settings)
next if options[:slave] || options[:replica]
if synchronous || options[:synchronous]
index.delete_object!(object_id)
else
index.delete_object(object_id)
end
end
nil
end
def algolia_clear_index!(synchronous = false)
algolia_configurations.each do |options, settings|
next if algolia_indexing_disabled?(options)
index = algolia_ensure_init(options, settings)
next if options[:slave] || options[:replica]
synchronous || options[:synchronous] ? index.clear! : index.clear
@algolia_indexes[settings] = nil
end
nil
end
def algolia_raw_search(q, params = {})
index_name = params.delete(:index) ||
params.delete('index') ||
params.delete(:slave) ||
params.delete('slave') ||
params.delete(:replica) ||
params.delete('replica')
index = algolia_index(index_name)
index.search(q, Hash[params.map { |k,v| [k.to_s, v.to_s] }])
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']]
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'] < json['nbPages'] * json['hitsPerPage'] ?
json['nbHits'] : json['nbPages'] * json['hitsPerPage']
res = AlgoliaSearch::Pagination.create(results, total_hits, algoliasearch_options.merge({ :page => json['page'] + 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(:slave) ||
params.delete('slave') ||
params.delete(:replica) ||
params.delete('replicas')
index = algolia_index(index_name)
query = Hash[params.map { |k, v| [k.to_s, v.to_s] }]
index.search_facet(facet, text, query)['facetHits']
end
# deprecated (renaming)
alias :algolia_search_facet :algolia_search_for_facet_values
def 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)
options ||= algoliasearch_options
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)
algolia_configurations.each do |options, settings|
next if options[:slave] || options[:replica]
return true if algolia_object_id_changed?(object, options)
settings.get_attribute_names(object).each do |k|
changed_method = "#{k}_changed?"
return true if !object.respond_to?(changed_method) || object.send(changed_method)
end
[options[:if], options[:unless]].each do |condition|
case condition
when nil
when String, Symbol
changed_method = "#{condition}_changed?"
return true if !object.respond_to?(changed_method) || object.send(changed_method)
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
return false
end
protected
def algolia_ensure_init(options = nil, settings = nil, index_settings = nil)
raise ArgumentError.new('No `algoliasearch` block found in your model.') if algoliasearch_settings.nil?
@algolia_indexes ||= {}
options ||= algoliasearch_options
settings ||= algoliasearch_settings
return @algolia_indexes[settings] if @algolia_indexes[settings]
@algolia_indexes[settings] = SafeIndex.new(algolia_index_name(options), algoliasearch_options[:raise_on_failure])
current_settings = @algolia_indexes[settings].get_settings rescue nil # if the index doesn't exist
if !algolia_indexing_disabled?(options) && (index_settings || algoliasearch_settings_changed?(current_settings, settings.to_settings))
index_settings ||= settings.to_settings
used_slaves = !current_settings.nil? && !current_settings['slaves'].nil?
replicas = index_settings.delete(:replicas) ||
index_settings.delete('replicas') ||
index_settings.delete(:slaves) ||
index_settings.delete('slaves')
index_settings[used_slaves ? :slaves : :replicas] = replicas
@algolia_indexes[settings].set_settings(index_settings)
end
@algolia_indexes[settings]
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)
m = "#{algolia_object_id_method(options)}_changed?"
o.respond_to?(m) ? o.send(m) : false
end
def algoliasearch_settings_changed?(prev, current)
return true if prev.nil?
current.each do |k, v|
prev_v = 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 }
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) && 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
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 =
if defined?(::Sequel) && 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