pg_search 2.3.7 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +38 -37
- data/.github/workflows/codeql.yml +86 -0
- data/.gitignore +0 -1
- data/.standard.yml +2 -2
- data/CHANGELOG.md +81 -64
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +4 -2
- data/LICENSE +1 -1
- data/README.md +89 -87
- data/Rakefile +1 -7
- data/doc/plans/2026-09-05-arel-stack.md +43 -0
- data/doc/plans/2026-09-06-arel-remaining-expressions.md +48 -0
- data/lib/pg_search/configuration/association.rb +37 -23
- data/lib/pg_search/configuration/column.rb +17 -12
- data/lib/pg_search/configuration/foreign_column.rb +3 -5
- data/lib/pg_search/configuration.rb +10 -26
- data/lib/pg_search/features/dmetaphone.rb +3 -7
- data/lib/pg_search/features/feature.rb +8 -4
- data/lib/pg_search/features/trigram.rb +6 -20
- data/lib/pg_search/features/tsearch.rb +47 -61
- data/lib/pg_search/features.rb +2 -0
- data/lib/pg_search/migration/generator.rb +1 -5
- data/lib/pg_search/model.rb +6 -8
- data/lib/pg_search/multisearch/rebuilder.rb +69 -64
- data/lib/pg_search/normalizer.rb +40 -11
- data/lib/pg_search/scope_options.rb +32 -49
- data/lib/pg_search/version.rb +1 -1
- data/pg_search.gemspec +5 -5
- data/spec/integration/associations_spec.rb +133 -0
- data/spec/integration/deprecation_spec.rb +5 -1
- data/spec/integration/pagination_spec.rb +1 -0
- data/spec/integration/pg_search_spec.rb +78 -1
- data/spec/integration/single_table_inheritance_spec.rb +2 -0
- data/spec/lib/pg_search/configuration/association_spec.rb +34 -58
- data/spec/lib/pg_search/configuration/column_spec.rb +73 -15
- data/spec/lib/pg_search/configuration/foreign_column_spec.rb +53 -20
- data/spec/lib/pg_search/features/dmetaphone_spec.rb +2 -2
- data/spec/lib/pg_search/features/trigram_spec.rb +8 -9
- data/spec/lib/pg_search/features/tsearch_spec.rb +8 -8
- data/spec/lib/pg_search/multisearch/rebuilder_spec.rb +251 -211
- data/spec/lib/pg_search/multisearch_spec.rb +16 -16
- data/spec/lib/pg_search/multisearchable_spec.rb +8 -0
- data/spec/lib/pg_search/normalizer_spec.rb +64 -11
- data/spec/lib/pg_search_spec.rb +6 -21
- data/spec/spec_helper.rb +0 -13
- metadata +11 -15
- data/spec/.rubocop.yml +0 -27
- data/spec/integration/.rubocop.yml +0 -11
data/README.md
CHANGED
|
@@ -13,10 +13,10 @@ Read the blog post introducing PgSearch at https://tanzu.vmware.com/content/blog
|
|
|
13
13
|
|
|
14
14
|
## REQUIREMENTS
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
- Ruby 3.3+
|
|
17
|
+
- Active Record 8.0+
|
|
18
|
+
- PostgreSQL 9.2+
|
|
19
|
+
- [PostgreSQL extensions](https://github.com/Casecommons/pg_search/wiki/Installing-PostgreSQL-Extensions) for certain features
|
|
20
20
|
|
|
21
21
|
## INSTALL
|
|
22
22
|
|
|
@@ -51,45 +51,46 @@ end
|
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
### Contents
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
54
|
+
|
|
55
|
+
- [Multi-search vs. search scopes](#multi-search-vs-search-scopes)
|
|
56
|
+
- [Multi-search](#multi-search)
|
|
57
|
+
- [Setup](#setup)
|
|
58
|
+
- [`multisearchable`](#multisearchable)
|
|
59
|
+
- [More Options ](#more-options)
|
|
60
|
+
- [Multi-search associations](#multi-search-associations)
|
|
61
|
+
- [Searching in the global search index](#searching-in-the-global-search-index)
|
|
62
|
+
- [Chaining method calls onto the results](#chaining-method-calls-onto-the-results)
|
|
63
|
+
- [Configuring multi-search](#configuring-multi-search)
|
|
64
|
+
- [Rebuilding search documents for a given class](#rebuilding-search-documents-for-a-given-class)
|
|
65
|
+
- [Disabling multi-search indexing temporarily](#disabling-multi-search-indexing-temporarily)
|
|
66
|
+
- [`pg_search_scope`](#pg_search_scope)
|
|
67
|
+
- [Searching against one column](#searching-against-one-column)
|
|
68
|
+
- [Searching against multiple columns](#searching-against-multiple-columns)
|
|
69
|
+
- [Dynamic search scopes](#dynamic-search-scopes)
|
|
70
|
+
- [Searching through associations](#searching-through-associations)
|
|
71
|
+
- [Searching using different search features](#searching-using-different-search-features)
|
|
72
|
+
- [`:tsearch` (Full Text Search)](#tsearch-full-text-search)
|
|
73
|
+
- [Weighting](#weighting)
|
|
74
|
+
- [`:prefix` (PostgreSQL 8.4 and newer only)](#prefix-postgresql-84-and-newer-only)
|
|
75
|
+
- [`:negation`](#negation)
|
|
76
|
+
- [`:dictionary`](#dictionary)
|
|
77
|
+
- [`:normalization`](#normalization)
|
|
78
|
+
- [`:any_word`](#any_word)
|
|
79
|
+
- [`:sort_only`](#sort_only)
|
|
80
|
+
- [`:highlight`](#highlight)
|
|
81
|
+
- [`:dmetaphone` (Double Metaphone soundalike search)](#dmetaphone-double-metaphone-soundalike-search)
|
|
82
|
+
- [`:trigram` (Trigram search)](#trigram-trigram-search)
|
|
83
|
+
- [`:threshold`](#threshold)
|
|
84
|
+
- [`:word_similarity`](#word_similarity)
|
|
85
|
+
- [Limiting Fields When Combining Features](#limiting-fields-when-combining-features)
|
|
86
|
+
- [Ignoring accent marks](#ignoring-accent-marks)
|
|
87
|
+
- [Using tsvector columns](#using-tsvector-columns)
|
|
88
|
+
- [Combining multiple tsvectors](#combining-multiple-tsvectors)
|
|
89
|
+
- [Configuring ranking and ordering](#configuring-ranking-and-ordering)
|
|
90
|
+
- [`:ranked_by` (Choosing a ranking algorithm)](#ranked_by-choosing-a-ranking-algorithm)
|
|
91
|
+
- [`:order_within_rank` (Breaking ties)](#order_within_rank-breaking-ties)
|
|
92
|
+
- [`PgSearch#pg_search_rank` (Reading a record's rank as a Float)](#pgsearchpg_search_rank-reading-a-records-rank-as-a-float)
|
|
93
|
+
- [Search rank and chained scopes](#search-rank-and-chained-scopes)
|
|
93
94
|
|
|
94
95
|
### Multi-search vs. search scopes
|
|
95
96
|
|
|
@@ -246,8 +247,8 @@ This allows much faster searches without joins later on by doing something like:
|
|
|
246
247
|
PgSearch.multisearch(params['search']).where(author_id: 2)
|
|
247
248
|
```
|
|
248
249
|
|
|
249
|
-
|
|
250
|
-
the additional attribute to be included in the pg_search_documents
|
|
250
|
+
_NOTE: You must currently manually call `record.update_pg_search_document` for
|
|
251
|
+
the additional attribute to be included in the pg_search_documents table_
|
|
251
252
|
|
|
252
253
|
#### Multi-search associations
|
|
253
254
|
|
|
@@ -331,18 +332,18 @@ To regenerate the documents for a given class, run:
|
|
|
331
332
|
PgSearch::Multisearch.rebuild(Product)
|
|
332
333
|
```
|
|
333
334
|
|
|
334
|
-
The
|
|
335
|
+
The `rebuild` method will delete all the documents for the given class
|
|
335
336
|
before regenerating them. In some situations this may not be desirable,
|
|
336
|
-
such as when you're using single-table inheritance and
|
|
337
|
-
is your base class. You can prevent
|
|
337
|
+
such as when you're using single-table inheritance and `searchable_type`
|
|
338
|
+
is your base class. You can prevent `rebuild` from deleting your records
|
|
338
339
|
like so:
|
|
339
340
|
|
|
340
341
|
```ruby
|
|
341
342
|
PgSearch::Multisearch.rebuild(Product, clean_up: false)
|
|
342
343
|
```
|
|
343
344
|
|
|
344
|
-
|
|
345
|
-
you can pass
|
|
345
|
+
`rebuild` runs inside a single transaction. To run outside of a transaction,
|
|
346
|
+
you can pass `transactional: false` like so:
|
|
346
347
|
|
|
347
348
|
```ruby
|
|
348
349
|
PgSearch::Multisearch.rebuild(Product, transactional: false)
|
|
@@ -399,7 +400,8 @@ class Movie < ActiveRecord::Base
|
|
|
399
400
|
end
|
|
400
401
|
end
|
|
401
402
|
```
|
|
402
|
-
|
|
403
|
+
|
|
404
|
+
**Note:** If using PostgreSQL before 9.1, replace the `CONCAT_WS()` function call with double-pipe concatenation, eg. `(movies.name || ' ' || directors.name)`. However, now be aware that if _any_ of the joined values is NULL then the final `content` value will also be NULL, whereas `CONCAT_WS()` will selectively ignore NULL values.
|
|
403
405
|
|
|
404
406
|
#### Disabling multi-search indexing temporarily
|
|
405
407
|
|
|
@@ -569,11 +571,10 @@ end
|
|
|
569
571
|
|
|
570
572
|
The currently implemented features are
|
|
571
573
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
574
|
+
- :tsearch - [Full text search](http://www.postgresql.org/docs/current/static/textsearch-intro.html), which is built-in to PostgreSQL
|
|
575
|
+
- :trigram - [Trigram search](http://www.postgresql.org/docs/current/static/pgtrgm.html), which
|
|
576
|
+
requires the trigram extension
|
|
577
|
+
- :dmetaphone - [Double Metaphone search](http://www.postgresql.org/docs/current/static/fuzzystrmatch.html#AEN177521), which requires the fuzzystrmatch extension
|
|
577
578
|
|
|
578
579
|
#### :tsearch (Full Text Search)
|
|
579
580
|
|
|
@@ -581,6 +582,7 @@ PostgreSQL's built-in full text search supports weighting, prefix searches,
|
|
|
581
582
|
and stemming in multiple languages.
|
|
582
583
|
|
|
583
584
|
##### Weighting
|
|
585
|
+
|
|
584
586
|
Each searchable column can be given a weight of "A", "B", "C", or "D". Columns
|
|
585
587
|
with earlier letters are weighted higher than those with later letters. So, in
|
|
586
588
|
the following example, the title is the most important, followed by the
|
|
@@ -644,6 +646,7 @@ robin = Superhero.create name: 'Robin'
|
|
|
644
646
|
|
|
645
647
|
Superhero.whose_name_starts_with("Bat") # => [batman, batgirl]
|
|
646
648
|
```
|
|
649
|
+
|
|
647
650
|
##### :negation
|
|
648
651
|
|
|
649
652
|
PostgreSQL's full text search matches all search terms by default. If you want
|
|
@@ -804,7 +807,6 @@ Person.search('ash hines') # => [exact, one_exact_one_close, one_exact]
|
|
|
804
807
|
Adding .with_pg_search_highlight after the pg_search_scope you can access to
|
|
805
808
|
`pg_highlight` attribute for each object.
|
|
806
809
|
|
|
807
|
-
|
|
808
810
|
```ruby
|
|
809
811
|
class Person < ActiveRecord::Base
|
|
810
812
|
include PgSearch::Model
|
|
@@ -815,8 +817,8 @@ class Person < ActiveRecord::Base
|
|
|
815
817
|
highlight: {
|
|
816
818
|
StartSel: '<b>',
|
|
817
819
|
StopSel: '</b>',
|
|
818
|
-
|
|
819
|
-
|
|
820
|
+
MinWords: 123,
|
|
821
|
+
MaxWords: 456,
|
|
820
822
|
ShortWord: 4,
|
|
821
823
|
HighlightAll: true,
|
|
822
824
|
MaxFragments: 3,
|
|
@@ -1053,31 +1055,31 @@ as it offloads creation of the tsvector that the tsquery is evaluated against.
|
|
|
1053
1055
|
|
|
1054
1056
|
To use this functionality you'll need to do a few things:
|
|
1055
1057
|
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1058
|
+
- Create a column of type tsvector that you'd like to search against. If you
|
|
1059
|
+
want to search using multiple search methods, for example tsearch and
|
|
1060
|
+
dmetaphone, you'll need a column for each.
|
|
1061
|
+
- Create a trigger function that will update the column(s) using the
|
|
1062
|
+
expression appropriate for that type of search. See:
|
|
1063
|
+
[the PostgreSQL documentation for text search triggers](http://www.postgresql.org/docs/current/static/textsearch-features.html#TEXTSEARCH-UPDATE-TRIGGERS)
|
|
1064
|
+
- Should you have any pre-existing data in the table, update the
|
|
1065
|
+
newly-created tsvector columns with the expression that your trigger
|
|
1066
|
+
function uses.
|
|
1067
|
+
- Add the option to pg_search_scope, e.g:
|
|
1068
|
+
|
|
1069
|
+
```ruby
|
|
1070
|
+
pg_search_scope :fast_content_search,
|
|
1071
|
+
against: :content,
|
|
1072
|
+
using: {
|
|
1073
|
+
dmetaphone: {
|
|
1074
|
+
tsvector_column: 'tsvector_content_dmetaphone'
|
|
1075
|
+
},
|
|
1076
|
+
tsearch: {
|
|
1077
|
+
dictionary: 'english',
|
|
1078
|
+
tsvector_column: 'tsvector_content_tsearch'
|
|
1079
|
+
},
|
|
1080
|
+
trigram: {} # trigram does not use tsvectors
|
|
1081
|
+
}
|
|
1082
|
+
```
|
|
1081
1083
|
|
|
1082
1084
|
Please note that the :against column is only used when the tsvector_column is
|
|
1083
1085
|
not present for the search type.
|
|
@@ -1190,9 +1192,9 @@ shirt_brands[1].pg_search_rank #=> 0.0607927
|
|
|
1190
1192
|
|
|
1191
1193
|
#### Search rank and chained scopes
|
|
1192
1194
|
|
|
1193
|
-
Each PgSearch scope generates a named subquery for the search rank.
|
|
1195
|
+
Each PgSearch scope generates a named subquery for the search rank. If you
|
|
1194
1196
|
chain multiple scopes then PgSearch will generate a ranking query for each
|
|
1195
|
-
scope, so the ranking queries must have unique names.
|
|
1197
|
+
scope, so the ranking queries must have unique names. If you need to reference
|
|
1196
1198
|
the ranking query (e.g. in a GROUP BY clause) you can regenerate the subquery
|
|
1197
1199
|
name with the `PgScope::Configuration.alias` method by passing the name of the
|
|
1198
1200
|
queried table.
|
|
@@ -1218,5 +1220,5 @@ for discussing pg_search and other Casebook PBC open source projects.
|
|
|
1218
1220
|
|
|
1219
1221
|
## LICENSE
|
|
1220
1222
|
|
|
1221
|
-
Copyright © 2010–
|
|
1223
|
+
Copyright © 2010–2026 [Casebook PBC](http://www.casebook.net).
|
|
1222
1224
|
Licensed under the MIT license, see [LICENSE](/LICENSE) file.
|
data/Rakefile
CHANGED
|
@@ -8,10 +8,4 @@ RSpec::Core::RakeTask.new(:spec)
|
|
|
8
8
|
|
|
9
9
|
require "standard/rake"
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
task :undercover do
|
|
13
|
-
system("git fetch --unshallow") if ENV["CI"]
|
|
14
|
-
exit(1) unless system("bin/undercover --compare origin/master")
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
task default: %w[spec standard undercover]
|
|
11
|
+
task default: %w[spec standard]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Incremental Arel migration
|
|
2
|
+
|
|
3
|
+
Replace useful SQL-string construction with composable Arel expressions in five
|
|
4
|
+
small, independently mergeable changes. Complete elimination of interpolation is
|
|
5
|
+
not required. Each change includes its own caller adaptations and tests; no later
|
|
6
|
+
change should be needed to make an earlier one work.
|
|
7
|
+
|
|
8
|
+
Use `to_arel` for expressions and `to_sql` for rendered SQL strings. Keep behavior
|
|
9
|
+
unchanged, including trusted SQL expressions and custom ordering. Preserve the
|
|
10
|
+
existing rank-join alias handling. Prefer named immutable values (`Data.define`)
|
|
11
|
+
for structured records, while retaining arrays for genuine expression lists.
|
|
12
|
+
|
|
13
|
+
## Review and test each layer
|
|
14
|
+
|
|
15
|
+
Before changing code, assess existing coverage and characterize the relevant
|
|
16
|
+
behavior. Demonstrate failing coverage for new contracts or missing regressions,
|
|
17
|
+
then implement the narrow change. Test SQL execution against PostgreSQL, not just
|
|
18
|
+
rendered strings. Run focused examples, the full suite, and Standard for every
|
|
19
|
+
layer. Review correctness, coverage quality, and unnecessary complexity before
|
|
20
|
+
publishing it.
|
|
21
|
+
|
|
22
|
+
Exercise expression behavior through `to_arel`. Keep `to_sql` coverage focused on
|
|
23
|
+
its rendering-adapter contract, so eventually removing it does not discard the
|
|
24
|
+
behavioral coverage or require duplicating that coverage across both interfaces.
|
|
25
|
+
|
|
26
|
+
- [x] **Column expressions:** add explicit Arel conversions for normal and foreign
|
|
27
|
+
columns, preserving string conversions. Cover NULLs, casts, quoted identifiers,
|
|
28
|
+
and trusted JSON expressions.
|
|
29
|
+
- [x] **Association queries:** build aggregated projections and joins with Arel.
|
|
30
|
+
Cover association types, missing associations, aggregation, and alias collisions.
|
|
31
|
+
- [x] **Rank selection and ordering:** convert projections and ordering without
|
|
32
|
+
replacing the existing rank-join fix. Cover selected ranks, tie-breaking,
|
|
33
|
+
custom ordering, associations, and chained scopes.
|
|
34
|
+
- [x] **Feature expressions and normalization:** compose search expressions as
|
|
35
|
+
nodes while preserving sanitization. Cover blank queries, quotes and accents,
|
|
36
|
+
prefix/negation, weighted and stored vectors, highlighting, and other features.
|
|
37
|
+
- [x] **Multisearch rebuilds:** convert useful statement components without forcing
|
|
38
|
+
every fixed SQL fragment into Arel. Cover actual rebuilds, STI, NULL content,
|
|
39
|
+
multiple columns, and custom identifiers.
|
|
40
|
+
|
|
41
|
+
Record remaining interpolation after the stack as deferred work, not a release
|
|
42
|
+
gate. Keep unrelated cleanup, historical planning artifacts, and obsolete
|
|
43
|
+
compatibility code out of these changes.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Remaining Arel expression construction
|
|
2
|
+
|
|
3
|
+
Continuation of the approved [Arel stack](2026-09-05-arel-stack.md), authorized
|
|
4
|
+
for autonomous follow-through while the earlier PRs are reviewed. Preserve
|
|
5
|
+
those PR heads; publish new independently green layers above #587.
|
|
6
|
+
|
|
7
|
+
## Contract
|
|
8
|
+
|
|
9
|
+
Replace remaining SQL-construction interpolation and avoidable raw SQL fragments
|
|
10
|
+
with composable expressions, without changing query behavior or existing
|
|
11
|
+
String-returning adapter contracts.
|
|
12
|
+
Do not rewrite ordinary messages, generated names, or PostgreSQL headline-option
|
|
13
|
+
data merely to eliminate Ruby interpolation. No new database functions/migrations.
|
|
14
|
+
|
|
15
|
+
- [x] **Query projections and vectors:** convert ScopeOptions star/highlight/rank
|
|
16
|
+
projections and qualified primary-key rendering, plus TSearch's stored-vector
|
|
17
|
+
references. Preserve explicit select behavior, trusted custom SQL, chained
|
|
18
|
+
aliases, quoted identifiers, and synthetic rank references without accidentally
|
|
19
|
+
applying model attribute aliases.
|
|
20
|
+
- [x] **Column source references:** replace full_name interpolation and the
|
|
21
|
+
aggregate's String-to-Arel round trip with a coherent source-attribute
|
|
22
|
+
expression. Preserve the distinction between a foreign column's original
|
|
23
|
+
association source and its derived search-document alias, non-coalesced
|
|
24
|
+
aggregation, and the existing full_name/to_sql String interfaces.
|
|
25
|
+
- [x] **Quoted values:** replace hand-quoted data and quote/render/wrap patterns
|
|
26
|
+
with quoted-value nodes: separators, empty strings, regex arguments, model
|
|
27
|
+
names, and timestamps. Preserve actual data, escaping, NULL behavior, and the
|
|
28
|
+
shared timestamp. Do not confuse a SQL type token or empty SQL fragment with
|
|
29
|
+
a string value.
|
|
30
|
+
- [x] **Expression boundaries:** preserve Arel attributes through normalization
|
|
31
|
+
instead of coercing their Ruby inspection into SQL; remove avoidable internal
|
|
32
|
+
render-and-wrap cycles for primary keys and default ranking. Preserve the
|
|
33
|
+
documented custom SQL inputs and legacy String adapters at their boundaries.
|
|
34
|
+
Investigate empty-expression fragments before changing their semantics.
|
|
35
|
+
- [x] **Final audit:** verify remaining interpolation/raw SQL is intentional
|
|
36
|
+
syntax, data serialization, or trusted escape-hatch input; report residue
|
|
37
|
+
rather than hiding it.
|
|
38
|
+
|
|
39
|
+
## Verification and delivery
|
|
40
|
+
|
|
41
|
+
Characterize meaningful missing behavior before refactoring; no private-helper
|
|
42
|
+
specs or test-only visibility subclasses. Simplify touched fixture setup using
|
|
43
|
+
normal schema names and Rails conventions. Prefer public database behavior over
|
|
44
|
+
additional SQL golden matrices. Run relevant specs and bin/rake independently
|
|
45
|
+
for each layer, including the existing pinned Rails-main environment; require
|
|
46
|
+
one focused independent review per layer and green hosted CI.
|
|
47
|
+
|
|
48
|
+
Check each item with its owning code.
|
|
@@ -19,8 +19,21 @@ module PgSearch
|
|
|
19
19
|
@model.reflect_on_association(@name).table_name
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
def
|
|
23
|
-
|
|
22
|
+
def arel_table = @model.reflect_on_association(@name).klass.arel_table
|
|
23
|
+
|
|
24
|
+
# Builds an outer join retaining association-scope binds.
|
|
25
|
+
#
|
|
26
|
+
# Arguments
|
|
27
|
+
#
|
|
28
|
+
# +primary_key+:: An Arel expression or trusted SQL String identifying the
|
|
29
|
+
# model's primary-key column, not a record's key value.
|
|
30
|
+
#
|
|
31
|
+
# Returns a composable outer join to the search subquery.
|
|
32
|
+
def to_arel(primary_key)
|
|
33
|
+
primary_key = Arel.sql(primary_key) if primary_key.is_a?(String)
|
|
34
|
+
subquery = relation(primary_key).arel.as(subselect_alias)
|
|
35
|
+
on_condition = Arel::Nodes::On.new(subquery[:id].eq(primary_key))
|
|
36
|
+
Arel::Nodes::OuterJoin.new(subquery, on_condition)
|
|
24
37
|
end
|
|
25
38
|
|
|
26
39
|
def subselect_alias
|
|
@@ -29,32 +42,33 @@ module PgSearch
|
|
|
29
42
|
|
|
30
43
|
private
|
|
31
44
|
|
|
32
|
-
def selects
|
|
33
|
-
if singular_association?
|
|
34
|
-
selects_for_singular_association
|
|
35
|
-
else
|
|
36
|
-
selects_for_multiple_association
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def selects_for_singular_association
|
|
41
|
-
columns.map do |column|
|
|
42
|
-
"#{column.full_name}::text AS #{column.alias}"
|
|
43
|
-
end.join(", ")
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def selects_for_multiple_association
|
|
47
|
-
columns.map do |column|
|
|
48
|
-
"string_agg(#{column.full_name}::text, ' ') AS #{column.alias}"
|
|
49
|
-
end.join(", ")
|
|
50
|
-
end
|
|
51
|
-
|
|
52
45
|
def relation(primary_key)
|
|
53
|
-
result = @model.unscoped.joins(@name).select(
|
|
46
|
+
result = @model.unscoped.joins(@name).select(
|
|
47
|
+
primary_key.as("id"),
|
|
48
|
+
*selects
|
|
49
|
+
)
|
|
54
50
|
result = result.group(primary_key) unless singular_association?
|
|
55
51
|
result
|
|
56
52
|
end
|
|
57
53
|
|
|
54
|
+
def selects
|
|
55
|
+
columns.map do |column|
|
|
56
|
+
cast_node = Arel::Nodes::NamedFunction.new(
|
|
57
|
+
"cast",
|
|
58
|
+
[column.source_attribute.as(Arel.sql("text"))]
|
|
59
|
+
)
|
|
60
|
+
projection = if singular_association?
|
|
61
|
+
cast_node
|
|
62
|
+
else
|
|
63
|
+
Arel::Nodes::NamedFunction.new(
|
|
64
|
+
"string_agg",
|
|
65
|
+
[cast_node, Arel::Nodes.build_quoted(" ")]
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
projection.as(column.alias)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
58
72
|
def singular_association?
|
|
59
73
|
%i[has_one belongs_to].include?(@model.reflect_on_association(@name).macro)
|
|
60
74
|
end
|
|
@@ -12,32 +12,37 @@ module PgSearch
|
|
|
12
12
|
@column_name = column_name
|
|
13
13
|
@weight = weight
|
|
14
14
|
@model = model
|
|
15
|
-
@connection = model.connection
|
|
16
15
|
end
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
# Physical source-column attribute, or the supplied trusted SQL literal.
|
|
18
|
+
# Foreign columns refer to the association table, not the search alias.
|
|
19
|
+
def source_attribute
|
|
19
20
|
return @column_name if @column_name.is_a?(Arel::Nodes::SqlLiteral)
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
Arel::Attributes::Attribute.new(source_table, @name)
|
|
22
23
|
end
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
# Composable search expression cast to text, with NULL mapped to "".
|
|
26
|
+
# Foreign columns use their derived search alias rather than the source.
|
|
27
|
+
def to_arel = coalesce_to_blank_string(cast_to_text(attribute))
|
|
27
28
|
|
|
28
29
|
private
|
|
29
30
|
|
|
30
|
-
def
|
|
31
|
-
@
|
|
31
|
+
def attribute
|
|
32
|
+
return @column_name if @column_name.is_a?(Arel::Nodes::SqlLiteral)
|
|
33
|
+
|
|
34
|
+
@model.arel_table[@name]
|
|
32
35
|
end
|
|
33
36
|
|
|
34
|
-
def
|
|
35
|
-
|
|
37
|
+
def cast_to_text(node)
|
|
38
|
+
Arel::Nodes::NamedFunction.new("cast", [node.as(Arel.sql("text"))])
|
|
36
39
|
end
|
|
37
40
|
|
|
38
|
-
def
|
|
39
|
-
|
|
41
|
+
def coalesce_to_blank_string(node)
|
|
42
|
+
Arel::Nodes::NamedFunction.new("coalesce", [node, Arel::Nodes.build_quoted("")])
|
|
40
43
|
end
|
|
44
|
+
|
|
45
|
+
def source_table = @model.arel_table
|
|
41
46
|
end
|
|
42
47
|
end
|
|
43
48
|
end
|
|
@@ -18,13 +18,11 @@ module PgSearch
|
|
|
18
18
|
|
|
19
19
|
private
|
|
20
20
|
|
|
21
|
-
def
|
|
22
|
-
|
|
21
|
+
def attribute
|
|
22
|
+
@model.arel_table.alias(@association.subselect_alias)[self.alias]
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
def
|
|
26
|
-
@connection.quote_table_name(@association.table_name)
|
|
27
|
-
end
|
|
25
|
+
def source_table = @association.arel_table
|
|
28
26
|
end
|
|
29
27
|
end
|
|
30
28
|
end
|
|
@@ -23,9 +23,7 @@ module PgSearch
|
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
def columns
|
|
27
|
-
regular_columns + associated_columns
|
|
28
|
-
end
|
|
26
|
+
def columns = regular_columns + associated_columns
|
|
29
27
|
|
|
30
28
|
def regular_columns
|
|
31
29
|
return [] unless options[:against]
|
|
@@ -43,37 +41,21 @@ module PgSearch
|
|
|
43
41
|
end.flatten
|
|
44
42
|
end
|
|
45
43
|
|
|
46
|
-
def associated_columns
|
|
47
|
-
associations.map(&:columns).flatten
|
|
48
|
-
end
|
|
44
|
+
def associated_columns = associations.map(&:columns).flatten
|
|
49
45
|
|
|
50
|
-
def query
|
|
51
|
-
options[:query].to_s
|
|
52
|
-
end
|
|
46
|
+
def query = options[:query].to_s
|
|
53
47
|
|
|
54
|
-
def ignore
|
|
55
|
-
Array(options[:ignoring])
|
|
56
|
-
end
|
|
48
|
+
def ignore = Array(options[:ignoring])
|
|
57
49
|
|
|
58
|
-
def ranking_sql
|
|
59
|
-
options[:ranked_by]
|
|
60
|
-
end
|
|
50
|
+
def ranking_sql = options[:ranked_by]
|
|
61
51
|
|
|
62
|
-
def features
|
|
63
|
-
Array(options[:using])
|
|
64
|
-
end
|
|
52
|
+
def features = Array(options[:using])
|
|
65
53
|
|
|
66
54
|
def feature_options
|
|
67
|
-
@feature_options ||=
|
|
68
|
-
features.map do |feature_name, feature_options|
|
|
69
|
-
hash[feature_name] = feature_options
|
|
70
|
-
end
|
|
71
|
-
end
|
|
55
|
+
@feature_options ||= features.to_h { |name, opts| [name, opts] }
|
|
72
56
|
end
|
|
73
57
|
|
|
74
|
-
def order_within_rank
|
|
75
|
-
options[:order_within_rank]
|
|
76
|
-
end
|
|
58
|
+
def order_within_rank = options[:order_within_rank]
|
|
77
59
|
|
|
78
60
|
private
|
|
79
61
|
|
|
@@ -83,6 +65,7 @@ module PgSearch
|
|
|
83
65
|
{using: :tsearch}
|
|
84
66
|
end
|
|
85
67
|
|
|
68
|
+
# standard:disable Lint/UselessConstantScoping
|
|
86
69
|
VALID_KEYS = %w[
|
|
87
70
|
against ranked_by ignoring using query associated_against order_within_rank
|
|
88
71
|
].map(&:to_sym)
|
|
@@ -90,6 +73,7 @@ module PgSearch
|
|
|
90
73
|
VALID_VALUES = {
|
|
91
74
|
ignoring: [:accents]
|
|
92
75
|
}.freeze
|
|
76
|
+
# standard:enable Lint/UselessConstantScoping
|
|
93
77
|
|
|
94
78
|
def assert_valid_options(options)
|
|
95
79
|
unless options[:against] || options[:associated_against] || using_tsvector_column?(options[:using])
|
|
@@ -25,15 +25,11 @@ module PgSearch
|
|
|
25
25
|
@normalizer_to_wrap = normalizer_to_wrap
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
def add_normalization(
|
|
29
|
-
otherwise_normalized_sql = Arel.sql(
|
|
30
|
-
normalizer_to_wrap.add_normalization(original_sql)
|
|
31
|
-
)
|
|
32
|
-
|
|
28
|
+
def add_normalization(expression)
|
|
33
29
|
Arel::Nodes::NamedFunction.new(
|
|
34
30
|
"pg_search_dmetaphone",
|
|
35
|
-
[
|
|
36
|
-
)
|
|
31
|
+
[normalizer_to_wrap.add_normalization(expression)]
|
|
32
|
+
)
|
|
37
33
|
end
|
|
38
34
|
|
|
39
35
|
private
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "active_support/core_ext/module/delegation"
|
|
4
3
|
require "active_support/core_ext/hash/keys"
|
|
5
4
|
|
|
6
5
|
module PgSearch
|
|
@@ -10,8 +9,6 @@ module PgSearch
|
|
|
10
9
|
%i[only sort_only]
|
|
11
10
|
end
|
|
12
11
|
|
|
13
|
-
delegate :connection, :quoted_table_name, to: :@model
|
|
14
|
-
|
|
15
12
|
def initialize(query, options, all_columns, model, normalizer)
|
|
16
13
|
@query = query
|
|
17
14
|
@options = (options || {}).assert_valid_keys(self.class.valid_options)
|
|
@@ -25,7 +22,14 @@ module PgSearch
|
|
|
25
22
|
attr_reader :query, :options, :all_columns, :model, :normalizer
|
|
26
23
|
|
|
27
24
|
def document
|
|
28
|
-
|
|
25
|
+
space = Arel::Nodes.build_quoted(" ")
|
|
26
|
+
columns.map(&:to_arel).inject do |memo, col|
|
|
27
|
+
Arel::Nodes::InfixOperation.new(
|
|
28
|
+
"||",
|
|
29
|
+
Arel::Nodes::InfixOperation.new("||", memo, space),
|
|
30
|
+
col
|
|
31
|
+
)
|
|
32
|
+
end
|
|
29
33
|
end
|
|
30
34
|
|
|
31
35
|
def columns
|