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 module AlgoliaSearch class NotConfigured < StandardError; end class BadConfiguration < StandardError; end class NoBlockGiven < 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.send :include, InstanceMethods klass.extend ClassMethods end end class IndexOptions # AlgoliaSearch settings OPTIONS = [:attributesToIndex, :minWordSizefor1Typo, :minWordSizefor2Typos, :hitsPerPage, :attributesToRetrieve, :attributesToHighlight, :attributesToSnippet, :attributesToIndex, :ranking, :customRanking, :queryType, :attributesForFaceting] OPTIONS.each do |k| define_method k do |v| instance_variable_set("@#{k}", v) end end # attributes to consider attr_accessor :attributes def initialize(block) instance_exec(&block) if block end def attribute(*names) self.attributes ||= [] self.attributes += names end def get(setting) instance_variable_get("@#{setting}") end def to_settings settings = {} OPTIONS.each do |k| v = get(k) settings[k] = v if !v.nil? end settings end end # these are the class methods added when AlgoliaSearch is included module ClassMethods def algoliasearch(options = {}, &block) @index_options = IndexOptions.new(block_given? ? Proc.new : nil) attr_accessor :highlight_result if options[:synchronous] == true after_validation :mark_synchronous if respond_to?(:before_validation) end unless options[:auto_index] == false after_validation :mark_must_reindex if respond_to?(:after_validation) before_save :mark_for_auto_indexing if respond_to?(:before_save) after_save :perform_index_tasks if respond_to?(:after_save) end unless options[:auto_remove] == false after_destroy { |searchable| searchable.remove_from_index! } if respond_to?(:after_destroy) end @options = { type: model_name, per_page: @index_options.get(:hitsPerPage) || 10, page: 1 }.merge(options) end def reindex!(batch_size = 1000, synchronous = false) ensure_init last_task = nil find_in_batches(batch_size: batch_size) do |group| objects = group.map { |o| attributes(o).merge 'objectID' => o.id.to_s } last_task = @index.save_objects(objects) end @index.wait_task(last_task["taskID"]) if last_task and synchronous == true end def index!(object, synchronous = false) ensure_init if synchronous @index.add_object!(attributes(object), object.id.to_s) else @index.add_object(attributes(object), object.id.to_s) end end def remove_from_index!(object, synchronous = false) ensure_init if synchronous @index.delete_object!(object.id.to_s) else @index.delete_object(object.id.to_s) end end def clear_index! ensure_init @index.delete @index = nil end def search(q, settings = {}) json = @index.search(q, Hash[settings.map { |k,v| [k.to_s, v.to_s] }]) results = json['hits'].map do |hit| o = Object.const_get(@options[:type]).find(hit['objectID']) o.highlight_result = hit['_highlightResult'] o end AlgoliaSearch::Pagination.create(results, json['nbHits'].to_i, @options) end def ensure_init new_settings = @index_options.to_settings return if @index and !index_settings_changed?(@settings, new_settings) @index = Algolia::Index.new(index_name) current_settings = @index.get_settings rescue nil # if the index doesn't exist @index.set_settings(new_settings) if index_settings_changed?(current_settings, new_settings) @settings = new_settings end def must_reindex?(object) return true if object.id_changed? attributes(object).each do |k, v| changed_method = "#{k}_changed?" return true if object.respond_to?(changed_method) && object.send(changed_method) end return false end def index_name name = @options[:index_name] || model_name name = "#{name}_#{Rails.env.to_s}" if @options[:per_environment] name end private def attributes(object) return object.attributes if @index_options.attributes.nil? or @index_options.attributes.length == 0 Hash[@index_options.attributes.map { |attr| [attr.to_s, object.send(attr)] }] end def index_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 end # these are the instance methods included module InstanceMethods def index! self.class.index!(self, synchronous?) end def remove_from_index! self.class.remove_from_index!(self, synchronous?) end private def synchronous? @synchronous == true end def mark_synchronous @synchronous = true end def mark_for_auto_indexing @auto_indexing = true end def mark_must_reindex @must_reindex = self.class.must_reindex?(self) true end def perform_index_tasks return if !@auto_indexing || @must_reindex == false index! remove_instance_variable(:@auto_indexing) if instance_variable_defined?(:@auto_indexing) remove_instance_variable(:@synchronous) if instance_variable_defined?(:@synchronous) remove_instance_variable(:@must_reindex) if instance_variable_defined?(:@must_reindex) end end end