Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
bmaland committed Mar 31, 2009
0 parents commit c7d94e5
Show file tree
Hide file tree
Showing 22 changed files with 365 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
doc/*
pkg/*
test/debug.log
1 change: 1 addition & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v0.0.1 Test release
20 changes: 20 additions & 0 deletions MIT-LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Copyright (c) 2009 [name of plugin creator]

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 changes: 21 additions & 0 deletions Manifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Rakefile
README.markdown
tasks/no_fuzz_tasks.rake
uninstall.rb
init.rb
generators/no_fuzz/no_fuzz_generator.rb
generators/no_fuzz/templates/model.rb
generators/no_fuzz/templates/migration.rb
generators/no_fuzz/USAGE
rails/init.rb
CHANGELOG
lib/no_fuzz.rb
MIT-LICENSE
no_fuzz.gemspec
install.rb
test/no_fuzz_test.rb
test/test_helper.rb
test/database.yml
test/schema.rb
test/test.sqlite3
Manifest
30 changes: 30 additions & 0 deletions README.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# NoFuzz

Based on code and ideas in Steven Ruttenberg's nice blog entry "Live fuzzy
search using n-grams in Rails" [1].

Note that this family of fuzzy search techniques work best on dictionary-type
lookups, i.e not for large amounts of text.

kristian's acts_as_fuzzy_search [2] is a similar plugin, but it targets DataMapper.

1: http://unirec.blogspot.com/2007/12/live-fuzzy-search-using-n-grams-in.html
2: http://github.com/mkristian/kristians_rails_plugins/tree/master/act_as_fuzzy_search

# Basic Usage

Add the following code in the model you'd like to index:

include NoFuzz
fuzzy :field

Where field is the field used for the indexing data (you can use multiple fields
if you want).

Populate the index by running 'Model.populate_trigram_index'. Then, you can
search fuzzily with the fuzzy_find method:

Model.fuzzy_find("query")
Model.fuzzy_find("query", 10) # find maximum 10 rows

Copyright (c) 2009 Bjørn Arild Mæland, released under the MIT license
37 changes: 37 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
require 'rubygems'
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
#require 'spec/rake/spectask'
require 'echoe'

desc 'Default: run unit tests.'
task :default => :test

Echoe.new('no_fuzz') do |p|
p.author = "Bjørn Arild Mæland"
p.email = "bjorn.maeland@gmail.com"
p.summary = "No Fuzz"
p.url = "http://www.github.com/Chrononaut/no_fuzz/"
p.ignore_pattern = FileList[".gitignore"]
p.include_rakefile = true
end

desc 'Test the no_fuzz plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.libs << 'test'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end

DOCUMENTED_FILES = FileList['README.markdown', 'MIT-LICENSE', 'lib/**/*.rb']

desc 'Generate documentation for the no_fuzz plugin.'
Rake::RDocTask.new do |rdoc|
rdoc.rdoc_dir = 'doc'
rdoc.title = "HostConnect"
rdoc.options << '--line-numbers' << '--inline-source' << '--main' << 'README.markdown'
rdoc.rdoc_files.include(*DOCUMENTED_FILES)
end
5 changes: 5 additions & 0 deletions generators/no_fuzz/USAGE
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Description:
Creates a migration and a modle for trigram indexing for a given model.

Example:
./script/generate no_fuzz MODELNAME
33 changes: 33 additions & 0 deletions generators/no_fuzz/no_fuzz_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
class NoFuzzGenerator < Rails::Generator::NamedBase
def manifest
record do |m|
m.template "model.rb", "app/models/#{class_name.underscore.downcase}_trigram.rb", {
:assigns => local_assigns
}

m.migration_template "migration.rb", "db/migrate", {
:assigns => local_assigns,
:migration_file_name => local_assigns[:migration_class_name].underscore.downcase
}
end
end

private
def local_custom_name
class_name.underscore.downcase
end

def gracefully_pluralize(str)
str.pluralize! if ActiveRecord::Base.pluralize_table_names
str
end

def local_assigns
returning(assigns = {}) do
assigns[:class_name] = local_custom_name.classify
assigns[:migration_class_name] = "CreateTrigramsTableFor#{assigns[:class_name]}"
assigns[:table_name] = gracefully_pluralize(local_custom_name + "_trigram")
assigns[:foreign_key] = (class_name.underscore.downcase + "_id")
end
end
end
15 changes: 15 additions & 0 deletions generators/no_fuzz/templates/migration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class <%= migration_name -%> < ActiveRecord::Migration
def self.up
create_table :<%= table_name -%>, :force => true do |t|
t.integer :<%= foreign_key -%>, :null => false
t.string :tg, :length => 3, :null => false # trigrams
t.integer :score, :default => 1, :null => false
end
add_index :<%= table_name -%>, :tg
add_index :<%= table_name -%>, :<%= foreign_key %>
end

def self.down
drop_table :<%= table_name %>
end
end
2 changes: 2 additions & 0 deletions generators/no_fuzz/templates/model.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class <%= class_name -%>Trigram < ActiveRecord::Base
end
2 changes: 2 additions & 0 deletions init.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Include hook code here
require File.dirname(__FILE__) + "/rails/init"
1 change: 1 addition & 0 deletions install.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Install hook code here
70 changes: 70 additions & 0 deletions lib/no_fuzz.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# TODO:
# - scores
# I.e fuzzy {:first_name => 1, :last_name => 2}, last_name gives double score
# Currently everything gets scored with 1
# - normalization
# - weighting of fuzzy_find results

module NoFuzz
def self.included(model)
model.extend ClassMethods
end

module ClassMethods
def self.extended(model)
@@model = model
end

def fuzzy(*fields)
# put the parameters as instance variable of the model
@@model.instance_variable_set(:@fuzzy_fields, fields)
@@model.instance_variable_set(:@fuzzy_ref_id, "#{@@model}_id".downcase)
@@model.instance_variable_set(:@fuzzy_trigram_model, "#{@@model}Trigram".constantize)
end

def populate_trigram_index
clear_trigram_index

fuzzy_ref_id = self.instance_variable_get(:@fuzzy_ref_id)
trigram_model = self.instance_variable_get(:@fuzzy_trigram_model)
fields = self.instance_variable_get(:@fuzzy_fields)

fields.each do |f|
self.all.each do |i|
word = ' ' + i.send(f)
(0..word.length-3).each do |idx|
tg = word[idx,3]
trigram_model.create(:tg => tg, fuzzy_ref_id => i.id)
end
end
end
true
end

def clear_trigram_index
self.instance_variable_get(:@fuzzy_trigram_model).delete_all
end

def fuzzy_find(word, limit = 0)
fuzzy_ref_id = self.instance_variable_get(:@fuzzy_ref_id)
trigram_model = self.instance_variable_get(:@fuzzy_trigram_model)
fields = self.instance_variable_get(:@fuzzy_fields)

word = ' ' + word + ' '
trigrams = (0..word.length-3).collect { |idx| word[idx,3] }

# ordered hash of package_id => score pairs
trigram_groups = trigram_model.sum(:score, :conditions => [ "tg IN (?)", trigrams],
:group => fuzzy_ref_id.to_s)

count = 0
@res = []
trigram_groups.sort_by {|a| -a[1]}.each do |group|
@res << self.find(group[0])
count += 1
break if count == limit
end
@res
end
end
end
35 changes: 35 additions & 0 deletions no_fuzz.gemspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# -*- encoding: utf-8 -*-

Gem::Specification.new do |s|
s.name = %q{no_fuzz}
s.version = "0.0.1"

s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
s.authors = ["Bj\303\270rn Arild M\303\246land"]
s.date = %q{2009-03-30}
s.description = %q{No Fuzz}
s.email = %q{bjorn.maeland@gmail.com}
s.extra_rdoc_files = ["README.markdown", "tasks/no_fuzz_tasks.rake", "CHANGELOG", "lib/no_fuzz.rb"]
s.files = ["Rakefile", "README.markdown", "tasks/no_fuzz_tasks.rake", "uninstall.rb", "init.rb", "generators/no_fuzz/no_fuzz_generator.rb", "generators/no_fuzz/templates/model.rb", "generators/no_fuzz/templates/migration.rb", "generators/no_fuzz/USAGE", "rails/init.rb", "CHANGELOG", "lib/no_fuzz.rb", "MIT-LICENSE", "install.rb", "test/no_fuzz_test.rb", "test/test_helper.rb", "test/database.yml", "test/schema.rb", "test/test.sqlite3", "Manifest", "no_fuzz.gemspec"]
s.has_rdoc = true
s.homepage = %q{http://www.github.com/Chrononaut/no_fuzz/}
s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "No_fuzz", "--main", "README.markdown"]
s.require_paths = ["lib"]
s.rubyforge_project = %q{no_fuzz}
s.rubygems_version = %q{1.3.1}
s.summary = %q{No Fuzz}
s.test_files = ["test/no_fuzz_test.rb", "test/test_helper.rb"]

if s.respond_to? :specification_version then
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
s.specification_version = 2

if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
s.add_development_dependency(%q<echoe>, [">= 0"])
else
s.add_dependency(%q<echoe>, [">= 0"])
end
else
s.add_dependency(%q<echoe>, [">= 0"])
end
end
1 change: 1 addition & 0 deletions rails/init.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require 'no_fuzz'
4 changes: 4 additions & 0 deletions tasks/no_fuzz_tasks.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# desc "Explaining what the task does"
# task :no_fuzz do
# # Task goes here
# end
21 changes: 21 additions & 0 deletions test/database.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
sqlite:
:adapter: sqlite
:dbfile: vendor/plugins/no_fuzz/test/test.sqlite

sqlite3:
:adapter: sqlite3
:dbfile: test/test.sqlite3

postgresql:
:adapter: postgresql
:username: postgres
:password: postgres
:database: no_fuzz_plugin_test
:min_messages: ERROR

mysql:
:adapter: mysql
:host: localhost
:username: root
:password: password
:database: no_fuzz_plugin_test
18 changes: 18 additions & 0 deletions test/no_fuzz_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
require File.dirname(__FILE__) + '/test_helper.rb'

class NoFuzzTest < ActiveSupport::TestCase

load_schema

class Package < ActiveRecord::Base
end

class PackageTrigram < ActiveRecord::Base
end

def test_schema_has_loaded_correctly
assert_equal [], Package.all
assert_equal [], PackageTrigram.all
end

end
11 changes: 11 additions & 0 deletions test/schema.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
ActiveRecord::Schema.define(:version => 0) do
create_table :packages, :force => true do |t|
t.string :name
end

create_table :package_trigrams, :force => true do |t|
t.integer :package_id
t.string :tg
t.integer :score
end
end
Binary file added test/test.sqlite3
Binary file not shown.
34 changes: 34 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
require 'test/unit'
require 'rubygems'
require 'active_record'
require 'active_support'
require 'active_support/test_case'

def load_schema
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")

db_adapter = ENV['DB']

# no db passed, try one of these fine config-free DBs before bombing.
db_adapter ||=
begin
require 'rubygems'
require 'sqlite'
'sqlite'
rescue MissingSourceFile
begin
require 'sqlite3'
'sqlite3'
rescue MissingSourceFile
end
end

if db_adapter.nil?
raise "No DB Adapter selected. Pass the DB= option to pick one, or install Sqlite or Sqlite3."
end

ActiveRecord::Base.establish_connection(config[db_adapter])
#load(File.dirname(__FILE__) + "/schema.rb")
require File.dirname(__FILE__) + '/../rails/init.rb'
end
1 change: 1 addition & 0 deletions uninstall.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Uninstall hook code here

0 comments on commit c7d94e5

Please sign in to comment.