full_search 0.4.8 → 0.4.10
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 +18 -1
- data/lib/full_search/config.rb +4 -1
- data/lib/full_search/index.rb +40 -12
- data/lib/full_search/model.rb +20 -0
- data/lib/full_search/version.rb +1 -1
- data/lib/generators/full_search/install/templates/full_search.rb +11 -0
- data/lib/tasks/full_search.rake +39 -0
- 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: b21fb40775574ba4e404c2409fb2b4a4a57095baeb2f4867ca52812391d31348
|
|
4
|
+
data.tar.gz: baf95666180a4702197d41deb90ea31a423c5f33a3aa90dc52d3709d5df44a3a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a52f22fffe5a0751a1be3178e65d8ea51c08b789e8240017d9f036e8d407ff68c45deffd7dc99e1e3a5c68813448eab82f06ee38b103838f24dbb42df182485c
|
|
7
|
+
data.tar.gz: 4b791aea84de3a56fda5674e4a3e74af7cec5d53ca4e24d241c54c9f368bbc9c579cc8a22a62c46e4956f5a2892515a6be0b0d4f3aab77cb4cc5d34e55145fbd
|
data/README.md
CHANGED
|
@@ -150,7 +150,9 @@ FTS indexes are SQLite virtual tables (`customers_fts`, `vehicles_fts`, etc.) th
|
|
|
150
150
|
These two operations are often confused:
|
|
151
151
|
|
|
152
152
|
- **Rebuild** (`full_search:rebuild` / `Customer.rebuild!`) — drops and recreates the FTS virtual table. Needed when the DSL changes (fields added/removed, tokenizer changed, etc.). The table is re-created from scratch, backfilled, triggers re-installed, and the index is optimized.
|
|
153
|
-
- **Reindex** (`Customer.reindex!` / `FullSearch::Index.reindex_source_fields!`) — updates existing FTS rows with fresh values from computed `source:` fields only. The table structure is untouched. Database triggers cover regular column changes automatically
|
|
153
|
+
- **Reindex** (`Customer.reindex!` / `Customer.reindex_ids([ids])` / `FullSearch::Index.reindex_source_fields!`) — updates existing FTS rows with fresh values from computed `source:` fields only. The table structure is untouched. Database triggers cover regular column changes automatically, and since v0.4.9 they **preserve** existing `source:` field values rather than wiping them to `''` on UPDATE. Only Ruby-evaluated `source:` blocks need an explicit reindex.
|
|
154
|
+
|
|
155
|
+
**Important:** raw SQL operations that bypass Rails callbacks (`update_all`, `update_column`, `insert_all`, `upsert_all`, `counter_cache`, `touch`) will not run the `after_save_commit` reindex hook. Use `Model.reindex_ids(ids)` after a callback-free `update_all` that changes data backing a `source:` field, or `Model.reindex!` after a bulk insert, to keep the FTS source fields in sync.
|
|
154
156
|
|
|
155
157
|
### Setup lifecycle
|
|
156
158
|
|
|
@@ -235,6 +237,21 @@ end
|
|
|
235
237
|
|
|
236
238
|
When disabled, you must run `bin/rails full_search:prepare` after every `db:schema:load` to recreate the indexes.
|
|
237
239
|
|
|
240
|
+
`full_search` also ensures FTS tables exist before `db:schema:dump` reads the database and after `db:schema:load` finishes, so virtual tables are never silently dropped from `schema.rb` when models are registered but their tables haven't been created yet (e.g. development with `eager_load = false`). These hooks are gated by two config options, both on by default:
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
FullSearch.configure do |config|
|
|
244
|
+
# Run full_search:prepare before db:schema:dump so every registered
|
|
245
|
+
# FTS table is present in sqlite_master when the dumper reads it.
|
|
246
|
+
config.auto_prepare_on_schema_dump = true
|
|
247
|
+
|
|
248
|
+
# Create any FTS tables missing from schema.rb after db:schema:load.
|
|
249
|
+
config.auto_prepare_after_schema_load = true
|
|
250
|
+
end
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Set either to `false` if you prefer to run `bin/rails full_search:prepare` manually.
|
|
254
|
+
|
|
238
255
|
### Rake tasks
|
|
239
256
|
|
|
240
257
|
| Task | Description |
|
data/lib/full_search/config.rb
CHANGED
|
@@ -5,7 +5,8 @@ module FullSearch
|
|
|
5
5
|
attr_accessor :auto_rebuild_schema, :stale_query_behavior, :lock_rebuilds,
|
|
6
6
|
:default_async_reindex, :default_async_source_reindex,
|
|
7
7
|
:default_tokenizer, :auto_rebuild_on_stale_query, :min_like_prefix_length,
|
|
8
|
-
:dump_schema_virtual_tables, :auto_rebuild_missing_tables
|
|
8
|
+
:dump_schema_virtual_tables, :auto_rebuild_missing_tables,
|
|
9
|
+
:auto_prepare_on_schema_dump, :auto_prepare_after_schema_load
|
|
9
10
|
|
|
10
11
|
def initialize
|
|
11
12
|
@auto_rebuild_schema = false
|
|
@@ -18,6 +19,8 @@ module FullSearch
|
|
|
18
19
|
@min_like_prefix_length = FullSearch::Constants::DEFAULT_MIN_LIKE_PREFIX_LENGTH
|
|
19
20
|
@dump_schema_virtual_tables = true
|
|
20
21
|
@auto_rebuild_missing_tables = false
|
|
22
|
+
@auto_prepare_on_schema_dump = true
|
|
23
|
+
@auto_prepare_after_schema_load = true
|
|
21
24
|
end
|
|
22
25
|
end
|
|
23
26
|
|
data/lib/full_search/index.rb
CHANGED
|
@@ -452,24 +452,27 @@ module FullSearch
|
|
|
452
452
|
|
|
453
453
|
when_clause = SoftDelete.active_update_clause(model)
|
|
454
454
|
|
|
455
|
+
update_assignments = update_assignments_sql(cols)
|
|
456
|
+
|
|
455
457
|
if dsl.conditional_index?
|
|
456
458
|
select_parts = cols.map { |c| column_ref(c, prefix: "new") }.join(", ")
|
|
457
459
|
<<~SQL
|
|
458
460
|
CREATE TRIGGER #{qt(trigger_names(model)[2])} AFTER UPDATE ON #{qt(model.table_name)} #{when_clause}
|
|
459
461
|
BEGIN
|
|
460
|
-
DELETE FROM #{fts_table} WHERE rowid = old.id
|
|
462
|
+
DELETE FROM #{fts_table} WHERE rowid = old.id
|
|
463
|
+
AND NOT EXISTS (SELECT 1 FROM #{tbl} WHERE #{tbl}.id = old.id AND (#{dsl.index_if_sql}));
|
|
464
|
+
UPDATE #{fts_table} SET #{update_assignments} WHERE rowid = new.id
|
|
465
|
+
AND EXISTS (SELECT 1 FROM #{tbl} WHERE #{tbl}.id = new.id AND (#{dsl.index_if_sql}));
|
|
461
466
|
INSERT INTO #{fts_table}(rowid, #{cols_str})
|
|
462
|
-
|
|
467
|
+
SELECT new.id, #{select_parts} FROM #{tbl} WHERE #{tbl}.id = new.id AND (#{dsl.index_if_sql})
|
|
468
|
+
AND NOT EXISTS (SELECT 1 FROM #{fts_table} WHERE #{fts_table}.rowid = new.id);
|
|
463
469
|
END;
|
|
464
470
|
SQL
|
|
465
471
|
else
|
|
466
|
-
values = cols.map { |c| column_ref(c, prefix: "new") }.join(", ")
|
|
467
472
|
<<~SQL
|
|
468
473
|
CREATE TRIGGER #{qt(trigger_names(model)[2])} AFTER UPDATE ON #{qt(model.table_name)} #{when_clause}
|
|
469
474
|
BEGIN
|
|
470
|
-
|
|
471
|
-
INSERT INTO #{fts_table}(rowid, #{cols_str})
|
|
472
|
-
VALUES (new.id, #{values});
|
|
475
|
+
UPDATE #{fts_table} SET #{update_assignments} WHERE rowid = new.id;
|
|
473
476
|
END;
|
|
474
477
|
SQL
|
|
475
478
|
end
|
|
@@ -562,24 +565,27 @@ module FullSearch
|
|
|
562
565
|
|
|
563
566
|
when_clause = SoftDelete.active_update_clause(model)
|
|
564
567
|
|
|
568
|
+
update_assignments = update_assignments_sql(cols)
|
|
569
|
+
|
|
565
570
|
if dsl.conditional_index?
|
|
566
571
|
select_parts = cols.map { |c| column_ref(c, prefix: "new") }.join(", ")
|
|
567
572
|
<<~SQL
|
|
568
573
|
CREATE TRIGGER #{qt(trigram_trigger_names(model)[2])} AFTER UPDATE ON #{qt(model.table_name)} #{when_clause}
|
|
569
574
|
BEGIN
|
|
570
|
-
DELETE FROM #{trigram_table} WHERE rowid = old.id
|
|
575
|
+
DELETE FROM #{trigram_table} WHERE rowid = old.id
|
|
576
|
+
AND NOT EXISTS (SELECT 1 FROM #{tbl} WHERE #{tbl}.id = old.id AND (#{dsl.index_if_sql}));
|
|
577
|
+
UPDATE #{trigram_table} SET #{update_assignments} WHERE rowid = new.id
|
|
578
|
+
AND EXISTS (SELECT 1 FROM #{tbl} WHERE #{tbl}.id = new.id AND (#{dsl.index_if_sql}));
|
|
571
579
|
INSERT INTO #{trigram_table}(rowid, #{cols_str})
|
|
572
|
-
|
|
580
|
+
SELECT new.id, #{select_parts} FROM #{tbl} WHERE #{tbl}.id = new.id AND (#{dsl.index_if_sql})
|
|
581
|
+
AND NOT EXISTS (SELECT 1 FROM #{trigram_table} WHERE #{trigram_table}.rowid = new.id);
|
|
573
582
|
END;
|
|
574
583
|
SQL
|
|
575
584
|
else
|
|
576
|
-
values = cols.map { |c| column_ref(c, prefix: "new") }.join(", ")
|
|
577
585
|
<<~SQL
|
|
578
586
|
CREATE TRIGGER #{qt(trigram_trigger_names(model)[2])} AFTER UPDATE ON #{qt(model.table_name)} #{when_clause}
|
|
579
587
|
BEGIN
|
|
580
|
-
|
|
581
|
-
INSERT INTO #{trigram_table}(rowid, #{cols_str})
|
|
582
|
-
VALUES (new.id, #{values});
|
|
588
|
+
UPDATE #{trigram_table} SET #{update_assignments} WHERE rowid = new.id;
|
|
583
589
|
END;
|
|
584
590
|
SQL
|
|
585
591
|
end
|
|
@@ -604,6 +610,28 @@ module FullSearch
|
|
|
604
610
|
end
|
|
605
611
|
end
|
|
606
612
|
|
|
613
|
+
# Builds `col = value` assignments for the AFTER UPDATE trigger. Source
|
|
614
|
+
# fields are deliberately excluded so a raw SQL UPDATE (e.g. a
|
|
615
|
+
# counter_cache or touch) does not wipe the previously computed source
|
|
616
|
+
# value back to an empty string. Source fields are refreshed later by the
|
|
617
|
+
# after_save_commit reindex callback on normal Rails saves.
|
|
618
|
+
#
|
|
619
|
+
# When every column is a source field (nothing to update), emit a valid
|
|
620
|
+
# no-op assignment so the generated trigger SQL stays syntactically valid.
|
|
621
|
+
def update_assignments_sql(cols)
|
|
622
|
+
assignments = cols.filter_map do |col|
|
|
623
|
+
if col.respond_to?(:unindexed?) && col.name == "indexed"
|
|
624
|
+
"#{qc(col.name)} = '1'"
|
|
625
|
+
elsif col.respond_to?(:source) && col.source
|
|
626
|
+
nil
|
|
627
|
+
else
|
|
628
|
+
"#{qc(column_name(col))} = new.#{qc(column_name(col))}"
|
|
629
|
+
end
|
|
630
|
+
end
|
|
631
|
+
assignments << "#{qc(column_name(cols.first))} = #{qc(column_name(cols.first))}" if assignments.empty?
|
|
632
|
+
assignments.join(", ")
|
|
633
|
+
end
|
|
634
|
+
|
|
607
635
|
# NOTE: This lock prevents concurrent rebuilds within the same process/connection only.
|
|
608
636
|
# For multi-process or multi-host deployments, run full_search:rebuild from a single
|
|
609
637
|
# deployment step. See README.
|
data/lib/full_search/model.rb
CHANGED
|
@@ -40,6 +40,19 @@ module FullSearch
|
|
|
40
40
|
FullSearch::Index.reindex_source_fields!(self)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
# Re-evaluates source: fields for a specific set of records and updates
|
|
44
|
+
# the FTS table in place. Use after bulk operations that bypass model
|
|
45
|
+
# callbacks (e.g. `update_all`, `insert_all`, `upsert_all`, `update_column`)
|
|
46
|
+
# so the source fields don't drift out of sync with the underlying rows.
|
|
47
|
+
def reindex_ids(ids)
|
|
48
|
+
source_ids = ids.to_a.map(&:to_i).reject(&:zero?)
|
|
49
|
+
return if source_ids.empty?
|
|
50
|
+
|
|
51
|
+
where(id: source_ids).find_each(batch_size: 500) do |record|
|
|
52
|
+
FullSearch::Callbacks.reindex_record!(record)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
43
56
|
def bulk_import(&block)
|
|
44
57
|
FullSearch.bulk_import(self, &block)
|
|
45
58
|
end
|
|
@@ -75,6 +88,13 @@ module FullSearch
|
|
|
75
88
|
|
|
76
89
|
FullSearch::Model.evaluate_source(self, field)
|
|
77
90
|
end
|
|
91
|
+
|
|
92
|
+
# Re-evaluates this record's source: fields and writes them to the FTS
|
|
93
|
+
# table immediately (synchronously). Use after a callback-free mutation
|
|
94
|
+
# (update_column, update_all, counter_cache/touch) to refresh the index.
|
|
95
|
+
def reindex_source_fields!
|
|
96
|
+
FullSearch::Callbacks.reindex_record!(self)
|
|
97
|
+
end
|
|
78
98
|
end
|
|
79
99
|
end
|
|
80
100
|
end
|
data/lib/full_search/version.rb
CHANGED
|
@@ -16,6 +16,17 @@ FullSearch.configure do |config|
|
|
|
16
16
|
# Bulk imports should use FullSearch.bulk_import(Model) { ... } to defer reindexing.
|
|
17
17
|
config.default_async_source_reindex = true
|
|
18
18
|
|
|
19
|
+
# Ensure FTS tables exist before db:schema:dump reads the database, so
|
|
20
|
+
# virtual tables are never silently dropped from schema.rb. Set to false
|
|
21
|
+
# if you prefer to run `bin/rails full_search:prepare` manually before
|
|
22
|
+
# dumping the schema.
|
|
23
|
+
config.auto_prepare_on_schema_dump = true
|
|
24
|
+
|
|
25
|
+
# Create any FTS tables missing from schema.rb after db:schema:load.
|
|
26
|
+
# Set to false if you prefer to run `bin/rails full_search:prepare`
|
|
27
|
+
# manually after loading the schema.
|
|
28
|
+
config.auto_prepare_after_schema_load = true
|
|
29
|
+
|
|
19
30
|
# Uncomment to exclude full_search virtual tables from db/schema.rb.
|
|
20
31
|
# When false, you must run `bin/rails full_search:prepare` after db:schema:load.
|
|
21
32
|
# config.dump_schema_virtual_tables = false
|
data/lib/tasks/full_search.rake
CHANGED
|
@@ -140,4 +140,43 @@ namespace :full_search do
|
|
|
140
140
|
puts "[OK] All full_search indexes are healthy"
|
|
141
141
|
end
|
|
142
142
|
end
|
|
143
|
+
|
|
144
|
+
# Internal: ensure FTS tables exist before db:schema:dump reads
|
|
145
|
+
# sqlite_master. Gated by auto_prepare_on_schema_dump so apps that
|
|
146
|
+
# prefer manual control can opt out.
|
|
147
|
+
task auto_prepare_before_dump: :environment do
|
|
148
|
+
next unless FullSearch.config.auto_prepare_on_schema_dump
|
|
149
|
+
|
|
150
|
+
Rails.application.eager_load!
|
|
151
|
+
FullSearch.setup!
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Internal: create any FTS tables missing from schema.rb after
|
|
155
|
+
# db:schema:load. Gated by auto_prepare_after_schema_load.
|
|
156
|
+
task auto_prepare_after_load: :environment do
|
|
157
|
+
next unless FullSearch.config.auto_prepare_after_schema_load
|
|
158
|
+
|
|
159
|
+
Rails.application.eager_load!
|
|
160
|
+
FullSearch.setup!
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Ensure FTS tables exist before db:schema:dump reads sqlite_master,
|
|
165
|
+
# so virtual tables aren't silently dropped from schema.rb when models
|
|
166
|
+
# were registered but their FTS tables never got created (e.g. dev with
|
|
167
|
+
# eager_load off, or a fresh db:schema:load against an incomplete dump).
|
|
168
|
+
if Rake::Task.task_defined?("db:schema:dump")
|
|
169
|
+
Rake::Task["db:schema:dump"].enhance(["full_search:auto_prepare_before_dump"])
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Create any FTS tables missing from schema.rb after db:schema:load.
|
|
173
|
+
# schema.rb may be missing tables if it was dumped with
|
|
174
|
+
# auto_prepare_on_schema_dump = false, or if new searchable models were
|
|
175
|
+
# added since the last dump.
|
|
176
|
+
if Rake::Task.task_defined?("db:schema:load")
|
|
177
|
+
Rake::Task["db:schema:load"].enhance do
|
|
178
|
+
next unless FullSearch.config.auto_prepare_after_schema_load
|
|
179
|
+
Rails.application.eager_load!
|
|
180
|
+
FullSearch.setup!
|
|
181
|
+
end
|
|
143
182
|
end
|
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.10
|
|
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.18
|
|
193
193
|
specification_version: 4
|
|
194
194
|
summary: SQLite FTS5 full-text search for Rails/ActiveRecord
|
|
195
195
|
test_files: []
|