require "rast" module ActiveRecord module Acts module RastIndexed @@configurations = { "development" => { "path" => File.expand_path("index/development", RAILS_ROOT), "encoding" => "utf8" }, "test" => { "path" => File.expand_path("index/test", RAILS_ROOT), "encoding" => "utf8" }, "production" => { "path" => File.expand_path("index/production", RAILS_ROOT), "encoding" => "utf8" } } def self.configurations return @@configurations end def self.configurations=(configurations) @@configurations = configurations end def self.append_features(klass) super(klass) klass.extend(ClassMethods) end module ClassMethods def acts_as_rast_indexed create_rast_index include(InstanceMethods) after_create(:register_to_rast_index) after_update(:update_rast_index) before_destroy(:delete_from_rast_index) end def create_rast_index config = RastIndexed.configurations[RAILS_ENV] options = rast_create_options(config) index_path = rast_index_path unless File.exist?(index_path) FileUtils.mkdir_p(File.dirname(index_path)) Rast::DB.create(index_path, options) end end def rebuild_rast_index FileUtils.rm_rf(rast_index_path) create_rast_index for i in find(:all) i.register_to_rast_index end end def find_by_rast(query, options = {}) open_rast_index(Rast::DB::RDONLY) do |index| opts = {} opts["start_no"] = options[:offset] opts["num_items"] = options[:limit] opts["properties"] = ["id"] if options.key?(:order) order = options[:order].dup if order.slice!(/\s*desc/) opts["sort_order"] = Rast::SORT_ORDER_DESCENDING else order.slice!(/\s*asc/) opts["sort_order"] = Rast::SORT_ORDER_ASCENDING end opts["sort_method"] = Rast::SORT_METHOD_PROPERTY opts["sort_property"] = order end q = sanitize_rast_query(query) result = index.search(q, opts) return result.items.collect { |item| find(item.properties[0]) } end end def open_rast_index(flags = Rast::DB::RDWR) index = Rast::DB.open(rast_index_path, flags) begin yield(index) ensure index.close end end def rast_index_path path = RastIndexed.configurations[RAILS_ENV]["path"] return File.expand_path(table_name, path) end private def rast_create_options(config) return { "encoding" => config["encoding"] || "utf8", "preserve_text" => false, "properties" => columns.collect { |column| type = rast_type(column) { "name" => column.name, "type" => type, "search" => true, "text_search" => type == Rast::PROPERTY_TYPE_STRING, "full_text_search" => type == Rast::PROPERTY_TYPE_STRING, "unique" => false, "omit_property" => column.name != "id" } } } end def rast_type(column) case column.type when :integer, :boolean return Rast::PROPERTY_TYPE_UINT when :datetime, :date, :timestamp, :time return Rast::PROPERTY_TYPE_DATE else return Rast::PROPERTY_TYPE_STRING end end def sanitize_rast_query(query) return query unless query.is_a?(Array) fmt, *args = *query return fmt.gsub(/\?/) { arg = args.shift case arg when nil '""' when true "1" when false "0" when Integer arg.to_s when Time, DateTime, Date arg.strftime("%Y-%m-%dT%H:%M:%S") else '"' + arg.to_s.gsub(/[\\"]/, "\\\\\\&") + '"' end } end end module InstanceMethods def register_to_rast_index self.class.open_rast_index do |index| index.register("", rast_properties(index)) end end def update_rast_index self.class.open_rast_index do |index| result = index.search("id = #{id}") index.update(result.items[0].doc_id, "", rast_properties(index)) end end def delete_from_rast_index self.class.open_rast_index do |index| result = index.search("id = #{id}") index.delete(result.items[0].doc_id) end end private def rast_properties(index) return index.properties.inject({}) { |properties, property| val = read_attribute(property.name) case property.type when Rast::PROPERTY_TYPE_UINT val = val.to_i when Rast::PROPERTY_TYPE_DATE val = val.strftime("%Y-%m-%dT%H:%M:%S") else val = val.to_s end properties[property.name] = val properties } end end end end end