pg_search 0.0.2

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.
@@ -0,0 +1,12 @@
1
+ #!/bin/sh
2
+ POSTGRESQL_VERSION=`pg_config --version | awk '{print $2}'`
3
+
4
+ cd /tmp
5
+ test -e /tmp/postgresql-$POSTGRESQL_VERSION.tar.bz2 || wget http://ftp9.us.postgresql.org/pub/mirrors/postgresql/source/v$POSTGRESQL_VERSION/postgresql-$POSTGRESQL_VERSION.tar.bz2
6
+ test -d /tmp/postgresql-$POSTGRESQL_VERSION || tar zxvf postgresql-$POSTGRESQL_VERSION.tar.bz2
7
+ cd postgresql-$POSTGRESQL_VERSION && eval ./configure `pg_config --configure` && make
8
+ cd contrib/unaccent && make && make install
9
+ cd ..
10
+ cd contrib/pg_trgm && make && make install
11
+ cd ..
12
+ cd contrib/fuzzystrmatch && make && make install
@@ -0,0 +1,225 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe PgSearch do
4
+ context "joining to another table" do
5
+ if defined?(ActiveRecord::Relation)
6
+ context "with Arel support" do
7
+ context "through a belongs_to association" do
8
+ with_model :associated_model do
9
+ table do |t|
10
+ t.string 'title'
11
+ end
12
+ end
13
+
14
+ with_model :model_with_belongs_to do
15
+ table do |t|
16
+ t.string 'title'
17
+ t.belongs_to 'another_model'
18
+ end
19
+
20
+ model do
21
+ include PgSearch
22
+ belongs_to :another_model, :class_name => 'AssociatedModel'
23
+
24
+ pg_search_scope :with_associated, :against => :title, :associated_against => {:another_model => :title}
25
+ end
26
+ end
27
+
28
+ it "returns rows that match the query in either its own columns or the columns of the associated model" do
29
+ associated = associated_model.create!(:title => 'abcdef')
30
+ included = [
31
+ model_with_belongs_to.create!(:title => 'ghijkl', :another_model => associated),
32
+ model_with_belongs_to.create!(:title => 'abcdef')
33
+ ]
34
+ excluded = model_with_belongs_to.create!(:title => 'mnopqr',
35
+ :another_model => associated_model.create!(:title => 'stuvwx'))
36
+
37
+ results = model_with_belongs_to.with_associated('abcdef')
38
+ results.map(&:title).should =~ included.map(&:title)
39
+ results.should_not include(excluded)
40
+ end
41
+ end
42
+
43
+ context "through a has_many association" do
44
+ with_model :associated_model_with_has_many do
45
+ table do |t|
46
+ t.string 'title'
47
+ t.belongs_to 'model_with_has_many'
48
+ end
49
+ end
50
+
51
+ with_model :model_with_has_many do
52
+ table do |t|
53
+ t.string 'title'
54
+ end
55
+
56
+ model do
57
+ include PgSearch
58
+ has_many :other_models, :class_name => 'AssociatedModelWithHasMany', :foreign_key => 'model_with_has_many_id'
59
+
60
+ pg_search_scope :with_associated, :against => [:title], :associated_against => {:other_models => :title}
61
+ end
62
+ end
63
+
64
+ it "returns rows that match the query in either its own columns or the columns of the associated model" do
65
+ included = [
66
+ model_with_has_many.create!(:title => 'abcdef', :other_models => [
67
+ associated_model_with_has_many.create!(:title => 'foo'),
68
+ associated_model_with_has_many.create!(:title => 'bar')
69
+ ]),
70
+ model_with_has_many.create!(:title => 'ghijkl', :other_models => [
71
+ associated_model_with_has_many.create!(:title => 'foo bar'),
72
+ associated_model_with_has_many.create!(:title => 'mnopqr')
73
+ ]),
74
+ model_with_has_many.create!(:title => 'foo bar')
75
+ ]
76
+ excluded = model_with_has_many.create!(:title => 'stuvwx', :other_models => [
77
+ associated_model_with_has_many.create!(:title => 'abcdef')
78
+ ])
79
+
80
+ results = model_with_has_many.with_associated('foo bar')
81
+ results.map(&:title).should =~ included.map(&:title)
82
+ results.should_not include(excluded)
83
+ end
84
+ end
85
+
86
+ context "across multiple associations" do
87
+ context "on different tables" do
88
+ with_model :first_associated_model do
89
+ table do |t|
90
+ t.string 'title'
91
+ t.belongs_to 'model_with_many_associations'
92
+ end
93
+ model {}
94
+ end
95
+
96
+ with_model :second_associated_model do
97
+ table do |t|
98
+ t.string 'title'
99
+ end
100
+ model {}
101
+ end
102
+
103
+ with_model :model_with_many_associations do
104
+ table do |t|
105
+ t.string 'title'
106
+ t.belongs_to 'model_of_second_type'
107
+ end
108
+
109
+ model do
110
+ include PgSearch
111
+ has_many :models_of_first_type, :class_name => 'FirstAssociatedModel', :foreign_key => 'model_with_many_associations_id'
112
+ belongs_to :model_of_second_type, :class_name => 'SecondAssociatedModel'
113
+
114
+ pg_search_scope :with_associated, :against => :title,
115
+ :associated_against => {:models_of_first_type => :title, :model_of_second_type => :title}
116
+ end
117
+ end
118
+
119
+ it "returns rows that match the query in either its own columns or the columns of the associated model" do
120
+ matching_second = second_associated_model.create!(:title => "foo bar")
121
+ unmatching_second = second_associated_model.create!(:title => "uiop")
122
+
123
+ included = [
124
+ ModelWithManyAssociations.create!(:title => 'abcdef', :models_of_first_type => [
125
+ first_associated_model.create!(:title => 'foo'),
126
+ first_associated_model.create!(:title => 'bar')
127
+ ]),
128
+ ModelWithManyAssociations.create!(:title => 'ghijkl', :models_of_first_type => [
129
+ first_associated_model.create!(:title => 'foo bar'),
130
+ first_associated_model.create!(:title => 'mnopqr')
131
+ ]),
132
+ ModelWithManyAssociations.create!(:title => 'foo bar'),
133
+ ModelWithManyAssociations.create!(:title => 'qwerty', :model_of_second_type => matching_second)
134
+ ]
135
+ excluded = [
136
+ ModelWithManyAssociations.create!(:title => 'stuvwx', :models_of_first_type => [
137
+ first_associated_model.create!(:title => 'abcdef')
138
+ ]),
139
+ ModelWithManyAssociations.create!(:title => 'qwerty', :model_of_second_type => unmatching_second)
140
+ ]
141
+
142
+ results = ModelWithManyAssociations.with_associated('foo bar')
143
+ results.map(&:title).should =~ included.map(&:title)
144
+ excluded.each { |object| results.should_not include(object) }
145
+ end
146
+ end
147
+
148
+ context "on the same table" do
149
+ with_model :doubly_associated_model do
150
+ table do |t|
151
+ t.string 'title'
152
+ t.belongs_to 'model_with_double_association'
153
+ t.belongs_to 'model_with_double_association_again'
154
+ end
155
+ model {}
156
+ end
157
+
158
+ with_model :model_with_double_association do
159
+ table do |t|
160
+ t.string 'title'
161
+ end
162
+
163
+ model do
164
+ include PgSearch
165
+ has_many :things, :class_name => 'DoublyAssociatedModel', :foreign_key => 'model_with_double_association_id'
166
+ has_many :thingamabobs, :class_name => 'DoublyAssociatedModel', :foreign_key => 'model_with_double_association_again_id'
167
+
168
+ pg_search_scope :with_associated, :against => :title,
169
+ :associated_against => {:things => :title, :thingamabobs => :title}
170
+ end
171
+ end
172
+
173
+ it "returns rows that match the query in either its own columns or the columns of the associated model" do
174
+ included = [
175
+ ModelWithDoubleAssociation.create!(:title => 'abcdef', :things => [
176
+ DoublyAssociatedModel.create!(:title => 'foo'),
177
+ DoublyAssociatedModel.create!(:title => 'bar')
178
+ ]),
179
+ ModelWithDoubleAssociation.create!(:title => 'ghijkl', :things => [
180
+ DoublyAssociatedModel.create!(:title => 'foo bar'),
181
+ DoublyAssociatedModel.create!(:title => 'mnopqr')
182
+ ]),
183
+ ModelWithDoubleAssociation.create!(:title => 'foo bar'),
184
+ ModelWithDoubleAssociation.create!(:title => 'qwerty', :thingamabobs => [
185
+ DoublyAssociatedModel.create!(:title => "foo bar")
186
+ ])
187
+ ]
188
+ excluded = [
189
+ ModelWithDoubleAssociation.create!(:title => 'stuvwx', :things => [
190
+ DoublyAssociatedModel.create!(:title => 'abcdef')
191
+ ]),
192
+ ModelWithDoubleAssociation.create!(:title => 'qwerty', :thingamabobs => [
193
+ DoublyAssociatedModel.create!(:title => "uiop")
194
+ ])
195
+ ]
196
+
197
+ results = ModelWithDoubleAssociation.with_associated('foo bar')
198
+ results.map(&:title).should =~ included.map(&:title)
199
+ excluded.each { |object| results.should_not include(object) }
200
+ end
201
+ end
202
+ end
203
+ end
204
+ else
205
+ context "without Arel support" do
206
+ with_model :model do
207
+ table do |t|
208
+ t.string 'title'
209
+ end
210
+
211
+ model do
212
+ include PgSearch
213
+ pg_search_scope :with_joins, :against => :title, :joins => :another_model
214
+ end
215
+ end
216
+
217
+ it "should raise an error" do
218
+ lambda {
219
+ Model.with_joins('foo')
220
+ }.should raise_error(ArgumentError, /joins/)
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,596 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "an ActiveRecord model which includes PgSearch" do
4
+
5
+ with_model :model_with_pg_search do
6
+ table do |t|
7
+ t.string 'title'
8
+ t.text 'content'
9
+ t.integer 'importance'
10
+ end
11
+
12
+ model do
13
+ include PgSearch
14
+ end
15
+ end
16
+
17
+ describe ".pg_search_scope" do
18
+ it "builds a scope" do
19
+ model_with_pg_search.class_eval do
20
+ pg_search_scope "matching_query", :against => []
21
+ end
22
+
23
+ lambda {
24
+ model_with_pg_search.scoped({}).matching_query("foo").scoped({})
25
+ }.should_not raise_error
26
+ end
27
+
28
+ context "when passed a lambda" do
29
+ it "builds a dynamic scope" do
30
+ model_with_pg_search.class_eval do
31
+ pg_search_scope :search_title_or_content, lambda { |query, pick_content|
32
+ {
33
+ :query => query.gsub("-remove-", ""),
34
+ :against => pick_content ? :content : :title
35
+ }
36
+ }
37
+ end
38
+
39
+ included = model_with_pg_search.create!(:title => 'foo', :content => 'bar')
40
+ excluded = model_with_pg_search.create!(:title => 'bar', :content => 'foo')
41
+
42
+ model_with_pg_search.search_title_or_content('fo-remove-o', false).should == [included]
43
+ model_with_pg_search.search_title_or_content('b-remove-ar', true).should == [included]
44
+ end
45
+ end
46
+
47
+ context "when an unknown option is passed in" do
48
+ it "raises an exception when invoked" do
49
+ lambda {
50
+ model_with_pg_search.class_eval do
51
+ pg_search_scope :with_unknown_option, :against => :content, :foo => :bar
52
+ end
53
+ model_with_pg_search.with_unknown_option("foo")
54
+ }.should raise_error(ArgumentError, /foo/)
55
+ end
56
+
57
+ context "dynamically" do
58
+ it "raises an exception when invoked" do
59
+ lambda {
60
+ model_with_pg_search.class_eval do
61
+ pg_search_scope :with_unknown_option, lambda { |*| {:against => :content, :foo => :bar} }
62
+ end
63
+ model_with_pg_search.with_unknown_option("foo")
64
+ }.should raise_error(ArgumentError, /foo/)
65
+ end
66
+ end
67
+ end
68
+
69
+ context "when an unknown :using is passed" do
70
+ it "raises an exception when invoked" do
71
+ lambda {
72
+ model_with_pg_search.class_eval do
73
+ pg_search_scope :with_unknown_using, :against => :content, :using => :foo
74
+ end
75
+ model_with_pg_search.with_unknown_using("foo")
76
+ }.should raise_error(ArgumentError, /foo/)
77
+ end
78
+
79
+ context "dynamically" do
80
+ it "raises an exception when invoked" do
81
+ lambda {
82
+ model_with_pg_search.class_eval do
83
+ pg_search_scope :with_unknown_using, lambda { |*| {:against => :content, :using => :foo} }
84
+ end
85
+ model_with_pg_search.with_unknown_using("foo")
86
+ }.should raise_error(ArgumentError, /foo/)
87
+ end
88
+ end
89
+ end
90
+
91
+ context "when an unknown :normalizing is passed" do
92
+ it "raises an exception when invoked" do
93
+ lambda {
94
+ model_with_pg_search.class_eval do
95
+ pg_search_scope :with_unknown_normalizing, :against => :content, :normalizing => :foo
96
+ end
97
+ model_with_pg_search.with_unknown_normalizing("foo")
98
+ }.should raise_error(ArgumentError, /normalizing.*foo/)
99
+ end
100
+
101
+ context "dynamically" do
102
+ it "raises an exception when invoked" do
103
+ lambda {
104
+ model_with_pg_search.class_eval do
105
+ pg_search_scope :with_unknown_normalizing, lambda { |*| {:against => :content, :normalizing => :foo} }
106
+ end
107
+ model_with_pg_search.with_unknown_normalizing("foo")
108
+ }.should raise_error(ArgumentError, /normalizing.*foo/)
109
+ end
110
+ end
111
+
112
+ context "when :against is not passed in" do
113
+ it "raises an exception when invoked" do
114
+ lambda {
115
+ model_with_pg_search.class_eval do
116
+ pg_search_scope :with_unknown_normalizing, {}
117
+ end
118
+ model_with_pg_search.with_unknown_normalizing("foo")
119
+ }.should raise_error(ArgumentError, /against/)
120
+ end
121
+ context "dynamically" do
122
+ it "raises an exception when invoked" do
123
+ lambda {
124
+ model_with_pg_search.class_eval do
125
+ pg_search_scope :with_unknown_normalizing, lambda { |*| {} }
126
+ end
127
+ model_with_pg_search.with_unknown_normalizing("foo")
128
+ }.should raise_error(ArgumentError, /against/)
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+
135
+ describe "a search scope" do
136
+ context "against a single column" do
137
+ before do
138
+ model_with_pg_search.class_eval do
139
+ pg_search_scope :search_content, :against => :content
140
+ end
141
+ end
142
+
143
+ it "returns an empty array when a blank query is passed in" do
144
+ model_with_pg_search.create!(:content => 'foo')
145
+
146
+ results = model_with_pg_search.search_content('')
147
+ results.should == []
148
+ end
149
+
150
+ it "returns rows where the column contains the term in the query" do
151
+ included = model_with_pg_search.create!(:content => 'foo')
152
+ excluded = model_with_pg_search.create!(:content => 'bar')
153
+
154
+ results = model_with_pg_search.search_content('foo')
155
+ results.should include(included)
156
+ results.should_not include(excluded)
157
+ end
158
+
159
+ it "returns rows where the column contains all the terms in the query in any order" do
160
+ included = [model_with_pg_search.create!(:content => 'foo bar'),
161
+ model_with_pg_search.create!(:content => 'bar foo')]
162
+ excluded = model_with_pg_search.create!(:content => 'foo')
163
+
164
+ results = model_with_pg_search.search_content('foo bar')
165
+ results.should =~ included
166
+ results.should_not include(excluded)
167
+ end
168
+
169
+ it "returns rows that match the query but not its case" do
170
+ # \303\241 is a with acute accent
171
+ # \303\251 is e with acute accent
172
+
173
+ included = [model_with_pg_search.create!(:content => "foo"),
174
+ model_with_pg_search.create!(:content => "FOO")]
175
+
176
+ results = model_with_pg_search.search_content("Foo")
177
+ results.should =~ included
178
+ end
179
+
180
+ it "returns rows that match the query only if their diacritics match" do
181
+ # \303\241 is a with acute accent
182
+ # \303\251 is e with acute accent
183
+
184
+ included = model_with_pg_search.create!(:content => "abcd\303\251f")
185
+ excluded = model_with_pg_search.create!(:content => "\303\241bcdef")
186
+
187
+ results = model_with_pg_search.search_content("abcd\303\251f")
188
+ results.should == [included]
189
+ results.should_not include(excluded)
190
+ end
191
+
192
+ it "returns rows that match the query but not rows that are prefixed by the query" do
193
+ included = model_with_pg_search.create!(:content => 'pre')
194
+ excluded = model_with_pg_search.create!(:content => 'prefix')
195
+
196
+ results = model_with_pg_search.search_content("pre")
197
+ results.should == [included]
198
+ results.should_not include(excluded)
199
+ end
200
+
201
+ it "returns rows that match the query when stemmed by the default dictionary (english)" do
202
+ included = [model_with_pg_search.create!(:content => "jump"),
203
+ model_with_pg_search.create!(:content => "jumped"),
204
+ model_with_pg_search.create!(:content => "jumping")]
205
+
206
+ results = model_with_pg_search.search_content("jump")
207
+ results.should =~ included
208
+ end
209
+
210
+ it "returns rows that match sorted by rank" do
211
+ loser = model_with_pg_search.create!(:content => 'foo')
212
+ winner = model_with_pg_search.create!(:content => 'foo foo')
213
+
214
+ results = model_with_pg_search.search_content("foo")
215
+ results[0].rank.should > results[1].rank
216
+ results.should == [winner, loser]
217
+ end
218
+
219
+ it "returns results that match sorted by primary key for records that rank the same" do
220
+ sorted_results = [model_with_pg_search.create!(:content => 'foo'),
221
+ model_with_pg_search.create!(:content => 'foo')].sort_by(&:id)
222
+
223
+ results = model_with_pg_search.search_content("foo")
224
+ results.should == sorted_results
225
+ end
226
+
227
+ it "returns results that match a query with multiple space-separated search terms" do
228
+ included = [
229
+ model_with_pg_search.create!(:content => 'foo bar'),
230
+ model_with_pg_search.create!(:content => 'bar foo'),
231
+ model_with_pg_search.create!(:content => 'bar foo baz'),
232
+ ]
233
+ excluded = [
234
+ model_with_pg_search.create!(:content => 'foo'),
235
+ model_with_pg_search.create!(:content => 'foo baz')
236
+ ]
237
+
238
+ results = model_with_pg_search.search_content('foo bar')
239
+ results.should =~ included
240
+ results.should_not include(excluded)
241
+ end
242
+
243
+ it "returns rows that match a query with characters that are invalid in a tsquery expression" do
244
+ included = model_with_pg_search.create!(:content => "(Foo.) Bar?, \\")
245
+
246
+ results = model_with_pg_search.search_content("foo bar .,?() \\")
247
+ results.should == [included]
248
+ end
249
+ it "accepts non-string queries and calls #to_s on them" do
250
+ foo = model_with_pg_search.create!(:content => "foo")
251
+ not_a_string = stub(:to_s => "foo")
252
+ model_with_pg_search.search_content(not_a_string).should == [foo]
253
+ end
254
+ end
255
+
256
+ context "against multiple columns" do
257
+ before do
258
+ model_with_pg_search.class_eval do
259
+ pg_search_scope :search_title_and_content, :against => [:title, :content]
260
+ end
261
+ end
262
+
263
+ it "returns rows whose columns contain all of the terms in the query across columns" do
264
+ included = [
265
+ model_with_pg_search.create!(:title => 'foo', :content => 'bar'),
266
+ model_with_pg_search.create!(:title => 'bar', :content => 'foo')
267
+ ]
268
+ excluded = [
269
+ model_with_pg_search.create!(:title => 'foo', :content => 'foo'),
270
+ model_with_pg_search.create!(:title => 'bar', :content => 'bar')
271
+ ]
272
+
273
+ results = model_with_pg_search.search_title_and_content('foo bar')
274
+
275
+ results.should =~ included
276
+ excluded.each do |result|
277
+ results.should_not include(result)
278
+ end
279
+ end
280
+
281
+ it "returns rows where at one column contains all of the terms in the query and another does not" do
282
+ in_title = model_with_pg_search.create!(:title => 'foo', :content => 'bar')
283
+ in_content = model_with_pg_search.create!(:title => 'bar', :content => 'foo')
284
+
285
+ results = model_with_pg_search.search_title_and_content('foo')
286
+ results.should =~ [in_title, in_content]
287
+ end
288
+
289
+ # Searching with a NULL column will prevent any matches unless we coalesce it.
290
+ it "returns rows where at one column contains all of the terms in the query and another is NULL" do
291
+ included = model_with_pg_search.create!(:title => 'foo', :content => nil)
292
+ results = model_with_pg_search.search_title_and_content('foo')
293
+ results.should == [included]
294
+ end
295
+ end
296
+
297
+ context "using trigram" do
298
+ before do
299
+ model_with_pg_search.class_eval do
300
+ pg_search_scope :with_trigrams, :against => [:title, :content], :using => :trigram
301
+ end
302
+ end
303
+
304
+ it "returns rows where one searchable column and the query share enough trigrams" do
305
+ included = model_with_pg_search.create!(:title => 'abcdefghijkl', :content => nil)
306
+ results = model_with_pg_search.with_trigrams('cdefhijkl')
307
+ results.should == [included]
308
+ end
309
+
310
+ it "returns rows where multiple searchable columns and the query share enough trigrams" do
311
+ included = model_with_pg_search.create!(:title => 'abcdef', :content => 'ghijkl')
312
+ results = model_with_pg_search.with_trigrams('cdefhijkl')
313
+ results.should == [included]
314
+ end
315
+ end
316
+
317
+ context "using tsearch" do
318
+ context "with :prefix => true" do
319
+ before do
320
+ model_with_pg_search.class_eval do
321
+ pg_search_scope :search_title_with_prefixes,
322
+ :against => :title,
323
+ :using => {
324
+ :tsearch => {:prefix => true}
325
+ }
326
+ end
327
+ end
328
+
329
+ it "returns rows that match the query and that are prefixed by the query" do
330
+ included = model_with_pg_search.create!(:title => 'prefix')
331
+ excluded = model_with_pg_search.create!(:title => 'postfix')
332
+
333
+ results = model_with_pg_search.search_title_with_prefixes("pre")
334
+ results.should == [included]
335
+ results.should_not include(excluded)
336
+ end
337
+
338
+ it "returns rows that match the query when the query has a hyphen" do
339
+ included = [
340
+ model_with_pg_search.create!(:title => 'foo bar'),
341
+ model_with_pg_search.create!(:title => 'foo-bar')
342
+ ]
343
+ excluded = model_with_pg_search.create!(:title => 'baz quux')
344
+
345
+ results = model_with_pg_search.search_title_with_prefixes("foo-bar")
346
+ results.should =~ included
347
+ results.should_not include(excluded)
348
+ end
349
+ end
350
+
351
+ context "with the simple dictionary" do
352
+ before do
353
+ model_with_pg_search.class_eval do
354
+ pg_search_scope :search_title, :against => :title
355
+
356
+ pg_search_scope :search_title_with_simple,
357
+ :against => :title,
358
+ :using => {
359
+ :tsearch => {:dictionary => :simple}
360
+ }
361
+ end
362
+ end
363
+
364
+ it "returns rows that match the query exactly but not that match the query when stemmed by the default dictionary" do
365
+ included = model_with_pg_search.create!(:title => "jumped")
366
+ excluded = [model_with_pg_search.create!(:title => "jump"),
367
+ model_with_pg_search.create!(:title => "jumping")]
368
+
369
+ default_results = model_with_pg_search.search_title("jumped")
370
+ default_results.should =~ [included] + excluded
371
+
372
+ simple_results = model_with_pg_search.search_title_with_simple("jumped")
373
+ simple_results.should == [included]
374
+ excluded.each do |result|
375
+ simple_results.should_not include(result)
376
+ end
377
+ end
378
+ end
379
+
380
+ context "against columns ranked with arrays" do
381
+ before do
382
+ model_with_pg_search.class_eval do
383
+ pg_search_scope :search_weighted_by_array_of_arrays, :against => [[:content, 'B'], [:title, 'A']]
384
+ end
385
+ end
386
+
387
+ it "returns results sorted by weighted rank" do
388
+ loser = model_with_pg_search.create!(:title => 'bar', :content => 'foo')
389
+ winner = model_with_pg_search.create!(:title => 'foo', :content => 'bar')
390
+
391
+ results = model_with_pg_search.search_weighted_by_array_of_arrays('foo')
392
+ results[0].rank.should > results[1].rank
393
+ results.should == [winner, loser]
394
+ end
395
+ end
396
+
397
+ context "against columns ranked with a hash" do
398
+ before do
399
+ model_with_pg_search.class_eval do
400
+ pg_search_scope :search_weighted_by_hash, :against => {:content => 'B', :title => 'A'}
401
+ end
402
+ end
403
+
404
+ it "returns results sorted by weighted rank" do
405
+ loser = model_with_pg_search.create!(:title => 'bar', :content => 'foo')
406
+ winner = model_with_pg_search.create!(:title => 'foo', :content => 'bar')
407
+
408
+ results = model_with_pg_search.search_weighted_by_hash('foo')
409
+ results[0].rank.should > results[1].rank
410
+ results.should == [winner, loser]
411
+ end
412
+ end
413
+
414
+ context "against columns of which only some are ranked" do
415
+ before do
416
+ model_with_pg_search.class_eval do
417
+ pg_search_scope :search_weighted, :against => [:content, [:title, 'A']]
418
+ end
419
+ end
420
+
421
+ it "returns results sorted by weighted rank using an implied low rank for unranked columns" do
422
+ loser = model_with_pg_search.create!(:title => 'bar', :content => 'foo')
423
+ winner = model_with_pg_search.create!(:title => 'foo', :content => 'bar')
424
+
425
+ results = model_with_pg_search.search_weighted('foo')
426
+ results[0].rank.should > results[1].rank
427
+ results.should == [winner, loser]
428
+ end
429
+ end
430
+ end
431
+
432
+ context "using dmetaphone" do
433
+ before do
434
+ model_with_pg_search.class_eval do
435
+ pg_search_scope :with_dmetaphones, :against => [:title, :content], :using => :dmetaphone
436
+ end
437
+ end
438
+
439
+ it "returns rows where one searchable column and the query share enough dmetaphones" do
440
+ included = model_with_pg_search.create!(:title => 'Geoff', :content => nil)
441
+ excluded = model_with_pg_search.create!(:title => 'Bob', :content => nil)
442
+ results = model_with_pg_search.with_dmetaphones('Jeff')
443
+ results.should == [included]
444
+ end
445
+
446
+ it "returns rows where multiple searchable columns and the query share enough dmetaphones" do
447
+ included = model_with_pg_search.create!(:title => 'Geoff', :content => 'George')
448
+ excluded = model_with_pg_search.create!(:title => 'Bob', :content => 'Jones')
449
+ results = model_with_pg_search.with_dmetaphones('Jeff Jorge')
450
+ results.should == [included]
451
+ end
452
+
453
+ it "returns rows that match dmetaphones that are English stopwords" do
454
+ included = model_with_pg_search.create!(:title => 'White', :content => nil)
455
+ excluded = model_with_pg_search.create!(:title => 'Black', :content => nil)
456
+ results = model_with_pg_search.with_dmetaphones('Wight')
457
+ results.should == [included]
458
+ end
459
+ end
460
+
461
+ context "using multiple features" do
462
+ before do
463
+ model_with_pg_search.class_eval do
464
+ pg_search_scope :with_tsearch, :against => :title, :using => :tsearch
465
+
466
+ pg_search_scope :with_trigram, :against => :title, :using => :trigram
467
+
468
+ pg_search_scope :with_tsearch_and_trigram_using_array,
469
+ :against => :title,
470
+ :using => [:tsearch, :trigram]
471
+
472
+ end
473
+ end
474
+
475
+ it "returns rows that match using any of the features" do
476
+ record = model_with_pg_search.create!(:title => "tiling is grouty")
477
+
478
+ # matches trigram only
479
+ trigram_query = "ling is grouty"
480
+ model_with_pg_search.with_trigram(trigram_query).should include(record)
481
+ model_with_pg_search.with_tsearch(trigram_query).should_not include(record)
482
+ model_with_pg_search.with_tsearch_and_trigram_using_array(trigram_query).should == [record]
483
+
484
+ # matches tsearch only
485
+ tsearch_query = "tile"
486
+ model_with_pg_search.with_tsearch(tsearch_query).should include(record)
487
+ model_with_pg_search.with_trigram(tsearch_query).should_not include(record)
488
+ model_with_pg_search.with_tsearch_and_trigram_using_array(tsearch_query).should == [record]
489
+ end
490
+
491
+ context "with feature-specific configuration" do
492
+ before do
493
+ @tsearch_config = tsearch_config = {:dictionary => 'english'}
494
+ @trigram_config = trigram_config = {:foo => 'bar'}
495
+
496
+ model_with_pg_search.class_eval do
497
+ pg_search_scope :with_tsearch_and_trigram_using_hash,
498
+ :against => :title,
499
+ :using => {
500
+ :tsearch => tsearch_config,
501
+ :trigram => trigram_config
502
+ }
503
+ end
504
+ end
505
+
506
+ it "should pass the custom configuration down to the specified feature" do
507
+ stub_feature = stub(:conditions => "1 = 1", :rank => "1.0")
508
+ PgSearch::Features::TSearch.should_receive(:new).with(anything, @tsearch_config, anything, anything, anything).at_least(:once).and_return(stub_feature)
509
+ PgSearch::Features::Trigram.should_receive(:new).with(anything, @trigram_config, anything, anything, anything).at_least(:once).and_return(stub_feature)
510
+
511
+ model_with_pg_search.with_tsearch_and_trigram_using_hash("foo")
512
+ end
513
+ end
514
+ end
515
+
516
+ context "normalizing diacritics" do
517
+ before do
518
+ model_with_pg_search.class_eval do
519
+ pg_search_scope :search_title_without_diacritics, :against => :title, :normalizing => :diacritics
520
+ end
521
+ end
522
+
523
+ it "returns rows that match the query but not its diacritics" do
524
+ # \303\241 is a with acute accent
525
+ # \303\251 is e with acute accent
526
+
527
+ included = model_with_pg_search.create!(:title => "\303\241bcdef")
528
+
529
+ results = model_with_pg_search.search_title_without_diacritics("abcd\303\251f")
530
+ results.should == [included]
531
+ end
532
+ end
533
+
534
+ context "when passed a :ranked_by expression" do
535
+ before do
536
+ model_with_pg_search.class_eval do
537
+ pg_search_scope :search_content_with_default_rank,
538
+ :against => :content
539
+ pg_search_scope :search_content_with_importance_as_rank,
540
+ :against => :content,
541
+ :ranked_by => "importance"
542
+ pg_search_scope :search_content_with_importance_as_rank_multiplier,
543
+ :against => :content,
544
+ :ranked_by => ":tsearch * importance"
545
+ end
546
+ end
547
+
548
+ it "should return records with a rank attribute equal to the :ranked_by expression" do
549
+ model_with_pg_search.create!(:content => 'foo', :importance => 10)
550
+ results = model_with_pg_search.search_content_with_importance_as_rank("foo")
551
+ results.first.rank.should == 10
552
+ end
553
+
554
+ it "should substitute :tsearch with the tsearch rank expression in the :ranked_by expression" do
555
+ model_with_pg_search.create!(:content => 'foo', :importance => 10)
556
+
557
+ tsearch_rank = model_with_pg_search.search_content_with_default_rank("foo").first.rank
558
+ multiplied_rank = model_with_pg_search.search_content_with_importance_as_rank_multiplier("foo").first.rank
559
+
560
+ multiplied_rank.should be_within(0.001).of(tsearch_rank * 10)
561
+ end
562
+
563
+ it "should return results in descending order of the value of the rank expression" do
564
+ records = [
565
+ model_with_pg_search.create!(:content => 'foo', :importance => 1),
566
+ model_with_pg_search.create!(:content => 'foo', :importance => 3),
567
+ model_with_pg_search.create!(:content => 'foo', :importance => 2)
568
+ ]
569
+
570
+ results = model_with_pg_search.search_content_with_importance_as_rank("foo")
571
+ results.should == records.sort_by(&:importance).reverse
572
+ end
573
+
574
+ %w[tsearch trigram dmetaphone].each do |feature|
575
+
576
+ context "using the #{feature} ranking algorithm" do
577
+ before do
578
+ @scope_name = scope_name = :"search_content_ranked_by_#{feature}"
579
+ model_with_pg_search.class_eval do
580
+ pg_search_scope scope_name,
581
+ :against => :content,
582
+ :ranked_by => ":#{feature}"
583
+ end
584
+ end
585
+
586
+ it "should return results with a rank" do
587
+ model_with_pg_search.create!(:content => 'foo')
588
+
589
+ results = model_with_pg_search.send(@scope_name, 'foo')
590
+ results.first.rank.should_not be_nil
591
+ end
592
+ end
593
+ end
594
+ end
595
+ end
596
+ end