Gmane
From: Kasper Weibel <weibel@...>
Subject: ANN: acts_as_ferret
Newsgroups: gmane.comp.lang.ruby.rails
Date: 2005-12-02 18:22:48 GMT (2 years, 44 weeks, 2 days and 10 minutes ago)
Hi all

This week I have worked with Rails and Ferret to test Ferrets (and Lucenes)
capabilities. I decided to make a mixin for ActiveRecord as it seemed the
simplest possible solution and I ended up making this into a plugin.

For more info on Ferret see:
http://ferret.davebalmain.com/trac/

The plugin is functional but could easily be refined. Anyway I want to share it
with you. Regard it as a basic solution. Most of the ideas and code is taken
from these sources

Howtos and help on Ferret with Rails:
# http://wiki.rubyonrails.com/rails/pages/HowToIntegrateFerretWithRails
# http://article.gmane.org/gmane.comp.lang.ruby.rails/26859
# http://ferret.davebalmain.com/trac
# http://aslakhellesoy.com/articles/2005/11/18/using-ferret-with-activerecord
# http://rubyforge.org/pipermail/ferret-talk/2005-November/000014.html

Howtos on creating plugins:
# http://wiki.rubyonrails.com/rails/pages/HowToWriteAnActsAsFoxPlugin
# http://www.jamis.jamisbuck.org/articles/2005/10/11/plugging-into-rails
# http://lesscode.org/2005/10/27/rails-simplest-plugin-manager/
# http://wiki.rubyonrails.com/rails/pages/HowTosPlugins

The result is the acts_as_ferret Mixin for ActivcRecord.

Use it as follows:
In any model.rb add acts_as_ferret

class Foo < ActiveRecord::Base
  acts_as_ferret 
end

All CRUD operations will be performed on both ActiveRecord (as usual) and a
ferret index for further searching.

The following method is available in your controllers:

ActiveRecord::find_by_contents(query) # Query is a string representing you query

The plugin follows the usual plugin structure and consists of 2 files: 

{RAILS_ROOT}/vendor/plugins/acts_as_ferret/init.rb
{RAILS_ROOT}/vendor/plugins/acts_as_ferret/lib/acts_as_ferret.rb

The Ferret DB is stored in:

{RAILS_ROOT}/db/index.db

Here follows the code:

# CODE for init.rb
require 'acts_as_ferret'
# END init.rb

# CODE for acts_as_ferret.rb
require 'active_record'
require 'ferret'

module FerretMixin #(was: Foo)
   module Acts #:nodoc:
      module ARFerret #:nodoc:          

         def self.append_features(base)
            super
            base.extend(MacroMethods)
         end

# declare the class level helper methods
# which will load the relevant instance methods defined below when invoked

         module MacroMethods

            def acts_as_ferret
               extend FerretMixin::Acts::ARFerret::ClassMethods
               class_eval do
                  include FerretMixin::Acts::ARFerret::ClassMethods            

                  after_create :ferret_create
                  after_update :ferret_update
                  after_destroy :ferret_destroy
               end
            end

         end

         module ClassMethods
            include Ferret

            INDEX_DIR = "#{RAILS_ROOT}/db/index.db" 

            def self.reloadable?; false end

            # Finds instances by file contents.
            def find_by_contents(query, options = {})    
               index_searcher ||= Search::IndexSearcher.new(INDEX_DIR)
               query_parser   ||=
QueryParser.new(index_searcher.reader.get_field_names.to_a)              
               query = query_parser.parse(query)

               result = [] 
               index_searcher.search_each(query) do |doc, score|
                  id = index_searcher.reader.get_document(doc)["id"]
                  res = self.find(id)
                  result << res if res
               end
               index_searcher.close()
               result
            end

            # private

            def ferret_create
               index ||= Index::Index.new(:key => :id, 
                                       :path => INDEX_DIR, 
                                       :create_if_missing => true, 
                                       :default_field => "*") 
               index << self.to_doc
               index.optimize()
               index.close()
            end

            def ferret_update
               #code to update index
               index ||= Index::Index.new(:key => :id, 
                                       :path => INDEX_DIR, 
                                       :create_if_missing => true, 
                                       :default_field => "*") 
               index.delete(self.id.to_s)
               index << self.to_doc              
               index.optimize
               index.close()
            end

            def ferret_destroy
               # code to delete from index
               index ||= Index::Index.new(:key => :id, 
                                       :path => INDEX_DIR, 
                                       :create_if_missing => true, 
                                       :default_field => "*") 
               index_writer.delete(self.id.to_s)
               index_writer.optimize()
               index_writer.close()
            end

            def to_doc
# Churn through the complete Active Record and add it to the Ferret document
               doc = Ferret::Document::Document.new
               self.attributes.each_pair do |key,val| 
                  doc << Ferret::Document::Field.new(key, val.to_s,
Ferret::Document::Field::Store::YES, Ferret::Document::Field::Index::TOKENIZED)
               end
               doc
            end
         end         
      end
   end
end

# reopen ActiveRecord and include all the above to make
# them available to all our models if they want it

ActiveRecord::Base.class_eval do
   include FerretMixin::Acts::ARFerret
end

# END acts_as_ferret.rb