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:
Peak Obsession
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:
Peak Obsession
http://www.jamis.jamisbuck.org/articles/2005/10/11/plugging-into-rails
Simplest Possible Plugin Manager For Rails
Peak Obsession
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