fuzzily 0.2.4 → 0.3.0
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.
- checksums.yaml +4 -4
- data/.travis.yml +8 -2
- data/Appraisals +4 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +17 -16
- data/README.md +5 -2
- data/gemfiles/rails23.gemfile.lock +3 -3
- data/gemfiles/rails30.gemfile.lock +1 -1
- data/gemfiles/rails31.gemfile.lock +1 -1
- data/gemfiles/rails32.gemfile.lock +1 -1
- data/gemfiles/rails32_mysql.gemfile.lock +1 -1
- data/gemfiles/rails32_pg.gemfile.lock +1 -1
- data/gemfiles/rails40.gemfile +7 -0
- data/gemfiles/rails40.gemfile.lock +89 -0
- data/lib/fuzzily.rb +1 -1
- data/lib/fuzzily/model.rb +54 -21
- data/lib/fuzzily/searchable.rb +107 -47
- data/lib/fuzzily/version.rb +1 -1
- data/spec/fuzzily/searchable_spec.rb +27 -16
- data/spec/spec_helper.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c6b37d6f8b23347b04d0efb6d069c41654def4e5
|
|
4
|
+
data.tar.gz: 30c2b6d4362466e5cb63a9ae14496bd92e26ac54
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b65ff29d86fb3822ee426f1f7731d4f254f5cc180c5b4b0cf8f5489d2c492d02341ca3747a349f60291bb035783d617d333a72c55c7e80e794b5198860423348
|
|
7
|
+
data.tar.gz: f2993001f8f9189101644ff8ce52d243832e2479f6cc1b567632d6f2d7d2ffe4c6be5cef87a9082eaf62bd50fe482907065dec22a24e561244a8a118e989a4d6
|
data/.travis.yml
CHANGED
|
@@ -10,14 +10,20 @@ gemfile:
|
|
|
10
10
|
- gemfiles/rails32.gemfile
|
|
11
11
|
- gemfiles/rails32_pg.gemfile
|
|
12
12
|
- gemfiles/rails32_mysql.gemfile
|
|
13
|
+
- gemfiles/rails40.gemfile
|
|
13
14
|
before_install: bundle install
|
|
14
15
|
bundler_args:
|
|
15
16
|
matrix:
|
|
16
17
|
exclude:
|
|
17
18
|
- rvm: 2.0.0
|
|
18
19
|
gemfile: gemfiles/rails23.gemfile
|
|
19
|
-
|
|
20
|
+
- rvm: 1.8.7
|
|
21
|
+
gemfile: gemfiles/rails40.gemfile
|
|
22
|
+
- rvm: 1.9.2
|
|
23
|
+
gemfile: gemfiles/rails40.gemfile
|
|
20
24
|
before_script:
|
|
21
25
|
- psql -c 'create database fuzzily_test;' -U postgres
|
|
22
26
|
- mysql -e 'create database fuzzily_test;'
|
|
23
|
-
env:
|
|
27
|
+
env:
|
|
28
|
+
global:
|
|
29
|
+
- TRAVIS=TRUE
|
data/Appraisals
CHANGED
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
fuzzily (0.
|
|
4
|
+
fuzzily (0.3.0)
|
|
5
5
|
activerecord (>= 2.3.17)
|
|
6
6
|
|
|
7
7
|
GEM
|
|
8
|
-
remote:
|
|
8
|
+
remote: https://rubygems.org/
|
|
9
9
|
specs:
|
|
10
10
|
activemodel (3.2.14)
|
|
11
11
|
activesupport (= 3.2.14)
|
|
@@ -33,12 +33,12 @@ GEM
|
|
|
33
33
|
thor
|
|
34
34
|
diff-lcs (1.2.4)
|
|
35
35
|
i18n (0.6.5)
|
|
36
|
-
method_source (0.8.
|
|
37
|
-
mime-types (1.
|
|
36
|
+
method_source (0.8.2)
|
|
37
|
+
mime-types (1.25)
|
|
38
38
|
multi_json (1.7.9)
|
|
39
|
-
mysql2 (0.3.
|
|
40
|
-
pg (0.
|
|
41
|
-
pry (0.9.12.
|
|
39
|
+
mysql2 (0.3.13)
|
|
40
|
+
pg (0.16.0)
|
|
41
|
+
pry (0.9.12.2)
|
|
42
42
|
coderay (~> 1.0.5)
|
|
43
43
|
method_source (~> 0.8)
|
|
44
44
|
slop (~> 3.4)
|
|
@@ -47,20 +47,20 @@ GEM
|
|
|
47
47
|
rake (10.1.0)
|
|
48
48
|
rest-client (1.6.7)
|
|
49
49
|
mime-types (>= 1.16)
|
|
50
|
-
rspec (2.
|
|
51
|
-
rspec-core (~> 2.
|
|
52
|
-
rspec-expectations (~> 2.
|
|
53
|
-
rspec-mocks (~> 2.
|
|
54
|
-
rspec-core (2.
|
|
55
|
-
rspec-expectations (2.
|
|
50
|
+
rspec (2.14.1)
|
|
51
|
+
rspec-core (~> 2.14.0)
|
|
52
|
+
rspec-expectations (~> 2.14.0)
|
|
53
|
+
rspec-mocks (~> 2.14.0)
|
|
54
|
+
rspec-core (2.14.5)
|
|
55
|
+
rspec-expectations (2.14.2)
|
|
56
56
|
diff-lcs (>= 1.1.3, < 2.0)
|
|
57
|
-
rspec-mocks (2.
|
|
57
|
+
rspec-mocks (2.14.3)
|
|
58
58
|
simplecov (0.7.1)
|
|
59
59
|
multi_json (~> 1.0)
|
|
60
60
|
simplecov-html (~> 0.7.1)
|
|
61
61
|
simplecov-html (0.7.1)
|
|
62
|
-
slop (3.4.
|
|
63
|
-
sqlite3 (1.3.
|
|
62
|
+
slop (3.4.6)
|
|
63
|
+
sqlite3 (1.3.8)
|
|
64
64
|
thor (0.18.1)
|
|
65
65
|
tzinfo (0.3.37)
|
|
66
66
|
|
|
@@ -68,6 +68,7 @@ PLATFORMS
|
|
|
68
68
|
ruby
|
|
69
69
|
|
|
70
70
|
DEPENDENCIES
|
|
71
|
+
activerecord (~> 3.2.1)
|
|
71
72
|
appraisal
|
|
72
73
|
coveralls
|
|
73
74
|
fuzzily!
|
data/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Blurrily finds misspelled, prefix, or partial needles in a haystack of
|
|
|
15
15
|
strings. It's 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.
|
|
16
16
|
Loosely inspired from an [old blog post](http://unirec.blogspot.co.uk/2007/12/live-fuzzy-search-using-n-grams-in.html).
|
|
17
17
|
|
|
18
|
-
Tested with ActiveRecord (2.3, 3.0, 3.1, 3.2) on various Rubies (1.8.7, 1.9.2, 1.9.3, 2.0.0) and the most common adapters (SQLite3, MySQL, and PostgreSQL).
|
|
18
|
+
Tested with ActiveRecord (2.3, 3.0, 3.1, 3.2, 4.0) on various Rubies (1.8.7, 1.9.2, 1.9.3, 2.0.0) and the most common adapters (SQLite3, MySQL, and PostgreSQL).
|
|
19
19
|
|
|
20
20
|
If your dateset is big, if you need yet more speed, or do not use ActiveRecord,
|
|
21
21
|
check out [blurrily](http://github.com/mezis/blurrily), another gem (backed with a C extension)
|
|
@@ -71,7 +71,9 @@ Search!
|
|
|
71
71
|
MyStuff.find_by_fuzzy_name('Some Name', :limit => 10)
|
|
72
72
|
# => records
|
|
73
73
|
|
|
74
|
+
You can force an update on a specific record with
|
|
74
75
|
|
|
76
|
+
MyStuff.find(123).update_fuzzy_name!
|
|
75
77
|
|
|
76
78
|
## Indexing more than one field
|
|
77
79
|
|
|
@@ -131,4 +133,5 @@ Copyright (c) 2013 HouseTrip Ltd.
|
|
|
131
133
|
5. Create a new Pull Request
|
|
132
134
|
|
|
133
135
|
|
|
134
|
-
Thanks to @bclennox
|
|
136
|
+
Thanks to @bclennox, @fdegiuli, @nickbender, @Shanison for pointing out
|
|
137
|
+
and/or helping on various issues.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: /Users/mezis/Dropbox/Development/fuzzily
|
|
3
3
|
specs:
|
|
4
|
-
fuzzily (0.
|
|
4
|
+
fuzzily (0.3.0)
|
|
5
5
|
activerecord (>= 2.3.17)
|
|
6
6
|
|
|
7
7
|
GEM
|
|
@@ -48,8 +48,8 @@ GEM
|
|
|
48
48
|
multi_json (~> 1.0)
|
|
49
49
|
simplecov-html (~> 0.7.1)
|
|
50
50
|
simplecov-html (0.7.1)
|
|
51
|
-
slop (3.4.
|
|
52
|
-
sqlite3 (1.3.
|
|
51
|
+
slop (3.4.6)
|
|
52
|
+
sqlite3 (1.3.8)
|
|
53
53
|
thor (0.18.1)
|
|
54
54
|
|
|
55
55
|
PLATFORMS
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: /Users/mezis/Dropbox/Development/fuzzily
|
|
3
|
+
specs:
|
|
4
|
+
fuzzily (0.3.0)
|
|
5
|
+
activerecord (>= 2.3.17)
|
|
6
|
+
|
|
7
|
+
GEM
|
|
8
|
+
remote: https://rubygems.org/
|
|
9
|
+
specs:
|
|
10
|
+
activemodel (4.0.0)
|
|
11
|
+
activesupport (= 4.0.0)
|
|
12
|
+
builder (~> 3.1.0)
|
|
13
|
+
activerecord (4.0.0)
|
|
14
|
+
activemodel (= 4.0.0)
|
|
15
|
+
activerecord-deprecated_finders (~> 1.0.2)
|
|
16
|
+
activesupport (= 4.0.0)
|
|
17
|
+
arel (~> 4.0.0)
|
|
18
|
+
activerecord-deprecated_finders (1.0.3)
|
|
19
|
+
activesupport (4.0.0)
|
|
20
|
+
i18n (~> 0.6, >= 0.6.4)
|
|
21
|
+
minitest (~> 4.2)
|
|
22
|
+
multi_json (~> 1.3)
|
|
23
|
+
thread_safe (~> 0.1)
|
|
24
|
+
tzinfo (~> 0.3.37)
|
|
25
|
+
appraisal (0.5.2)
|
|
26
|
+
bundler
|
|
27
|
+
rake
|
|
28
|
+
arel (4.0.0)
|
|
29
|
+
atomic (1.1.13)
|
|
30
|
+
builder (3.1.4)
|
|
31
|
+
coderay (1.0.9)
|
|
32
|
+
colorize (0.5.8)
|
|
33
|
+
coveralls (0.6.7)
|
|
34
|
+
colorize
|
|
35
|
+
multi_json (~> 1.3)
|
|
36
|
+
rest-client
|
|
37
|
+
simplecov (>= 0.7)
|
|
38
|
+
thor
|
|
39
|
+
diff-lcs (1.2.4)
|
|
40
|
+
i18n (0.6.5)
|
|
41
|
+
method_source (0.8.2)
|
|
42
|
+
mime-types (1.25)
|
|
43
|
+
minitest (4.7.5)
|
|
44
|
+
multi_json (1.7.9)
|
|
45
|
+
mysql2 (0.3.13)
|
|
46
|
+
pg (0.16.0)
|
|
47
|
+
pry (0.9.12.2)
|
|
48
|
+
coderay (~> 1.0.5)
|
|
49
|
+
method_source (~> 0.8)
|
|
50
|
+
slop (~> 3.4)
|
|
51
|
+
pry-nav (0.2.3)
|
|
52
|
+
pry (~> 0.9.10)
|
|
53
|
+
rake (10.1.0)
|
|
54
|
+
rest-client (1.6.7)
|
|
55
|
+
mime-types (>= 1.16)
|
|
56
|
+
rspec (2.14.1)
|
|
57
|
+
rspec-core (~> 2.14.0)
|
|
58
|
+
rspec-expectations (~> 2.14.0)
|
|
59
|
+
rspec-mocks (~> 2.14.0)
|
|
60
|
+
rspec-core (2.14.5)
|
|
61
|
+
rspec-expectations (2.14.2)
|
|
62
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
|
63
|
+
rspec-mocks (2.14.3)
|
|
64
|
+
simplecov (0.7.1)
|
|
65
|
+
multi_json (~> 1.0)
|
|
66
|
+
simplecov-html (~> 0.7.1)
|
|
67
|
+
simplecov-html (0.7.1)
|
|
68
|
+
slop (3.4.6)
|
|
69
|
+
sqlite3 (1.3.8)
|
|
70
|
+
thor (0.18.1)
|
|
71
|
+
thread_safe (0.1.2)
|
|
72
|
+
atomic
|
|
73
|
+
tzinfo (0.3.37)
|
|
74
|
+
|
|
75
|
+
PLATFORMS
|
|
76
|
+
ruby
|
|
77
|
+
|
|
78
|
+
DEPENDENCIES
|
|
79
|
+
activerecord (~> 4.0.0)
|
|
80
|
+
appraisal
|
|
81
|
+
coveralls
|
|
82
|
+
fuzzily!
|
|
83
|
+
mysql2
|
|
84
|
+
pg
|
|
85
|
+
pry
|
|
86
|
+
pry-nav
|
|
87
|
+
rake
|
|
88
|
+
rspec
|
|
89
|
+
sqlite3
|
data/lib/fuzzily.rb
CHANGED
data/lib/fuzzily/model.rb
CHANGED
|
@@ -5,8 +5,7 @@ module Fuzzily
|
|
|
5
5
|
|
|
6
6
|
def self.included(by)
|
|
7
7
|
by.ancestors.include?(ActiveRecord::Base) or raise 'Not included in an ActiveRecord subclass'
|
|
8
|
-
|
|
9
|
-
scope_method = ActiveRecord::VERSION::MAJOR == 2 ? :named_scope : :scope
|
|
8
|
+
by.extend(ClassMethods)
|
|
10
9
|
|
|
11
10
|
by.class_eval do
|
|
12
11
|
return if class_variable_defined?(:@@fuzzily_trigram_model)
|
|
@@ -18,31 +17,65 @@ module Fuzzily
|
|
|
18
17
|
validates_presence_of :score
|
|
19
18
|
validates_presence_of :fuzzy_field
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
:conditions => { :owner_type => model.kind_of?(Class) ? model.name : model }
|
|
23
|
-
}}
|
|
24
|
-
send scope_method, :for_field, lambda { |field_name| {
|
|
25
|
-
:conditions => { :fuzzy_field => field_name }
|
|
26
|
-
}}
|
|
27
|
-
send scope_method, :with_trigram, lambda { |trigrams| {
|
|
28
|
-
:conditions => { :trigram => trigrams }
|
|
29
|
-
}}
|
|
30
|
-
|
|
20
|
+
_add_fuzzy_scopes
|
|
31
21
|
class_variable_set(:@@fuzzily_trigram_model, true)
|
|
32
22
|
end
|
|
33
|
-
|
|
34
|
-
by.extend(ClassMethods)
|
|
35
23
|
end
|
|
36
24
|
|
|
37
25
|
module ClassMethods
|
|
38
26
|
def matches_for(text)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
27
|
+
_matches_for_trigrams Fuzzily::String.new(text).trigrams
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
module Rails2
|
|
33
|
+
def _matches_for_trigrams(trigrams)
|
|
34
|
+
self.
|
|
35
|
+
scoped(:select => 'owner_id, owner_type, count(*) AS matches, MAX(score) AS score').
|
|
36
|
+
scoped(:group => 'owner_id, owner_type').
|
|
37
|
+
scoped(:order => 'matches DESC, score ASC').
|
|
38
|
+
with_trigram(trigrams).
|
|
39
|
+
map(&:owner)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def _add_fuzzy_scopes
|
|
43
|
+
named_scope :for_model, lambda { |model| {
|
|
44
|
+
:conditions => { :owner_type => model.kind_of?(Class) ? model.name : model }
|
|
45
|
+
}}
|
|
46
|
+
named_scope :for_field, lambda { |field_name| {
|
|
47
|
+
:conditions => { :fuzzy_field => field_name }
|
|
48
|
+
}}
|
|
49
|
+
named_scope :with_trigram, lambda { |trigrams| {
|
|
50
|
+
:conditions => { :trigram => trigrams }
|
|
51
|
+
}}
|
|
52
|
+
named_scope :limit, lambda { |count| { :limit => count }}
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
module Rails3
|
|
57
|
+
def _matches_for_trigrams(trigrams)
|
|
58
|
+
self.
|
|
59
|
+
select('owner_id, owner_type, count(*) AS matches, MAX(score) AS score').
|
|
60
|
+
group('owner_id, owner_type').
|
|
61
|
+
order('matches DESC, score ASC').
|
|
62
|
+
with_trigram(trigrams).
|
|
63
|
+
map(&:owner)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def _add_fuzzy_scopes
|
|
67
|
+
scope :for_model, lambda { |model|
|
|
68
|
+
where(:owner_type => model.kind_of?(Class) ? model.name : model)
|
|
69
|
+
}
|
|
70
|
+
scope :for_field, lambda { |field_name| where(:fuzzy_field => field_name) }
|
|
71
|
+
scope :with_trigram, lambda { |trigrams| where(:trigram => trigrams) }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
if ActiveRecord::VERSION::MAJOR == 2
|
|
76
|
+
include Rails2
|
|
77
|
+
else
|
|
78
|
+
include Rails3
|
|
46
79
|
end
|
|
47
80
|
end
|
|
48
81
|
end
|
data/lib/fuzzily/searchable.rb
CHANGED
|
@@ -1,61 +1,62 @@
|
|
|
1
1
|
require 'fuzzily/trigram'
|
|
2
|
+
require 'ostruct'
|
|
2
3
|
|
|
3
4
|
module Fuzzily
|
|
4
5
|
module Searchable
|
|
5
|
-
# fuzzily_searchable <field> [, <field>...] [, <options>]
|
|
6
|
-
def fuzzily_searchable(*fields)
|
|
7
|
-
options = fields.last.kind_of?(Hash) ? fields.pop : {}
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
def self.included(by)
|
|
8
|
+
case ActiveRecord::VERSION::MAJOR
|
|
9
|
+
when 2 then by.extend Rails2ClassMethods
|
|
10
|
+
when 3 then by.extend Rails3ClassMethods
|
|
11
|
+
when 4 then by.extend Rails4ClassMethods
|
|
11
12
|
end
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
private
|
|
15
16
|
|
|
16
|
-
def
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
:dependent => :destroy,
|
|
32
|
-
:autosave => true
|
|
33
|
-
|
|
34
|
-
singleton_class.send(:define_method,"find_by_fuzzy_#{field}".to_sym) do |*args|
|
|
35
|
-
case args.size
|
|
36
|
-
when 1 then pattern = args.first ; options = {}
|
|
37
|
-
when 2 then pattern, options = args
|
|
38
|
-
else raise 'Wrong # of arguments'
|
|
17
|
+
def _update_fuzzy!(_o)
|
|
18
|
+
self.send(_o.trigram_association).delete_all
|
|
19
|
+
String.new(self.send(_o.field)).scored_trigrams.each do |trigram, score|
|
|
20
|
+
self.send(_o.trigram_association).create!(:score => score, :trigram => trigram, :owner_type => self.class.name)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
module ClassMethods
|
|
26
|
+
# fuzzily_searchable <field> [, <field>...] [, <options>]
|
|
27
|
+
def fuzzily_searchable(*fields)
|
|
28
|
+
options = fields.last.kind_of?(Hash) ? fields.pop : {}
|
|
29
|
+
|
|
30
|
+
fields.each do |field|
|
|
31
|
+
make_field_fuzzily_searchable(field, options)
|
|
39
32
|
end
|
|
33
|
+
end
|
|
40
34
|
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def _find_by_fuzzy(_o, pattern, options={})
|
|
41
38
|
options[:limit] ||= 10
|
|
42
39
|
|
|
43
|
-
trigram_class_name.constantize.
|
|
44
|
-
|
|
40
|
+
_o.trigram_class_name.constantize.
|
|
41
|
+
limit(options[:limit]).
|
|
45
42
|
for_model(self.name).
|
|
46
|
-
for_field(field.to_s).
|
|
43
|
+
for_field(_o.field.to_s).
|
|
47
44
|
matches_for(pattern)
|
|
48
45
|
end
|
|
49
46
|
|
|
50
|
-
|
|
51
|
-
trigram_class = trigram_class_name.constantize
|
|
47
|
+
def _bulk_update_fuzzy(_o)
|
|
48
|
+
trigram_class = _o.trigram_class_name.constantize
|
|
49
|
+
|
|
50
|
+
supports_bulk_inserts =
|
|
51
|
+
connection.class.name !~ /sqlite/i ||
|
|
52
|
+
connection.send(:sqlite_version) >= '3.7.11'
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
_with_included_trigrams(_o).find_in_batches(:batch_size => 100) do |batch|
|
|
54
55
|
inserts = []
|
|
55
56
|
batch.each do |record|
|
|
56
|
-
data = Fuzzily::String.new(record.send(field))
|
|
57
|
+
data = Fuzzily::String.new(record.send(_o.field))
|
|
57
58
|
data.scored_trigrams.each do |trigram, score|
|
|
58
|
-
inserts << sanitize_sql_array(['(?,?,?,?,?)', self.name, record.id, field.to_s, score, trigram])
|
|
59
|
+
inserts << sanitize_sql_array(['(?,?,?,?,?)', self.name, record.id, _o.field.to_s, score, trigram])
|
|
59
60
|
end
|
|
60
61
|
end
|
|
61
62
|
|
|
@@ -74,7 +75,7 @@ module Fuzzily
|
|
|
74
75
|
]
|
|
75
76
|
|
|
76
77
|
trigram_class.transaction do
|
|
77
|
-
batch.each { |record| record.send(trigram_association).delete_all }
|
|
78
|
+
batch.each { |record| record.send(_o.trigram_association).delete_all }
|
|
78
79
|
break if inserts.empty?
|
|
79
80
|
|
|
80
81
|
if supports_bulk_inserts
|
|
@@ -88,20 +89,79 @@ module Fuzzily
|
|
|
88
89
|
end
|
|
89
90
|
end
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
def make_field_fuzzily_searchable(field, options={})
|
|
93
|
+
class_variable_defined?(:"@@fuzzily_searchable_#{field}") and return
|
|
94
|
+
|
|
95
|
+
_o = OpenStruct.new(
|
|
96
|
+
:field => field,
|
|
97
|
+
:trigram_class_name => options.fetch(:class_name, 'Trigram'),
|
|
98
|
+
:trigram_association => "trigrams_for_#{field}".to_sym,
|
|
99
|
+
:update_trigrams_method => "update_fuzzy_#{field}!".to_sym
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
_add_trigram_association(_o)
|
|
103
|
+
|
|
104
|
+
singleton_class.send(:define_method,"find_by_fuzzy_#{field}".to_sym) do |*args|
|
|
105
|
+
_find_by_fuzzy(_o, *args)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
singleton_class.send(:define_method,"bulk_update_fuzzy_#{field}".to_sym) do
|
|
109
|
+
_bulk_update_fuzzy(_o)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
define_method _o.update_trigrams_method do
|
|
113
|
+
_update_fuzzy!(_o)
|
|
95
114
|
end
|
|
115
|
+
|
|
116
|
+
after_save do |record|
|
|
117
|
+
next unless record.send("#{field}_changed?".to_sym)
|
|
118
|
+
record.send(_o.update_trigrams_method)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
class_variable_set(:"@@fuzzily_searchable_#{field}", true)
|
|
122
|
+
self
|
|
96
123
|
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
module Rails2ClassMethods
|
|
127
|
+
include ClassMethods
|
|
97
128
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
def _add_trigram_association(_o)
|
|
132
|
+
has_many _o.trigram_association,
|
|
133
|
+
:class_name => _o.trigram_class_name,
|
|
134
|
+
:as => :owner,
|
|
135
|
+
:conditions => { :fuzzy_field => _o.field.to_s },
|
|
136
|
+
:dependent => :destroy,
|
|
137
|
+
:autosave => true
|
|
101
138
|
end
|
|
102
139
|
|
|
103
|
-
|
|
104
|
-
|
|
140
|
+
def _with_included_trigrams(_o)
|
|
141
|
+
self.scoped(:include => _o.trigram_association)
|
|
142
|
+
end
|
|
105
143
|
end
|
|
144
|
+
|
|
145
|
+
Rails3ClassMethods = Rails2ClassMethods
|
|
146
|
+
|
|
147
|
+
module Rails4ClassMethods
|
|
148
|
+
include ClassMethods
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
def _add_trigram_association(_o)
|
|
153
|
+
has_many _o.trigram_association,
|
|
154
|
+
lambda { where(:fuzzy_field => _o.field.to_s) },
|
|
155
|
+
:class_name => _o.trigram_class_name,
|
|
156
|
+
:as => :owner,
|
|
157
|
+
:dependent => :delete_all,
|
|
158
|
+
:autosave => true
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def _with_included_trigrams(_o)
|
|
162
|
+
self.includes(_o.trigram_association)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
106
166
|
end
|
|
107
|
-
end
|
|
167
|
+
end
|
data/lib/fuzzily/version.rb
CHANGED
|
@@ -43,22 +43,27 @@ describe Fuzzily::Searchable do
|
|
|
43
43
|
before { subject.fuzzily_searchable :name }
|
|
44
44
|
|
|
45
45
|
it 'generates trigram records on creation' do
|
|
46
|
-
subject.create(:name => 'Paris')
|
|
46
|
+
subject.create!(:name => 'Paris')
|
|
47
47
|
subject.last.trigrams_for_name.should_not be_empty
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
it 'generates the correct trigrams' do
|
|
51
|
-
record = subject.create(:name => 'FOO')
|
|
51
|
+
record = subject.create!(:name => 'FOO')
|
|
52
52
|
Trigram.first.trigram.should == '**f'
|
|
53
53
|
Trigram.first.owner_id.should == record.id
|
|
54
54
|
Trigram.first.owner_type.should == 'Stuff'
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
it 'updates all trigram records on save' do
|
|
58
|
-
subject.create(:name => 'Paris')
|
|
58
|
+
subject.create!(:name => 'Paris')
|
|
59
59
|
subject.first.update_attribute :name, 'Rome'
|
|
60
60
|
Trigram.all.map(&:trigram).should =~ %w(**r *ro rom ome me*)
|
|
61
61
|
end
|
|
62
|
+
|
|
63
|
+
it 'deletes all trigrams on destroy' do
|
|
64
|
+
subject.create!(:name => 'Paris').destroy
|
|
65
|
+
Trigram.all.should be_empty
|
|
66
|
+
end
|
|
62
67
|
end
|
|
63
68
|
|
|
64
69
|
describe '#update_fuzzy_<field>!' do
|
|
@@ -67,14 +72,14 @@ describe Fuzzily::Searchable do
|
|
|
67
72
|
end
|
|
68
73
|
|
|
69
74
|
it 're-creates trigrams' do
|
|
70
|
-
subject.create(:name => 'Paris')
|
|
75
|
+
subject.create!(:name => 'Paris')
|
|
71
76
|
old_ids = Trigram.all.map(&:id)
|
|
72
77
|
subject.last.update_fuzzy_name!
|
|
73
78
|
(old_ids & Trigram.all.map(&:id)).should be_empty
|
|
74
79
|
end
|
|
75
80
|
|
|
76
81
|
it 'ignores nil values' do
|
|
77
|
-
subject.create(:name => nil)
|
|
82
|
+
subject.create!(:name => nil)
|
|
78
83
|
subject.last.update_fuzzy_name!
|
|
79
84
|
Trigram.all.should be_empty
|
|
80
85
|
end
|
|
@@ -84,14 +89,14 @@ describe Fuzzily::Searchable do
|
|
|
84
89
|
before { subject.fuzzily_searchable :name }
|
|
85
90
|
|
|
86
91
|
it 'creates all trigrams' do
|
|
87
|
-
subject.create(:name => 'Paris')
|
|
92
|
+
subject.create!(:name => 'Paris')
|
|
88
93
|
Trigram.delete_all
|
|
89
94
|
subject.bulk_update_fuzzy_name
|
|
90
95
|
Trigram.all.should_not be_empty
|
|
91
96
|
end
|
|
92
97
|
|
|
93
98
|
it 'ignores nil values' do
|
|
94
|
-
subject.create(:name => nil)
|
|
99
|
+
subject.create!(:name => nil)
|
|
95
100
|
Trigram.delete_all
|
|
96
101
|
subject.bulk_update_fuzzy_name
|
|
97
102
|
Trigram.all.should be_empty
|
|
@@ -102,9 +107,9 @@ describe Fuzzily::Searchable do
|
|
|
102
107
|
describe '#find_by_fuzzy_<field>' do
|
|
103
108
|
it 'returns records' do
|
|
104
109
|
subject.fuzzily_searchable :name
|
|
105
|
-
@paris = subject.create(:name => 'Paris')
|
|
106
|
-
@palma = subject.create(:name => 'Palma de Majorca')
|
|
107
|
-
@palmyre = subject.create(:name => 'La Palmyre')
|
|
110
|
+
@paris = subject.create!(:name => 'Paris')
|
|
111
|
+
@palma = subject.create!(:name => 'Palma de Majorca')
|
|
112
|
+
@palmyre = subject.create!(:name => 'La Palmyre')
|
|
108
113
|
|
|
109
114
|
subject.find_by_fuzzy_name('Piris').should_not be_empty
|
|
110
115
|
subject.find_by_fuzzy_name('Piris').should =~ [@paris, @palma]
|
|
@@ -113,10 +118,10 @@ describe Fuzzily::Searchable do
|
|
|
113
118
|
|
|
114
119
|
it 'favours exact matches' do
|
|
115
120
|
subject.fuzzily_searchable :name
|
|
116
|
-
@new_york = subject.create(:name => 'New York')
|
|
117
|
-
@yorkshire = subject.create(:name => 'Yorkshire')
|
|
118
|
-
@york = subject.create(:name => 'York')
|
|
119
|
-
@yorkisthan = subject.create(:name => 'Yorkisthan')
|
|
121
|
+
@new_york = subject.create!(:name => 'New York')
|
|
122
|
+
@yorkshire = subject.create!(:name => 'Yorkshire')
|
|
123
|
+
@york = subject.create!(:name => 'York')
|
|
124
|
+
@yorkisthan = subject.create!(:name => 'Yorkisthan')
|
|
120
125
|
|
|
121
126
|
subject.find_by_fuzzy_name('York').should == [@york, @new_york, @yorkshire, @yorkisthan]
|
|
122
127
|
subject.find_by_fuzzy_name('Yorkshire').should == [@yorkshire, @york, @yorkisthan, @new_york]
|
|
@@ -124,11 +129,17 @@ describe Fuzzily::Searchable do
|
|
|
124
129
|
|
|
125
130
|
it 'does not favour short words' do
|
|
126
131
|
subject.fuzzily_searchable :name
|
|
127
|
-
@lo = subject.create(:name => 'Lo') # **l *lo lo*
|
|
128
|
-
@london = subject.create(:name => 'London') # **l *lo lon ond ndo don on*
|
|
132
|
+
@lo = subject.create!(:name => 'Lo') # **l *lo lo*
|
|
133
|
+
@london = subject.create!(:name => 'London') # **l *lo lon ond ndo don on*
|
|
129
134
|
# **l *lo lon
|
|
130
135
|
subject.find_by_fuzzy_name('Lon').should == [@london, @lo]
|
|
131
136
|
end
|
|
137
|
+
|
|
138
|
+
it 'honours limie option' do
|
|
139
|
+
subject.fuzzily_searchable :name
|
|
140
|
+
3.times { subject.create!(:name => 'Paris') }
|
|
141
|
+
subject.find_by_fuzzy_name('Paris', :limit => 2).length.should == 2
|
|
142
|
+
end
|
|
132
143
|
end
|
|
133
144
|
end
|
|
134
145
|
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fuzzily
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Julien Letessier
|
|
@@ -180,6 +180,8 @@ files:
|
|
|
180
180
|
- gemfiles/rails32_mysql.gemfile.lock
|
|
181
181
|
- gemfiles/rails32_pg.gemfile
|
|
182
182
|
- gemfiles/rails32_pg.gemfile.lock
|
|
183
|
+
- gemfiles/rails40.gemfile
|
|
184
|
+
- gemfiles/rails40.gemfile.lock
|
|
183
185
|
- lib/fuzzily.rb
|
|
184
186
|
- lib/fuzzily/migration.rb
|
|
185
187
|
- lib/fuzzily/model.rb
|