pg_search 2.3.2 → 2.3.7

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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +11 -0
  3. data/.github/workflows/ci.yml +80 -0
  4. data/.jrubyrc +1 -0
  5. data/.standard.yml +6 -0
  6. data/CHANGELOG.md +55 -20
  7. data/CODE_OF_CONDUCT.md +76 -0
  8. data/Gemfile +19 -6
  9. data/LICENSE +1 -1
  10. data/README.md +106 -43
  11. data/Rakefile +9 -6
  12. data/lib/pg_search/configuration/column.rb +6 -4
  13. data/lib/pg_search/configuration/foreign_column.rb +1 -1
  14. data/lib/pg_search/configuration.rb +13 -3
  15. data/lib/pg_search/document.rb +9 -9
  16. data/lib/pg_search/features/dmetaphone.rb +5 -7
  17. data/lib/pg_search/features/feature.rb +1 -1
  18. data/lib/pg_search/features/trigram.rb +4 -4
  19. data/lib/pg_search/features/tsearch.rb +26 -24
  20. data/lib/pg_search/migration/dmetaphone_generator.rb +2 -2
  21. data/lib/pg_search/migration/generator.rb +5 -5
  22. data/lib/pg_search/migration/multisearch_generator.rb +2 -2
  23. data/lib/pg_search/migration/templates/add_pg_search_dmetaphone_support_functions.rb.erb +6 -6
  24. data/lib/pg_search/migration/templates/create_pg_search_documents.rb.erb +2 -2
  25. data/lib/pg_search/model.rb +6 -6
  26. data/lib/pg_search/multisearch/rebuilder.rb +2 -2
  27. data/lib/pg_search/multisearch.rb +23 -4
  28. data/lib/pg_search/multisearchable.rb +7 -7
  29. data/lib/pg_search/normalizer.rb +5 -5
  30. data/lib/pg_search/scope_options.rb +31 -13
  31. data/lib/pg_search/tasks.rb +3 -3
  32. data/lib/pg_search/version.rb +1 -1
  33. data/lib/pg_search.rb +5 -5
  34. data/pg_search.gemspec +16 -24
  35. data/spec/.rubocop.yml +20 -7
  36. data/spec/integration/.rubocop.yml +11 -0
  37. data/spec/integration/associations_spec.rb +121 -160
  38. data/spec/integration/deprecation_spec.rb +7 -8
  39. data/spec/integration/pg_search_spec.rb +390 -332
  40. data/spec/integration/single_table_inheritance_spec.rb +5 -5
  41. data/spec/lib/pg_search/configuration/association_spec.rb +21 -19
  42. data/spec/lib/pg_search/configuration/column_spec.rb +13 -1
  43. data/spec/lib/pg_search/configuration/foreign_column_spec.rb +4 -4
  44. data/spec/lib/pg_search/features/dmetaphone_spec.rb +4 -4
  45. data/spec/lib/pg_search/features/trigram_spec.rb +32 -28
  46. data/spec/lib/pg_search/features/tsearch_spec.rb +57 -33
  47. data/spec/lib/pg_search/multisearch/rebuilder_spec.rb +94 -63
  48. data/spec/lib/pg_search/multisearch_spec.rb +57 -29
  49. data/spec/lib/pg_search/multisearchable_spec.rb +160 -107
  50. data/spec/lib/pg_search/normalizer_spec.rb +12 -10
  51. data/spec/lib/pg_search_spec.rb +75 -64
  52. data/spec/spec_helper.rb +21 -9
  53. data/spec/support/database.rb +10 -8
  54. metadata +20 -134
  55. data/.autotest +0 -5
  56. data/.codeclimate.yml +0 -17
  57. data/.rubocop.yml +0 -56
  58. data/.travis.yml +0 -50
@@ -2,13 +2,14 @@
2
2
 
3
3
  require "spec_helper"
4
4
 
5
+ # standard:disable RSpec/NestedGroups
5
6
  describe "an Active Record model which includes PgSearch" do
6
7
  with_model :ModelWithPgSearch do
7
8
  table do |t|
8
- t.string 'title'
9
- t.text 'content'
10
- t.integer 'parent_model_id'
11
- t.integer 'importance'
9
+ t.string "title"
10
+ t.text "content"
11
+ t.integer "parent_model_id"
12
+ t.integer "importance"
12
13
  end
13
14
 
14
15
  model do
@@ -38,36 +39,48 @@ describe "an Active Record model which includes PgSearch" do
38
39
  context "when passed a lambda" do
39
40
  it "builds a dynamic scope" do
40
41
  ModelWithPgSearch.pg_search_scope :search_title_or_content,
41
- lambda { |query, pick_content|
42
- {
43
- query: query.gsub("-remove-", ""),
44
- against: pick_content ? :content : :title
45
- }
46
- }
42
+ lambda { |query, pick_content|
43
+ {
44
+ query: query.gsub("-remove-", ""),
45
+ against: pick_content ? :content : :title
46
+ }
47
+ }
48
+
49
+ included = ModelWithPgSearch.create!(title: "foo", content: "bar")
50
+ ModelWithPgSearch.create!(title: "bar", content: "foo")
47
51
 
48
- included = ModelWithPgSearch.create!(title: 'foo', content: 'bar')
49
- excluded = ModelWithPgSearch.create!(title: 'bar', content: 'foo')
52
+ expect(ModelWithPgSearch.search_title_or_content("fo-remove-o", false)).to eq([included])
53
+ expect(ModelWithPgSearch.search_title_or_content("b-remove-ar", true)).to eq([included])
54
+ end
55
+ end
50
56
 
51
- expect(ModelWithPgSearch.search_title_or_content('fo-remove-o', false)).to eq([included])
52
- expect(ModelWithPgSearch.search_title_or_content('b-remove-ar', true)).to eq([included])
57
+ context "when passed an invalid argument" do
58
+ it "builds a dynamic scope" do
59
+ expect {
60
+ ModelWithPgSearch.pg_search_scope :search_title_or_content, :some_symbol
61
+ }.to(
62
+ raise_exception(ArgumentError).with_message(
63
+ "pg_search_scope expects a Hash or Proc"
64
+ )
65
+ )
53
66
  end
54
67
  end
55
68
 
56
69
  context "when an unknown option is passed in" do
57
70
  it "raises an exception when invoked" do
58
71
  ModelWithPgSearch.pg_search_scope :with_unknown_option,
59
- against: :content,
60
- foo: :bar
72
+ against: :content,
73
+ foo: :bar
61
74
 
62
75
  expect {
63
76
  ModelWithPgSearch.with_unknown_option("foo")
64
77
  }.to raise_error(ArgumentError, /foo/)
65
78
  end
66
79
 
67
- context "dynamically" do
80
+ context "with a lambda" do
68
81
  it "raises an exception when invoked" do
69
82
  ModelWithPgSearch.pg_search_scope :with_unknown_option,
70
- ->(*) { { against: :content, foo: :bar } }
83
+ ->(*) { {against: :content, foo: :bar} }
71
84
 
72
85
  expect {
73
86
  ModelWithPgSearch.with_unknown_option("foo")
@@ -79,18 +92,18 @@ describe "an Active Record model which includes PgSearch" do
79
92
  context "when an unknown :using is passed" do
80
93
  it "raises an exception when invoked" do
81
94
  ModelWithPgSearch.pg_search_scope :with_unknown_using,
82
- against: :content,
83
- using: :foo
95
+ against: :content,
96
+ using: :foo
84
97
 
85
98
  expect {
86
99
  ModelWithPgSearch.with_unknown_using("foo")
87
100
  }.to raise_error(ArgumentError, /foo/)
88
101
  end
89
102
 
90
- context "dynamically" do
103
+ context "with a lambda" do
91
104
  it "raises an exception when invoked" do
92
105
  ModelWithPgSearch.pg_search_scope :with_unknown_using,
93
- ->(*) { { against: :content, using: :foo } }
106
+ ->(*) { {against: :content, using: :foo} }
94
107
 
95
108
  expect {
96
109
  ModelWithPgSearch.with_unknown_using("foo")
@@ -102,18 +115,18 @@ describe "an Active Record model which includes PgSearch" do
102
115
  context "when an unknown :ignoring is passed" do
103
116
  it "raises an exception when invoked" do
104
117
  ModelWithPgSearch.pg_search_scope :with_unknown_ignoring,
105
- against: :content,
106
- ignoring: :foo
118
+ against: :content,
119
+ ignoring: :foo
107
120
 
108
121
  expect {
109
122
  ModelWithPgSearch.with_unknown_ignoring("foo")
110
123
  }.to raise_error(ArgumentError, /ignoring.*foo/)
111
124
  end
112
125
 
113
- context "dynamically" do
126
+ context "with a lambda" do
114
127
  it "raises an exception when invoked" do
115
128
  ModelWithPgSearch.pg_search_scope :with_unknown_ignoring,
116
- ->(*) { { against: :content, ignoring: :foo } }
129
+ ->(*) { {against: :content, ignoring: :foo} }
117
130
 
118
131
  expect {
119
132
  ModelWithPgSearch.with_unknown_ignoring("foo")
@@ -130,7 +143,7 @@ describe "an Active Record model which includes PgSearch" do
130
143
  }.to raise_error(ArgumentError, /against/)
131
144
  end
132
145
 
133
- context "dynamically" do
146
+ context "with a lambda" do
134
147
  it "raises an exception when invoked" do
135
148
  ModelWithPgSearch.pg_search_scope :with_unknown_ignoring, ->(*) { {} }
136
149
 
@@ -139,27 +152,43 @@ describe "an Active Record model which includes PgSearch" do
139
152
  }.to raise_error(ArgumentError, /against/)
140
153
  end
141
154
  end
155
+
156
+ context "when a tsvector column is specified" do
157
+ it "does not raise an exception when invoked" do
158
+ ModelWithPgSearch.pg_search_scope :with_unknown_ignoring, {
159
+ using: {
160
+ tsearch: {
161
+ tsvector_column: "tsv"
162
+ }
163
+ }
164
+ }
165
+
166
+ expect {
167
+ ModelWithPgSearch.with_unknown_ignoring("foo")
168
+ }.not_to raise_error
169
+ end
170
+ end
142
171
  end
143
172
  end
144
173
  end
145
174
 
146
175
  describe "a search scope" do
147
- context "against a single column" do
176
+ context "when against a single column" do
148
177
  before do
149
178
  ModelWithPgSearch.pg_search_scope :search_content, against: :content
150
179
  end
151
180
 
152
181
  context "when chained after a select() scope" do
153
182
  it "honors the select" do
154
- included = ModelWithPgSearch.create!(content: 'foo', title: 'bar')
155
- excluded = ModelWithPgSearch.create!(content: 'bar', title: 'foo')
183
+ included = ModelWithPgSearch.create!(content: "foo", title: "bar")
184
+ excluded = ModelWithPgSearch.create!(content: "bar", title: "foo")
156
185
 
157
- results = ModelWithPgSearch.select('id, title').search_content('foo')
186
+ results = ModelWithPgSearch.select("id, title").search_content("foo")
158
187
 
159
188
  expect(results).to include(included)
160
- expect(results).to_not include(excluded)
189
+ expect(results).not_to include(excluded)
161
190
 
162
- expect(results.first.attributes.key?('content')).to eq false
191
+ expect(results.first.attributes.key?("content")).to be false
163
192
 
164
193
  expect(results.select { |record| record.title == "bar" }).to eq [included]
165
194
  expect(results.reject { |record| record.title == "bar" }).to be_empty
@@ -168,15 +197,15 @@ describe "an Active Record model which includes PgSearch" do
168
197
 
169
198
  context "when chained before a select() scope" do
170
199
  it "honors the select" do
171
- included = ModelWithPgSearch.create!(content: 'foo', title: 'bar')
172
- excluded = ModelWithPgSearch.create!(content: 'bar', title: 'foo')
200
+ included = ModelWithPgSearch.create!(content: "foo", title: "bar")
201
+ excluded = ModelWithPgSearch.create!(content: "bar", title: "foo")
173
202
 
174
- results = ModelWithPgSearch.search_content('foo').select('id, title')
203
+ results = ModelWithPgSearch.search_content("foo").select("id, title")
175
204
 
176
205
  expect(results).to include(included)
177
- expect(results).to_not include(excluded)
206
+ expect(results).not_to include(excluded)
178
207
 
179
- expect(results.first.attributes.key?('content')).to eq false
208
+ expect(results.first.attributes.key?("content")).to be false
180
209
 
181
210
  expect(results.select { |record| record.title == "bar" }).to eq [included]
182
211
  expect(results.reject { |record| record.title == "bar" }).to be_empty
@@ -185,22 +214,22 @@ describe "an Active Record model which includes PgSearch" do
185
214
 
186
215
  context "when surrouned by select() scopes" do
187
216
  it "honors the select" do
188
- included = ModelWithPgSearch.create!(content: 'foo', title: 'bar')
189
- excluded = ModelWithPgSearch.create!(content: 'bar', title: 'foo')
217
+ included = ModelWithPgSearch.create!(content: "foo", title: "bar")
218
+ excluded = ModelWithPgSearch.create!(content: "bar", title: "foo")
190
219
 
191
- results = ModelWithPgSearch.select('id').search_content('foo').select('title')
220
+ results = ModelWithPgSearch.select("id").search_content("foo").select("title")
192
221
 
193
222
  expect(results).to include(included)
194
- expect(results).to_not include(excluded)
223
+ expect(results).not_to include(excluded)
195
224
 
196
- expect(results.first.attributes.key?('content')).to eq false
225
+ expect(results.first.attributes.key?("content")).to be false
197
226
 
198
227
  expect(results.select { |record| record.title == "bar" }).to eq [included]
199
228
  expect(results.reject { |record| record.title == "bar" }).to be_empty
200
229
  end
201
230
  end
202
231
 
203
- context "chained to a cross-table scope" do
232
+ context "when chained to a cross-table scope" do
204
233
  with_model :House do
205
234
  table do |t|
206
235
  t.references :person
@@ -224,7 +253,7 @@ describe "an Active Record model which includes PgSearch" do
224
253
  has_many :houses
225
254
  pg_search_scope :named, against: [:name]
226
255
  scope :with_house_in_city, lambda { |city|
227
- joins(:houses).where(House.table_name.to_sym => { city: city })
256
+ joins(:houses).where(House.table_name.to_sym => {city: city})
228
257
  }
229
258
  scope :house_search_city, lambda { |query|
230
259
  joins(:houses).merge(House.search_city(query))
@@ -268,9 +297,9 @@ describe "an Active Record model which includes PgSearch" do
268
297
 
269
298
  context "when chaining merged scopes" do
270
299
  it "does not raise an exception" do
271
- relation = Person.named('foo').house_search_city('bar')
300
+ relation = Person.named("foo").house_search_city("bar")
272
301
 
273
- expect { relation.to_a }.to_not raise_error
302
+ expect { relation.to_a }.not_to raise_error
274
303
  end
275
304
  end
276
305
  end
@@ -281,49 +310,49 @@ describe "an Active Record model which includes PgSearch" do
281
310
  end
282
311
 
283
312
  it "does not raise an exception" do
284
- relation = ModelWithPgSearch.search_content('foo').search_title('bar')
313
+ relation = ModelWithPgSearch.search_content("foo").search_title("bar")
285
314
 
286
- expect { relation.to_a }.to_not raise_error
315
+ expect { relation.to_a }.not_to raise_error
287
316
  end
288
317
  end
289
318
 
290
319
  it "returns an empty array when a blank query is passed in" do
291
- ModelWithPgSearch.create!(content: 'foo')
320
+ ModelWithPgSearch.create!(content: "foo")
292
321
 
293
- results = ModelWithPgSearch.search_content('')
322
+ results = ModelWithPgSearch.search_content("")
294
323
  expect(results).to eq([])
295
324
  end
296
325
 
297
326
  it "returns rows where the column contains the term in the query" do
298
- included = ModelWithPgSearch.create!(content: 'foo')
299
- excluded = ModelWithPgSearch.create!(content: 'bar')
327
+ included = ModelWithPgSearch.create!(content: "foo")
328
+ excluded = ModelWithPgSearch.create!(content: "bar")
300
329
 
301
- results = ModelWithPgSearch.search_content('foo')
330
+ results = ModelWithPgSearch.search_content("foo")
302
331
  expect(results).to include(included)
303
332
  expect(results).not_to include(excluded)
304
333
  end
305
334
 
306
335
  it "returns the correct count" do
307
- ModelWithPgSearch.create!(content: 'foo')
308
- ModelWithPgSearch.create!(content: 'bar')
336
+ ModelWithPgSearch.create!(content: "foo")
337
+ ModelWithPgSearch.create!(content: "bar")
309
338
 
310
- results = ModelWithPgSearch.search_content('foo')
339
+ results = ModelWithPgSearch.search_content("foo")
311
340
  expect(results.count).to eq 1
312
341
  end
313
342
 
314
343
  it "returns the correct count(:all)" do
315
- ModelWithPgSearch.create!(content: 'foo')
316
- ModelWithPgSearch.create!(content: 'bar')
344
+ ModelWithPgSearch.create!(content: "foo")
345
+ ModelWithPgSearch.create!(content: "bar")
317
346
 
318
- results = ModelWithPgSearch.search_content('foo')
347
+ results = ModelWithPgSearch.search_content("foo")
319
348
  expect(results.count(:all)).to eq 1
320
349
  end
321
350
 
322
351
  it "supports #select" do
323
- record = ModelWithPgSearch.create!(content: 'foo')
324
- other_record = ModelWithPgSearch.create!(content: 'bar')
352
+ record = ModelWithPgSearch.create!(content: "foo")
353
+ ModelWithPgSearch.create!(content: "bar")
325
354
 
326
- records_with_only_id = ModelWithPgSearch.search_content('foo').select('id')
355
+ records_with_only_id = ModelWithPgSearch.search_content("foo").select("id")
327
356
  expect(records_with_only_id.length).to eq 1
328
357
 
329
358
  returned_record = records_with_only_id.first
@@ -332,36 +361,36 @@ describe "an Active Record model which includes PgSearch" do
332
361
  end
333
362
 
334
363
  it "supports #pluck" do
335
- record = ModelWithPgSearch.create!(content: 'foo')
336
- other_record = ModelWithPgSearch.create!(content: 'bar')
364
+ record = ModelWithPgSearch.create!(content: "foo")
365
+ ModelWithPgSearch.create!(content: "bar")
337
366
 
338
- ids = ModelWithPgSearch.search_content('foo').pluck('id')
367
+ ids = ModelWithPgSearch.search_content("foo").pluck("id")
339
368
  expect(ids).to eq [record.id]
340
369
  end
341
370
 
342
371
  it "supports adding where clauses using the pg_search.rank" do
343
- once = ModelWithPgSearch.create!(content: 'foo bar')
344
- twice = ModelWithPgSearch.create!(content: 'foo foo')
372
+ ModelWithPgSearch.create!(content: "foo bar")
373
+ twice = ModelWithPgSearch.create!(content: "foo foo")
345
374
 
346
- records = ModelWithPgSearch.search_content('foo')
347
- .where("#{PgSearch::Configuration.alias(ModelWithPgSearch.table_name)}.rank > 0.07")
375
+ records = ModelWithPgSearch.search_content("foo")
376
+ .where("#{PgSearch::Configuration.alias(ModelWithPgSearch.table_name)}.rank > 0.07")
348
377
 
349
378
  expect(records).to eq [twice]
350
379
  end
351
380
 
352
381
  it "returns rows where the column contains all the terms in the query in any order" do
353
- included = [ModelWithPgSearch.create!(content: 'foo bar'),
354
- ModelWithPgSearch.create!(content: 'bar foo')]
355
- excluded = ModelWithPgSearch.create!(content: 'foo')
382
+ included = [ModelWithPgSearch.create!(content: "foo bar"),
383
+ ModelWithPgSearch.create!(content: "bar foo")]
384
+ excluded = ModelWithPgSearch.create!(content: "foo")
356
385
 
357
- results = ModelWithPgSearch.search_content('foo bar')
386
+ results = ModelWithPgSearch.search_content("foo bar")
358
387
  expect(results).to match_array(included)
359
388
  expect(results).not_to include(excluded)
360
389
  end
361
390
 
362
391
  it "returns rows that match the query but not its case" do
363
392
  included = [ModelWithPgSearch.create!(content: "foo"),
364
- ModelWithPgSearch.create!(content: "FOO")]
393
+ ModelWithPgSearch.create!(content: "FOO")]
365
394
 
366
395
  results = ModelWithPgSearch.search_content("Foo")
367
396
  expect(results).to match_array(included)
@@ -380,8 +409,8 @@ describe "an Active Record model which includes PgSearch" do
380
409
  end
381
410
 
382
411
  it "returns rows that match the query but not rows that are prefixed by the query" do
383
- included = ModelWithPgSearch.create!(content: 'pre')
384
- excluded = ModelWithPgSearch.create!(content: 'prefix')
412
+ included = ModelWithPgSearch.create!(content: "pre")
413
+ excluded = ModelWithPgSearch.create!(content: "prefix")
385
414
 
386
415
  results = ModelWithPgSearch.search_content("pre")
387
416
  expect(results).to eq([included])
@@ -390,27 +419,36 @@ describe "an Active Record model which includes PgSearch" do
390
419
 
391
420
  it "returns rows that match the query exactly and not those that match the query when stemmed by the default english dictionary" do
392
421
  included = ModelWithPgSearch.create!(content: "jumped")
393
- excluded = [ModelWithPgSearch.create!(content: "jump"),
394
- ModelWithPgSearch.create!(content: "jumping")]
422
+ ModelWithPgSearch.create!(content: "jump")
423
+ ModelWithPgSearch.create!(content: "jumping")
395
424
 
396
425
  results = ModelWithPgSearch.search_content("jumped")
397
426
  expect(results).to eq([included])
398
427
  end
399
428
 
400
429
  it "returns rows that match sorted by rank" do
401
- loser = ModelWithPgSearch.create!(content: 'foo')
402
- winner = ModelWithPgSearch.create!(content: 'foo foo')
430
+ loser = ModelWithPgSearch.create!(content: "foo")
431
+ winner = ModelWithPgSearch.create!(content: "foo foo")
403
432
 
404
433
  results = ModelWithPgSearch.search_content("foo").with_pg_search_rank
405
434
  expect(results[0].pg_search_rank).to be > results[1].pg_search_rank
406
435
  expect(results).to eq([winner, loser])
407
436
  end
408
437
 
409
- it 'allows pg_search_rank along with a join' do
438
+ it "preserves column selection when with_pg_search_rank is chained after a select()" do
439
+ ModelWithPgSearch.create!(title: "foo", content: "bar")
440
+
441
+ results = ModelWithPgSearch.search_content("bar").select(:content).with_pg_search_rank
442
+
443
+ expect(results.length).to be 1
444
+ expect(results.first.as_json.keys).to contain_exactly("id", "content", "pg_search_rank")
445
+ end
446
+
447
+ it "allows pg_search_rank along with a join" do
410
448
  parent_1 = ParentModel.create!(id: 98)
411
449
  parent_2 = ParentModel.create!(id: 99)
412
- loser = ModelWithPgSearch.create!(content: 'foo', parent_model: parent_2)
413
- winner = ModelWithPgSearch.create!(content: 'foo foo', parent_model: parent_1)
450
+ loser = ModelWithPgSearch.create!(content: "foo", parent_model: parent_2)
451
+ winner = ModelWithPgSearch.create!(content: "foo foo", parent_model: parent_1)
414
452
 
415
453
  results = ModelWithPgSearch.joins(:parent_model).merge(ParentModel.active).search_content("foo").with_pg_search_rank
416
454
  expect(results.map(&:id)).to eq [winner.id, loser.id]
@@ -419,8 +457,8 @@ describe "an Active Record model which includes PgSearch" do
419
457
  end
420
458
 
421
459
  it "returns results that match sorted by primary key for records that rank the same" do
422
- sorted_results = [ModelWithPgSearch.create!(content: 'foo'),
423
- ModelWithPgSearch.create!(content: 'foo')].sort_by(&:id)
460
+ sorted_results = [ModelWithPgSearch.create!(content: "foo"),
461
+ ModelWithPgSearch.create!(content: "foo")].sort_by(&:id)
424
462
 
425
463
  results = ModelWithPgSearch.search_content("foo")
426
464
  expect(results).to eq(sorted_results)
@@ -428,16 +466,16 @@ describe "an Active Record model which includes PgSearch" do
428
466
 
429
467
  it "returns results that match a query with multiple space-separated search terms" do
430
468
  included = [
431
- ModelWithPgSearch.create!(content: 'foo bar'),
432
- ModelWithPgSearch.create!(content: 'bar foo'),
433
- ModelWithPgSearch.create!(content: 'bar foo baz')
469
+ ModelWithPgSearch.create!(content: "foo bar"),
470
+ ModelWithPgSearch.create!(content: "bar foo"),
471
+ ModelWithPgSearch.create!(content: "bar foo baz")
434
472
  ]
435
473
  excluded = [
436
- ModelWithPgSearch.create!(content: 'foo'),
437
- ModelWithPgSearch.create!(content: 'foo baz')
474
+ ModelWithPgSearch.create!(content: "foo"),
475
+ ModelWithPgSearch.create!(content: "foo baz")
438
476
  ]
439
477
 
440
- results = ModelWithPgSearch.search_content('foo bar')
478
+ results = ModelWithPgSearch.search_content("foo bar")
441
479
  expect(results).to match_array(included)
442
480
  expect(results).not_to include(excluded)
443
481
  end
@@ -451,7 +489,7 @@ describe "an Active Record model which includes PgSearch" do
451
489
 
452
490
  it "accepts non-string queries and calls #to_s on them" do
453
491
  foo = ModelWithPgSearch.create!(content: "foo")
454
- not_a_string = double(to_s: "foo")
492
+ not_a_string = instance_double(Object, to_s: "foo")
455
493
  expect(ModelWithPgSearch.search_content(not_a_string)).to eq([foo])
456
494
  end
457
495
 
@@ -467,7 +505,7 @@ describe "an Active Record model which includes PgSearch" do
467
505
  # WARNING: searching timestamps is not something PostgreSQL
468
506
  # full-text search is good at. Use at your own risk.
469
507
  pg_search_scope :search_timestamps,
470
- against: %i[created_at updated_at]
508
+ against: %i[created_at updated_at]
471
509
  end
472
510
  end
473
511
 
@@ -481,22 +519,22 @@ describe "an Active Record model which includes PgSearch" do
481
519
  end
482
520
  end
483
521
 
484
- context "against multiple columns" do
522
+ context "when against multiple columns" do
485
523
  before do
486
524
  ModelWithPgSearch.pg_search_scope :search_title_and_content, against: %i[title content]
487
525
  end
488
526
 
489
527
  it "returns rows whose columns contain all of the terms in the query across columns" do
490
528
  included = [
491
- ModelWithPgSearch.create!(title: 'foo', content: 'bar'),
492
- ModelWithPgSearch.create!(title: 'bar', content: 'foo')
529
+ ModelWithPgSearch.create!(title: "foo", content: "bar"),
530
+ ModelWithPgSearch.create!(title: "bar", content: "foo")
493
531
  ]
494
532
  excluded = [
495
- ModelWithPgSearch.create!(title: 'foo', content: 'foo'),
496
- ModelWithPgSearch.create!(title: 'bar', content: 'bar')
533
+ ModelWithPgSearch.create!(title: "foo", content: "foo"),
534
+ ModelWithPgSearch.create!(title: "bar", content: "bar")
497
535
  ]
498
536
 
499
- results = ModelWithPgSearch.search_title_and_content('foo bar')
537
+ results = ModelWithPgSearch.search_title_and_content("foo bar")
500
538
 
501
539
  expect(results).to match_array(included)
502
540
  excluded.each do |result|
@@ -505,42 +543,42 @@ describe "an Active Record model which includes PgSearch" do
505
543
  end
506
544
 
507
545
  it "returns rows where at one column contains all of the terms in the query and another does not" do
508
- in_title = ModelWithPgSearch.create!(title: 'foo', content: 'bar')
509
- in_content = ModelWithPgSearch.create!(title: 'bar', content: 'foo')
546
+ in_title = ModelWithPgSearch.create!(title: "foo", content: "bar")
547
+ in_content = ModelWithPgSearch.create!(title: "bar", content: "foo")
510
548
 
511
- results = ModelWithPgSearch.search_title_and_content('foo')
512
- expect(results).to match_array([in_title, in_content])
549
+ results = ModelWithPgSearch.search_title_and_content("foo")
550
+ expect(results).to contain_exactly(in_title, in_content)
513
551
  end
514
552
 
515
553
  # Searching with a NULL column will prevent any matches unless we coalesce it.
516
554
  it "returns rows where at one column contains all of the terms in the query and another is NULL" do
517
- included = ModelWithPgSearch.create!(title: 'foo', content: nil)
518
- results = ModelWithPgSearch.search_title_and_content('foo')
555
+ included = ModelWithPgSearch.create!(title: "foo", content: nil)
556
+ results = ModelWithPgSearch.search_title_and_content("foo")
519
557
  expect(results).to eq([included])
520
558
  end
521
559
  end
522
560
 
523
- context "using trigram" do
561
+ context "when using trigram" do
524
562
  before do
525
563
  ModelWithPgSearch.pg_search_scope :with_trigrams, against: %i[title content], using: :trigram
526
564
  end
527
565
 
528
566
  it "returns rows where one searchable column and the query share enough trigrams" do
529
- included = ModelWithPgSearch.create!(title: 'abcdefghijkl', content: nil)
530
- results = ModelWithPgSearch.with_trigrams('cdefhijkl')
567
+ included = ModelWithPgSearch.create!(title: "abcdefghijkl", content: nil)
568
+ results = ModelWithPgSearch.with_trigrams("cdefhijkl")
531
569
  expect(results).to eq([included])
532
570
  end
533
571
 
534
572
  it "returns rows where multiple searchable columns and the query share enough trigrams" do
535
- included = ModelWithPgSearch.create!(title: 'abcdef', content: 'ghijkl')
536
- results = ModelWithPgSearch.with_trigrams('cdefhijkl')
573
+ included = ModelWithPgSearch.create!(title: "abcdef", content: "ghijkl")
574
+ results = ModelWithPgSearch.with_trigrams("cdefhijkl")
537
575
  expect(results).to eq([included])
538
576
  end
539
577
 
540
578
  context "when a threshold is specified" do
541
579
  before do
542
- ModelWithPgSearch.pg_search_scope :with_strict_trigrams, against: %i[title content], using: { trigram: { threshold: 0.5 } }
543
- ModelWithPgSearch.pg_search_scope :with_permissive_trigrams, against: %i[title content], using: { trigram: { threshold: 0.1 } }
580
+ ModelWithPgSearch.pg_search_scope :with_strict_trigrams, against: %i[title content], using: {trigram: {threshold: 0.5}}
581
+ ModelWithPgSearch.pg_search_scope :with_permissive_trigrams, against: %i[title content], using: {trigram: {threshold: 0.1}}
544
582
  end
545
583
 
546
584
  it "uses the threshold in the trigram expression" do
@@ -562,19 +600,19 @@ describe "an Active Record model which includes PgSearch" do
562
600
  end
563
601
  end
564
602
 
565
- context "using tsearch" do
603
+ context "when using tsearch" do
566
604
  before do
567
605
  ModelWithPgSearch.pg_search_scope :search_title_with_prefixes,
568
- against: :title,
569
- using: {
570
- tsearch: { prefix: true }
571
- }
606
+ against: :title,
607
+ using: {
608
+ tsearch: {prefix: true}
609
+ }
572
610
  end
573
611
 
574
612
  context "with prefix: true" do
575
613
  it "returns rows that match the query and that are prefixed by the query" do
576
- included = ModelWithPgSearch.create!(title: 'prefix')
577
- excluded = ModelWithPgSearch.create!(title: 'postfix')
614
+ included = ModelWithPgSearch.create!(title: "prefix")
615
+ excluded = ModelWithPgSearch.create!(title: "postfix")
578
616
 
579
617
  results = ModelWithPgSearch.search_title_with_prefixes("pre")
580
618
  expect(results).to eq([included])
@@ -582,8 +620,8 @@ describe "an Active Record model which includes PgSearch" do
582
620
  end
583
621
 
584
622
  it "returns rows that match the query when the query has a hyphen" do
585
- included = ModelWithPgSearch.create!(title: 'foo-bar')
586
- excluded = ModelWithPgSearch.create!(title: 'foo bar')
623
+ included = ModelWithPgSearch.create!(title: "foo-bar")
624
+ excluded = ModelWithPgSearch.create!(title: "foo bar")
587
625
 
588
626
  results = ModelWithPgSearch.search_title_with_prefixes("foo-bar")
589
627
  expect(results).to include(included)
@@ -594,16 +632,16 @@ describe "an Active Record model which includes PgSearch" do
594
632
  context "with the english dictionary" do
595
633
  before do
596
634
  ModelWithPgSearch.pg_search_scope :search_content_with_english,
597
- against: :content,
598
- using: {
599
- tsearch: { dictionary: :english }
600
- }
635
+ against: :content,
636
+ using: {
637
+ tsearch: {dictionary: :english}
638
+ }
601
639
  end
602
640
 
603
641
  it "returns rows that match the query when stemmed by the english dictionary" do
604
642
  included = [ModelWithPgSearch.create!(content: "jump"),
605
- ModelWithPgSearch.create!(content: "jumped"),
606
- ModelWithPgSearch.create!(content: "jumping")]
643
+ ModelWithPgSearch.create!(content: "jumped"),
644
+ ModelWithPgSearch.create!(content: "jumping")]
607
645
 
608
646
  results = ModelWithPgSearch.search_content_with_english("jump")
609
647
  expect(results).to match_array(included)
@@ -613,14 +651,14 @@ describe "an Active Record model which includes PgSearch" do
613
651
  describe "highlighting" do
614
652
  before do
615
653
  ["Strip Down", "Down", "Down and Out", "Won't Let You Down"].each do |name|
616
- ModelWithPgSearch.create! content: name
654
+ ModelWithPgSearch.create! title: "Just a title", content: name
617
655
  end
618
656
  end
619
657
 
620
658
  context "with highlight turned on" do
621
659
  before do
622
660
  ModelWithPgSearch.pg_search_scope :search_content,
623
- against: :content
661
+ against: :content
624
662
  end
625
663
 
626
664
  it "adds a #pg_search_highlight method to each returned model record" do
@@ -634,26 +672,32 @@ describe "an Active Record model which includes PgSearch" do
634
672
 
635
673
  expect(result.pg_search_highlight).to eq("Won't <b>Let</b> You Down")
636
674
  end
675
+
676
+ it "preserves column selection when with_pg_search_highlight is chained after a select()" do
677
+ result = ModelWithPgSearch.search_content("Let").select(:content).with_pg_search_highlight.first
678
+
679
+ expect(result.as_json.keys).to contain_exactly("id", "content", "pg_search_highlight")
680
+ end
637
681
  end
638
682
 
639
683
  context "with custom highlighting options" do
640
684
  before do
641
- ModelWithPgSearch.create! content: "#{'text ' * 2}Let #{'text ' * 2}Let #{'text ' * 2}"
685
+ ModelWithPgSearch.create! content: "#{"text " * 2}Let #{"text " * 2}Let #{"text " * 2}"
642
686
 
643
687
  ModelWithPgSearch.pg_search_scope :search_content,
644
- against: :content,
645
- using: {
646
- tsearch: {
647
- highlight: {
648
- StartSel: '<mark class="highlight">',
649
- StopSel: '</mark>',
650
- FragmentDelimiter: '<delim class="my_delim">',
651
- MaxFragments: 2,
652
- MaxWords: 2,
653
- MinWords: 1
654
- }
655
- }
656
- }
688
+ against: :content,
689
+ using: {
690
+ tsearch: {
691
+ highlight: {
692
+ StartSel: '<mark class="highlight">',
693
+ StopSel: "</mark>",
694
+ FragmentDelimiter: '<delim class="my_delim">',
695
+ MaxFragments: 2,
696
+ MaxWords: 2,
697
+ MinWords: 1
698
+ }
699
+ }
700
+ }
657
701
  end
658
702
 
659
703
  it "applies the options to the excerpts" do
@@ -682,10 +726,10 @@ describe "an Active Record model which includes PgSearch" do
682
726
  context "with a normalization specified" do
683
727
  before do
684
728
  ModelWithPgSearch.pg_search_scope :search_content_with_normalization,
685
- against: :content,
686
- using: {
687
- tsearch: { normalization: 2 }
688
- }
729
+ against: :content,
730
+ using: {
731
+ tsearch: {normalization: 2}
732
+ }
689
733
  end
690
734
 
691
735
  it "ranks the results for documents with less text higher" do
@@ -699,8 +743,8 @@ describe "an Active Record model which includes PgSearch" do
699
743
  context "with no normalization" do
700
744
  before do
701
745
  ModelWithPgSearch.pg_search_scope :search_content_without_normalization,
702
- against: :content,
703
- using: :tsearch
746
+ against: :content,
747
+ using: :tsearch
704
748
  end
705
749
 
706
750
  it "ranks the results equally" do
@@ -712,68 +756,68 @@ describe "an Active Record model which includes PgSearch" do
712
756
  end
713
757
  end
714
758
 
715
- context "against columns ranked with arrays" do
759
+ context "when against columns ranked with arrays" do
716
760
  before do
717
761
  ModelWithPgSearch.pg_search_scope :search_weighted_by_array_of_arrays,
718
- against: [[:content, 'B'], [:title, 'A']]
762
+ against: [[:content, "B"], [:title, "A"]]
719
763
  end
720
764
 
721
765
  it "returns results sorted by weighted rank" do
722
- loser = ModelWithPgSearch.create!(title: 'bar', content: 'foo')
723
- winner = ModelWithPgSearch.create!(title: 'foo', content: 'bar')
766
+ loser = ModelWithPgSearch.create!(title: "bar", content: "foo")
767
+ winner = ModelWithPgSearch.create!(title: "foo", content: "bar")
724
768
 
725
- results = ModelWithPgSearch.search_weighted_by_array_of_arrays('foo').with_pg_search_rank
769
+ results = ModelWithPgSearch.search_weighted_by_array_of_arrays("foo").with_pg_search_rank
726
770
  expect(results[0].pg_search_rank).to be > results[1].pg_search_rank
727
771
  expect(results).to eq([winner, loser])
728
772
  end
729
773
  end
730
774
 
731
- context "against columns ranked with a hash" do
775
+ context "when against columns ranked with a hash" do
732
776
  before do
733
777
  ModelWithPgSearch.pg_search_scope :search_weighted_by_hash,
734
- against: { content: 'B', title: 'A' }
778
+ against: {content: "B", title: "A"}
735
779
  end
736
780
 
737
781
  it "returns results sorted by weighted rank" do
738
- loser = ModelWithPgSearch.create!(title: 'bar', content: 'foo')
739
- winner = ModelWithPgSearch.create!(title: 'foo', content: 'bar')
782
+ loser = ModelWithPgSearch.create!(title: "bar", content: "foo")
783
+ winner = ModelWithPgSearch.create!(title: "foo", content: "bar")
740
784
 
741
- results = ModelWithPgSearch.search_weighted_by_hash('foo').with_pg_search_rank
785
+ results = ModelWithPgSearch.search_weighted_by_hash("foo").with_pg_search_rank
742
786
  expect(results[0].pg_search_rank).to be > results[1].pg_search_rank
743
787
  expect(results).to eq([winner, loser])
744
788
  end
745
789
  end
746
790
 
747
- context "against columns of which only some are ranked" do
791
+ context "when against columns of which only some are ranked" do
748
792
  before do
749
793
  ModelWithPgSearch.pg_search_scope :search_weighted,
750
- against: [:content, [:title, 'A']]
794
+ against: [:content, [:title, "A"]]
751
795
  end
752
796
 
753
797
  it "returns results sorted by weighted rank using an implied low rank for unranked columns" do
754
- loser = ModelWithPgSearch.create!(title: 'bar', content: 'foo')
755
- winner = ModelWithPgSearch.create!(title: 'foo', content: 'bar')
798
+ loser = ModelWithPgSearch.create!(title: "bar", content: "foo")
799
+ winner = ModelWithPgSearch.create!(title: "foo", content: "bar")
756
800
 
757
- results = ModelWithPgSearch.search_weighted('foo').with_pg_search_rank
801
+ results = ModelWithPgSearch.search_weighted("foo").with_pg_search_rank
758
802
  expect(results[0].pg_search_rank).to be > results[1].pg_search_rank
759
803
  expect(results).to eq([winner, loser])
760
804
  end
761
805
  end
762
806
 
763
- context "searching any_word option" do
807
+ context "when searching any_word option" do
764
808
  before do
765
809
  ModelWithPgSearch.pg_search_scope :search_title_with_any_word,
766
- against: :title,
767
- using: {
768
- tsearch: { any_word: true }
769
- }
810
+ against: :title,
811
+ using: {
812
+ tsearch: {any_word: true}
813
+ }
770
814
 
771
815
  ModelWithPgSearch.pg_search_scope :search_title_with_all_words,
772
- against: :title
816
+ against: :title
773
817
  end
774
818
 
775
819
  it "returns all results containing any word in their title" do
776
- numbers = %w[one two three four].map { |number| ModelWithPgSearch.create!(title: number) }
820
+ %w[one two three four].map { |number| ModelWithPgSearch.create!(title: number) }
777
821
 
778
822
  results = ModelWithPgSearch.search_title_with_any_word("one two three four")
779
823
 
@@ -788,10 +832,10 @@ describe "an Active Record model which includes PgSearch" do
788
832
  context "with :negation" do
789
833
  before do
790
834
  ModelWithPgSearch.pg_search_scope :search_with_negation,
791
- against: :title,
792
- using: {
793
- tsearch: { negation: true }
794
- }
835
+ against: :title,
836
+ using: {
837
+ tsearch: {negation: true}
838
+ }
795
839
  end
796
840
 
797
841
  it "doesn't return results that contain terms prepended with '!'" do
@@ -815,10 +859,10 @@ describe "an Active Record model which includes PgSearch" do
815
859
  context "without :negation" do
816
860
  before do
817
861
  ModelWithPgSearch.pg_search_scope :search_without_negation,
818
- against: :title,
819
- using: {
820
- tsearch: {}
821
- }
862
+ against: :title,
863
+ using: {
864
+ tsearch: {}
865
+ }
822
866
  end
823
867
 
824
868
  it "return results that contain terms prepended with '!'" do
@@ -838,77 +882,76 @@ describe "an Active Record model which includes PgSearch" do
838
882
  end
839
883
  end
840
884
 
841
- context "using dmetaphone" do
885
+ context "when using dmetaphone" do
842
886
  before do
843
887
  ModelWithPgSearch.pg_search_scope :with_dmetaphones,
844
- against: %i[title content],
845
- using: :dmetaphone
888
+ against: %i[title content],
889
+ using: :dmetaphone
846
890
  end
847
891
 
848
892
  it "returns rows where one searchable column and the query share enough dmetaphones" do
849
- included = ModelWithPgSearch.create!(title: 'Geoff', content: nil)
850
- excluded = ModelWithPgSearch.create!(title: 'Bob', content: nil)
851
- results = ModelWithPgSearch.with_dmetaphones('Jeff')
893
+ included = ModelWithPgSearch.create!(title: "Geoff", content: nil)
894
+ ModelWithPgSearch.create!(title: "Bob", content: nil)
895
+ results = ModelWithPgSearch.with_dmetaphones("Jeff")
852
896
  expect(results).to eq([included])
853
897
  end
854
898
 
855
899
  it "returns rows where multiple searchable columns and the query share enough dmetaphones" do
856
- included = ModelWithPgSearch.create!(title: 'Geoff', content: 'George')
857
- excluded = ModelWithPgSearch.create!(title: 'Bob', content: 'Jones')
858
- results = ModelWithPgSearch.with_dmetaphones('Jeff Jorge')
900
+ included = ModelWithPgSearch.create!(title: "Geoff", content: "George")
901
+ ModelWithPgSearch.create!(title: "Bob", content: "Jones")
902
+ results = ModelWithPgSearch.with_dmetaphones("Jeff Jorge")
859
903
  expect(results).to eq([included])
860
904
  end
861
905
 
862
906
  it "returns rows that match dmetaphones that are English stopwords" do
863
- included = ModelWithPgSearch.create!(title: 'White', content: nil)
864
- excluded = ModelWithPgSearch.create!(title: 'Black', content: nil)
865
- results = ModelWithPgSearch.with_dmetaphones('Wight')
907
+ included = ModelWithPgSearch.create!(title: "White", content: nil)
908
+ ModelWithPgSearch.create!(title: "Black", content: nil)
909
+ results = ModelWithPgSearch.with_dmetaphones("Wight")
866
910
  expect(results).to eq([included])
867
911
  end
868
912
 
869
913
  it "can handle terms that do not have a dmetaphone equivalent" do
870
- term_with_blank_metaphone = "w"
871
-
872
- included = ModelWithPgSearch.create!(title: 'White', content: nil)
873
- excluded = ModelWithPgSearch.create!(title: 'Black', content: nil)
914
+ included = ModelWithPgSearch.create!(title: "White", content: nil)
915
+ ModelWithPgSearch.create!(title: "Black", content: nil)
874
916
 
875
- results = ModelWithPgSearch.with_dmetaphones('Wight W')
917
+ # "W" does not have a dmetaphone equivalent
918
+ results = ModelWithPgSearch.with_dmetaphones("Wight W")
876
919
  expect(results).to eq([included])
877
920
  end
878
921
  end
879
922
 
880
- context "using multiple features" do
923
+ context "when using multiple features" do
881
924
  before do
882
925
  ModelWithPgSearch.pg_search_scope :with_tsearch,
883
- against: :title,
884
- using: [
885
- [:tsearch, { dictionary: 'english' }]
886
- ]
926
+ against: :title,
927
+ using: [
928
+ [:tsearch, {dictionary: "english"}]
929
+ ]
887
930
 
888
931
  ModelWithPgSearch.pg_search_scope :with_trigram,
889
- against: :title,
890
- using: :trigram
932
+ against: :title,
933
+ using: :trigram
891
934
 
892
935
  ModelWithPgSearch.pg_search_scope :with_trigram_and_ignoring_accents,
893
- against: :title,
894
- ignoring: :accents,
895
- using: :trigram
936
+ against: :title,
937
+ ignoring: :accents,
938
+ using: :trigram
896
939
 
897
940
  ModelWithPgSearch.pg_search_scope :with_tsearch_and_trigram,
898
- against: :title,
899
- using: [
900
- [:tsearch, { dictionary: 'english' }],
901
- :trigram
902
- ]
941
+ against: :title,
942
+ using: [
943
+ [:tsearch, {dictionary: "english"}],
944
+ :trigram
945
+ ]
903
946
 
904
947
  ModelWithPgSearch.pg_search_scope :complex_search,
905
- against: %i[content title],
906
- ignoring: :accents,
907
- using: {
908
- tsearch: { dictionary: 'english' },
909
- dmetaphone: {},
910
- trigram: {}
911
- }
948
+ against: %i[content title],
949
+ ignoring: :accents,
950
+ using: {
951
+ tsearch: {dictionary: "english"},
952
+ dmetaphone: {},
953
+ trigram: {}
954
+ }
912
955
  end
913
956
 
914
957
  it "returns rows that match using any of the features" do
@@ -950,33 +993,40 @@ describe "an Active Record model which includes PgSearch" do
950
993
  end
951
994
 
952
995
  context "with feature-specific configuration" do
953
- before do
954
- @tsearch_config = tsearch_config = { dictionary: 'english' }
955
- @trigram_config = trigram_config = { foo: 'bar' }
996
+ let(:tsearch_config) { {dictionary: "english"} }
997
+ let(:trigram_config) { {foo: "bar"} }
956
998
 
999
+ before do
957
1000
  ModelWithPgSearch.pg_search_scope :with_tsearch_and_trigram_using_hash,
958
- against: :title,
959
- using: {
960
- tsearch: tsearch_config,
961
- trigram: trigram_config
962
- }
1001
+ against: :title,
1002
+ using: {tsearch: tsearch_config, trigram: trigram_config}
963
1003
  end
964
1004
 
965
- it "should pass the custom configuration down to the specified feature" do
966
- stub_feature = double(
1005
+ it "passes the custom configuration down to the specified feature" do
1006
+ tsearch_feature = instance_double(
1007
+ PgSearch::Features::TSearch,
1008
+ conditions: Arel::Nodes::Grouping.new(Arel.sql("1 = 1")),
1009
+ rank: Arel::Nodes::Grouping.new(Arel.sql("1.0"))
1010
+ )
1011
+
1012
+ trigram_feature = instance_double(
1013
+ PgSearch::Features::Trigram,
967
1014
  conditions: Arel::Nodes::Grouping.new(Arel.sql("1 = 1")),
968
1015
  rank: Arel::Nodes::Grouping.new(Arel.sql("1.0"))
969
1016
  )
970
1017
 
971
- expect(PgSearch::Features::TSearch).to receive(:new).with(anything, @tsearch_config, anything, anything, anything).at_least(:once).and_return(stub_feature)
972
- expect(PgSearch::Features::Trigram).to receive(:new).with(anything, @trigram_config, anything, anything, anything).at_least(:once).and_return(stub_feature)
1018
+ allow(PgSearch::Features::TSearch).to receive(:new).with(anything, tsearch_config, anything, anything, anything).and_return(tsearch_feature)
1019
+ allow(PgSearch::Features::Trigram).to receive(:new).with(anything, trigram_config, anything, anything, anything).and_return(trigram_feature)
973
1020
 
974
1021
  ModelWithPgSearch.with_tsearch_and_trigram_using_hash("foo")
1022
+
1023
+ expect(PgSearch::Features::TSearch).to have_received(:new).with(anything, tsearch_config, anything, anything, anything).at_least(:once)
1024
+ expect(PgSearch::Features::Trigram).to have_received(:new).with(anything, trigram_config, anything, anything, anything).at_least(:once)
975
1025
  end
976
1026
  end
977
1027
  end
978
1028
 
979
- context "using a tsvector column and an association" do
1029
+ context "when using a tsvector column and an association" do
980
1030
  with_model :Comment do
981
1031
  table do |t|
982
1032
  t.integer :post_id
@@ -990,8 +1040,8 @@ describe "an Active Record model which includes PgSearch" do
990
1040
 
991
1041
  with_model :Post do
992
1042
  table do |t|
993
- t.text 'content'
994
- t.tsvector 'content_tsvector'
1043
+ t.text "content"
1044
+ t.tsvector "content_tsvector"
995
1045
  end
996
1046
 
997
1047
  model do
@@ -1000,99 +1050,100 @@ describe "an Active Record model which includes PgSearch" do
1000
1050
  end
1001
1051
  end
1002
1052
 
1003
- let!(:expected) { Post.create!(content: 'phooey') }
1004
- let!(:unexpected) { Post.create!(content: 'longcat is looooooooong') }
1053
+ let!(:expected) { Post.create!(content: "phooey") }
1054
+ let!(:unexpected) { Post.create!(content: "longcat is looooooooong") }
1005
1055
 
1006
1056
  before do
1007
- ActiveRecord::Base.connection.execute <<-SQL.strip_heredoc
1057
+ ActiveRecord::Base.connection.execute <<~SQL.squish
1008
1058
  UPDATE #{Post.quoted_table_name}
1009
1059
  SET content_tsvector = to_tsvector('english'::regconfig, #{Post.quoted_table_name}."content")
1010
1060
  SQL
1011
1061
 
1012
- expected.comments.create(body: 'commentone')
1013
- unexpected.comments.create(body: 'commentwo')
1062
+ expected.comments.create!(body: "commentone")
1063
+ unexpected.comments.create!(body: "commentwo")
1014
1064
 
1015
1065
  Post.pg_search_scope :search_by_content_with_tsvector,
1016
- associated_against: { comments: [:body] },
1017
- using: {
1018
- tsearch: {
1019
- tsvector_column: 'content_tsvector',
1020
- dictionary: 'english'
1021
- }
1022
- }
1066
+ associated_against: {comments: [:body]},
1067
+ using: {
1068
+ tsearch: {
1069
+ tsvector_column: "content_tsvector",
1070
+ dictionary: "english"
1071
+ }
1072
+ }
1023
1073
  end
1024
1074
 
1025
- it "should find by the tsvector column" do
1075
+ it "finds by the tsvector column" do
1026
1076
  expect(Post.search_by_content_with_tsvector("phooey").map(&:id)).to eq([expected.id])
1027
1077
  end
1028
1078
 
1029
- it "should find by the associated record" do
1079
+ it "finds by the associated record" do
1030
1080
  expect(Post.search_by_content_with_tsvector("commentone").map(&:id)).to eq([expected.id])
1031
1081
  end
1032
1082
 
1033
- it 'should find by a combination of the two' do
1083
+ it "finds by a combination of the two" do
1034
1084
  expect(Post.search_by_content_with_tsvector("phooey commentone").map(&:id)).to eq([expected.id])
1035
1085
  end
1036
1086
  end
1037
1087
 
1038
- context 'using multiple tsvector columns' do
1088
+ context "when using multiple tsvector columns" do
1039
1089
  with_model :ModelWithTsvector do
1040
1090
  model do
1041
1091
  include PgSearch::Model
1042
1092
 
1043
1093
  pg_search_scope :search_by_multiple_tsvector_columns,
1044
- against: ['content', 'message'],
1045
- using: {
1046
- tsearch: {
1047
- tsvector_column: ['content_tsvector', 'message_tsvector'],
1048
- dictionary: 'english'
1049
- }
1050
- }
1094
+ against: ["content", "message"],
1095
+ using: {
1096
+ tsearch: {
1097
+ tsvector_column: ["content_tsvector", "message_tsvector"],
1098
+ dictionary: "english"
1099
+ }
1100
+ }
1051
1101
  end
1052
1102
  end
1053
1103
 
1054
- it 'concats tsvector columns' do
1055
- expected = "#{ModelWithTsvector.quoted_table_name}.\"content_tsvector\" || "\
1104
+ it "concats tsvector columns" do
1105
+ expected = "#{ModelWithTsvector.quoted_table_name}.\"content_tsvector\" || " \
1056
1106
  "#{ModelWithTsvector.quoted_table_name}.\"message_tsvector\""
1057
1107
 
1058
1108
  expect(ModelWithTsvector.search_by_multiple_tsvector_columns("something").to_sql).to include(expected)
1059
1109
  end
1060
1110
  end
1061
1111
 
1062
- context "using a tsvector column with" do
1112
+ context "when using a tsvector column with" do
1063
1113
  with_model :ModelWithTsvector do
1064
1114
  table do |t|
1065
- t.text 'content'
1066
- t.tsvector 'content_tsvector'
1115
+ t.text "content"
1116
+ t.tsvector "content_tsvector"
1067
1117
  end
1068
1118
 
1069
1119
  model { include PgSearch::Model }
1070
1120
  end
1071
1121
 
1072
- let!(:expected) { ModelWithTsvector.create!(content: 'tiling is grouty') }
1073
- let!(:unexpected) { ModelWithTsvector.create!(content: 'longcat is looooooooong') }
1122
+ let!(:expected) { ModelWithTsvector.create!(content: "tiling is grouty") }
1074
1123
 
1075
1124
  before do
1076
- ActiveRecord::Base.connection.execute <<-SQL.strip_heredoc
1125
+ ModelWithTsvector.create!(content: "longcat is looooooooong")
1126
+
1127
+ ActiveRecord::Base.connection.execute <<~SQL.squish
1077
1128
  UPDATE #{ModelWithTsvector.quoted_table_name}
1078
1129
  SET content_tsvector = to_tsvector('english'::regconfig, #{ModelWithTsvector.quoted_table_name}."content")
1079
1130
  SQL
1080
1131
 
1081
1132
  ModelWithTsvector.pg_search_scope :search_by_content_with_tsvector,
1082
- against: :content,
1083
- using: {
1084
- tsearch: {
1085
- tsvector_column: 'content_tsvector',
1086
- dictionary: 'english'
1087
- }
1088
- }
1133
+ against: :content,
1134
+ using: {
1135
+ tsearch: {
1136
+ tsvector_column: "content_tsvector",
1137
+ dictionary: "english"
1138
+ }
1139
+ }
1089
1140
  end
1090
1141
 
1091
- it "should not use to_tsvector in the query" do
1142
+ it "does not use to_tsvector in the query" do
1092
1143
  expect(ModelWithTsvector.search_by_content_with_tsvector("tiles").to_sql).not_to match(/to_tsvector/)
1093
1144
  end
1094
1145
 
1095
- it "should find the expected result" do
1146
+ it "finds the expected result" do
1096
1147
  expect(ModelWithTsvector.search_by_content_with_tsvector("tiles").map(&:id)).to eq([expected.id])
1097
1148
  end
1098
1149
 
@@ -1108,7 +1159,7 @@ describe "an Active Record model which includes PgSearch" do
1108
1159
  ModelWithTsvector.has_many :another_models
1109
1160
  end
1110
1161
 
1111
- it "should refer to the tsvector column in the query unambiguously" do
1162
+ it "refers to the tsvector column in the query unambiguously" do
1112
1163
  expect {
1113
1164
  ModelWithTsvector.joins(:another_models).search_by_content_with_tsvector("test").to_a
1114
1165
  }.not_to raise_exception
@@ -1116,11 +1167,11 @@ describe "an Active Record model which includes PgSearch" do
1116
1167
  end
1117
1168
  end
1118
1169
 
1119
- context "ignoring accents" do
1170
+ context "when ignoring accents" do
1120
1171
  before do
1121
1172
  ModelWithPgSearch.pg_search_scope :search_title_without_accents,
1122
- against: :title,
1123
- ignoring: :accents
1173
+ against: :title,
1174
+ ignoring: :accents
1124
1175
  end
1125
1176
 
1126
1177
  it "returns rows that match the query but not its accents" do
@@ -1134,11 +1185,16 @@ describe "an Active Record model which includes PgSearch" do
1134
1185
  end
1135
1186
 
1136
1187
  context "when the query includes accents" do
1137
- it "does not create an erroneous tsquery expression" do
1138
- included = ModelWithPgSearch.create!(title: "Weird L‘Content")
1188
+ let(:term) { "L#{%w[‘ ʻ ʼ].sample}Content" }
1189
+ let(:included) { ModelWithPgSearch.create!(title: "Weird #{term}") }
1190
+ let(:results) { ModelWithPgSearch.search_title_without_accents(term) }
1139
1191
 
1140
- results = ModelWithPgSearch.search_title_without_accents("L‘Content")
1141
- expect(results).to eq([included])
1192
+ before do
1193
+ ModelWithPgSearch.create!(title: "FooBar")
1194
+ end
1195
+
1196
+ it "does not create an erroneous tsquery expression" do
1197
+ expect(results).to contain_exactly(included)
1142
1198
  end
1143
1199
  end
1144
1200
  end
@@ -1146,25 +1202,25 @@ describe "an Active Record model which includes PgSearch" do
1146
1202
  context "when passed a :ranked_by expression" do
1147
1203
  before do
1148
1204
  ModelWithPgSearch.pg_search_scope :search_content_with_default_rank,
1149
- against: :content
1205
+ against: :content
1150
1206
 
1151
1207
  ModelWithPgSearch.pg_search_scope :search_content_with_importance_as_rank,
1152
- against: :content,
1153
- ranked_by: "importance"
1208
+ against: :content,
1209
+ ranked_by: "importance"
1154
1210
 
1155
1211
  ModelWithPgSearch.pg_search_scope :search_content_with_importance_as_rank_multiplier,
1156
- against: :content,
1157
- ranked_by: ":tsearch * importance"
1212
+ against: :content,
1213
+ ranked_by: ":tsearch * importance"
1158
1214
  end
1159
1215
 
1160
- it "should return records with a rank attribute equal to the :ranked_by expression" do
1161
- ModelWithPgSearch.create!(content: 'foo', importance: 10)
1216
+ it "returns records with a rank attribute equal to the :ranked_by expression" do
1217
+ ModelWithPgSearch.create!(content: "foo", importance: 10)
1162
1218
  results = ModelWithPgSearch.search_content_with_importance_as_rank("foo").with_pg_search_rank
1163
1219
  expect(results.first.pg_search_rank).to eq(10)
1164
1220
  end
1165
1221
 
1166
- it "should substitute :tsearch with the tsearch rank expression in the :ranked_by expression" do
1167
- ModelWithPgSearch.create!(content: 'foo', importance: 10)
1222
+ it "substitutes :tsearch with the tsearch rank expression in the :ranked_by expression" do
1223
+ ModelWithPgSearch.create!(content: "foo", importance: 10)
1168
1224
 
1169
1225
  tsearch_result =
1170
1226
  ModelWithPgSearch.search_content_with_default_rank("foo").with_pg_search_rank.first
@@ -1173,19 +1229,19 @@ describe "an Active Record model which includes PgSearch" do
1173
1229
 
1174
1230
  multiplied_result =
1175
1231
  ModelWithPgSearch.search_content_with_importance_as_rank_multiplier("foo")
1176
- .with_pg_search_rank
1177
- .first
1232
+ .with_pg_search_rank
1233
+ .first
1178
1234
 
1179
1235
  multiplied_rank = multiplied_result.pg_search_rank
1180
1236
 
1181
1237
  expect(multiplied_rank).to be_within(0.001).of(tsearch_rank * 10)
1182
1238
  end
1183
1239
 
1184
- it "should return results in descending order of the value of the rank expression" do
1240
+ it "returns results in descending order of the value of the rank expression" do
1185
1241
  records = [
1186
- ModelWithPgSearch.create!(content: 'foo', importance: 1),
1187
- ModelWithPgSearch.create!(content: 'foo', importance: 3),
1188
- ModelWithPgSearch.create!(content: 'foo', importance: 2)
1242
+ ModelWithPgSearch.create!(content: "foo", importance: 1),
1243
+ ModelWithPgSearch.create!(content: "foo", importance: 3),
1244
+ ModelWithPgSearch.create!(content: "foo", importance: 2)
1189
1245
  ]
1190
1246
 
1191
1247
  results = ModelWithPgSearch.search_content_with_importance_as_rank("foo")
@@ -1195,34 +1251,35 @@ describe "an Active Record model which includes PgSearch" do
1195
1251
  %w[tsearch trigram dmetaphone].each do |feature|
1196
1252
  context "using the #{feature} ranking algorithm" do
1197
1253
  let(:scope_name) { :"search_content_ranked_by_#{feature}" }
1254
+
1198
1255
  before do
1199
1256
  ModelWithPgSearch.pg_search_scope scope_name,
1200
- against: :content,
1201
- ranked_by: ":#{feature}"
1257
+ against: :content,
1258
+ ranked_by: ":#{feature}"
1202
1259
 
1203
- ModelWithPgSearch.create!(content: 'foo')
1260
+ ModelWithPgSearch.create!(content: "foo")
1204
1261
  end
1205
1262
 
1206
1263
  context "when .with_pg_search_rank is chained after" do
1207
1264
  specify "its results respond to #pg_search_rank" do
1208
- result = ModelWithPgSearch.send(scope_name, 'foo').with_pg_search_rank.first
1265
+ result = ModelWithPgSearch.send(scope_name, "foo").with_pg_search_rank.first
1209
1266
  expect(result).to respond_to(:pg_search_rank)
1210
1267
  end
1211
1268
 
1212
1269
  it "returns the rank when #pg_search_rank is called on a result" do
1213
- results = ModelWithPgSearch.send(scope_name, 'foo').with_pg_search_rank
1270
+ results = ModelWithPgSearch.send(scope_name, "foo").with_pg_search_rank
1214
1271
  expect(results.first.pg_search_rank).to be_a Float
1215
1272
  end
1216
1273
  end
1217
1274
 
1218
1275
  context "when .with_pg_search_rank is not chained after" do
1219
1276
  specify "its results do not respond to #pg_search_rank" do
1220
- result = ModelWithPgSearch.send(scope_name, 'foo').first
1277
+ result = ModelWithPgSearch.send(scope_name, "foo").first
1221
1278
  expect(result).not_to respond_to(:pg_search_rank)
1222
1279
  end
1223
1280
 
1224
1281
  it "raises PgSearch::PgSearchRankNotSelected when #pg_search_rank is called on a result" do
1225
- result = ModelWithPgSearch.send(scope_name, 'foo').first
1282
+ result = ModelWithPgSearch.send(scope_name, "foo").first
1226
1283
  expect {
1227
1284
  result.pg_search_rank
1228
1285
  }.to raise_exception(PgSearch::PgSearchRankNotSelected)
@@ -1231,47 +1288,47 @@ describe "an Active Record model which includes PgSearch" do
1231
1288
  end
1232
1289
  end
1233
1290
 
1234
- context "using the tsearch ranking algorithm" do
1291
+ context "when using the tsearch ranking algorithm" do
1235
1292
  it "sorts results by the tsearch rank" do
1236
1293
  ModelWithPgSearch.pg_search_scope :search_content_ranked_by_tsearch,
1237
- using: :tsearch,
1238
- against: :content,
1239
- ranked_by: ":tsearch"
1294
+ using: :tsearch,
1295
+ against: :content,
1296
+ ranked_by: ":tsearch"
1240
1297
 
1241
- once = ModelWithPgSearch.create!(content: 'foo bar')
1242
- twice = ModelWithPgSearch.create!(content: 'foo foo')
1298
+ once = ModelWithPgSearch.create!(content: "foo bar")
1299
+ twice = ModelWithPgSearch.create!(content: "foo foo")
1243
1300
 
1244
- results = ModelWithPgSearch.search_content_ranked_by_tsearch('foo')
1301
+ results = ModelWithPgSearch.search_content_ranked_by_tsearch("foo")
1245
1302
  expect(results.find_index(twice)).to be < results.find_index(once)
1246
1303
  end
1247
1304
  end
1248
1305
 
1249
- context "using the trigram ranking algorithm" do
1306
+ context "when using the trigram ranking algorithm" do
1250
1307
  it "sorts results by the trigram rank" do
1251
1308
  ModelWithPgSearch.pg_search_scope :search_content_ranked_by_trigram,
1252
- using: :trigram,
1253
- against: :content,
1254
- ranked_by: ":trigram"
1309
+ using: :trigram,
1310
+ against: :content,
1311
+ ranked_by: ":trigram"
1255
1312
 
1256
- close = ModelWithPgSearch.create!(content: 'abcdef')
1257
- exact = ModelWithPgSearch.create!(content: 'abc')
1313
+ close = ModelWithPgSearch.create!(content: "abcdef")
1314
+ exact = ModelWithPgSearch.create!(content: "abc")
1258
1315
 
1259
- results = ModelWithPgSearch.search_content_ranked_by_trigram('abc')
1316
+ results = ModelWithPgSearch.search_content_ranked_by_trigram("abc")
1260
1317
  expect(results.find_index(exact)).to be < results.find_index(close)
1261
1318
  end
1262
1319
  end
1263
1320
 
1264
- context "using the dmetaphone ranking algorithm" do
1321
+ context "when using the dmetaphone ranking algorithm" do
1265
1322
  it "sorts results by the dmetaphone rank" do
1266
1323
  ModelWithPgSearch.pg_search_scope :search_content_ranked_by_dmetaphone,
1267
- using: :dmetaphone,
1268
- against: :content,
1269
- ranked_by: ":dmetaphone"
1324
+ using: :dmetaphone,
1325
+ against: :content,
1326
+ ranked_by: ":dmetaphone"
1270
1327
 
1271
- once = ModelWithPgSearch.create!(content: 'Phoo Bar')
1272
- twice = ModelWithPgSearch.create!(content: 'Phoo Fu')
1328
+ once = ModelWithPgSearch.create!(content: "Phoo Bar")
1329
+ twice = ModelWithPgSearch.create!(content: "Phoo Fu")
1273
1330
 
1274
- results = ModelWithPgSearch.search_content_ranked_by_dmetaphone('foo')
1331
+ results = ModelWithPgSearch.search_content_ranked_by_dmetaphone("foo")
1275
1332
  expect(results.find_index(twice)).to be < results.find_index(once)
1276
1333
  end
1277
1334
  end
@@ -1280,17 +1337,17 @@ describe "an Active Record model which includes PgSearch" do
1280
1337
  context "when there is a sort only feature" do
1281
1338
  it "excludes that feature from the conditions, but uses it in the sorting" do
1282
1339
  ModelWithPgSearch.pg_search_scope :search_content_ranked_by_dmetaphone,
1283
- against: :content,
1284
- using: {
1285
- tsearch: { any_word: true, prefix: true },
1286
- dmetaphone: { any_word: true, prefix: true, sort_only: true }
1287
- },
1288
- ranked_by: ":tsearch + (0.5 * :dmetaphone)"
1340
+ against: :content,
1341
+ using: {
1342
+ tsearch: {any_word: true, prefix: true},
1343
+ dmetaphone: {any_word: true, prefix: true, sort_only: true}
1344
+ },
1345
+ ranked_by: ":tsearch + (0.5 * :dmetaphone)"
1289
1346
 
1290
1347
  exact = ModelWithPgSearch.create!(content: "ash hines")
1291
1348
  one_exact_one_close = ModelWithPgSearch.create!(content: "ash heinz")
1292
1349
  one_exact = ModelWithPgSearch.create!(content: "ash smith")
1293
- one_close = ModelWithPgSearch.create!(content: "leigh heinz")
1350
+ ModelWithPgSearch.create!(content: "leigh heinz")
1294
1351
 
1295
1352
  results = ModelWithPgSearch.search_content_ranked_by_dmetaphone("ash hines")
1296
1353
  expect(results).to eq [exact, one_exact_one_close, one_exact]
@@ -1298,3 +1355,4 @@ describe "an Active Record model which includes PgSearch" do
1298
1355
  end
1299
1356
  end
1300
1357
  end
1358
+ # standard:enable RSpec/NestedGroups