full_search 0.3.9 → 0.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/README.md +197 -0
- data/lib/full_search/index.rb +1 -1
- data/lib/full_search/search.rb +1 -1
- data/lib/full_search/test_helpers.rb +68 -2
- data/lib/full_search/version.rb +1 -1
- data/lib/generators/full_search/install/install_generator.rb +1 -1
- data/lib/tasks/full_search.rake +26 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8175568a013e8a29d107a5b3bceebc7e9be6b9c85f56d8ef178d3c2c72a73678
|
|
4
|
+
data.tar.gz: d77fe210358656fe1b471baeb374c27d2f493544739d4ec326c06aee3dddadf1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6e786b098bc3cd046151835f11bbf98b4a5213feb46e70a0ae6872a4f24bba6eb22ec5e8a17ebe045328eaab1d2016682fda67e47c5582fb73ae72b262c7bf08
|
|
7
|
+
data.tar.gz: 4d5284fe9b059ee7010ff099cd88e494beffe1804084f29be86921bd895059cc98dd567fdf9c09ef20a2ec9af99badef3416f6085bb3f85e019f44657028244a
|
data/README.md
CHANGED
|
@@ -245,6 +245,7 @@ When disabled, you must run `bin/rails full_search:prepare` after every `db:sche
|
|
|
245
245
|
| `full_search:optimize` | Run FTS5 [`optimize`](https://www.sqlite.org/fts5.html#the_optimize_command) to merge b-tree segments. Useful after bulk updates. |
|
|
246
246
|
| `full_search:backfill` | Force-rebuild FTS indexes for specified models (or all). Useful for recovery after bulk operations. |
|
|
247
247
|
| `full_search:status` | Show each model's index status (`ok` / `stale`) and count of empty sourced fields. |
|
|
248
|
+
| `full_search:health_check` | Verify all indexes are present and current; exits `1` if any table is missing or stale. Useful for deploy gates or Docker healthchecks. |
|
|
248
249
|
|
|
249
250
|
## Background jobs
|
|
250
251
|
|
|
@@ -387,6 +388,202 @@ FullSearch.multi_search(
|
|
|
387
388
|
)
|
|
388
389
|
```
|
|
389
390
|
|
|
391
|
+
## Testing
|
|
392
|
+
|
|
393
|
+
`full_search` ships with `FullSearch::TestHelpers`, a framework-agnostic module you can include in Minitest or RSpec. It provides helpers to rebuild, reindex, and reset FTS tables from your tests.
|
|
394
|
+
|
|
395
|
+
### Setup
|
|
396
|
+
|
|
397
|
+
Call `setup_for_tests!` once in your test boot file. It applies safe defaults:
|
|
398
|
+
|
|
399
|
+
- Disables rebuild locking (`lock_rebuilds = false`)
|
|
400
|
+
- Enables query-time auto-rebuild for stale indexes (`auto_rebuild_on_stale_query = true`)
|
|
401
|
+
- Keeps stale-query behaviour as `raise` so real config drift fails fast
|
|
402
|
+
- Forces Active Job to run inline so background reindex jobs execute synchronously
|
|
403
|
+
|
|
404
|
+
#### Minitest (`test/test_helper.rb`)
|
|
405
|
+
|
|
406
|
+
```ruby
|
|
407
|
+
require "minitest/autorun"
|
|
408
|
+
require "full_search"
|
|
409
|
+
require "full_search/test_helpers"
|
|
410
|
+
|
|
411
|
+
class ActiveSupport::TestCase
|
|
412
|
+
include FullSearch::TestHelpers
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
FullSearch::TestHelpers.setup_for_tests!
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
#### RSpec (`spec/rails_helper.rb`)
|
|
419
|
+
|
|
420
|
+
```ruby
|
|
421
|
+
require "full_search"
|
|
422
|
+
require "full_search/test_helpers"
|
|
423
|
+
|
|
424
|
+
RSpec.configure do |config|
|
|
425
|
+
config.include FullSearch::TestHelpers, type: :model
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
FullSearch::TestHelpers.setup_for_tests!
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### Why you must rebuild after creating data
|
|
432
|
+
|
|
433
|
+
FTS tables are updated by database triggers on normal inserts/updates, but computed `source:` fields are evaluated by Ruby during a rebuild/reindex. The safest pattern is to create your records, then call `rebuild_full_search_index(model)` before searching.
|
|
434
|
+
|
|
435
|
+
### Available helpers
|
|
436
|
+
|
|
437
|
+
```ruby
|
|
438
|
+
# Rebuild a single model's FTS table from scratch (drops and recreates it).
|
|
439
|
+
# Accepts a model class, symbol, or string class name.
|
|
440
|
+
rebuild_full_search_index(Customer)
|
|
441
|
+
rebuild_full_search_index(:customer)
|
|
442
|
+
rebuild_full_search_index("Customer")
|
|
443
|
+
|
|
444
|
+
# Re-evaluate computed source: fields only. Leaves table structure untouched.
|
|
445
|
+
reindex_full_search(Customer)
|
|
446
|
+
|
|
447
|
+
# Rebuild every registered search model, or only the ones passed in.
|
|
448
|
+
reset_full_search!
|
|
449
|
+
reset_full_search!(Customer, Vehicle)
|
|
450
|
+
|
|
451
|
+
# Idempotently create any missing FTS tables/triggers for registered models.
|
|
452
|
+
ensure_full_search_tables
|
|
453
|
+
|
|
454
|
+
# Run a block with inline Active Job, restoring the adapter afterwards.
|
|
455
|
+
with_full_search_async_jobs_inline do
|
|
456
|
+
# ReindexJob / BackfillJob execute synchronously here
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# Run a block inside a rebuild, dropping the table at the end.
|
|
460
|
+
with_full_search_rebuild(Customer) do
|
|
461
|
+
# search and assert here
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
# Scope anonymous searchable models to a block so they don't leak into
|
|
465
|
+
# the global registry and affect later tests or Rake tasks.
|
|
466
|
+
with_full_search_models_registered do
|
|
467
|
+
model = Class.new(Customer) do
|
|
468
|
+
full_search { field :first_name, weight: 5 }
|
|
469
|
+
end
|
|
470
|
+
model.table_name = "customers"
|
|
471
|
+
rebuild_full_search_index(model)
|
|
472
|
+
# ... search and assert
|
|
473
|
+
end
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### Minitest example
|
|
477
|
+
|
|
478
|
+
```ruby
|
|
479
|
+
class CustomerSearchTest < ActiveSupport::TestCase
|
|
480
|
+
def setup
|
|
481
|
+
@account = Account.create!(name: "Acme")
|
|
482
|
+
@customer = Customer.create!(account: @account, first_name: "Sam")
|
|
483
|
+
rebuild_full_search_index(Customer)
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def test_finds_by_first_name
|
|
487
|
+
results = Customer.search("Sam", filters: {account_id: @account.id})
|
|
488
|
+
assert_includes results.to_a, @customer
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### RSpec example
|
|
494
|
+
|
|
495
|
+
```ruby
|
|
496
|
+
RSpec.describe Customer, type: :model do
|
|
497
|
+
let(:account) { create(:account) }
|
|
498
|
+
let(:customer) { create(:customer, account: account, first_name: "Sam") }
|
|
499
|
+
|
|
500
|
+
before { rebuild_full_search_index(Customer) }
|
|
501
|
+
|
|
502
|
+
it "finds by first name" do
|
|
503
|
+
results = Customer.search("Sam", filters: {account_id: account.id})
|
|
504
|
+
expect(results).to include(customer)
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
### Advanced configuration
|
|
510
|
+
|
|
511
|
+
If you need to override the defaults set by `setup_for_tests!`, use `FullSearch::TestHelpers.configure`:
|
|
512
|
+
|
|
513
|
+
```ruby
|
|
514
|
+
FullSearch::TestHelpers.configure do |config|
|
|
515
|
+
config.lock_rebuilds = true
|
|
516
|
+
config.auto_rebuild_on_stale_query = false
|
|
517
|
+
config.stale_query_behavior = :log_and_fallback
|
|
518
|
+
end
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
## Production readiness
|
|
522
|
+
|
|
523
|
+
`full_search` is designed for small to medium SQLite-backed Rails apps. Before running in production, review this checklist.
|
|
524
|
+
|
|
525
|
+
### Initial deploy
|
|
526
|
+
|
|
527
|
+
1. Run your normal database setup so application tables exist:
|
|
528
|
+
```bash
|
|
529
|
+
bin/rails db:prepare:with_data
|
|
530
|
+
```
|
|
531
|
+
2. Create the FTS virtual tables and triggers:
|
|
532
|
+
```bash
|
|
533
|
+
bin/rails full_search:prepare
|
|
534
|
+
```
|
|
535
|
+
3. For containerized deploys, add step 2 to your entrypoint after `db:prepare:with_data`.
|
|
536
|
+
|
|
537
|
+
### Configuration
|
|
538
|
+
|
|
539
|
+
The generated initializer defaults to production-safe values. Do **not** change these in production:
|
|
540
|
+
|
|
541
|
+
- `auto_rebuild_schema` must be `false`. If enabled, every web/worker/console process tries to rebuild indexes on boot.
|
|
542
|
+
- `auto_rebuild_on_stale_query` must be `false`. A query-time rebuild under load can cause timeouts and race conditions.
|
|
543
|
+
|
|
544
|
+
If you need to change the search DSL, ship the change and then run:
|
|
545
|
+
|
|
546
|
+
```bash
|
|
547
|
+
bin/rails full_search:rebuild
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
from a single deployment step. The gem checks each model's stored config hash and only rebuilds indexes whose DSL has changed.
|
|
551
|
+
|
|
552
|
+
### Monitoring and maintenance
|
|
553
|
+
|
|
554
|
+
- **Health check** — use the built-in Rake task for deploy gates or container healthchecks:
|
|
555
|
+
```bash
|
|
556
|
+
bin/rails full_search:health_check
|
|
557
|
+
```
|
|
558
|
+
It exits `0` when every registered model has a current FTS table, or `1` if any table is missing or stale.
|
|
559
|
+
|
|
560
|
+
- **Status overview** — `bin/rails full_search:status` prints `ok` / `stale` and the count of empty sourced fields per model.
|
|
561
|
+
|
|
562
|
+
- **Scheduled optimize** — queue `FullSearch::OptimizeJob` once a day during a low-traffic window to merge FTS5 b-tree segments:
|
|
563
|
+
```yaml
|
|
564
|
+
# config/recurring.yml
|
|
565
|
+
full_search_optimize:
|
|
566
|
+
class: FullSearch::OptimizeJob
|
|
567
|
+
schedule: daily at 4am
|
|
568
|
+
description: "Merge FTS5 b-tree segments for full_search indexes"
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
- **Recovery after bulk operations** — if a bulk operation bypassed triggers, rebuild the affected index:
|
|
572
|
+
```bash
|
|
573
|
+
bin/rails 'full_search:backfill[customers]'
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### Common errors
|
|
577
|
+
|
|
578
|
+
| Error | Meaning | Fix |
|
|
579
|
+
|-------|---------|-----|
|
|
580
|
+
| `FullSearch::MissingTableError` | The FTS table does not exist yet. | Run `bin/rails full_search:prepare`. |
|
|
581
|
+
| `FullSearch::ConfigChangedError` | The DSL has changed and the index is stale. | Run `bin/rails full_search:rebuild`. |
|
|
582
|
+
|
|
583
|
+
### Locking note
|
|
584
|
+
|
|
585
|
+
`lock_rebuilds` uses a Ruby `Mutex` and only prevents concurrent rebuilds within the same process/connection. It does not coordinate across processes or hosts. Always run `full_search:rebuild` from a single deployment step.
|
|
586
|
+
|
|
390
587
|
## Known limitations
|
|
391
588
|
|
|
392
589
|
- Queries run with `highlight: true` return an Array of records, not an `ActiveRecord::Relation`. No further chaining (`.where`, `.order`, `.limit`) is possible after highlighting is applied.
|
data/lib/full_search/index.rb
CHANGED
data/lib/full_search/search.rb
CHANGED
|
@@ -2,16 +2,82 @@
|
|
|
2
2
|
|
|
3
3
|
module FullSearch
|
|
4
4
|
module TestHelpers
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
class << self
|
|
6
|
+
def setup_for_tests!
|
|
7
|
+
configure do |config|
|
|
8
|
+
config.lock_rebuilds = false
|
|
9
|
+
config.auto_rebuild_on_stale_query = true
|
|
10
|
+
config.stale_query_behavior = :raise
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
inline_active_job_if_configured
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def configure
|
|
17
|
+
yield FullSearch.config
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def inline_active_job_if_configured
|
|
23
|
+
return unless defined?(ActiveJob::Base)
|
|
24
|
+
|
|
25
|
+
ActiveJob::Base.queue_adapter = :inline
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def rebuild_full_search_index(model)
|
|
30
|
+
model = resolve_full_search_model(model)
|
|
7
31
|
FullSearch::Index.drop!(model)
|
|
8
32
|
FullSearch::Index.rebuild!(model)
|
|
9
33
|
end
|
|
10
34
|
|
|
35
|
+
def reindex_full_search(model)
|
|
36
|
+
model = resolve_full_search_model(model)
|
|
37
|
+
FullSearch::Index.reindex_source_fields!(model)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def reset_full_search!(*models)
|
|
41
|
+
models = FullSearch.models.to_a if models.empty?
|
|
42
|
+
models.each { |model| rebuild_full_search_index(model) }
|
|
43
|
+
end
|
|
44
|
+
|
|
11
45
|
def ensure_full_search_tables
|
|
12
46
|
FullSearch.models.each do |model|
|
|
13
47
|
FullSearch::Index.ensure_table!(model)
|
|
14
48
|
end
|
|
15
49
|
end
|
|
50
|
+
|
|
51
|
+
def with_full_search_rebuild(model)
|
|
52
|
+
model = resolve_full_search_model(model)
|
|
53
|
+
rebuild_full_search_index(model)
|
|
54
|
+
yield
|
|
55
|
+
ensure
|
|
56
|
+
FullSearch::Index.drop!(model)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def with_full_search_async_jobs_inline
|
|
60
|
+
original_adapter = ActiveJob::Base.queue_adapter if defined?(ActiveJob::Base)
|
|
61
|
+
ActiveJob::Base.queue_adapter = :inline if defined?(ActiveJob::Base)
|
|
62
|
+
yield
|
|
63
|
+
ensure
|
|
64
|
+
ActiveJob::Base.queue_adapter = original_adapter if defined?(ActiveJob::Base)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def with_full_search_models_registered(*models)
|
|
68
|
+
previous_registry = FullSearch.models.dup
|
|
69
|
+
models.each { |model| FullSearch.register_model(model) }
|
|
70
|
+
yield
|
|
71
|
+
ensure
|
|
72
|
+
FullSearch.models.replace(previous_registry)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def resolve_full_search_model(model)
|
|
78
|
+
return model if model.is_a?(Class)
|
|
79
|
+
|
|
80
|
+
model.to_s.camelize.constantize
|
|
81
|
+
end
|
|
16
82
|
end
|
|
17
83
|
end
|
data/lib/full_search/version.rb
CHANGED
|
@@ -19,7 +19,7 @@ module FullSearch
|
|
|
19
19
|
|
|
20
20
|
say "Running full_search:prepare to create FTS tables..."
|
|
21
21
|
rake("full_search:prepare")
|
|
22
|
-
rescue => e
|
|
22
|
+
rescue RuntimeError => e
|
|
23
23
|
say "Skipping full_search:prepare — #{e.message}", :yellow
|
|
24
24
|
say "Run `bin/rails full_search:prepare` after your database is ready.", :yellow
|
|
25
25
|
end
|
data/lib/tasks/full_search.rake
CHANGED
|
@@ -108,4 +108,30 @@ namespace :full_search do
|
|
|
108
108
|
puts "#{model.table_name}: #{status}#{drift_info}"
|
|
109
109
|
end
|
|
110
110
|
end
|
|
111
|
+
|
|
112
|
+
desc "Verify full_search indexes are present and up to date (exit 1 if unhealthy)"
|
|
113
|
+
task health_check: :environment do
|
|
114
|
+
Rails.application.eager_load!
|
|
115
|
+
unhealthy = []
|
|
116
|
+
|
|
117
|
+
FullSearch.sorted_models.each do |model|
|
|
118
|
+
if FullSearch::Index.missing_table?(model)
|
|
119
|
+
unhealthy << "#{model.table_name}: missing FTS table"
|
|
120
|
+
next
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
stored = FullSearch::Index.stored_config_hash(model)
|
|
124
|
+
current = model.full_search_dsl.config_hash
|
|
125
|
+
if stored != current
|
|
126
|
+
unhealthy << "#{model.table_name}: stale config hash"
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
if unhealthy.any?
|
|
131
|
+
unhealthy.each { |message| puts "[FAIL] #{message}" }
|
|
132
|
+
exit 1
|
|
133
|
+
else
|
|
134
|
+
puts "[OK] All full_search indexes are healthy"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
111
137
|
end
|