require 'thinking_sphinx/active_record/attribute_updates' require 'thinking_sphinx/active_record/delta' require 'thinking_sphinx/active_record/has_many_association' require 'thinking_sphinx/active_record/scopes' module ThinkingSphinx # Core additions to ActiveRecord models - define_index for creating indexes # for models. If you want to interrogate the index objects created for the # model, you can use the class-level accessor :sphinx_indexes. # module ActiveRecord def self.included(base) base.class_eval do class_inheritable_array :sphinx_indexes, :sphinx_facets class << self def set_sphinx_primary_key(attribute) @sphinx_primary_key_attribute = attribute end def primary_key_for_sphinx @sphinx_primary_key_attribute || primary_key end # Allows creation of indexes for Sphinx. If you don't do this, there # isn't much point trying to search (or using this plugin at all, # really). # # An example or two: # # define_index # indexes :id, :as => :model_id # indexes name # end # # You can also grab fields from associations - multiple levels deep # if necessary. # # define_index do # indexes tags.name, :as => :tag # indexes articles.content # indexes orders.line_items.product.name, :as => :product # end # # And it will automatically concatenate multiple fields: # # define_index do # indexes [author.first_name, author.last_name], :as => :author # end # # The #indexes method is for fields - if you want attributes, use # #has instead. All the same rules apply - but keep in mind that # attributes are for sorting, grouping and filtering, not searching. # # define_index do # # fields ... # # has created_at, updated_at # end # # One last feature is the delta index. This requires the model to # have a boolean field named 'delta', and is enabled as follows: # # define_index do # # fields ... # # attributes ... # # set_property :delta => true # end # # Check out the more detailed documentation for each of these methods # at ThinkingSphinx::Index::Builder. # def define_index(&block) return unless ThinkingSphinx.define_indexes? self.sphinx_indexes ||= [] self.sphinx_facets ||= [] index = ThinkingSphinx::Index::Builder.generate(self, &block) self.sphinx_indexes << index unless ThinkingSphinx.indexed_models.include?(self.name) ThinkingSphinx.indexed_models << self.name end if index.delta? before_save :toggle_delta after_commit :index_delta end after_destroy :toggle_deleted include ThinkingSphinx::SearchMethods include ThinkingSphinx::ActiveRecord::AttributeUpdates include ThinkingSphinx::ActiveRecord::Scopes index # We want to make sure that if the database doesn't exist, then Thinking # Sphinx doesn't mind when running non-TS tasks (like db:create, db:drop # and db:migrate). It's a bit hacky, but I can't think of a better way. rescue StandardError => err case err.class.name when "Mysql::Error", "Java::JavaSql::SQLException", "ActiveRecord::StatementInvalid" return else raise err end end alias_method :sphinx_index, :define_index def sphinx_index_options sphinx_indexes.last.options end # Generate a unique CRC value for the model's name, to use to # determine which Sphinx documents belong to which AR records. # # Really only written for internal use - but hey, if it's useful to # you in some other way, awesome. # def to_crc32 self.name.to_crc32 end def to_crc32s (subclasses << self).collect { |klass| klass.to_crc32 } end def source_of_sphinx_index possible_models = self.sphinx_indexes.collect { |index| index.model } return self if possible_models.include?(self) parent = self.superclass while !possible_models.include?(parent) && parent != ::ActiveRecord::Base parent = parent.superclass end return parent end def to_riddle(offset) sphinx_database_adapter.setup indexes = [to_riddle_for_core(offset)] indexes << to_riddle_for_delta(offset) if sphinx_delta? indexes << to_riddle_for_distributed end def sphinx_database_adapter @sphinx_database_adapter ||= ThinkingSphinx::AbstractAdapter.detect(self) end def sphinx_name self.name.underscore.tr(':/\\', '_') end def sphinx_index_names klass = source_of_sphinx_index names = ["#{klass.sphinx_name}_core"] names << "#{klass.sphinx_name}_delta" if sphinx_delta? names end private def sphinx_delta? self.sphinx_indexes.any? { |index| index.delta? } end def to_riddle_for_core(offset) index = Riddle::Configuration::Index.new("#{sphinx_name}_core") index.path = File.join( ThinkingSphinx::Configuration.instance.searchd_file_path, index.name ) set_configuration_options_for_indexes index set_field_settings_for_indexes index self.sphinx_indexes.select { |ts_index| ts_index.model == self }.each_with_index do |ts_index, i| index.sources += ts_index.sources.collect { |source| source.to_riddle_for_core(offset, i) } end index end def to_riddle_for_delta(offset) index = Riddle::Configuration::Index.new("#{sphinx_name}_delta") index.parent = "#{sphinx_name}_core" index.path = File.join(ThinkingSphinx::Configuration.instance.searchd_file_path, index.name) self.sphinx_indexes.each_with_index do |ts_index, i| index.sources += ts_index.sources.collect { |source| source.to_riddle_for_delta(offset, i) } if ts_index.delta? end index end def to_riddle_for_distributed index = Riddle::Configuration::DistributedIndex.new(sphinx_name) index.local_indexes << "#{sphinx_name}_core" index.local_indexes.unshift "#{sphinx_name}_delta" if sphinx_delta? index end def set_configuration_options_for_indexes(index) ThinkingSphinx::Configuration.instance.index_options.each do |key, value| index.send("#{key}=".to_sym, value) end self.sphinx_indexes.each do |ts_index| ts_index.options.each do |key, value| index.send("#{key}=".to_sym, value) if ThinkingSphinx::Configuration::IndexOptions.include?(key.to_s) && !value.nil? end end end def set_field_settings_for_indexes(index) field_names = lambda { |field| field.unique_name.to_s } self.sphinx_indexes.each do |ts_index| index.prefix_field_names += ts_index.prefix_fields.collect(&field_names) index.infix_field_names += ts_index.infix_fields.collect(&field_names) end end end end base.send(:include, ThinkingSphinx::ActiveRecord::Delta) ::ActiveRecord::Associations::HasManyAssociation.send( :include, ThinkingSphinx::ActiveRecord::HasManyAssociation ) ::ActiveRecord::Associations::HasManyThroughAssociation.send( :include, ThinkingSphinx::ActiveRecord::HasManyAssociation ) end def in_index?(suffix) self.class.search_for_id self.sphinx_document_id, sphinx_index_name(suffix) end def in_core_index? in_index? "core" end def in_delta_index? in_index? "delta" end def in_both_indexes? in_core_index? && in_delta_index? end def toggle_deleted return unless ThinkingSphinx.updates_enabled? && ThinkingSphinx.sphinx_running? config = ThinkingSphinx::Configuration.instance client = Riddle::Client.new config.address, config.port client.update( "#{self.class.sphinx_indexes.first.name}_core", ['sphinx_deleted'], {self.sphinx_document_id => [1]} ) if self.in_core_index? client.update( "#{self.class.sphinx_indexes.first.name}_delta", ['sphinx_deleted'], {self.sphinx_document_id => [1]} ) if self.class.sphinx_indexes.any? { |index| index.delta? } && self.toggled_delta? rescue ::ThinkingSphinx::ConnectionError # nothing end # Returns the unique integer id for the object. This method uses the # attribute hash to get around ActiveRecord always mapping the #id method # to whatever the real primary key is (which may be a unique string hash). # # @return [Integer] Unique record id for the purposes of Sphinx. # def primary_key_for_sphinx @primary_key_for_sphinx ||= read_attribute(self.class.primary_key_for_sphinx) end def sphinx_document_id primary_key_for_sphinx * ThinkingSphinx.indexed_models.size + ThinkingSphinx.indexed_models.index(self.class.source_of_sphinx_index.name) end private def sphinx_index_name(suffix) "#{self.class.source_of_sphinx_index.name.underscore.tr(':/\\', '_')}_#{suffix}" end end end