# Makes a model searchable using MySQL's fulltext index. # # = Usage # class MyModel < ActiveRecord::Base # acts_as_fulltext %w(), %w(), {} # end # # The first parameter to #acts_as_fulltext is what will appear as the label # of search results. The second parameter identifies additional fields that # should be indexed. These won't show up in the search results. The third # parameter is a Hash of options. # # The values returned from the fields will be flattened (if Arrays), and # normalized to date/time strings if Date, Time or DateTime instances. module ActsAsFulltext def self.included(base) base.send :include, ActsAsFulltext::InstanceMethods base.send :extend, ActsAsFulltext::ClassMethods end module ClassMethods # Declares this model to be fulltext searchable. def acts_as_fulltext(*args) options = args.last.kind_of?(Hash) ? args.pop : {} options.reverse_merge(:weight => 100) case args.size when 1 options[:labels] = [args[0].shift].flatten options[:fields] = [args[0]].flatten when 2 options[:labels] = [args.shift].flatten options[:fields] = [args.shift].flatten else raise ArgumentError, "Expected 1 or 2 args describing the fields to index, found #{args.size} (plus options)" end write_inheritable_hash :fulltext_options, options logger.debug {"#{self.name} fulltext options: #{options.inspect}"} has_one :fulltext_row, :as => :subject, :dependent => :delete after_save :update_fulltext_index end # Rebuilds the fulltext index of this class from scratch. def rebuild_index self.transaction do FulltextRow.delete_all("subject_type = '#{self.name}'") self.find(:all).each(&:update_fulltext_index) end end # Searches for instances of this class using a natural language query. # Returns instances of this model, ordered by relevancy. +options+ # are regular ActiveRecord #find options (:limit, :offset, :conditions, etc). def search(query, options={}) FulltextRow.search_by_class(self, query, options) end def count_results(query, options={}) FulltextRow.count_by_class(self, query, options) end end module InstanceMethods def fulltext_options #:nodoc: self.class.read_inheritable_attribute(:fulltext_options) end # after save callback that updates the fulltext row associated with this model. def update_fulltext_index options = self.fulltext_options label_fields, other_fields = options[:labels], options[:fields] row = self.fulltext_row || self.build_fulltext_row row.label = fulltext_field_values(label_fields) row.body = fulltext_field_values(other_fields) row.subject_updated_at = self.respond_to?(:updated_at) ? self.updated_at : nil row.weight = options[:weight] row.save end def fulltext_field_values(field_names) #:nodoc: fulltext_normalize(field_names.map {|field| send(field)}) end def fulltext_normalize(value) #:nodoc: case value when String # String is Enumerable, so... we must take care of it before Array, Enumerable below value when Date, Time, DateTime [value.to_s(:iso), value.to_s(:long), value.to_s(:short), value.to_s].join(" ") when Array, Enumerable value.flatten.reject(&:blank?).map {|v| fulltext_normalize(v)}.join(" ") when Hash [value.keys, value.values].flatten.reject(&:blank?).map {|v| fulltext_normalize(v)}.join(" ") else value.to_s end end end end ActiveRecord::Base.send :include, ActsAsFulltext