pg_search 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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