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
|
@@ -29,43 +29,29 @@ module PgSearch
|
|
|
29
29
|
|
|
30
30
|
private
|
|
31
31
|
|
|
32
|
-
def word_similarity?
|
|
33
|
-
options[:word_similarity]
|
|
34
|
-
end
|
|
32
|
+
def word_similarity? = options[:word_similarity]
|
|
35
33
|
|
|
36
34
|
def similarity_function
|
|
37
|
-
|
|
38
|
-
"word_similarity"
|
|
39
|
-
else
|
|
40
|
-
"similarity"
|
|
41
|
-
end
|
|
35
|
+
word_similarity? ? "word_similarity" : "similarity"
|
|
42
36
|
end
|
|
43
37
|
|
|
44
38
|
def infix_operator
|
|
45
|
-
|
|
46
|
-
"<%"
|
|
47
|
-
else
|
|
48
|
-
"%"
|
|
49
|
-
end
|
|
39
|
+
word_similarity? ? "<%" : "%"
|
|
50
40
|
end
|
|
51
41
|
|
|
52
42
|
def similarity
|
|
53
43
|
Arel::Nodes::NamedFunction.new(
|
|
54
44
|
similarity_function,
|
|
55
|
-
[
|
|
56
|
-
normalized_query,
|
|
57
|
-
normalized_document
|
|
58
|
-
]
|
|
45
|
+
[normalized_query, normalized_document]
|
|
59
46
|
)
|
|
60
47
|
end
|
|
61
48
|
|
|
62
49
|
def normalized_document
|
|
63
|
-
Arel::Nodes::Grouping.new(
|
|
50
|
+
Arel::Nodes::Grouping.new(normalize(document))
|
|
64
51
|
end
|
|
65
52
|
|
|
66
53
|
def normalized_query
|
|
67
|
-
|
|
68
|
-
Arel.sql(normalize(sanitized_query))
|
|
54
|
+
normalize(Arel::Nodes.build_quoted(query))
|
|
69
55
|
end
|
|
70
56
|
end
|
|
71
57
|
end
|
|
@@ -12,16 +12,16 @@ module PgSearch
|
|
|
12
12
|
|
|
13
13
|
def conditions
|
|
14
14
|
Arel::Nodes::Grouping.new(
|
|
15
|
-
Arel::Nodes::InfixOperation.new("@@",
|
|
15
|
+
Arel::Nodes::InfixOperation.new("@@", Arel::Nodes::Grouping.new(tsdocument), Arel::Nodes::Grouping.new(tsquery))
|
|
16
16
|
)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def rank
|
|
20
|
-
|
|
20
|
+
Arel::Nodes::Grouping.new(tsearch_rank)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def highlight
|
|
24
|
-
|
|
24
|
+
Arel::Nodes::Grouping.new(ts_headline)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
private
|
|
@@ -29,10 +29,10 @@ module PgSearch
|
|
|
29
29
|
def ts_headline
|
|
30
30
|
Arel::Nodes::NamedFunction.new("ts_headline", [
|
|
31
31
|
dictionary,
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
Arel::Nodes::Grouping.new(document),
|
|
33
|
+
Arel::Nodes::Grouping.new(tsquery),
|
|
34
34
|
Arel::Nodes.build_quoted(ts_headline_options)
|
|
35
|
-
])
|
|
35
|
+
])
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def ts_headline_options
|
|
@@ -49,12 +49,8 @@ module PgSearch
|
|
|
49
49
|
|
|
50
50
|
%w[
|
|
51
51
|
StartSel StopSel MaxFragments MaxWords MinWords ShortWord FragmentDelimiter HighlightAll
|
|
52
|
-
].
|
|
53
|
-
|
|
54
|
-
value = indifferent_options[:highlight][key]
|
|
55
|
-
|
|
56
|
-
hash[key] = ts_headline_option_value(value)
|
|
57
|
-
end
|
|
52
|
+
].to_h do |key|
|
|
53
|
+
[key, ts_headline_option_value(indifferent_options[:highlight][key])]
|
|
58
54
|
end
|
|
59
55
|
end
|
|
60
56
|
|
|
@@ -63,23 +59,20 @@ module PgSearch
|
|
|
63
59
|
|
|
64
60
|
%w[
|
|
65
61
|
start_sel stop_sel max_fragments max_words min_words short_word fragment_delimiter highlight_all
|
|
66
|
-
].
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
hash[key] = ts_headline_option_value(value)
|
|
81
|
-
end
|
|
82
|
-
end
|
|
62
|
+
].each_with_object({}) do |deprecated_key, hash|
|
|
63
|
+
value = indifferent_options[:highlight][deprecated_key]
|
|
64
|
+
next if value.nil?
|
|
65
|
+
|
|
66
|
+
key = deprecated_key.camelize
|
|
67
|
+
|
|
68
|
+
warn(
|
|
69
|
+
"pg_search 3.0 will no longer accept :#{deprecated_key} as an argument to :ts_headline, " \
|
|
70
|
+
"use :#{key} instead.",
|
|
71
|
+
category: :deprecated,
|
|
72
|
+
uplevel: 1
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
hash[key] = ts_headline_option_value(value)
|
|
83
76
|
end
|
|
84
77
|
end
|
|
85
78
|
|
|
@@ -96,8 +89,7 @@ module PgSearch
|
|
|
96
89
|
end
|
|
97
90
|
end
|
|
98
91
|
|
|
99
|
-
DISALLOWED_TSQUERY_CHARACTERS = /['
|
|
100
|
-
|
|
92
|
+
DISALLOWED_TSQUERY_CHARACTERS = /['?\\:]/ # standard:disable Lint/UselessConstantScoping
|
|
101
93
|
def tsquery_for_term(unsanitized_term)
|
|
102
94
|
if options[:negation] && unsanitized_term.start_with?("!")
|
|
103
95
|
unsanitized_term[0] = ""
|
|
@@ -105,22 +97,20 @@ module PgSearch
|
|
|
105
97
|
end
|
|
106
98
|
|
|
107
99
|
sanitized_term = unsanitized_term.gsub(DISALLOWED_TSQUERY_CHARACTERS, " ")
|
|
100
|
+
term_node = normalize(Arel::Nodes.build_quoted(sanitized_term))
|
|
101
|
+
tsquery_expr = tsquery_expression(term_node, negated: negated, prefix: options[:prefix])
|
|
108
102
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
tsquery = tsquery_expression(term_sql, negated: negated, prefix: options[:prefix])
|
|
112
|
-
|
|
113
|
-
Arel::Nodes::NamedFunction.new("to_tsquery", [dictionary, tsquery]).to_sql
|
|
103
|
+
Arel::Nodes::NamedFunction.new("to_tsquery", [dictionary, tsquery_expr])
|
|
114
104
|
end
|
|
115
105
|
|
|
116
106
|
# After this, the SQL expression evaluates to a string containing the term surrounded by single-quotes.
|
|
117
107
|
# If :prefix is true, then the term will have :* appended to the end.
|
|
118
108
|
# If :negated is true, then the term will have ! prepended to the front.
|
|
119
|
-
def tsquery_expression(
|
|
109
|
+
def tsquery_expression(term_node, negated:, prefix:)
|
|
120
110
|
terms = [
|
|
121
111
|
(Arel::Nodes.build_quoted("!") if negated),
|
|
122
112
|
Arel::Nodes.build_quoted("' "),
|
|
123
|
-
|
|
113
|
+
term_node,
|
|
124
114
|
Arel::Nodes.build_quoted(" '"),
|
|
125
115
|
(Arel::Nodes.build_quoted(":*") if prefix)
|
|
126
116
|
].compact
|
|
@@ -131,29 +121,26 @@ module PgSearch
|
|
|
131
121
|
end
|
|
132
122
|
|
|
133
123
|
def tsquery
|
|
134
|
-
return "
|
|
124
|
+
return Arel::Nodes.build_quoted("") if query.blank?
|
|
135
125
|
|
|
136
126
|
query_terms = query.split.compact
|
|
137
|
-
|
|
138
|
-
|
|
127
|
+
query_terms.map { |term| tsquery_for_term(term) }
|
|
128
|
+
.inject { |memo, t| Arel::Nodes::InfixOperation.new(options[:any_word] ? "||" : "&&", memo, t) }
|
|
139
129
|
end
|
|
140
130
|
|
|
141
131
|
def tsdocument
|
|
142
|
-
|
|
143
|
-
column_to_tsvector(search_column)
|
|
144
|
-
end
|
|
132
|
+
terms = (columns_to_use || []).map { |col| column_to_tsvector(col) }
|
|
145
133
|
|
|
146
134
|
if options[:tsvector_column]
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
"#{quoted_table_name}.#{column_name}"
|
|
135
|
+
Array.wrap(options[:tsvector_column]).each do |tsvector_column|
|
|
136
|
+
terms << Arel::Attributes::Attribute.new(
|
|
137
|
+
model.arel_table,
|
|
138
|
+
tsvector_column.to_s
|
|
139
|
+
)
|
|
153
140
|
end
|
|
154
141
|
end
|
|
155
142
|
|
|
156
|
-
|
|
143
|
+
terms.inject { |memo, t| Arel::Nodes::InfixOperation.new("||", memo, t) } || Arel.sql("")
|
|
157
144
|
end
|
|
158
145
|
|
|
159
146
|
# From http://www.postgresql.org/docs/8.3/static/textsearch-controls.html
|
|
@@ -171,20 +158,16 @@ module PgSearch
|
|
|
171
158
|
|
|
172
159
|
def tsearch_rank
|
|
173
160
|
Arel::Nodes::NamedFunction.new("ts_rank", [
|
|
174
|
-
|
|
175
|
-
|
|
161
|
+
Arel::Nodes::Grouping.new(tsdocument),
|
|
162
|
+
Arel::Nodes::Grouping.new(tsquery),
|
|
176
163
|
normalization
|
|
177
|
-
])
|
|
164
|
+
])
|
|
178
165
|
end
|
|
179
166
|
|
|
180
167
|
def dictionary
|
|
181
168
|
Arel::Nodes.build_quoted(options[:dictionary] || :simple)
|
|
182
169
|
end
|
|
183
170
|
|
|
184
|
-
def arel_wrap(sql_string)
|
|
185
|
-
Arel::Nodes::Grouping.new(Arel.sql(sql_string))
|
|
186
|
-
end
|
|
187
|
-
|
|
188
171
|
def columns_to_use
|
|
189
172
|
if options[:tsvector_column]
|
|
190
173
|
columns.select { |c| c.is_a?(PgSearch::Configuration::ForeignColumn) }
|
|
@@ -196,13 +179,16 @@ module PgSearch
|
|
|
196
179
|
def column_to_tsvector(search_column)
|
|
197
180
|
tsvector = Arel::Nodes::NamedFunction.new(
|
|
198
181
|
"to_tsvector",
|
|
199
|
-
[dictionary,
|
|
200
|
-
)
|
|
182
|
+
[dictionary, normalize(search_column.to_arel)]
|
|
183
|
+
)
|
|
201
184
|
|
|
202
185
|
if search_column.weight.nil?
|
|
203
186
|
tsvector
|
|
204
187
|
else
|
|
205
|
-
|
|
188
|
+
Arel::Nodes::NamedFunction.new(
|
|
189
|
+
"setweight",
|
|
190
|
+
[tsvector, Arel::Nodes.build_quoted(search_column.weight)]
|
|
191
|
+
)
|
|
206
192
|
end
|
|
207
193
|
end
|
|
208
194
|
end
|
data/lib/pg_search/features.rb
CHANGED
|
@@ -7,6 +7,8 @@ require "pg_search/features/trigram"
|
|
|
7
7
|
require "pg_search/features/tsearch"
|
|
8
8
|
|
|
9
9
|
module PgSearch
|
|
10
|
+
# Search features provide conditions and rank as composable Arel expressions,
|
|
11
|
+
# not SQL Strings. TSearch also provides a highlight expression for selection.
|
|
10
12
|
module Features
|
|
11
13
|
end
|
|
12
14
|
end
|
|
@@ -28,11 +28,7 @@ module PgSearch
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def migration_version
|
|
31
|
-
|
|
32
|
-
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
|
33
|
-
else
|
|
34
|
-
""
|
|
35
|
-
end
|
|
31
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
|
36
32
|
end
|
|
37
33
|
end
|
|
38
34
|
end
|
data/lib/pg_search/model.rb
CHANGED
|
@@ -23,13 +23,14 @@ module PgSearch
|
|
|
23
23
|
|
|
24
24
|
def multisearchable(options = {})
|
|
25
25
|
include PgSearch::Multisearchable
|
|
26
|
+
|
|
26
27
|
class_attribute :pg_search_multisearchable_options
|
|
27
28
|
self.pg_search_multisearchable_options = options
|
|
28
29
|
end
|
|
29
30
|
end
|
|
30
31
|
|
|
31
|
-
def method_missing(
|
|
32
|
-
case
|
|
32
|
+
def method_missing(method, *, **)
|
|
33
|
+
case method
|
|
33
34
|
when :pg_search_rank
|
|
34
35
|
raise PgSearchRankNotSelected unless respond_to?(:pg_search_rank)
|
|
35
36
|
|
|
@@ -43,12 +44,9 @@ module PgSearch
|
|
|
43
44
|
end
|
|
44
45
|
end
|
|
45
46
|
|
|
46
|
-
def respond_to_missing?(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
attributes.key?(:pg_search_rank)
|
|
50
|
-
when :pg_search_highlight
|
|
51
|
-
attributes.key?(:pg_search_highlight)
|
|
47
|
+
def respond_to_missing?(method, *, **)
|
|
48
|
+
if method.in?(%i[pg_search_rank pg_search_highlight])
|
|
49
|
+
attributes.key?(method)
|
|
52
50
|
else
|
|
53
51
|
super
|
|
54
52
|
end
|
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
module PgSearch
|
|
4
4
|
module Multisearch
|
|
5
5
|
class Rebuilder
|
|
6
|
+
# Arguments
|
|
7
|
+
#
|
|
8
|
+
# +model+:: Active Record model configured for multisearch.
|
|
9
|
+
# +time_source+:: Clock returning a timestamp for bulk inserts. Defaults to
|
|
10
|
+
# +Time.method(:now)+ and is called once per bulk rebuild so
|
|
11
|
+
# +created_at+ and +updated_at+ share a timestamp.
|
|
12
|
+
#
|
|
13
|
+
# Raises
|
|
14
|
+
#
|
|
15
|
+
# ModelNotMultisearchable:: If +model+ is not configured for multisearch.
|
|
6
16
|
def initialize(model, time_source = Time.method(:now))
|
|
7
17
|
raise ModelNotMultisearchable, model unless model.respond_to?(:pg_search_multisearchable_options)
|
|
8
18
|
|
|
@@ -10,6 +20,10 @@ module PgSearch
|
|
|
10
20
|
@time_source = time_source
|
|
11
21
|
end
|
|
12
22
|
|
|
23
|
+
# Populates search documents without first clearing existing documents.
|
|
24
|
+
# Uses the model's rebuild_pg_search_documents override when available;
|
|
25
|
+
# otherwise updates records individually for conditions, dynamic content,
|
|
26
|
+
# or additional attributes, and bulk-inserts for plain column content.
|
|
13
27
|
def rebuild
|
|
14
28
|
if model.respond_to?(:rebuild_pg_search_documents)
|
|
15
29
|
model.rebuild_pg_search_documents
|
|
@@ -25,7 +39,8 @@ module PgSearch
|
|
|
25
39
|
attr_reader :model
|
|
26
40
|
|
|
27
41
|
def conditional?
|
|
28
|
-
model.pg_search_multisearchable_options.key?(:if) ||
|
|
42
|
+
model.pg_search_multisearchable_options.key?(:if) ||
|
|
43
|
+
model.pg_search_multisearchable_options.key?(:unless)
|
|
29
44
|
end
|
|
30
45
|
|
|
31
46
|
def dynamic?
|
|
@@ -37,76 +52,66 @@ module PgSearch
|
|
|
37
52
|
model.pg_search_multisearchable_options.key?(:additional_attributes)
|
|
38
53
|
end
|
|
39
54
|
|
|
40
|
-
def connection
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
55
|
+
def connection = model.connection
|
|
56
|
+
|
|
57
|
+
def rebuild_sql = connection.to_sql(insert_manager.ast)
|
|
58
|
+
|
|
59
|
+
def insert_manager
|
|
60
|
+
time = @time_source.call
|
|
61
|
+
quoted_time = Arel::Nodes.build_quoted(time)
|
|
62
|
+
src = model.arel_table
|
|
63
|
+
doc = PgSearch::Document.arel_table
|
|
64
|
+
|
|
65
|
+
sel = Arel::SelectManager.new(src)
|
|
66
|
+
sel.project(
|
|
67
|
+
Arel::Nodes.build_quoted(model.base_class.name).as("searchable_type"),
|
|
68
|
+
src[model.primary_key].as("searchable_id"),
|
|
69
|
+
content_expression.as("content"),
|
|
70
|
+
quoted_time.as("created_at"),
|
|
71
|
+
quoted_time.as("updated_at")
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
apply_sti_condition(sel, src)
|
|
75
|
+
|
|
76
|
+
mgr = Arel::InsertManager.new
|
|
77
|
+
mgr.into(doc)
|
|
78
|
+
mgr.columns.concat([
|
|
79
|
+
doc[:searchable_type],
|
|
80
|
+
doc[:searchable_id],
|
|
81
|
+
doc[:content],
|
|
82
|
+
doc[:created_at],
|
|
83
|
+
doc[:updated_at]
|
|
84
|
+
])
|
|
85
|
+
mgr.select(sel.ast)
|
|
86
|
+
mgr
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def content_expression
|
|
90
|
+
expressions = columns.map { |column| Configuration::Column.new(column, nil, model).to_arel }
|
|
91
|
+
expressions.inject do |document, expression|
|
|
92
|
+
Arel::Nodes::InfixOperation.new(
|
|
93
|
+
"||",
|
|
94
|
+
Arel::Nodes::InfixOperation.new("||", document, Arel::Nodes.build_quoted(" ")),
|
|
95
|
+
expression
|
|
96
|
+
)
|
|
65
97
|
end
|
|
66
98
|
end
|
|
67
99
|
|
|
68
|
-
def
|
|
69
|
-
clause = ""
|
|
70
|
-
if model.column_names.include? model.inheritance_column
|
|
71
|
-
clause = "WHERE"
|
|
72
|
-
clause = "#{clause} #{model.inheritance_column} IS NULL OR" if model.base_class == model
|
|
73
|
-
clause = "#{clause} #{model.inheritance_column} = #{model_name}"
|
|
74
|
-
end
|
|
75
|
-
clause
|
|
76
|
-
end
|
|
100
|
+
def columns = Array(model.pg_search_multisearchable_options[:against])
|
|
77
101
|
|
|
78
|
-
def
|
|
79
|
-
|
|
80
|
-
end
|
|
102
|
+
def apply_sti_condition(select_manager, src_table)
|
|
103
|
+
return unless model.column_names.include?(model.inheritance_column)
|
|
81
104
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
%{coalesce(:model_table.#{connection.quote_column_name(column)}::text, '')}
|
|
85
|
-
end.join(" || ' ' || ")
|
|
86
|
-
end
|
|
105
|
+
inheritance_col = src_table[model.inheritance_column]
|
|
106
|
+
type_match = inheritance_col.eq(model.name)
|
|
87
107
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
connection.quote(model.name)
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def base_model_name
|
|
97
|
-
connection.quote(model.base_class.name)
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def model_table
|
|
101
|
-
model.quoted_table_name
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def documents_table
|
|
105
|
-
PgSearch::Document.quoted_table_name
|
|
106
|
-
end
|
|
108
|
+
condition = if model.base_class == model
|
|
109
|
+
inheritance_col.eq(nil).or(type_match)
|
|
110
|
+
else
|
|
111
|
+
type_match
|
|
112
|
+
end
|
|
107
113
|
|
|
108
|
-
|
|
109
|
-
connection.quote(connection.quoted_date(@time_source.call))
|
|
114
|
+
select_manager.where(condition)
|
|
110
115
|
end
|
|
111
116
|
end
|
|
112
117
|
end
|
data/lib/pg_search/normalizer.rb
CHANGED
|
@@ -2,28 +2,57 @@
|
|
|
2
2
|
|
|
3
3
|
module PgSearch
|
|
4
4
|
class Normalizer
|
|
5
|
+
# Arguments
|
|
6
|
+
#
|
|
7
|
+
# +config+:: Search configuration whose ignore list controls accent
|
|
8
|
+
# normalization.
|
|
5
9
|
def initialize(config)
|
|
6
10
|
@config = config
|
|
7
11
|
end
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
return sql_expression unless config.ignore.include?(:accents)
|
|
13
|
+
DISALLOWED_CHARACTERS = "['?\\:]"
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
# Applies configured accent normalization, preserving the input expression
|
|
16
|
+
# without an unaccent wrapper when accents are not ignored.
|
|
17
|
+
#
|
|
18
|
+
# Arguments
|
|
19
|
+
#
|
|
20
|
+
# +expression+:: An Arel node, attribute, or trusted SQL String. A String is
|
|
21
|
+
# an SQL expression, not a value to quote.
|
|
22
|
+
#
|
|
23
|
+
# Returns a composable Arel expression.
|
|
24
|
+
#
|
|
25
|
+
# Raises
|
|
26
|
+
#
|
|
27
|
+
# TypeError:: If +expression+ is not an Arel node, attribute, or String.
|
|
28
|
+
def add_normalization(expression)
|
|
29
|
+
node = to_node(expression)
|
|
30
|
+
return node unless config.ignore.include?(:accents)
|
|
18
31
|
|
|
19
32
|
Arel::Nodes::NamedFunction.new(
|
|
20
|
-
|
|
21
|
-
[
|
|
22
|
-
|
|
33
|
+
"regexp_replace",
|
|
34
|
+
[
|
|
35
|
+
Arel::Nodes::NamedFunction.new(PgSearch.unaccent_function, [node]),
|
|
36
|
+
Arel::Nodes.build_quoted(DISALLOWED_CHARACTERS),
|
|
37
|
+
Arel::Nodes.build_quoted(""),
|
|
38
|
+
Arel::Nodes.build_quoted("g")
|
|
39
|
+
]
|
|
40
|
+
)
|
|
23
41
|
end
|
|
24
42
|
|
|
25
43
|
private
|
|
26
44
|
|
|
27
45
|
attr_reader :config
|
|
46
|
+
|
|
47
|
+
def to_node(expression)
|
|
48
|
+
case expression
|
|
49
|
+
when Arel::Nodes::Node, Arel::Attributes::Attribute
|
|
50
|
+
expression
|
|
51
|
+
when String
|
|
52
|
+
Arel.sql(expression)
|
|
53
|
+
else
|
|
54
|
+
raise TypeError
|
|
55
|
+
end
|
|
56
|
+
end
|
|
28
57
|
end
|
|
29
58
|
end
|