full_search 0.4.4 → 0.4.6
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 +25 -13
- data/lib/full_search/multi_search.rb +31 -29
- data/lib/full_search/once_rebuilt.rb +49 -0
- data/lib/full_search/search.rb +15 -0
- data/lib/full_search/test_helpers.rb +4 -0
- data/lib/full_search/version.rb +1 -1
- data/lib/full_search.rb +1 -0
- data/lib/tasks/full_search.rake +7 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ffdca4041a68a79686da2b8d636b3feadb88d39fd6ca2ff8f3c6ec955d52355c
|
|
4
|
+
data.tar.gz: 8438b904a538c0d8c8e0250bf751209b44f8a095aacfc1089cb180a5cbb21d8f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 447a5e7b51909e3098a38439935facf534ad7a97c84eb399af7fbd5e844c966787ac74651a58993000d12b0c18d3a6851fe3ea1e32b854629ff97b5c9e1906a0
|
|
7
|
+
data.tar.gz: d43d646f9e95ad6bc8d4425c8601b9c5f2695f106965cba1b53881cf8e155581a3421055f1b6babc0468aaf0a02c589e1e615f95dfecb42cd776adebae320bb5
|
|
@@ -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
|
@@ -184,6 +184,7 @@ module FullSearch
|
|
|
184
184
|
drop_fts_table_safely!(fts_table_name(model))
|
|
185
185
|
drop_fts_table_safely!(trigram_table_name(model))
|
|
186
186
|
end
|
|
187
|
+
|
|
187
188
|
# Drops an FTS5 virtual table, recovering from corruption where a bare
|
|
188
189
|
# DROP TABLE IF EXISTS raises "invalid fts5 file format" because the
|
|
189
190
|
# shadow tables were tampered with out-of-band. When that happens we
|
|
@@ -266,6 +267,22 @@ module FullSearch
|
|
|
266
267
|
).any?
|
|
267
268
|
end
|
|
268
269
|
|
|
270
|
+
def missing_triggers?(model)
|
|
271
|
+
return false if FullSearch.bulk_importing?(model)
|
|
272
|
+
return false unless model_table_exists?(model)
|
|
273
|
+
return false unless table_exists?(model)
|
|
274
|
+
|
|
275
|
+
IndexCache.fetch("missing_triggers:#{model.table_name}") do
|
|
276
|
+
existing = connection.execute(
|
|
277
|
+
"SELECT name FROM sqlite_master WHERE type='trigger' AND tbl_name=#{q(model.table_name)}"
|
|
278
|
+
).map { |r| r["name"] }
|
|
279
|
+
|
|
280
|
+
expected = trigger_names(model)
|
|
281
|
+
expected += trigram_trigger_names(model) if trigram_table_needed?(model)
|
|
282
|
+
(expected - existing).any?
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
269
286
|
private
|
|
270
287
|
|
|
271
288
|
def connection
|
|
@@ -289,9 +306,11 @@ module FullSearch
|
|
|
289
306
|
end
|
|
290
307
|
|
|
291
308
|
def model_table_exists?(model)
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
309
|
+
IndexCache.fetch("model_table_exists:#{model.table_name}") do
|
|
310
|
+
connection.execute(
|
|
311
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name=#{q(model.table_name)} LIMIT 1"
|
|
312
|
+
).any?
|
|
313
|
+
end
|
|
295
314
|
end
|
|
296
315
|
|
|
297
316
|
def table_exists?(model)
|
|
@@ -353,16 +372,7 @@ module FullSearch
|
|
|
353
372
|
|
|
354
373
|
def ensure_triggers!(model)
|
|
355
374
|
return if FullSearch.bulk_importing?(model)
|
|
356
|
-
|
|
357
|
-
existing = connection.execute(
|
|
358
|
-
"SELECT name FROM sqlite_master WHERE type='trigger' AND tbl_name=#{q(model.table_name)}"
|
|
359
|
-
).map { |r| r["name"] }
|
|
360
|
-
|
|
361
|
-
expected = trigger_names(model)
|
|
362
|
-
expected += trigram_trigger_names(model) if trigram_table_needed?(model)
|
|
363
|
-
return if (expected - existing).empty? && (existing - expected).empty?
|
|
364
|
-
|
|
365
|
-
rebuild!(model)
|
|
375
|
+
rebuild!(model) if missing_triggers?(model)
|
|
366
376
|
end
|
|
367
377
|
|
|
368
378
|
def trigger_names(model)
|
|
@@ -377,6 +387,8 @@ module FullSearch
|
|
|
377
387
|
%W[#{base}_ai #{base}_ad #{base}_au]
|
|
378
388
|
end
|
|
379
389
|
|
|
390
|
+
private
|
|
391
|
+
|
|
380
392
|
def insert_trigger_sql(model)
|
|
381
393
|
dsl = model.full_search_dsl
|
|
382
394
|
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
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FullSearch
|
|
4
|
+
module OnceRebuilt
|
|
5
|
+
class << self
|
|
6
|
+
def per_test_case(*models)
|
|
7
|
+
models = FullSearch.models.to_a if models.empty?
|
|
8
|
+
unless already_rebuilt?(models)
|
|
9
|
+
rebuild_models!(models)
|
|
10
|
+
mark_rebuilt!(models)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
yield if block_given?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def clear!
|
|
17
|
+
@rebuilt = Set.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def already_rebuilt?(models)
|
|
23
|
+
keys = models.map { |model| cache_key(model) }
|
|
24
|
+
@rebuilt ||= Set.new
|
|
25
|
+
keys.all? { |key| @rebuilt.include?(key) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def mark_rebuilt!(models)
|
|
29
|
+
@rebuilt ||= Set.new
|
|
30
|
+
models.each { |model| @rebuilt << cache_key(model) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def cache_key(model)
|
|
34
|
+
[
|
|
35
|
+
model.connection_db_config.database,
|
|
36
|
+
model.table_name,
|
|
37
|
+
model.full_search_dsl&.config_hash,
|
|
38
|
+
model.connection_db_config.schema_cache_path
|
|
39
|
+
].compact.join(":")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
include FullSearch::TestHelpers
|
|
43
|
+
|
|
44
|
+
def rebuild_models!(models)
|
|
45
|
+
models.each { |model| rebuild_full_search_index(model) }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
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
|
|
@@ -54,6 +54,10 @@ module FullSearch
|
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
def once_full_search_rebuilt(*models, &block)
|
|
58
|
+
FullSearch::OnceRebuilt.per_test_case(*models, &block)
|
|
59
|
+
end
|
|
60
|
+
|
|
57
61
|
def with_full_search_rebuild(model)
|
|
58
62
|
model = resolve_full_search_model(model)
|
|
59
63
|
rebuild_full_search_index(model)
|
data/lib/full_search/version.rb
CHANGED
data/lib/full_search.rb
CHANGED
|
@@ -30,6 +30,7 @@ require "full_search/backfill_job"
|
|
|
30
30
|
require "full_search/test_helpers"
|
|
31
31
|
require "full_search/multi_search"
|
|
32
32
|
require "full_search/schema_dumper_patch"
|
|
33
|
+
require "full_search/once_rebuilt"
|
|
33
34
|
|
|
34
35
|
ActiveSupport.on_load(:active_record) do
|
|
35
36
|
include FullSearch::Model
|
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.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ben D'Angelo
|
|
@@ -155,6 +155,7 @@ files:
|
|
|
155
155
|
- lib/full_search/instrumentation.rb
|
|
156
156
|
- lib/full_search/model.rb
|
|
157
157
|
- lib/full_search/multi_search.rb
|
|
158
|
+
- lib/full_search/once_rebuilt.rb
|
|
158
159
|
- lib/full_search/optimize_job.rb
|
|
159
160
|
- lib/full_search/query_parser.rb
|
|
160
161
|
- lib/full_search/quoting.rb
|
|
@@ -188,7 +189,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
188
189
|
- !ruby/object:Gem::Version
|
|
189
190
|
version: '2.0'
|
|
190
191
|
requirements: []
|
|
191
|
-
rubygems_version: 4.0.
|
|
192
|
+
rubygems_version: 4.0.15
|
|
192
193
|
specification_version: 4
|
|
193
194
|
summary: SQLite FTS5 full-text search for Rails/ActiveRecord
|
|
194
195
|
test_files: []
|