Skip to content
This repository has been archived by the owner on Sep 27, 2022. It is now read-only.

Commit

Permalink
First working version, specs not complete
Browse files Browse the repository at this point in the history
  • Loading branch information
mezis committed Oct 25, 2012
1 parent f23186d commit b3e5b61
Show file tree
Hide file tree
Showing 20 changed files with 482 additions and 130 deletions.
19 changes: 0 additions & 19 deletions ..gemspec

This file was deleted.

3 changes: 3 additions & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
--colour
--format d
--backtrace
2 changes: 1 addition & 1 deletion LICENSE.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ 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.
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
46 changes: 44 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Fuzzily

TODO: Write a gem description
A fast, trigram-based, database-backed fuzzy string search/match engine for Rails.

## Installation

Expand All @@ -18,7 +18,49 @@ Or install it yourself as:

## Usage

TODO: Write usage instructions here
You'll need to setup 3 things:

- a trigram model (your search index)
- its migration
- the model you want to search for

Create and ActiveRecord model in your app:

class Trigram < ActiveRecord::Base
include Fuzzily::Model
end

Create a migration file:

class AddTrigramsModel < ActiveRecord::Migration
extend Fuzzily::Migration

# if you named your trigram model anything but 'Trigram', e.g. 'CustomTrigram'
# trigrams_table_name = :custom_trigrams
end

Instrument your model (your searchable fields do not have to be stored, they can be dynamic methods too):

class MyStuff < ActiveRecord::Base
# assuming my_stuffs has a 'name' attribute
fuzzily_searchable :name
end

Index your model (will happen automatically for new/updated records):

MyStuff.find_each do |record|
record.update_fuzzy_name!
end

Search!

MyStuff.find_by_fuzzy_name('Some Name', :limit => 10)
# => records


## License

MIT licence. Quite permissive if you ask me.

## Contributing

Expand Down
5 changes: 5 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
require "bundler/gem_tasks"
require 'rspec/core/rake_task'

RSpec::Core::RakeTask.new(:spec)

task :default => :spec
95 changes: 0 additions & 95 deletions acts_as_fuzzy.rb

This file was deleted.

12 changes: 10 additions & 2 deletions fuzzily.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,18 @@ Gem::Specification.new do |gem|
gem.version = Fuzzily::VERSION
gem.authors = ["Julien Letessier"]
gem.email = ["[email protected]"]
gem.description = %q{TODO: Write a gem description}
gem.summary = %q{TODO: Write a gem summary}
gem.description = %q{Fast fuzzy string matching for rails}
gem.summary = %q{A fast, trigram-based, database-backed fuzzy string search/match engine for Rails.}
gem.homepage = ""

gem.add_runtime_dependency 'activerecord', '~> 2.3'

gem.add_development_dependency 'rspec'
gem.add_development_dependency 'appraisal'
gem.add_development_dependency 'pry'
gem.add_development_dependency 'pry-nav'
gem.add_development_dependency 'sqlite3'

gem.files = `git ls-files`.split($/)
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
Expand Down
5 changes: 0 additions & 5 deletions lib/..rb

This file was deleted.

8 changes: 5 additions & 3 deletions lib/fuzzily.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
require "fuzzily/version"
require "fuzzily/searchable"
require "fuzzily/migration"
require "fuzzily/model"
require "active_record"

module Fuzzily
# Your code goes here...
end
ActiveRecord::Base.extend(Fuzzily::Searchable)
35 changes: 35 additions & 0 deletions lib/fuzzily/migration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
require 'active_record'

module Fuzzily
module Migration

def trigrams_table_name=(custom_name)
@trigrams_table_name = custom_name
end

def trigrams_table_name
@trigrams_table_name ||= :trigrams
end

def up
create_table trigrams_table_name do |t|
t.string :trigram, :limit => 3
t.integer :score
t.integer :owner_id
t.string :owner_type
t.string :fuzzy_field
end

add_index trigrams_table_name,
[:owner_type, :fuzzy_field, :trigram, :owner_id, :score],
:name => :index_for_match
add_index trigrams_table_name,
[:owner_type, :owner_id],
:name => :index_by_owner
end

def down
drop_table trigrams_table_name
end
end
end
51 changes: 51 additions & 0 deletions lib/fuzzily/model.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
module Fuzzily
module Model
# Needs fields: trigram, owner_type, owner_id, score
# Needs index on [owner_type, trigram] and [owner_type, owner_id]

def self.included(by)
by.ancestors.include?(ActiveRecord::Base) or raise 'Not included in an ActiveRecord subclass'
by.class_eval do
return if class_variable_defined?(:@@fuzzily_trigram_model)

belongs_to :owner, :polymorphic => true
validates_presence_of :owner
validates_uniqueness_of :trigram, :scope => [:owner_type, :owner_id]
validates_length_of :trigram, :is => 3
validates_presence_of :score
validates_presence_of :fuzzy_field

named_scope :for_model, lambda { |model| {
:conditions => { :owner_type => model.kind_of?(Class) ? model.name : model }
}}
named_scope :for_field, lambda { |field_name| {
:conditions => { :fuzzy_field => field_name }
}}
named_scope :with_trigram, lambda { |trigrams| {
:conditions => { :trigram => trigrams }
}}

class_variable_set(:@@fuzzily_trigram_model, true)
end

by.extend(ClassMethods)
end

module ClassMethods
# options:
# - model (mandatory)
# - field (mandatory)
# - limit (default 10)
def matches_for(text, options = {})
options[:limit] ||= 10
self.
scoped(:select => 'owner_id, owner_type, SUM(score) AS score').
scoped(:group => :owner_id).
scoped(:order => 'score DESC', :limit => options[:limit]).
with_trigram(text.extend(String).trigrams).
map(&:owner)
end
end
end
end

55 changes: 55 additions & 0 deletions lib/fuzzily/searchable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
require 'fuzzily/trigram'

module Fuzzily
module Searchable
# fuzzily_searchable <field> [, <field>...] [, <options>]
def fuzzily_searchable(*fields)
options = fields.last.kind_of?(Hash) ? fields.pop : {}

fields.each do |field|
make_field_fuzzily_searchable(field, options)
end
end

private

def make_field_fuzzily_searchable(field, options={})
class_variable_defined?(:"@@fuzzily_searchable_#{field}") and return

trigram_class_name = options.fetch(:class_name, 'Trigram')
trigram_association = "trigrams_for_#{field}".to_sym
update_trigrams_method = "update_fuzzy_#{field}!".to_sym

has_many trigram_association,
:class_name => trigram_class_name,
:as => :owner,
:conditions => { :fuzzy_field => field.to_s },
:dependent => :destroy

singleton_class.send(:define_method,"find_by_fuzzy_#{field}".to_sym) do |*args|
case args.size
when 1 then pattern = args.first ; options = {}
when 2 then pattern, options = args
else raise 'Wrong # of arguments'
end
Trigram.scoped(options).for_model(self.name).for_field(field).matches(pattern)
end

define_method update_trigrams_method do
self.send(trigram_association).destroy_all
self.send(field).extend(String).trigrams.each do |trigram|
self.send(trigram_association).create!(:score => 1, :trigram => trigram)
end
end

after_save do |record|
next unless record.send("#{field}_changed?".to_sym)
self.send(update_trigrams_method)
end

class_variable_set(:"@@fuzzily_searchable_#{field}", true)
self
end

end
end
Loading

0 comments on commit b3e5b61

Please sign in to comment.