fuzzily 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +98 -0
- data/Rakefile +6 -0
- data/fuzzily.gemspec +27 -0
- data/lib/fuzzily/migration.rb +35 -0
- data/lib/fuzzily/model.rb +51 -0
- data/lib/fuzzily/searchable.rb +55 -0
- data/lib/fuzzily/trigram.rb +25 -0
- data/lib/fuzzily/version.rb +3 -0
- data/lib/fuzzily.rb +7 -0
- data/spec/fuzzily/migration_spec.rb +33 -0
- data/spec/fuzzily/model_spec.rb +79 -0
- data/spec/fuzzily/searchable_spec.rb +72 -0
- data/spec/fuzzily/trigram_spec.rb +8 -0
- data/spec/meta_spec.rb +8 -0
- data/spec/spec_helper.rb +48 -0
- metadata +175 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Julien Letessier
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
# Fuzzily
|
2
|
+
|
3
|
+
A fast, [trigram](http://en.wikipedia.org/wiki/N-gram)-based, database-backed [fuzzy](http://en.wikipedia.org/wiki/Approximate_string_matching) string search/match engine for Rails.
|
4
|
+
|
5
|
+
Loosely inspired from an [old blog post](http://unirec.blogspot.co.uk/2007/12/live-fuzzy-search-using-n-grams-in.html).
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
gem 'fuzzily'
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install fuzzily
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
You'll need to setup 2 things:
|
24
|
+
|
25
|
+
- a trigram model (your search index) and its migration
|
26
|
+
- the model you want to search for
|
27
|
+
|
28
|
+
Create and ActiveRecord model in your app (this will be used to store a "fuzzy index" of all the models and fields you will be indexing):
|
29
|
+
|
30
|
+
class Trigram < ActiveRecord::Base
|
31
|
+
include Fuzzily::Model
|
32
|
+
end
|
33
|
+
|
34
|
+
Create a migration for it:
|
35
|
+
|
36
|
+
class AddTrigramsModel < ActiveRecord::Migration
|
37
|
+
extend Fuzzily::Migration
|
38
|
+
end
|
39
|
+
|
40
|
+
Instrument your model (your searchable fields do not have to be stored, they can be dynamic methods too):
|
41
|
+
|
42
|
+
class MyStuff < ActiveRecord::Base
|
43
|
+
# assuming my_stuffs has a 'name' attribute
|
44
|
+
fuzzily_searchable :name
|
45
|
+
end
|
46
|
+
|
47
|
+
Index your model (will happen automatically for new/updated records):
|
48
|
+
|
49
|
+
MyStuff.find_each do |record|
|
50
|
+
record.update_fuzzy_name!
|
51
|
+
end
|
52
|
+
|
53
|
+
Search!
|
54
|
+
|
55
|
+
MyStuff.find_by_fuzzy_name('Some Name', :limit => 10)
|
56
|
+
# => records
|
57
|
+
|
58
|
+
|
59
|
+
|
60
|
+
## Indexing more than one field
|
61
|
+
|
62
|
+
Just list all the field you want to index, or call `fuzzily_searchable` more than once:
|
63
|
+
|
64
|
+
class MyStuff < ActiveRecord::Base
|
65
|
+
fuzzily_searchable :name_fr, :name_en
|
66
|
+
fuzzily_searchable :name_de
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
## Custom name for the index model
|
71
|
+
|
72
|
+
If you want or need to name your index model differently (e.g. because you already have a class called `Trigram`):
|
73
|
+
|
74
|
+
class CustomTrigram < ActiveRecord::Base
|
75
|
+
include Fuzzily::Model
|
76
|
+
end
|
77
|
+
|
78
|
+
class AddTrigramsModel < ActiveRecord::Migration
|
79
|
+
extend Fuzzily::Migration
|
80
|
+
trigrams_table_name = :custom_trigrams
|
81
|
+
end
|
82
|
+
|
83
|
+
class MyStuff < ActiveRecord::Base
|
84
|
+
fuzzily_searchable :name, :class_name => 'CustomTrigram'
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
## License
|
89
|
+
|
90
|
+
MIT licence. Quite permissive if you ask me.
|
91
|
+
|
92
|
+
## Contributing
|
93
|
+
|
94
|
+
1. Fork it
|
95
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
96
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
97
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
98
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/fuzzily.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'fuzzily/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "fuzzily"
|
8
|
+
gem.version = Fuzzily::VERSION
|
9
|
+
gem.authors = ["Julien Letessier"]
|
10
|
+
gem.email = ["julien.letessier@gmail.com"]
|
11
|
+
gem.description = %q{Fast fuzzy string matching for rails}
|
12
|
+
gem.summary = %q{A fast, trigram-based, database-backed fuzzy string search/match engine for Rails.}
|
13
|
+
gem.homepage = ""
|
14
|
+
|
15
|
+
gem.add_runtime_dependency 'activerecord', '~> 2.3'
|
16
|
+
|
17
|
+
gem.add_development_dependency 'rspec'
|
18
|
+
gem.add_development_dependency 'appraisal'
|
19
|
+
gem.add_development_dependency 'pry'
|
20
|
+
gem.add_development_dependency 'pry-nav'
|
21
|
+
gem.add_development_dependency 'sqlite3'
|
22
|
+
|
23
|
+
gem.files = `git ls-files`.split($/)
|
24
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
25
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
26
|
+
gem.require_paths = ["lib"]
|
27
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
module Fuzzily
|
4
|
+
module Migration
|
5
|
+
|
6
|
+
def trigrams_table_name=(custom_name)
|
7
|
+
@trigrams_table_name = custom_name
|
8
|
+
end
|
9
|
+
|
10
|
+
def trigrams_table_name
|
11
|
+
@trigrams_table_name ||= :trigrams
|
12
|
+
end
|
13
|
+
|
14
|
+
def up
|
15
|
+
create_table trigrams_table_name do |t|
|
16
|
+
t.string :trigram, :limit => 3
|
17
|
+
t.integer :score
|
18
|
+
t.integer :owner_id
|
19
|
+
t.string :owner_type
|
20
|
+
t.string :fuzzy_field
|
21
|
+
end
|
22
|
+
|
23
|
+
add_index trigrams_table_name,
|
24
|
+
[:owner_type, :fuzzy_field, :trigram, :owner_id, :score],
|
25
|
+
:name => :index_for_match
|
26
|
+
add_index trigrams_table_name,
|
27
|
+
[:owner_type, :owner_id],
|
28
|
+
:name => :index_by_owner
|
29
|
+
end
|
30
|
+
|
31
|
+
def down
|
32
|
+
drop_table trigrams_table_name
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Fuzzily
|
2
|
+
module Model
|
3
|
+
# Needs fields: trigram, owner_type, owner_id, score
|
4
|
+
# Needs index on [owner_type, trigram] and [owner_type, owner_id]
|
5
|
+
|
6
|
+
def self.included(by)
|
7
|
+
by.ancestors.include?(ActiveRecord::Base) or raise 'Not included in an ActiveRecord subclass'
|
8
|
+
by.class_eval do
|
9
|
+
return if class_variable_defined?(:@@fuzzily_trigram_model)
|
10
|
+
|
11
|
+
belongs_to :owner, :polymorphic => true
|
12
|
+
validates_presence_of :owner
|
13
|
+
validates_uniqueness_of :trigram, :scope => [:owner_type, :owner_id]
|
14
|
+
validates_length_of :trigram, :is => 3
|
15
|
+
validates_presence_of :score
|
16
|
+
validates_presence_of :fuzzy_field
|
17
|
+
|
18
|
+
named_scope :for_model, lambda { |model| {
|
19
|
+
:conditions => { :owner_type => model.kind_of?(Class) ? model.name : model }
|
20
|
+
}}
|
21
|
+
named_scope :for_field, lambda { |field_name| {
|
22
|
+
:conditions => { :fuzzy_field => field_name }
|
23
|
+
}}
|
24
|
+
named_scope :with_trigram, lambda { |trigrams| {
|
25
|
+
:conditions => { :trigram => trigrams }
|
26
|
+
}}
|
27
|
+
|
28
|
+
class_variable_set(:@@fuzzily_trigram_model, true)
|
29
|
+
end
|
30
|
+
|
31
|
+
by.extend(ClassMethods)
|
32
|
+
end
|
33
|
+
|
34
|
+
module ClassMethods
|
35
|
+
# options:
|
36
|
+
# - model (mandatory)
|
37
|
+
# - field (mandatory)
|
38
|
+
# - limit (default 10)
|
39
|
+
def matches_for(text, options = {})
|
40
|
+
options[:limit] ||= 10
|
41
|
+
self.
|
42
|
+
scoped(:select => 'owner_id, owner_type, SUM(score) AS score').
|
43
|
+
scoped(:group => :owner_id).
|
44
|
+
scoped(:order => 'score DESC', :limit => options[:limit]).
|
45
|
+
with_trigram(text.extend(String).trigrams).
|
46
|
+
map(&:owner)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'fuzzily/trigram'
|
2
|
+
|
3
|
+
module Fuzzily
|
4
|
+
module Searchable
|
5
|
+
# fuzzily_searchable <field> [, <field>...] [, <options>]
|
6
|
+
def fuzzily_searchable(*fields)
|
7
|
+
options = fields.last.kind_of?(Hash) ? fields.pop : {}
|
8
|
+
|
9
|
+
fields.each do |field|
|
10
|
+
make_field_fuzzily_searchable(field, options)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def make_field_fuzzily_searchable(field, options={})
|
17
|
+
class_variable_defined?(:"@@fuzzily_searchable_#{field}") and return
|
18
|
+
|
19
|
+
trigram_class_name = options.fetch(:class_name, 'Trigram')
|
20
|
+
trigram_association = "trigrams_for_#{field}".to_sym
|
21
|
+
update_trigrams_method = "update_fuzzy_#{field}!".to_sym
|
22
|
+
|
23
|
+
has_many trigram_association,
|
24
|
+
:class_name => trigram_class_name,
|
25
|
+
:as => :owner,
|
26
|
+
:conditions => { :fuzzy_field => field.to_s },
|
27
|
+
:dependent => :destroy
|
28
|
+
|
29
|
+
singleton_class.send(:define_method,"find_by_fuzzy_#{field}".to_sym) do |*args|
|
30
|
+
case args.size
|
31
|
+
when 1 then pattern = args.first ; options = {}
|
32
|
+
when 2 then pattern, options = args
|
33
|
+
else raise 'Wrong # of arguments'
|
34
|
+
end
|
35
|
+
Trigram.scoped(options).for_model(self.name).for_field(field).matches(pattern)
|
36
|
+
end
|
37
|
+
|
38
|
+
define_method update_trigrams_method do
|
39
|
+
self.send(trigram_association).destroy_all
|
40
|
+
self.send(field).extend(String).trigrams.each do |trigram|
|
41
|
+
self.send(trigram_association).create!(:score => 1, :trigram => trigram)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
after_save do |record|
|
46
|
+
next unless record.send("#{field}_changed?".to_sym)
|
47
|
+
record.send(update_trigrams_method)
|
48
|
+
end
|
49
|
+
|
50
|
+
class_variable_set(:"@@fuzzily_searchable_#{field}", true)
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'iconv'
|
2
|
+
|
3
|
+
module Fuzzily
|
4
|
+
module String
|
5
|
+
def trigrams
|
6
|
+
normalized_words.map do |word|
|
7
|
+
(0..(word.length - 3)).map { |index| word[index,3] }
|
8
|
+
end.flatten.uniq
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
# Remove accents, downcase, replace spaces and word start with '*',
|
14
|
+
# return list of normalized words
|
15
|
+
def normalized_words
|
16
|
+
self.split(/\s+/).map { |word|
|
17
|
+
Iconv.iconv('ascii//translit//ignore', 'utf-8', word).first.downcase.gsub(/\W/,'')
|
18
|
+
}.
|
19
|
+
delete_if(&:empty?).
|
20
|
+
map { |word|
|
21
|
+
"**#{word}"
|
22
|
+
}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/fuzzily.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Fuzzily::Migration do
|
4
|
+
subject { Class.new(ActiveRecord::Migration).extend(described_class) }
|
5
|
+
|
6
|
+
it 'is a proper migration' do
|
7
|
+
subject.ancestors.should include(ActiveRecord::Migration)
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'applies cleanly' do
|
11
|
+
silence_stream(STDOUT) { subject.up }
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'rolls back cleanly' do
|
15
|
+
silence_stream(STDOUT) { subject.up ; subject.down }
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'has a customizable table name' do
|
19
|
+
subject.trigrams_table_name = :foobars
|
20
|
+
silence_stream(STDOUT) { subject.up }
|
21
|
+
expect {
|
22
|
+
ActiveRecord::Base.connection.execute('INSERT INTO `foobars` (score) VALUES (1)')
|
23
|
+
}.to_not raise_error
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'results in a functional model' do
|
27
|
+
silence_stream(STDOUT) { subject.up }
|
28
|
+
model_class = Class.new(ActiveRecord::Base)
|
29
|
+
model_class.table_name = 'trigrams'
|
30
|
+
model_class.create(:trigram => 'abc')
|
31
|
+
model_class.count.should == 1
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Fuzzily::Model do
|
4
|
+
subject do
|
5
|
+
Class.new(ActiveRecord::Base).tap do |model|
|
6
|
+
model.table_name = :trigrams
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
before(:each) { prepare_trigrams_table }
|
11
|
+
|
12
|
+
it 'can be included into an ActiveRecord model' do
|
13
|
+
subject.send(:include, described_class)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'can be included twice' do
|
17
|
+
subject.send(:include, described_class)
|
18
|
+
subject.send(:include, described_class)
|
19
|
+
end
|
20
|
+
|
21
|
+
context '(derived model instance)' do
|
22
|
+
before { prepare_owners_table }
|
23
|
+
let(:model) { subject.send(:include, described_class) }
|
24
|
+
|
25
|
+
it 'belongs to an owner' do
|
26
|
+
model.new.should respond_to(:owner)
|
27
|
+
end
|
28
|
+
|
29
|
+
describe '.create' do
|
30
|
+
it 'can create instances' do
|
31
|
+
model.create(:owner => Stuff.create, :score => 1, :trigram => 'abc', :fuzzy_field => :name)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe '.matches_for' do
|
36
|
+
before do
|
37
|
+
@paris = Stuff.create(:name => 'Paris')
|
38
|
+
%w(**p *pa par ari ris).each do |trigram|
|
39
|
+
model.create(:owner => @paris, :score => 1, :fuzzy_field => :name, :trigram => trigram)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'finds matches' do
|
44
|
+
model.matches_for('Paris').should == [@paris]
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'finds close matches' do
|
48
|
+
model.matches_for('Piriss').should == [@paris]
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'does not confuse fields' do
|
52
|
+
model.for_field(:name).matches_for('Paris').should == [@paris]
|
53
|
+
model.for_field(:data).matches_for('Paris').should be_empty
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'does not confuse owner types' do
|
57
|
+
model.for_model(Stuff).matches_for('Paris').should == [@paris]
|
58
|
+
model.for_model(Object).matches_for('Paris').should be_empty
|
59
|
+
end
|
60
|
+
|
61
|
+
context '(with more than one entry)' do
|
62
|
+
before do
|
63
|
+
@palma = Stuff.create(:name => 'Palma')
|
64
|
+
%w(**p *pa pal alm lma).each do |trigram|
|
65
|
+
model.create(:owner => @palma, :score => 1, :fuzzy_field => :name, :trigram => trigram)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'honors the limit option' do
|
70
|
+
model.matches_for('Palmyre', :limit => 1).should == [@palma]
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'returns ordered results' do
|
74
|
+
model.matches_for('Palmyre').should == [@palma, @paris]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Fuzzily::Searchable do
|
4
|
+
# Prepare ourselves a Trigram repository
|
5
|
+
class Trigram < ActiveRecord::Base
|
6
|
+
include Fuzzily::Model
|
7
|
+
end
|
8
|
+
|
9
|
+
before(:each) { prepare_trigrams_table }
|
10
|
+
before(:each) { prepare_owners_table }
|
11
|
+
|
12
|
+
subject do
|
13
|
+
Stuff.clone.class_eval do
|
14
|
+
def self.name ; 'Stuff' ; end
|
15
|
+
self
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '.fuzzily_searchable' do
|
20
|
+
it 'is available to all of ActiveRecord' do
|
21
|
+
subject.should respond_to(:fuzzily_searchable)
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'adds a find_by_fuzzy_<field> method' do
|
25
|
+
subject.fuzzily_searchable :name
|
26
|
+
subject.should respond_to(:find_by_fuzzy_name)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'is idempotent' do
|
30
|
+
subject.fuzzily_searchable :name
|
31
|
+
subject.fuzzily_searchable :name
|
32
|
+
subject.should respond_to(:find_by_fuzzy_name)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'creates the trigrams_for_<field> association' do
|
36
|
+
subject.fuzzily_searchable :name
|
37
|
+
subject.new.should respond_to(:trigrams_for_name)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe '(callbacks)' do
|
42
|
+
it 'generates trigram records on creation' do
|
43
|
+
subject.fuzzily_searchable :name
|
44
|
+
subject.create(:name => 'Paris')
|
45
|
+
subject.last.trigrams_for_name.should_not be_empty
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'generates the correct trigrams' do
|
49
|
+
subject.fuzzily_searchable :name
|
50
|
+
record = subject.create(:name => 'FOO')
|
51
|
+
Trigram.first.trigram.should == '**f'
|
52
|
+
Trigram.first.owner_id.should == record.id
|
53
|
+
Trigram.first.owner_type.should == 'Stuff'
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'updates all trigram records on save' do
|
57
|
+
subject.fuzzily_searchable :name
|
58
|
+
subject.create(:name => 'Paris')
|
59
|
+
subject.first.update_attribute :name, 'Rome'
|
60
|
+
Trigram.all.map(&:trigram).should =~ %w(**r *ro rom ome)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe '#find_by_fuzzy_<field>' do
|
65
|
+
it 'works'
|
66
|
+
end
|
67
|
+
|
68
|
+
describe '#update_fuzzy_<field>!' do
|
69
|
+
it 'works'
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
data/spec/meta_spec.rb
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'fuzzily'
|
2
|
+
require 'pathname'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
Database = Pathname.new 'test.sqlite3'
|
6
|
+
|
7
|
+
# A test model we'll need as a source of trigrams
|
8
|
+
class Stuff < ActiveRecord::Base ; end
|
9
|
+
class StuffMigration < ActiveRecord::Migration
|
10
|
+
def self.up
|
11
|
+
create_table :stuffs do |t|
|
12
|
+
t.string :name
|
13
|
+
t.string :data
|
14
|
+
t.timestamps
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.down
|
19
|
+
drop_table :stuffs
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
RSpec.configure do |config|
|
24
|
+
config.before(:each) do
|
25
|
+
# Setup test database
|
26
|
+
ActiveRecord::Base.establish_connection(
|
27
|
+
:adapter => 'sqlite3',
|
28
|
+
:database => Database.to_s
|
29
|
+
)
|
30
|
+
|
31
|
+
def prepare_trigrams_table
|
32
|
+
silence_stream(STDOUT) do
|
33
|
+
Class.new(ActiveRecord::Migration).extend(Fuzzily::Migration).up
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def prepare_owners_table
|
38
|
+
silence_stream(STDOUT) do
|
39
|
+
StuffMigration.up
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
config.after(:each) do
|
46
|
+
Database.delete if Database.exist?
|
47
|
+
end
|
48
|
+
end
|
metadata
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fuzzily
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Julien Letessier
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-10-25 00:00:00 +01:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
23
|
+
none: false
|
24
|
+
requirements:
|
25
|
+
- - ~>
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
hash: 5
|
28
|
+
segments:
|
29
|
+
- 2
|
30
|
+
- 3
|
31
|
+
version: "2.3"
|
32
|
+
prerelease: false
|
33
|
+
name: activerecord
|
34
|
+
type: :runtime
|
35
|
+
version_requirements: *id001
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
none: false
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
hash: 3
|
43
|
+
segments:
|
44
|
+
- 0
|
45
|
+
version: "0"
|
46
|
+
prerelease: false
|
47
|
+
name: rspec
|
48
|
+
type: :development
|
49
|
+
version_requirements: *id002
|
50
|
+
- !ruby/object:Gem::Dependency
|
51
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
hash: 3
|
57
|
+
segments:
|
58
|
+
- 0
|
59
|
+
version: "0"
|
60
|
+
prerelease: false
|
61
|
+
name: appraisal
|
62
|
+
type: :development
|
63
|
+
version_requirements: *id003
|
64
|
+
- !ruby/object:Gem::Dependency
|
65
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
hash: 3
|
71
|
+
segments:
|
72
|
+
- 0
|
73
|
+
version: "0"
|
74
|
+
prerelease: false
|
75
|
+
name: pry
|
76
|
+
type: :development
|
77
|
+
version_requirements: *id004
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
80
|
+
none: false
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
hash: 3
|
85
|
+
segments:
|
86
|
+
- 0
|
87
|
+
version: "0"
|
88
|
+
prerelease: false
|
89
|
+
name: pry-nav
|
90
|
+
type: :development
|
91
|
+
version_requirements: *id005
|
92
|
+
- !ruby/object:Gem::Dependency
|
93
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
hash: 3
|
99
|
+
segments:
|
100
|
+
- 0
|
101
|
+
version: "0"
|
102
|
+
prerelease: false
|
103
|
+
name: sqlite3
|
104
|
+
type: :development
|
105
|
+
version_requirements: *id006
|
106
|
+
description: Fast fuzzy string matching for rails
|
107
|
+
email:
|
108
|
+
- julien.letessier@gmail.com
|
109
|
+
executables: []
|
110
|
+
|
111
|
+
extensions: []
|
112
|
+
|
113
|
+
extra_rdoc_files: []
|
114
|
+
|
115
|
+
files:
|
116
|
+
- .gitignore
|
117
|
+
- .rspec
|
118
|
+
- Gemfile
|
119
|
+
- LICENSE.txt
|
120
|
+
- README.md
|
121
|
+
- Rakefile
|
122
|
+
- fuzzily.gemspec
|
123
|
+
- lib/fuzzily.rb
|
124
|
+
- lib/fuzzily/migration.rb
|
125
|
+
- lib/fuzzily/model.rb
|
126
|
+
- lib/fuzzily/searchable.rb
|
127
|
+
- lib/fuzzily/trigram.rb
|
128
|
+
- lib/fuzzily/version.rb
|
129
|
+
- spec/fuzzily/migration_spec.rb
|
130
|
+
- spec/fuzzily/model_spec.rb
|
131
|
+
- spec/fuzzily/searchable_spec.rb
|
132
|
+
- spec/fuzzily/trigram_spec.rb
|
133
|
+
- spec/meta_spec.rb
|
134
|
+
- spec/spec_helper.rb
|
135
|
+
has_rdoc: true
|
136
|
+
homepage: ""
|
137
|
+
licenses: []
|
138
|
+
|
139
|
+
post_install_message:
|
140
|
+
rdoc_options: []
|
141
|
+
|
142
|
+
require_paths:
|
143
|
+
- lib
|
144
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
145
|
+
none: false
|
146
|
+
requirements:
|
147
|
+
- - ">="
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
hash: 3
|
150
|
+
segments:
|
151
|
+
- 0
|
152
|
+
version: "0"
|
153
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
154
|
+
none: false
|
155
|
+
requirements:
|
156
|
+
- - ">="
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
hash: 3
|
159
|
+
segments:
|
160
|
+
- 0
|
161
|
+
version: "0"
|
162
|
+
requirements: []
|
163
|
+
|
164
|
+
rubyforge_project:
|
165
|
+
rubygems_version: 1.3.9.5
|
166
|
+
signing_key:
|
167
|
+
specification_version: 3
|
168
|
+
summary: A fast, trigram-based, database-backed fuzzy string search/match engine for Rails.
|
169
|
+
test_files:
|
170
|
+
- spec/fuzzily/migration_spec.rb
|
171
|
+
- spec/fuzzily/model_spec.rb
|
172
|
+
- spec/fuzzily/searchable_spec.rb
|
173
|
+
- spec/fuzzily/trigram_spec.rb
|
174
|
+
- spec/meta_spec.rb
|
175
|
+
- spec/spec_helper.rb
|