full_search 0.4.5 → 0.4.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.
- checksums.yaml +4 -4
- data/lib/full_search/bulk_import.rb +13 -0
- data/lib/full_search/errors.rb +1 -0
- data/lib/full_search/index.rb +51 -22
- data/lib/full_search/multi_search.rb +31 -29
- data/lib/full_search/search.rb +15 -0
- data/lib/full_search/version.rb +1 -1
- data/lib/tasks/full_search.rake +7 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a47cd95db7244715db988cdce15e8466dc2131d3a995769071ca7b513f7b91e4
|
|
4
|
+
data.tar.gz: 35303e2f06de22ccfcd1e20018d8f1a83cd1d32bd2d883cdd7ba60c1919d3541
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1316924b824ef6814d2899ee165426b8eee7b021fc7bde0ee358f6a9066f2668561f27fb1de12f12bd0d3b836f91a2d428eb1c9da9e0db7dc494cf037ea707e5
|
|
7
|
+
data.tar.gz: 3c359e74e2898302ef51ada7f864c22c1b2dd340fb27e5e097b59baa78bae97b800d74ad020f172f202c2da85d3094925aba3fbad9cdc00dc5929fd990803dab
|
|
@@ -19,6 +19,19 @@ module FullSearch
|
|
|
19
19
|
models_in_bulk_import.delete(model)
|
|
20
20
|
FullSearch::Index.create_triggers!(model)
|
|
21
21
|
FullSearch::BackfillJob.perform_later(model.name)
|
|
22
|
+
rescue => e
|
|
23
|
+
# Triggers are still dropped at this point; the FTS index will rot on
|
|
24
|
+
# every subsequent write. Retry once, then raise loudly so the caller
|
|
25
|
+
# knows the index is compromised instead of failing silent.
|
|
26
|
+
retry_count = (Thread.current[:_fs_bulk_import_retry] ||= 0)
|
|
27
|
+
if retry_count < 1
|
|
28
|
+
Thread.current[:_fs_bulk_import_retry] = retry_count + 1
|
|
29
|
+
retry
|
|
30
|
+
end
|
|
31
|
+
raise FullSearch::TriggerRestoreError,
|
|
32
|
+
"Failed to restore FTS triggers for #{model.table_name} after bulk import: #{e.message}"
|
|
33
|
+
ensure
|
|
34
|
+
Thread.current[:_fs_bulk_import_retry] = nil
|
|
22
35
|
end
|
|
23
36
|
|
|
24
37
|
def bulk_importing?(model)
|
data/lib/full_search/errors.rb
CHANGED
data/lib/full_search/index.rb
CHANGED
|
@@ -167,8 +167,8 @@ module FullSearch
|
|
|
167
167
|
|
|
168
168
|
conn = connection
|
|
169
169
|
begin
|
|
170
|
-
conn.execute("DELETE FROM #{qt(fts_table_name(model))};")
|
|
171
|
-
conn.execute("DELETE FROM #{qt(trigram_table_name(model))};") if trigram_table_needed?(model) && trigram_table_exists?(model)
|
|
170
|
+
with_busy_retry { conn.execute("DELETE FROM #{qt(fts_table_name(model))};") }
|
|
171
|
+
with_busy_retry { conn.execute("DELETE FROM #{qt(trigram_table_name(model))};") } if trigram_table_needed?(model) && trigram_table_exists?(model)
|
|
172
172
|
rescue ActiveRecord::StatementInvalid => e
|
|
173
173
|
raise unless e.message.match?(/fts5: corruption|invalid fts5 file format|malformed database schema/i)
|
|
174
174
|
|
|
@@ -191,12 +191,12 @@ module FullSearch
|
|
|
191
191
|
# drop the shadow tables directly, then drop the (now-empty) virtual
|
|
192
192
|
# table wrapper, so the caller can recreate a clean index.
|
|
193
193
|
def drop_fts_table_safely!(table_name)
|
|
194
|
-
connection.execute("DROP TABLE IF EXISTS #{qt(table_name)};")
|
|
194
|
+
with_busy_retry { connection.execute("DROP TABLE IF EXISTS #{qt(table_name)};") }
|
|
195
195
|
rescue ActiveRecord::StatementInvalid => e
|
|
196
196
|
raise unless e.message.match?(/fts5: corruption|invalid fts5 file format|malformed database schema/i)
|
|
197
197
|
|
|
198
198
|
drop_shadow_tables!(table_name)
|
|
199
|
-
connection.execute("DROP TABLE IF EXISTS #{qt(table_name)};")
|
|
199
|
+
with_busy_retry { connection.execute("DROP TABLE IF EXISTS #{qt(table_name)};") }
|
|
200
200
|
end
|
|
201
201
|
|
|
202
202
|
# FTS5 stores its index data in a fixed set of shadow tables named
|
|
@@ -249,12 +249,14 @@ module FullSearch
|
|
|
249
249
|
def healthy?(model)
|
|
250
250
|
return false unless table_exists?(model)
|
|
251
251
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
252
|
+
begin
|
|
253
|
+
with_busy_retry { connection.execute("SELECT count(*) FROM #{qt(fts_table_name(model))}") }
|
|
254
|
+
true
|
|
255
|
+
rescue ActiveRecord::StatementInvalid => e
|
|
256
|
+
raise unless e.message.match?(/fts5: corruption|invalid fts5 file format|malformed database schema/i)
|
|
256
257
|
|
|
257
|
-
|
|
258
|
+
false
|
|
259
|
+
end
|
|
258
260
|
end
|
|
259
261
|
|
|
260
262
|
def trigram_table_needed?(model)
|
|
@@ -267,8 +269,40 @@ module FullSearch
|
|
|
267
269
|
).any?
|
|
268
270
|
end
|
|
269
271
|
|
|
272
|
+
def missing_triggers?(model)
|
|
273
|
+
return false if FullSearch.bulk_importing?(model)
|
|
274
|
+
return false unless model_table_exists?(model)
|
|
275
|
+
return false unless table_exists?(model)
|
|
276
|
+
|
|
277
|
+
IndexCache.fetch("missing_triggers:#{model.table_name}") do
|
|
278
|
+
existing = connection.execute(
|
|
279
|
+
"SELECT name FROM sqlite_master WHERE type='trigger' AND tbl_name=#{q(model.table_name)}"
|
|
280
|
+
).map { |r| r["name"] }
|
|
281
|
+
|
|
282
|
+
expected = trigger_names(model)
|
|
283
|
+
expected += trigram_trigger_names(model) if trigram_table_needed?(model)
|
|
284
|
+
(expected - existing).any?
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
270
288
|
private
|
|
271
289
|
|
|
290
|
+
BUSY_RETRIES = 3
|
|
291
|
+
BUSY_BACKOFF = 0.1
|
|
292
|
+
|
|
293
|
+
def with_busy_retry
|
|
294
|
+
attempts = 0
|
|
295
|
+
begin
|
|
296
|
+
yield
|
|
297
|
+
rescue ActiveRecord::StatementInvalid => e
|
|
298
|
+
raise unless e.message.match?(/database is locked/i) && attempts < BUSY_RETRIES
|
|
299
|
+
attempts += 1
|
|
300
|
+
sleep(BUSY_BACKOFF * attempts)
|
|
301
|
+
IndexCache.clear!
|
|
302
|
+
retry
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
272
306
|
def connection
|
|
273
307
|
ActiveRecord::Base.connection
|
|
274
308
|
end
|
|
@@ -290,9 +324,11 @@ module FullSearch
|
|
|
290
324
|
end
|
|
291
325
|
|
|
292
326
|
def model_table_exists?(model)
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
327
|
+
IndexCache.fetch("model_table_exists:#{model.table_name}") do
|
|
328
|
+
connection.execute(
|
|
329
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name=#{q(model.table_name)} LIMIT 1"
|
|
330
|
+
).any?
|
|
331
|
+
end
|
|
296
332
|
end
|
|
297
333
|
|
|
298
334
|
def table_exists?(model)
|
|
@@ -354,16 +390,7 @@ module FullSearch
|
|
|
354
390
|
|
|
355
391
|
def ensure_triggers!(model)
|
|
356
392
|
return if FullSearch.bulk_importing?(model)
|
|
357
|
-
|
|
358
|
-
existing = connection.execute(
|
|
359
|
-
"SELECT name FROM sqlite_master WHERE type='trigger' AND tbl_name=#{q(model.table_name)}"
|
|
360
|
-
).map { |r| r["name"] }
|
|
361
|
-
|
|
362
|
-
expected = trigger_names(model)
|
|
363
|
-
expected += trigram_trigger_names(model) if trigram_table_needed?(model)
|
|
364
|
-
return if (expected - existing).empty? && (existing - expected).empty?
|
|
365
|
-
|
|
366
|
-
rebuild!(model)
|
|
393
|
+
rebuild!(model) if missing_triggers?(model)
|
|
367
394
|
end
|
|
368
395
|
|
|
369
396
|
def trigger_names(model)
|
|
@@ -378,6 +405,8 @@ module FullSearch
|
|
|
378
405
|
%W[#{base}_ai #{base}_ad #{base}_au]
|
|
379
406
|
end
|
|
380
407
|
|
|
408
|
+
private
|
|
409
|
+
|
|
381
410
|
def insert_trigger_sql(model)
|
|
382
411
|
dsl = model.full_search_dsl
|
|
383
412
|
cols = dsl.fields + dsl.filters + extra_columns(model)
|
|
@@ -12,40 +12,42 @@ module FullSearch
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def call
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
IndexCache.with_cache do
|
|
16
|
+
searched = groups.map do |group|
|
|
17
|
+
model = fetch(group, :model)
|
|
18
|
+
raise FullSearch::NotConfiguredError, "#{model} is not full_search configured" unless model.full_search_dsl
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
filters = group[:filters] || {}
|
|
21
|
+
limit = positive_integer(group[:limit], 8)
|
|
22
|
+
offset = positive_integer(group[:offset], 0)
|
|
23
|
+
raw_limit = limit + 1
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
relation = model.full_search(
|
|
26
|
+
query,
|
|
27
|
+
filters: filters,
|
|
28
|
+
limit: raw_limit,
|
|
29
|
+
offset: offset,
|
|
30
|
+
highlight: group[:highlight],
|
|
31
|
+
highlight_fields: group[:highlight_fields],
|
|
32
|
+
matching_strategy: group[:matching_strategy],
|
|
33
|
+
per_strategy_limit: group[:per_strategy_limit],
|
|
34
|
+
scope: group[:scope],
|
|
35
|
+
includes: group[:includes]
|
|
36
|
+
)
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
records = relation.to_a
|
|
39
|
+
has_more = records.size > limit
|
|
40
|
+
records = records.first(limit) if has_more
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
group.slice(:key, :label, :icon, :model).merge(
|
|
43
|
+
results: records,
|
|
44
|
+
has_more: has_more,
|
|
45
|
+
total_count: records.size
|
|
46
|
+
)
|
|
47
|
+
end
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
{groups: searched, total_count: searched.sum { |g| g[:total_count] }}
|
|
50
|
+
end
|
|
49
51
|
end
|
|
50
52
|
|
|
51
53
|
private
|
data/lib/full_search/search.rb
CHANGED
|
@@ -92,6 +92,21 @@ module FullSearch
|
|
|
92
92
|
raise MissingTableError, "FTS table `#{FullSearch::Index.fts_table_name(model)}` does not exist. Run `bin/rails full_search:prepare` to create it."
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
+
if FullSearch::Index.missing_triggers?(model)
|
|
96
|
+
if FullSearch.config.auto_rebuild_on_stale_query
|
|
97
|
+
FullSearch::Index.rebuild!(model)
|
|
98
|
+
return
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
case FullSearch.config.stale_query_behavior
|
|
102
|
+
when :raise
|
|
103
|
+
raise ConfigChangedError, "FTS triggers for #{model.table_name} are missing; run full_search:rebuild"
|
|
104
|
+
when :log_and_fallback
|
|
105
|
+
Rails.logger.warn("[full_search] FTS triggers for #{model.table_name} are missing; results may be incomplete")
|
|
106
|
+
end
|
|
107
|
+
return
|
|
108
|
+
end
|
|
109
|
+
|
|
95
110
|
stored = begin
|
|
96
111
|
FullSearch::Index.stored_config_hash(model)
|
|
97
112
|
rescue ActiveRecord::StatementInvalid
|
data/lib/full_search/version.rb
CHANGED
data/lib/tasks/full_search.rake
CHANGED
|
@@ -105,7 +105,9 @@ namespace :full_search do
|
|
|
105
105
|
""
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
trigger_info = FullSearch::Index.missing_triggers?(model) ? " | MISSING TRIGGERS" : ""
|
|
109
|
+
|
|
110
|
+
puts "#{model.table_name}: #{status}#{drift_info}#{trigger_info}"
|
|
109
111
|
end
|
|
110
112
|
end
|
|
111
113
|
|
|
@@ -120,6 +122,10 @@ namespace :full_search do
|
|
|
120
122
|
next
|
|
121
123
|
end
|
|
122
124
|
|
|
125
|
+
if FullSearch::Index.missing_triggers?(model)
|
|
126
|
+
unhealthy << "#{model.table_name}: missing FTS triggers"
|
|
127
|
+
end
|
|
128
|
+
|
|
123
129
|
stored = FullSearch::Index.stored_config_hash(model)
|
|
124
130
|
current = model.full_search_dsl.config_hash
|
|
125
131
|
if stored != current
|
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.
|
|
4
|
+
version: 0.4.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ben D'Angelo
|
|
@@ -189,7 +189,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
189
189
|
- !ruby/object:Gem::Version
|
|
190
190
|
version: '2.0'
|
|
191
191
|
requirements: []
|
|
192
|
-
rubygems_version: 4.0.
|
|
192
|
+
rubygems_version: 4.0.10
|
|
193
193
|
specification_version: 4
|
|
194
194
|
summary: SQLite FTS5 full-text search for Rails/ActiveRecord
|
|
195
195
|
test_files: []
|