fuzzily 0.3.0 → 0.3.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.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +79 -33
- data/gemfiles/rails23.gemfile.lock +1 -1
- 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.lock +1 -1
- data/lib/fuzzily/migration.rb +9 -1
- data/lib/fuzzily/model.rb +2 -4
- data/lib/fuzzily/searchable.rb +31 -6
- data/lib/fuzzily/version.rb +1 -1
- data/spec/fuzzily/model_spec.rb +7 -7
- data/spec/fuzzily/searchable_spec.rb +8 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3c1f0be1e0222fd105f14a6021e65e0b92ecf154
|
4
|
+
data.tar.gz: a96c50991cf0d5f180960b2f81086dd22038bec7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 95b1281eba8a4898f740d2b5a2038ba05f8a83ada3766a136c9d468ab8d3091970d9ad26103f7fdb186001d0dbf67f6556d1c99bc53603d6389f901d1bc9a732
|
7
|
+
data.tar.gz: 0dedbefc9d1f58f78ee76e7b9777056d83b82f53396b5f4dd3edd647ccd6cf33d998959e10e5f5192b800a51bead6ba06516d0193f1e1254d3d3fe56f83ad08f
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -11,7 +11,7 @@
|
|
11
11
|
> Here aresome photos of **Marrakesh**, Morroco.
|
12
12
|
> Did you mean **Martanesh**, Albania, **Marakkanam**, India, or **Marasheshty**, Romania?
|
13
13
|
|
14
|
-
|
14
|
+
Fuzzily 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
|
|
@@ -45,63 +45,77 @@ You'll need to setup 2 things:
|
|
45
45
|
|
46
46
|
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):
|
47
47
|
|
48
|
-
|
49
|
-
|
50
|
-
|
48
|
+
```ruby
|
49
|
+
class Trigram < ActiveRecord::Base
|
50
|
+
include Fuzzily::Model
|
51
|
+
end
|
52
|
+
```
|
51
53
|
|
52
54
|
Create a migration for it:
|
53
55
|
|
54
|
-
|
55
|
-
|
56
|
-
|
56
|
+
```ruby
|
57
|
+
class AddTrigramsModel < ActiveRecord::Migration
|
58
|
+
extend Fuzzily::Migration
|
59
|
+
end
|
60
|
+
```
|
57
61
|
|
58
|
-
Instrument your model
|
62
|
+
Instrument your model:
|
59
63
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
+
```ruby
|
65
|
+
class MyStuff < ActiveRecord::Base
|
66
|
+
# assuming my_stuffs has a 'name' attribute
|
67
|
+
fuzzily_searchable :name
|
68
|
+
end
|
69
|
+
```
|
64
70
|
|
65
71
|
Index your model (will happen automatically for new/updated records):
|
66
72
|
|
67
|
-
|
73
|
+
```ruby
|
74
|
+
MyStuff.bulk_update_fuzzy_name
|
75
|
+
```
|
68
76
|
|
69
77
|
Search!
|
70
78
|
|
71
|
-
|
72
|
-
|
79
|
+
```ruby
|
80
|
+
MyStuff.find_by_fuzzy_name('Some Name', :limit => 10)
|
81
|
+
# => records
|
82
|
+
```
|
73
83
|
|
74
84
|
You can force an update on a specific record with
|
75
85
|
|
76
|
-
|
86
|
+
```ruby
|
87
|
+
MyStuff.find(123).update_fuzzy_name!
|
88
|
+
```
|
77
89
|
|
78
90
|
## Indexing more than one field
|
79
91
|
|
80
92
|
Just list all the field you want to index, or call `fuzzily_searchable` more than once:
|
81
93
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
94
|
+
```ruby
|
95
|
+
class MyStuff < ActiveRecord::Base
|
96
|
+
fuzzily_searchable :name_fr, :name_en
|
97
|
+
fuzzily_searchable :name_de
|
98
|
+
end
|
99
|
+
```
|
87
100
|
|
88
101
|
## Custom name for the index model
|
89
102
|
|
90
103
|
If you want or need to name your index model differently (e.g. because you already have a class called `Trigram`):
|
91
104
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
class AddTrigramsModel < ActiveRecord::Migration
|
97
|
-
extend Fuzzily::Migration
|
98
|
-
trigrams_table_name = :custom_trigrams
|
99
|
-
end
|
105
|
+
```ruby
|
106
|
+
class CustomTrigram < ActiveRecord::Base
|
107
|
+
include Fuzzily::Model
|
108
|
+
end
|
100
109
|
|
101
|
-
|
102
|
-
|
103
|
-
|
110
|
+
class AddTrigramsModel < ActiveRecord::Migration
|
111
|
+
extend Fuzzily::Migration
|
112
|
+
trigrams_table_name = :custom_trigrams
|
113
|
+
end
|
104
114
|
|
115
|
+
class MyStuff < ActiveRecord::Base
|
116
|
+
fuzzily_searchable :name, :class_name => 'CustomTrigram'
|
117
|
+
end
|
118
|
+
```
|
105
119
|
|
106
120
|
## Speeding things up
|
107
121
|
|
@@ -117,6 +131,38 @@ MySQL and pgSQL.
|
|
117
131
|
This is not the default in the gem as ActiveRecord does not suport `ENUM`
|
118
132
|
columns in any version.
|
119
133
|
|
134
|
+
## UUID's
|
135
|
+
|
136
|
+
When using Rails 4 with UUID's, you will need to change the `owner_id` column type to `UUID`.
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
class AddTrigramsModel < ActiveRecord::Migration
|
140
|
+
extend Fuzzily::Migration
|
141
|
+
trigrams_owner_id_column_type = :uuid
|
142
|
+
end
|
143
|
+
```
|
144
|
+
|
145
|
+
## Searching virtual attributes
|
146
|
+
|
147
|
+
Your searchable fields do not have to be stored, they can be dynamic methods
|
148
|
+
too. Just remember to add a virtual change method as well.
|
149
|
+
For instance, if you model has `first_name` and `last_name` attributes, and you
|
150
|
+
want to index a compound `name` dynamic attribute:
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
class Employee < ActiveRecord::Base
|
154
|
+
fuzzily_searchable :name
|
155
|
+
def name
|
156
|
+
"#{first_name} #{last_name}"
|
157
|
+
end
|
158
|
+
|
159
|
+
def name_changed?
|
160
|
+
first_name_changed? || last_name_changed?
|
161
|
+
end
|
162
|
+
end
|
163
|
+
```
|
164
|
+
|
165
|
+
|
120
166
|
|
121
167
|
## License
|
122
168
|
|
@@ -133,5 +179,5 @@ Copyright (c) 2013 HouseTrip Ltd.
|
|
133
179
|
5. Create a new Pull Request
|
134
180
|
|
135
181
|
|
136
|
-
Thanks to @bclennox, @fdegiuli, @nickbender, @Shanison for pointing out
|
182
|
+
Thanks to @bclennox, @fdegiuli, @nickbender, @Shanison, @rickbutton for pointing out
|
137
183
|
and/or helping on various issues.
|
data/lib/fuzzily/migration.rb
CHANGED
@@ -12,11 +12,19 @@ module Fuzzily
|
|
12
12
|
@trigrams_table_name ||= :trigrams
|
13
13
|
end
|
14
14
|
|
15
|
+
def trigrams_owner_id_column_type=(custom_type)
|
16
|
+
@trigrams_owner_id_column_type = custom_type
|
17
|
+
end
|
18
|
+
|
19
|
+
def trigrams_owner_id_column_type
|
20
|
+
@trigrams_owner_id_column_type ||= :integer
|
21
|
+
end
|
22
|
+
|
15
23
|
def up
|
16
24
|
create_table trigrams_table_name do |t|
|
17
25
|
t.string :trigram, :limit => 3
|
18
26
|
t.integer :score, :limit => 2
|
19
|
-
t.
|
27
|
+
t.send trigrams_owner_id_column_type, :owner_id
|
20
28
|
t.string :owner_type
|
21
29
|
t.string :fuzzy_field
|
22
30
|
end
|
data/lib/fuzzily/model.rb
CHANGED
@@ -35,8 +35,7 @@ module Fuzzily
|
|
35
35
|
scoped(:select => 'owner_id, owner_type, count(*) AS matches, MAX(score) AS score').
|
36
36
|
scoped(:group => 'owner_id, owner_type').
|
37
37
|
scoped(:order => 'matches DESC, score ASC').
|
38
|
-
with_trigram(trigrams)
|
39
|
-
map(&:owner)
|
38
|
+
with_trigram(trigrams)
|
40
39
|
end
|
41
40
|
|
42
41
|
def _add_fuzzy_scopes
|
@@ -59,8 +58,7 @@ module Fuzzily
|
|
59
58
|
select('owner_id, owner_type, count(*) AS matches, MAX(score) AS score').
|
60
59
|
group('owner_id, owner_type').
|
61
60
|
order('matches DESC, score ASC').
|
62
|
-
with_trigram(trigrams)
|
63
|
-
map(&:owner)
|
61
|
+
with_trigram(trigrams)
|
64
62
|
end
|
65
63
|
|
66
64
|
def _add_fuzzy_scopes
|
data/lib/fuzzily/searchable.rb
CHANGED
@@ -36,12 +36,23 @@ module Fuzzily
|
|
36
36
|
|
37
37
|
def _find_by_fuzzy(_o, pattern, options={})
|
38
38
|
options[:limit] ||= 10
|
39
|
+
options[:offset] ||= 0
|
39
40
|
|
40
|
-
_o.trigram_class_name.constantize.
|
41
|
+
trigrams = _o.trigram_class_name.constantize.
|
41
42
|
limit(options[:limit]).
|
43
|
+
offset(options[:offset]).
|
42
44
|
for_model(self.name).
|
43
45
|
for_field(_o.field.to_s).
|
44
46
|
matches_for(pattern)
|
47
|
+
records = _load_for_ids(trigrams.map(&:owner_id))
|
48
|
+
# order records as per trigram query (no portable way to do this in SQL)
|
49
|
+
trigrams.map { |t| records[t.owner_id] }
|
50
|
+
end
|
51
|
+
|
52
|
+
def _load_for_ids(ids)
|
53
|
+
{}.tap do |result|
|
54
|
+
find(ids).each { |_r| result[_r.id] = _r }
|
55
|
+
end
|
45
56
|
end
|
46
57
|
|
47
58
|
def _bulk_update_fuzzy(_o)
|
@@ -123,9 +134,7 @@ module Fuzzily
|
|
123
134
|
end
|
124
135
|
end
|
125
136
|
|
126
|
-
module
|
127
|
-
include ClassMethods
|
128
|
-
|
137
|
+
module Rails2Rails3ClassMethods
|
129
138
|
private
|
130
139
|
|
131
140
|
def _add_trigram_association(_o)
|
@@ -142,7 +151,23 @@ module Fuzzily
|
|
142
151
|
end
|
143
152
|
end
|
144
153
|
|
145
|
-
|
154
|
+
module Rails2ClassMethods
|
155
|
+
include ClassMethods
|
156
|
+
include Rails2Rails3ClassMethods
|
157
|
+
|
158
|
+
def self.extended(base)
|
159
|
+
base.class_eval do
|
160
|
+
named_scope :offset, lambda { |*args| { :offset => args.first } }
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
module Rails3ClassMethods
|
166
|
+
include ClassMethods
|
167
|
+
include Rails2Rails3ClassMethods
|
168
|
+
end
|
169
|
+
|
170
|
+
|
146
171
|
|
147
172
|
module Rails4ClassMethods
|
148
173
|
include ClassMethods
|
@@ -164,4 +189,4 @@ module Fuzzily
|
|
164
189
|
end
|
165
190
|
|
166
191
|
end
|
167
|
-
end
|
192
|
+
end
|
data/lib/fuzzily/version.rb
CHANGED
data/spec/fuzzily/model_spec.rb
CHANGED
@@ -43,21 +43,21 @@ describe Fuzzily::Model do
|
|
43
43
|
end
|
44
44
|
|
45
45
|
it 'finds matches' do
|
46
|
-
model.matches_for('Paris').should == [@paris]
|
46
|
+
model.matches_for('Paris').map(&:owner).should == [@paris]
|
47
47
|
end
|
48
48
|
|
49
49
|
it 'finds close matches' do
|
50
|
-
model.matches_for('Piriss').should == [@paris]
|
50
|
+
model.matches_for('Piriss').map(&:owner).should == [@paris]
|
51
51
|
end
|
52
52
|
|
53
53
|
it 'does not confuse fields' do
|
54
|
-
model.for_field(:name).matches_for('Paris').should == [@paris]
|
55
|
-
model.for_field(:data).matches_for('Paris').should be_empty
|
54
|
+
model.for_field(:name).matches_for('Paris').map(&:owner).should == [@paris]
|
55
|
+
model.for_field(:data).matches_for('Paris').map(&:owner).should be_empty
|
56
56
|
end
|
57
57
|
|
58
58
|
it 'does not confuse owner types' do
|
59
|
-
model.for_model(Stuff).matches_for('Paris').should == [@paris]
|
60
|
-
model.for_model(Object).matches_for('Paris').should be_empty
|
59
|
+
model.for_model(Stuff).matches_for('Paris').map(&:owner).should == [@paris]
|
60
|
+
model.for_model(Object).matches_for('Paris').map(&:owner).should be_empty
|
61
61
|
end
|
62
62
|
|
63
63
|
context '(with more than one entry)' do
|
@@ -69,7 +69,7 @@ describe Fuzzily::Model do
|
|
69
69
|
end
|
70
70
|
|
71
71
|
it 'returns ordered results' do
|
72
|
-
model.matches_for('Palmyre').should == [@palma, @paris]
|
72
|
+
model.matches_for('Palmyre').map(&:owner).should == [@palma, @paris]
|
73
73
|
end
|
74
74
|
end
|
75
75
|
end
|
@@ -135,12 +135,18 @@ describe Fuzzily::Searchable do
|
|
135
135
|
subject.find_by_fuzzy_name('Lon').should == [@london, @lo]
|
136
136
|
end
|
137
137
|
|
138
|
-
it 'honours
|
138
|
+
it 'honours limit option' do
|
139
139
|
subject.fuzzily_searchable :name
|
140
140
|
3.times { subject.create!(:name => 'Paris') }
|
141
141
|
subject.find_by_fuzzy_name('Paris', :limit => 2).length.should == 2
|
142
142
|
end
|
143
|
+
|
144
|
+
it 'honours offset option' do
|
145
|
+
subject.fuzzily_searchable :name
|
146
|
+
3.times { subject.create!(:name => 'Paris') }
|
147
|
+
subject.find_by_fuzzy_name('Paris', :offset => 2).length.should == 1
|
148
|
+
end
|
143
149
|
end
|
144
150
|
end
|
145
151
|
|
146
|
-
end
|
152
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fuzzily
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Julien Letessier
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-
|
11
|
+
date: 2013-12-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|