full_search 0.2.0 → 0.3.1
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/README.md +22 -4
- data/lib/full_search/callbacks.rb +26 -8
- data/lib/full_search/config.rb +0 -1
- data/lib/full_search/distance.rb +33 -0
- data/lib/full_search/dsl.rb +26 -12
- data/lib/full_search/errors.rb +2 -0
- data/lib/full_search/exact_match.rb +7 -13
- data/lib/full_search/highlighter.rb +166 -102
- data/lib/full_search/index.rb +107 -61
- data/lib/full_search/model.rb +2 -1
- data/lib/full_search/multi_search.rb +4 -1
- data/lib/full_search/query_parser.rb +101 -88
- data/lib/full_search/quoting.rb +11 -0
- data/lib/full_search/schema_dumper_patch.rb +45 -0
- data/lib/full_search/search.rb +97 -77
- data/lib/full_search/soft_delete.rb +15 -2
- data/lib/full_search/typo.rb +10 -8
- data/lib/full_search/version.rb +1 -1
- data/lib/full_search.rb +12 -1
- data/lib/tasks/full_search.rake +12 -4
- metadata +46 -17
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FullSearch
|
|
4
|
+
module VirtualTablesPatch
|
|
5
|
+
def virtual_tables
|
|
6
|
+
super.reject { |_, options| options.first.nil? }
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module SchemaDumperPatch
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def virtual_tables(stream)
|
|
14
|
+
virtual_tables = @connection.virtual_tables.reject { |name, _| ignored?(name) }
|
|
15
|
+
|
|
16
|
+
if virtual_tables.any?
|
|
17
|
+
stream.puts
|
|
18
|
+
stream.puts " # Virtual tables defined in this database."
|
|
19
|
+
stream.puts " # Note that virtual tables may not work with other database engines. Be careful if changing database."
|
|
20
|
+
virtual_tables.sort.each do |table_name, options|
|
|
21
|
+
module_name, arguments = options
|
|
22
|
+
next if module_name.nil? || arguments.nil?
|
|
23
|
+
|
|
24
|
+
stream.puts " create_virtual_table #{table_name.inspect}, #{module_name.inspect}, #{arguments.split(", ").inspect}"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
ActiveSupport.on_load(:active_record) do
|
|
32
|
+
begin
|
|
33
|
+
require "active_record/connection_adapters/sqlite3_adapter"
|
|
34
|
+
rescue LoadError
|
|
35
|
+
next
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
if defined?(ActiveRecord::ConnectionAdapters::SQLite3Adapter)
|
|
39
|
+
ActiveRecord::ConnectionAdapters::SQLite3Adapter.prepend(FullSearch::VirtualTablesPatch)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
if defined?(ActiveRecord::ConnectionAdapters::SQLite3::SchemaDumper)
|
|
43
|
+
ActiveRecord::ConnectionAdapters::SQLite3::SchemaDumper.prepend(FullSearch::SchemaDumperPatch)
|
|
44
|
+
end
|
|
45
|
+
end
|
data/lib/full_search/search.rb
CHANGED
|
@@ -16,14 +16,20 @@ module FullSearch
|
|
|
16
16
|
@matching_strategy = matching_strategy
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
MIN_TERM_LENGTH = 3
|
|
20
|
+
|
|
19
21
|
def relation
|
|
22
|
+
validate_filter_keys!
|
|
20
23
|
validate_required_filters!
|
|
24
|
+
check_stale_config!
|
|
25
|
+
|
|
26
|
+
return model.none if dsl.tokenize == "trigram" && query.length < MIN_TERM_LENGTH && !dsl.typo_tolerance?
|
|
21
27
|
|
|
22
28
|
parsed = QueryParser.parse(query)
|
|
23
29
|
exact_ids = ExactMatch.ids_for(model, query, filters)
|
|
24
30
|
primary_ids = fts_match_ids(parsed)
|
|
25
|
-
fallback_ids = dsl.typo_tolerance? && matching_strategy != "all" ? trigram_match_ids(parsed, primary_ids) : []
|
|
26
|
-
fuzzy_ids = dsl.typo_tolerance? && matching_strategy != "all" && primary_ids.empty? && fallback_ids.empty? ? fuzzy_match_ids(parsed) : []
|
|
31
|
+
fallback_ids = (dsl.typo_tolerance? && matching_strategy != "all") ? trigram_match_ids(parsed, primary_ids) : []
|
|
32
|
+
fuzzy_ids = (dsl.typo_tolerance? && matching_strategy != "all" && primary_ids.empty? && fallback_ids.empty?) ? fuzzy_match_ids(parsed) : []
|
|
27
33
|
|
|
28
34
|
all_ids = (exact_ids + primary_ids + fallback_ids + fuzzy_ids).uniq
|
|
29
35
|
return model.none if all_ids.empty?
|
|
@@ -50,6 +56,15 @@ module FullSearch
|
|
|
50
56
|
model.full_search_dsl
|
|
51
57
|
end
|
|
52
58
|
|
|
59
|
+
def validate_filter_keys!
|
|
60
|
+
allowed = dsl.filters.map(&:name).to_set
|
|
61
|
+
filters.each_key do |key|
|
|
62
|
+
unless allowed.include?(key.to_s)
|
|
63
|
+
raise UnknownFilterError, "Unknown filter: #{key}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
53
68
|
def validate_required_filters!
|
|
54
69
|
dsl.filters.each do |filter|
|
|
55
70
|
next unless filter.required
|
|
@@ -57,21 +72,36 @@ module FullSearch
|
|
|
57
72
|
end
|
|
58
73
|
end
|
|
59
74
|
|
|
75
|
+
def check_stale_config!
|
|
76
|
+
stored = FullSearch::Index.stored_config_hash(model)
|
|
77
|
+
return unless stored
|
|
78
|
+
|
|
79
|
+
if stored != dsl.config_hash
|
|
80
|
+
case FullSearch.config.stale_query_behavior
|
|
81
|
+
when :raise
|
|
82
|
+
raise ConfigChangedError, "FTS index for #{model.table_name} is stale; run full_search:rebuild"
|
|
83
|
+
when :log_and_fallback
|
|
84
|
+
Rails.logger.warn("[full_search] FTS index for #{model.table_name} is stale; results may be incomplete")
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
60
89
|
def fts_match_ids(parsed)
|
|
61
90
|
return [] if query.empty?
|
|
62
91
|
|
|
63
92
|
match_expr = QueryParser.to_match_expression(parsed)
|
|
64
|
-
fts_table = FullSearch::Index.fts_table_name(model)
|
|
93
|
+
fts_table = qt(FullSearch::Index.fts_table_name(model))
|
|
94
|
+
tbl = qt(model.table_name)
|
|
65
95
|
|
|
66
96
|
sql = <<~SQL
|
|
67
|
-
SELECT #{
|
|
97
|
+
SELECT #{tbl}.id
|
|
68
98
|
FROM #{fts_table}
|
|
69
|
-
JOIN #{
|
|
70
|
-
WHERE #{fts_table} MATCH #{
|
|
99
|
+
JOIN #{tbl} ON #{tbl}.id = #{fts_table}.rowid
|
|
100
|
+
WHERE #{fts_table} MATCH #{q(match_expr)}
|
|
71
101
|
SQL
|
|
72
102
|
|
|
73
103
|
filter_conditions = filters.map do |name, value|
|
|
74
|
-
"AND #{fts_table}.#{name} = #{
|
|
104
|
+
"AND #{fts_table}.#{qc(name)} = #{q(value)}"
|
|
75
105
|
end.join(" ")
|
|
76
106
|
|
|
77
107
|
connection.execute("#{sql} #{filter_conditions}").map { |r| r["id"] }
|
|
@@ -83,24 +113,25 @@ module FullSearch
|
|
|
83
113
|
match_expr = QueryParser.to_match_expression(parsed)
|
|
84
114
|
return [] if match_expr.empty?
|
|
85
115
|
|
|
86
|
-
term = parsed.last
|
|
116
|
+
term = parsed.last
|
|
87
117
|
return [] if term.nil?
|
|
88
118
|
|
|
89
119
|
if term.length < dsl.typo_tolerance_min_term_length.to_i
|
|
90
120
|
return like_prefix_ids(term)
|
|
91
121
|
end
|
|
92
122
|
|
|
93
|
-
trigram_table = FullSearch::Index.trigram_table_name(model)
|
|
123
|
+
trigram_table = qt(FullSearch::Index.trigram_table_name(model))
|
|
124
|
+
tbl = qt(model.table_name)
|
|
94
125
|
|
|
95
126
|
sql = <<~SQL
|
|
96
|
-
SELECT #{
|
|
127
|
+
SELECT #{tbl}.id
|
|
97
128
|
FROM #{trigram_table}
|
|
98
|
-
JOIN #{
|
|
99
|
-
WHERE #{trigram_table} MATCH #{
|
|
129
|
+
JOIN #{tbl} ON #{tbl}.id = #{trigram_table}.rowid
|
|
130
|
+
WHERE #{trigram_table} MATCH #{q(match_expr)}
|
|
100
131
|
SQL
|
|
101
132
|
|
|
102
133
|
filter_conditions = filters.map do |name, value|
|
|
103
|
-
"AND #{trigram_table}.#{name} = #{
|
|
134
|
+
"AND #{trigram_table}.#{qc(name)} = #{q(value)}"
|
|
104
135
|
end.join(" ")
|
|
105
136
|
|
|
106
137
|
connection.execute("#{sql} #{filter_conditions}").map { |r| r["id"] }
|
|
@@ -109,53 +140,59 @@ module FullSearch
|
|
|
109
140
|
def like_prefix_ids(term)
|
|
110
141
|
column_fields = dsl.fields.select { |f| f.source.nil? }
|
|
111
142
|
source_fields = dsl.fields.select { |f| f.source }
|
|
143
|
+
tbl = qt(model.table_name)
|
|
144
|
+
|
|
145
|
+
soft_delete_clause = ""
|
|
146
|
+
if dsl.soft_delete_column && !include_soft_deleted
|
|
147
|
+
soft_delete_clause = "AND #{tbl}.#{qc(dsl.soft_delete_column)} IS NULL"
|
|
148
|
+
end
|
|
112
149
|
|
|
113
150
|
ids = []
|
|
114
151
|
|
|
115
152
|
if column_fields.any?
|
|
116
153
|
like_conditions = column_fields.map do |field|
|
|
117
|
-
"#{
|
|
154
|
+
"#{tbl}.#{qc(field.name)} LIKE #{q("#{term}%")}"
|
|
118
155
|
end.join(" OR ")
|
|
119
156
|
|
|
120
|
-
sql = <<~SQL
|
|
121
|
-
SELECT #{model.table_name}.id
|
|
122
|
-
FROM #{model.table_name}
|
|
123
|
-
WHERE (#{like_conditions})
|
|
124
|
-
SQL
|
|
125
|
-
|
|
126
157
|
filter_conditions = filters.map do |name, value|
|
|
127
|
-
"AND #{
|
|
158
|
+
"AND #{tbl}.#{qc(name)} = #{q(value)}"
|
|
128
159
|
end.join(" ")
|
|
129
160
|
|
|
130
|
-
|
|
161
|
+
sql = <<~SQL
|
|
162
|
+
SELECT #{tbl}.id
|
|
163
|
+
FROM #{tbl}
|
|
164
|
+
WHERE (#{like_conditions}) #{filter_conditions} #{soft_delete_clause}
|
|
165
|
+
SQL
|
|
166
|
+
|
|
167
|
+
ids = connection.execute(sql).map { |r| r["id"] }
|
|
131
168
|
return ids if ids.any?
|
|
132
169
|
end
|
|
133
170
|
|
|
134
171
|
if source_fields.any?
|
|
135
|
-
fts_table = FullSearch::Index.fts_table_name(model)
|
|
172
|
+
fts_table = qt(FullSearch::Index.fts_table_name(model))
|
|
136
173
|
like_conditions = source_fields.map do |field|
|
|
137
|
-
"#{fts_table}.#{field.name} LIKE #{
|
|
174
|
+
"#{fts_table}.#{qc(field.name)} LIKE #{q("#{term}%")}"
|
|
138
175
|
end.join(" OR ")
|
|
139
176
|
|
|
177
|
+
filter_conditions = filters.map do |name, value|
|
|
178
|
+
"AND #{fts_table}.#{qc(name)} = #{q(value)}"
|
|
179
|
+
end.join(" ")
|
|
180
|
+
|
|
140
181
|
sql = <<~SQL
|
|
141
|
-
SELECT #{
|
|
182
|
+
SELECT #{tbl}.id
|
|
142
183
|
FROM #{fts_table}
|
|
143
|
-
JOIN #{
|
|
144
|
-
WHERE (#{like_conditions})
|
|
184
|
+
JOIN #{tbl} ON #{tbl}.id = #{fts_table}.rowid
|
|
185
|
+
WHERE (#{like_conditions}) #{filter_conditions} #{soft_delete_clause}
|
|
145
186
|
SQL
|
|
146
187
|
|
|
147
|
-
|
|
148
|
-
"AND #{fts_table}.#{name} = #{connection.quote(value)}"
|
|
149
|
-
end.join(" ")
|
|
150
|
-
|
|
151
|
-
ids = connection.execute("#{sql} #{filter_conditions}").map { |r| r["id"] }
|
|
188
|
+
ids = connection.execute(sql).map { |r| r["id"] }
|
|
152
189
|
end
|
|
153
190
|
|
|
154
191
|
ids
|
|
155
192
|
end
|
|
156
193
|
|
|
157
194
|
def fuzzy_match_ids(parsed)
|
|
158
|
-
term = parsed.last
|
|
195
|
+
term = parsed.last
|
|
159
196
|
return [] if term.nil?
|
|
160
197
|
|
|
161
198
|
term_str = term.is_a?(Array) ? extract_last_term_string(term) : term.to_s
|
|
@@ -168,22 +205,28 @@ module FullSearch
|
|
|
168
205
|
return [] if column_fields.empty?
|
|
169
206
|
|
|
170
207
|
register_levenshtein!
|
|
208
|
+
tbl = qt(model.table_name)
|
|
209
|
+
|
|
210
|
+
soft_delete_clause = ""
|
|
211
|
+
if dsl.soft_delete_column && !include_soft_deleted
|
|
212
|
+
soft_delete_clause = "AND #{tbl}.#{qc(dsl.soft_delete_column)} IS NULL"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
filter_conditions = filters.map do |name, value|
|
|
216
|
+
"AND #{tbl}.#{qc(name)} = #{q(value)}"
|
|
217
|
+
end.join(" ")
|
|
171
218
|
|
|
172
219
|
conditions = column_fields.map do |field|
|
|
173
|
-
"levenshtein(LOWER(#{
|
|
220
|
+
"levenshtein(LOWER(#{tbl}.#{qc(field.name)}), #{q(term_str.downcase)}) <= #{max_typos}"
|
|
174
221
|
end.join(" OR ")
|
|
175
222
|
|
|
176
223
|
sql = <<~SQL
|
|
177
|
-
SELECT #{
|
|
178
|
-
FROM #{
|
|
179
|
-
WHERE (#{conditions})
|
|
224
|
+
SELECT #{tbl}.id
|
|
225
|
+
FROM #{tbl}
|
|
226
|
+
WHERE (#{conditions}) #{filter_conditions} #{soft_delete_clause}
|
|
180
227
|
SQL
|
|
181
228
|
|
|
182
|
-
|
|
183
|
-
"AND #{model.table_name}.#{name} = #{connection.quote(value)}"
|
|
184
|
-
end.join(" ")
|
|
185
|
-
|
|
186
|
-
connection.execute("#{sql} #{filter_conditions}").map { |r| r["id"] }
|
|
229
|
+
connection.execute(sql).map { |r| r["id"] }
|
|
187
230
|
end
|
|
188
231
|
|
|
189
232
|
def max_allowed_typos(length)
|
|
@@ -203,65 +246,38 @@ module FullSearch
|
|
|
203
246
|
|
|
204
247
|
raw = connection.raw_connection
|
|
205
248
|
raw.create_function("levenshtein", 2) do |func, s1, s2|
|
|
206
|
-
func.result = damerau_levenshtein(s1.to_s, s2.to_s)
|
|
249
|
+
func.result = Distance.damerau_levenshtein(s1.to_s, s2.to_s)
|
|
207
250
|
end
|
|
208
251
|
@levenshtein_registered = true
|
|
209
252
|
end
|
|
210
253
|
|
|
211
|
-
def damerau_levenshtein(a, b)
|
|
212
|
-
a_len = a.length
|
|
213
|
-
b_len = b.length
|
|
214
|
-
return a_len if b_len == 0
|
|
215
|
-
return b_len if a_len == 0
|
|
216
|
-
|
|
217
|
-
d = Array.new(a_len + 1) { Array.new(b_len + 1, 0) }
|
|
218
|
-
(0..a_len).each { |i| d[i][0] = i }
|
|
219
|
-
(0..b_len).each { |j| d[0][j] = j }
|
|
220
|
-
|
|
221
|
-
(1..a_len).each do |i|
|
|
222
|
-
(1..b_len).each do |j|
|
|
223
|
-
cost = a[i - 1] == b[j - 1] ? 0 : 1
|
|
224
|
-
d[i][j] = [
|
|
225
|
-
d[i - 1][j] + 1,
|
|
226
|
-
d[i][j - 1] + 1,
|
|
227
|
-
d[i - 1][j - 1] + cost
|
|
228
|
-
].min
|
|
229
|
-
|
|
230
|
-
if i > 1 && j > 1 && a[i - 1] == b[j - 2] && a[i - 2] == b[j - 1]
|
|
231
|
-
d[i][j] = [d[i][j], d[i - 2][j - 2] + 1].min
|
|
232
|
-
end
|
|
233
|
-
end
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
d[a_len][b_len]
|
|
237
|
-
end
|
|
238
|
-
|
|
239
254
|
def apply_ranking(rel, all_ids, exact_ids)
|
|
240
255
|
return rel if all_ids.empty?
|
|
241
256
|
|
|
242
257
|
order_parts = []
|
|
258
|
+
tbl = qt(model.table_name)
|
|
243
259
|
|
|
244
260
|
if exact_ids.any?
|
|
245
|
-
order_parts << "CASE #{
|
|
261
|
+
order_parts << "CASE #{tbl}.id #{exact_ids.map { |id| "WHEN #{q(id)} THEN 0" }.join(" ")} ELSE 1 END"
|
|
246
262
|
end
|
|
247
263
|
|
|
248
|
-
fts_table = FullSearch::Index.fts_table_name(model)
|
|
264
|
+
fts_table = qt(FullSearch::Index.fts_table_name(model))
|
|
249
265
|
match_expr = QueryParser.to_match_expression(QueryParser.parse(query))
|
|
250
266
|
|
|
251
267
|
rank_subquery = <<~SQL
|
|
252
268
|
SELECT rowid, rank
|
|
253
269
|
FROM #{fts_table}
|
|
254
|
-
WHERE #{fts_table} MATCH #{
|
|
270
|
+
WHERE #{fts_table} MATCH #{q(match_expr)}
|
|
255
271
|
SQL
|
|
256
272
|
|
|
257
273
|
rel = rel
|
|
258
|
-
.select("#{
|
|
259
|
-
.joins("LEFT JOIN (#{rank_subquery}) AS fts_rank ON fts_rank.rowid = #{
|
|
274
|
+
.select("#{tbl}.*, fts_rank.rank AS full_search_rank")
|
|
275
|
+
.joins("LEFT JOIN (#{rank_subquery}) AS fts_rank ON fts_rank.rowid = #{tbl}.id")
|
|
260
276
|
|
|
261
277
|
order_parts << "COALESCE(fts_rank.rank, 1)"
|
|
262
278
|
|
|
263
279
|
dsl.rank_bys.each do |rank_by|
|
|
264
|
-
col = "#{
|
|
280
|
+
col = "#{tbl}.#{qc(rank_by.column)}"
|
|
265
281
|
order_parts << "#{col} #{rank_by.direction.to_s.upcase} NULLS LAST"
|
|
266
282
|
end
|
|
267
283
|
|
|
@@ -269,7 +285,11 @@ module FullSearch
|
|
|
269
285
|
end
|
|
270
286
|
|
|
271
287
|
def connection
|
|
288
|
+
model.connection
|
|
289
|
+
rescue NoMethodError
|
|
272
290
|
ActiveRecord::Base.connection
|
|
273
291
|
end
|
|
292
|
+
|
|
293
|
+
include Quoting
|
|
274
294
|
end
|
|
275
295
|
end
|
|
@@ -2,11 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
module FullSearch
|
|
4
4
|
module SoftDelete
|
|
5
|
-
def self.
|
|
5
|
+
def self.active_update_clause(model)
|
|
6
6
|
dsl = model.full_search_dsl
|
|
7
7
|
return nil unless dsl&.soft_delete_column
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
col = model.connection.quote_column_name(dsl.soft_delete_column)
|
|
10
|
+
"WHEN new.#{col} IS NULL"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.soft_delete_remove_clause(model)
|
|
14
|
+
dsl = model.full_search_dsl
|
|
15
|
+
return nil unless dsl&.soft_delete_column
|
|
16
|
+
|
|
17
|
+
col = model.connection.quote_column_name(dsl.soft_delete_column)
|
|
18
|
+
"WHEN old.#{col} IS NULL AND new.#{col} IS NOT NULL"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.delete_transition_sql(model)
|
|
22
|
+
active_update_clause(model)
|
|
10
23
|
end
|
|
11
24
|
end
|
|
12
25
|
end
|
data/lib/full_search/typo.rb
CHANGED
|
@@ -5,23 +5,25 @@ module FullSearch
|
|
|
5
5
|
MIN_SQLITE_VERSION = [3, 34].freeze
|
|
6
6
|
|
|
7
7
|
class << self
|
|
8
|
-
def supported?
|
|
9
|
-
|
|
8
|
+
def supported?(model = nil)
|
|
9
|
+
conn = model&.connection || ActiveRecord::Base.connection
|
|
10
|
+
major, minor = sqlite_version_parts(conn)
|
|
10
11
|
major > MIN_SQLITE_VERSION.first || (major == MIN_SQLITE_VERSION.first && minor >= MIN_SQLITE_VERSION.last)
|
|
11
12
|
end
|
|
12
13
|
|
|
13
|
-
def warn_unsupported!
|
|
14
|
-
|
|
14
|
+
def warn_unsupported!(model = nil)
|
|
15
|
+
version = sqlite_version(model&.connection || ActiveRecord::Base.connection)
|
|
16
|
+
warn "[full_search] SQLite #{version} does not support the trigram tokenizer. typo_tolerance requires SQLite >= 3.34."
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
private
|
|
18
20
|
|
|
19
|
-
def sqlite_version_parts
|
|
20
|
-
sqlite_version.split(".").first(2).map(&:to_i)
|
|
21
|
+
def sqlite_version_parts(conn)
|
|
22
|
+
sqlite_version(conn).split(".").first(2).map(&:to_i)
|
|
21
23
|
end
|
|
22
24
|
|
|
23
|
-
def sqlite_version
|
|
24
|
-
|
|
25
|
+
def sqlite_version(conn)
|
|
26
|
+
conn.execute("SELECT sqlite_version() AS v").first["v"]
|
|
25
27
|
end
|
|
26
28
|
end
|
|
27
29
|
end
|
data/lib/full_search/version.rb
CHANGED
data/lib/full_search.rb
CHANGED
|
@@ -6,6 +6,8 @@ require "active_support/concern"
|
|
|
6
6
|
|
|
7
7
|
require "full_search/version"
|
|
8
8
|
require "full_search/config"
|
|
9
|
+
require "full_search/distance"
|
|
10
|
+
require "full_search/quoting"
|
|
9
11
|
require "full_search/errors"
|
|
10
12
|
require "full_search/tokenizer"
|
|
11
13
|
require "full_search/dsl"
|
|
@@ -22,6 +24,7 @@ require "full_search/reindex_job"
|
|
|
22
24
|
require "full_search/optimize_job"
|
|
23
25
|
require "full_search/test_helpers"
|
|
24
26
|
require "full_search/multi_search"
|
|
27
|
+
require "full_search/schema_dumper_patch"
|
|
25
28
|
|
|
26
29
|
ActiveSupport.on_load(:active_record) do
|
|
27
30
|
include FullSearch::Model
|
|
@@ -30,7 +33,15 @@ end
|
|
|
30
33
|
module FullSearch
|
|
31
34
|
class << self
|
|
32
35
|
def models
|
|
33
|
-
@models ||=
|
|
36
|
+
@models ||= Set.new
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def register_model(model)
|
|
40
|
+
models << model
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def deregister_model(model)
|
|
44
|
+
models.delete(model)
|
|
34
45
|
end
|
|
35
46
|
|
|
36
47
|
def optimize!
|
data/lib/tasks/full_search.rake
CHANGED
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
def resolve_full_search_models(args)
|
|
4
4
|
if args[:models]
|
|
5
5
|
args[:models].split(",").map do |name|
|
|
6
|
-
klass =
|
|
6
|
+
klass = begin
|
|
7
|
+
name.singularize.camelize.constantize
|
|
8
|
+
rescue NameError, LoadError => e
|
|
9
|
+
warn "[full_search] Could not resolve model '#{name}': #{e.message}"
|
|
10
|
+
nil
|
|
11
|
+
end
|
|
7
12
|
klass || FullSearch.models.find { |m| m.table_name == name }
|
|
8
13
|
end.compact
|
|
9
14
|
else
|
|
@@ -50,14 +55,17 @@ namespace :full_search do
|
|
|
50
55
|
FullSearch.models.each do |model|
|
|
51
56
|
stored = FullSearch::Index.stored_config_hash(model)
|
|
52
57
|
current = model.full_search_dsl.config_hash
|
|
53
|
-
status = stored == current ? "ok" : "stale"
|
|
58
|
+
status = (stored == current) ? "ok" : "stale"
|
|
54
59
|
source_fields = model.full_search_dsl.fields.select(&:source).map(&:name)
|
|
55
60
|
|
|
56
61
|
drift_info = if source_fields.any?
|
|
62
|
+
conn = ActiveRecord::Base.connection
|
|
63
|
+
qc = ->(name) { conn.quote_column_name(name) }
|
|
64
|
+
qt = ->(name) { conn.quote_table_name(name) }
|
|
57
65
|
empty_count = ActiveRecord::Base.connection.execute(<<~SQL).first["c"]
|
|
58
66
|
SELECT COUNT(*) AS c
|
|
59
|
-
FROM #{FullSearch::Index.fts_table_name(model)}
|
|
60
|
-
WHERE #{source_fields.map { |c| "#{c} = ''" }.join(
|
|
67
|
+
FROM #{qt.call(FullSearch::Index.fts_table_name(model))}
|
|
68
|
+
WHERE #{source_fields.map { |c| "#{qc.call(c)} = ''" }.join(" OR ")}
|
|
61
69
|
SQL
|
|
62
70
|
" | empty source fields: #{empty_count}"
|
|
63
71
|
else
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: full_search
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ben D'Angelo
|
|
@@ -16,6 +16,9 @@ dependencies:
|
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
18
|
version: '8.0'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '9'
|
|
19
22
|
type: :runtime
|
|
20
23
|
prerelease: false
|
|
21
24
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -23,6 +26,9 @@ dependencies:
|
|
|
23
26
|
- - ">="
|
|
24
27
|
- !ruby/object:Gem::Version
|
|
25
28
|
version: '8.0'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '9'
|
|
26
32
|
- !ruby/object:Gem::Dependency
|
|
27
33
|
name: railties
|
|
28
34
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -30,6 +36,9 @@ dependencies:
|
|
|
30
36
|
- - ">="
|
|
31
37
|
- !ruby/object:Gem::Version
|
|
32
38
|
version: '8.0'
|
|
39
|
+
- - "<"
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '9'
|
|
33
42
|
type: :runtime
|
|
34
43
|
prerelease: false
|
|
35
44
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -37,6 +46,9 @@ dependencies:
|
|
|
37
46
|
- - ">="
|
|
38
47
|
- !ruby/object:Gem::Version
|
|
39
48
|
version: '8.0'
|
|
49
|
+
- - "<"
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '9'
|
|
40
52
|
- !ruby/object:Gem::Dependency
|
|
41
53
|
name: sqlite3
|
|
42
54
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -51,20 +63,6 @@ dependencies:
|
|
|
51
63
|
- - ">="
|
|
52
64
|
- !ruby/object:Gem::Version
|
|
53
65
|
version: '2.0'
|
|
54
|
-
- !ruby/object:Gem::Dependency
|
|
55
|
-
name: ostruct
|
|
56
|
-
requirement: !ruby/object:Gem::Requirement
|
|
57
|
-
requirements:
|
|
58
|
-
- - ">="
|
|
59
|
-
- !ruby/object:Gem::Version
|
|
60
|
-
version: '0.6'
|
|
61
|
-
type: :runtime
|
|
62
|
-
prerelease: false
|
|
63
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
-
requirements:
|
|
65
|
-
- - ">="
|
|
66
|
-
- !ruby/object:Gem::Version
|
|
67
|
-
version: '0.6'
|
|
68
66
|
- !ruby/object:Gem::Dependency
|
|
69
67
|
name: minitest
|
|
70
68
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -93,6 +91,20 @@ dependencies:
|
|
|
93
91
|
- - "~>"
|
|
94
92
|
- !ruby/object:Gem::Version
|
|
95
93
|
version: '13.0'
|
|
94
|
+
- !ruby/object:Gem::Dependency
|
|
95
|
+
name: ostruct
|
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
|
97
|
+
requirements:
|
|
98
|
+
- - ">="
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: '0.6'
|
|
101
|
+
type: :development
|
|
102
|
+
prerelease: false
|
|
103
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
104
|
+
requirements:
|
|
105
|
+
- - ">="
|
|
106
|
+
- !ruby/object:Gem::Version
|
|
107
|
+
version: '0.6'
|
|
96
108
|
- !ruby/object:Gem::Dependency
|
|
97
109
|
name: irb
|
|
98
110
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -107,6 +119,20 @@ dependencies:
|
|
|
107
119
|
- - ">="
|
|
108
120
|
- !ruby/object:Gem::Version
|
|
109
121
|
version: '0'
|
|
122
|
+
- !ruby/object:Gem::Dependency
|
|
123
|
+
name: standard
|
|
124
|
+
requirement: !ruby/object:Gem::Requirement
|
|
125
|
+
requirements:
|
|
126
|
+
- - "~>"
|
|
127
|
+
- !ruby/object:Gem::Version
|
|
128
|
+
version: '1.40'
|
|
129
|
+
type: :development
|
|
130
|
+
prerelease: false
|
|
131
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
132
|
+
requirements:
|
|
133
|
+
- - "~>"
|
|
134
|
+
- !ruby/object:Gem::Version
|
|
135
|
+
version: '1.40'
|
|
110
136
|
description: Declarative full-text search for Rails apps backed by SQLite FTS5
|
|
111
137
|
executables: []
|
|
112
138
|
extensions: []
|
|
@@ -116,6 +142,7 @@ files:
|
|
|
116
142
|
- lib/full_search.rb
|
|
117
143
|
- lib/full_search/callbacks.rb
|
|
118
144
|
- lib/full_search/config.rb
|
|
145
|
+
- lib/full_search/distance.rb
|
|
119
146
|
- lib/full_search/dsl.rb
|
|
120
147
|
- lib/full_search/errors.rb
|
|
121
148
|
- lib/full_search/exact_match.rb
|
|
@@ -125,8 +152,10 @@ files:
|
|
|
125
152
|
- lib/full_search/multi_search.rb
|
|
126
153
|
- lib/full_search/optimize_job.rb
|
|
127
154
|
- lib/full_search/query_parser.rb
|
|
155
|
+
- lib/full_search/quoting.rb
|
|
128
156
|
- lib/full_search/railtie.rb
|
|
129
157
|
- lib/full_search/reindex_job.rb
|
|
158
|
+
- lib/full_search/schema_dumper_patch.rb
|
|
130
159
|
- lib/full_search/search.rb
|
|
131
160
|
- lib/full_search/soft_delete.rb
|
|
132
161
|
- lib/full_search/test_helpers.rb
|
|
@@ -147,12 +176,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
147
176
|
requirements:
|
|
148
177
|
- - ">="
|
|
149
178
|
- !ruby/object:Gem::Version
|
|
150
|
-
version:
|
|
179
|
+
version: 3.1.0
|
|
151
180
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
152
181
|
requirements:
|
|
153
182
|
- - ">="
|
|
154
183
|
- !ruby/object:Gem::Version
|
|
155
|
-
version: '0'
|
|
184
|
+
version: '2.0'
|
|
156
185
|
requirements: []
|
|
157
186
|
rubygems_version: 4.0.15
|
|
158
187
|
specification_version: 4
|