full_search 0.3.4 → 0.3.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/README.md +49 -0
- data/lib/full_search/callbacks.rb +1 -1
- data/lib/full_search/config.rb +2 -2
- data/lib/full_search/dsl.rb +3 -3
- data/lib/full_search/errors.rb +1 -0
- data/lib/full_search/index.rb +14 -6
- data/lib/full_search/search.rb +9 -1
- data/lib/full_search/version.rb +1 -1
- data/lib/tasks/full_search.rake +23 -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: 01b6db287587e57e3bc4f7226eff10257303eadf30c6a6a64e53be1e2e52afb5
|
|
4
|
+
data.tar.gz: 01ec5e01cf2d479245338c5a7ab23f778a9f953c78c7a0a1ab4f428a4472c98c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6a3bd6143a47be3e7abd89df28506f247ee562205372ee9b5bedf6064d463413e212b7bca67dffb3e980531963e55984750adfed5504cf70b7e0dc7fa250185b
|
|
7
|
+
data.tar.gz: 3b793c3bfd872cf1a247a18c030ade28433d16d40bc71438d35dc79be2cd306d15ec79f0afc371c0866ef9feeca8d3292ed9f8af35c9bb6c572b8073be88a12b
|
data/README.md
CHANGED
|
@@ -45,6 +45,27 @@ end
|
|
|
45
45
|
Customer.search("sam", filters: { account_id: 1 }).page(params[:page])
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
### Field aliasing with `as:`
|
|
49
|
+
|
|
50
|
+
Use `as:` to give a field a different column name in the FTS table. This is useful when
|
|
51
|
+
the model's accessor name differs from the desired search index key — for example, storing
|
|
52
|
+
a computed value (last 8 chars of VIN) in a shorter or semantically-named column:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
class Vehicle < ApplicationRecord
|
|
56
|
+
full_search do
|
|
57
|
+
field :vin_last8, weight: 2, source: -> { vin&.last(8)&.upcase }, as: :vin
|
|
58
|
+
field :make, weight: 4
|
|
59
|
+
field :model, weight: 4
|
|
60
|
+
filter :account_id, required: true
|
|
61
|
+
highlight
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The FTS table stores the value under `vin` (not `vin_last8`). Search results highlight
|
|
67
|
+
keyed as `vin`, and the `full_search_text_for(:vin)` lookup works as expected.
|
|
68
|
+
|
|
48
69
|
## Features
|
|
49
70
|
|
|
50
71
|
- Declarative `full_search` DSL with `search` / `full_search` query methods
|
|
@@ -104,6 +125,22 @@ These two operations are often confused:
|
|
|
104
125
|
> within the same process/connection. For multi-process or multi-host deployments,
|
|
105
126
|
> run `full_search:rebuild` from a single deployment step.
|
|
106
127
|
|
|
128
|
+
### First-deploy setup
|
|
129
|
+
|
|
130
|
+
After `db:prepare` creates your application tables, FTS virtual tables like `customers_fts` do not yet exist. Run `full_search:prepare` to create them:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
bin/rails full_search:prepare
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
For Docker-based deployments, add this to your entrypoint script after `db:prepare:with_data`:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
./bin/rails full_search:prepare
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The `full_search:prepare` task is idempotent — it only creates missing tables and installs triggers. It will not drop or rebuild existing indexes, making it safe to run on every deploy.
|
|
143
|
+
|
|
107
144
|
### Auto-rebuild on app load
|
|
108
145
|
|
|
109
146
|
When `auto_rebuild_schema` is enabled (default in the generated initializer), the railtie hooks into Rails `after_initialize` and:
|
|
@@ -141,6 +178,7 @@ bin/rails full_search:reset
|
|
|
141
178
|
|
|
142
179
|
| Task | Description |
|
|
143
180
|
|------|-------------|
|
|
181
|
+
| `full_search:prepare` | Idempotent setup — creates missing FTS tables and triggers without dropping existing ones. Safe to run on every deploy. Add to your Docker entrypoint after `db:prepare:with_data` for first-deploy automation. |
|
|
144
182
|
| `full_search:rebuild` | Drops and recreates the FTS virtual table only when the DSL config hash has changed (safe for production). Pass model names to target specific tables. |
|
|
145
183
|
| `full_search:reset` | Force a full rebuild — drops and recreates all FTS tables regardless of config hash. Use when data may be out of sync. |
|
|
146
184
|
| `full_search:optimize` | Run FTS5 [`optimize`](https://www.sqlite.org/fts5.html#the_optimize_command) to merge b-tree segments. Useful after bulk updates. |
|
|
@@ -202,6 +240,17 @@ end
|
|
|
202
240
|
|
|
203
241
|
When this option is `true`, the first search that detects a stale index automatically calls `FullSearch::Index.rebuild_if_needed!` and then proceeds with the query, so you don't need to restart the server or run a Rake task during iterative development. In production, this defaults to `false`; use `full_search:rebuild` or boot-time `auto_rebuild_schema` instead.
|
|
204
242
|
|
|
243
|
+
### Missing table error
|
|
244
|
+
|
|
245
|
+
When the FTS table does not exist and a search is attempted, `full_search` raises `MissingTableError` with a clear message:
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
FullSearch::MissingTableError: FTS table `customers_fts` does not exist.
|
|
249
|
+
Run `bin/rails full_search:prepare` to create it.
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
This mirrors how Rails handles missing database tables — you see the error, run the task, and move on. Add `full_search:prepare` to your deploy pipeline or Docker entrypoint after `db:prepare:with_data` to prevent the error in production.
|
|
253
|
+
|
|
205
254
|
## Query operators
|
|
206
255
|
|
|
207
256
|
```ruby
|
|
@@ -66,7 +66,7 @@ module FullSearch
|
|
|
66
66
|
table = qt(FullSearch::Index.fts_table_name(record.class))
|
|
67
67
|
conn = ActiveRecord::Base.connection
|
|
68
68
|
conn.execute(
|
|
69
|
-
"UPDATE #{table} SET #{qc(field.name)} = #{q(value.to_s)} WHERE rowid = #{q(record.id)}"
|
|
69
|
+
"UPDATE #{table} SET #{qc(field.as || field.name)} = #{q(value.to_s)} WHERE rowid = #{q(record.id)}"
|
|
70
70
|
)
|
|
71
71
|
rescue => e
|
|
72
72
|
raise unless e.message.include?("no such table")
|
data/lib/full_search/config.rb
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
module FullSearch
|
|
4
4
|
class Config
|
|
5
5
|
attr_accessor :auto_rebuild_schema, :stale_query_behavior, :lock_rebuilds,
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
:default_async_reindex, :default_async_source_reindex,
|
|
7
|
+
:default_tokenizer, :auto_rebuild_on_stale_query
|
|
8
8
|
|
|
9
9
|
def initialize
|
|
10
10
|
@auto_rebuild_schema = false
|
data/lib/full_search/dsl.rb
CHANGED
|
@@ -20,9 +20,9 @@ module FullSearch
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def field(name, weight: 1, source: nil, reindex_on: nil,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
async: FullSearch.config.default_async_reindex,
|
|
24
|
+
async_source: FullSearch.config.default_async_source_reindex,
|
|
25
|
+
as: nil, version: nil)
|
|
26
26
|
unless valid_name?(name)
|
|
27
27
|
raise InvalidFieldError, "#{model_class.name}: invalid field name #{name.inspect}"
|
|
28
28
|
end
|
data/lib/full_search/errors.rb
CHANGED
data/lib/full_search/index.rb
CHANGED
|
@@ -46,9 +46,9 @@ module FullSearch
|
|
|
46
46
|
conn.execute(backfill_sql(model)) if fts_was_created
|
|
47
47
|
conn.execute(backfill_trigram_sql(model)) if trigram_was_created
|
|
48
48
|
reindex_source_fields!(model) if dsl.fields.any?(&:source)
|
|
49
|
+
store_config_hash!(model)
|
|
49
50
|
end
|
|
50
51
|
|
|
51
|
-
store_config_hash!(model)
|
|
52
52
|
ensure_triggers!(model) if model_table_exists?(model)
|
|
53
53
|
|
|
54
54
|
verified_tables.add(model.table_name)
|
|
@@ -174,6 +174,10 @@ module FullSearch
|
|
|
174
174
|
row&.[]("config_hash")
|
|
175
175
|
end
|
|
176
176
|
|
|
177
|
+
def missing_table?(model)
|
|
178
|
+
!table_exists?(model)
|
|
179
|
+
end
|
|
180
|
+
|
|
177
181
|
private
|
|
178
182
|
|
|
179
183
|
def connection
|
|
@@ -188,6 +192,10 @@ module FullSearch
|
|
|
188
192
|
connection.quote_table_name(name)
|
|
189
193
|
end
|
|
190
194
|
|
|
195
|
+
def column_name(col)
|
|
196
|
+
(col.respond_to?(:as) && col.as) ? col.as : col.name
|
|
197
|
+
end
|
|
198
|
+
|
|
191
199
|
def qc(name)
|
|
192
200
|
connection.quote_column_name(name)
|
|
193
201
|
end
|
|
@@ -223,7 +231,7 @@ module FullSearch
|
|
|
223
231
|
def create_virtual_table_sql(model)
|
|
224
232
|
dsl = model.full_search_dsl
|
|
225
233
|
columns = (dsl.fields + dsl.filters.map { |f| FilterColumnPlaceholder.new(name: f.name) } + extra_columns(model))
|
|
226
|
-
column_list = columns.map { |c| (c.respond_to?(:unindexed?) && c.unindexed?) ? "#{qc(c
|
|
234
|
+
column_list = columns.map { |c| (c.respond_to?(:unindexed?) && c.unindexed?) ? "#{qc(column_name(c))} UNINDEXED" : qc(column_name(c)) }.join(", ")
|
|
227
235
|
|
|
228
236
|
"CREATE VIRTUAL TABLE #{qt(fts_table_name(model))} USING fts5(#{column_list}, tokenize='#{dsl.tokenize}');"
|
|
229
237
|
end
|
|
@@ -244,7 +252,7 @@ module FullSearch
|
|
|
244
252
|
where_clause = dsl.conditional_index? ? " WHERE (#{dsl.index_if_sql})" : ""
|
|
245
253
|
|
|
246
254
|
<<~SQL
|
|
247
|
-
INSERT INTO #{qt(fts_table_name(model))}(rowid, #{cols.map { |c| qc(c
|
|
255
|
+
INSERT INTO #{qt(fts_table_name(model))}(rowid, #{cols.map { |c| qc(column_name(c)) }.join(", ")})
|
|
248
256
|
SELECT #{qt(model.table_name)}.id, #{select} FROM #{qt(model.table_name)}#{where_clause};
|
|
249
257
|
SQL
|
|
250
258
|
end
|
|
@@ -369,7 +377,7 @@ module FullSearch
|
|
|
369
377
|
def create_trigram_virtual_table_sql(model)
|
|
370
378
|
dsl = model.full_search_dsl
|
|
371
379
|
columns = (dsl.fields + dsl.filters.map { |f| FilterColumnPlaceholder.new(name: f.name) } + extra_columns(model))
|
|
372
|
-
column_list = columns.map { |c| (c.respond_to?(:unindexed?) && c.unindexed?) ? "#{qc(c
|
|
380
|
+
column_list = columns.map { |c| (c.respond_to?(:unindexed?) && c.unindexed?) ? "#{qc(column_name(c))} UNINDEXED" : qc(column_name(c)) }.join(", ")
|
|
373
381
|
|
|
374
382
|
"CREATE VIRTUAL TABLE #{qt(trigram_table_name(model))} USING fts5(#{column_list}, tokenize='trigram');"
|
|
375
383
|
end
|
|
@@ -390,7 +398,7 @@ module FullSearch
|
|
|
390
398
|
where_clause = dsl.conditional_index? ? " WHERE (#{dsl.index_if_sql})" : ""
|
|
391
399
|
|
|
392
400
|
<<~SQL
|
|
393
|
-
INSERT INTO #{qt(trigram_table_name(model))}(rowid, #{cols.map { |c| qc(c
|
|
401
|
+
INSERT INTO #{qt(trigram_table_name(model))}(rowid, #{cols.map { |c| qc(column_name(c)) }.join(", ")})
|
|
394
402
|
SELECT #{qt(model.table_name)}.id, #{select} FROM #{qt(model.table_name)}#{where_clause};
|
|
395
403
|
SQL
|
|
396
404
|
end
|
|
@@ -469,7 +477,7 @@ module FullSearch
|
|
|
469
477
|
end
|
|
470
478
|
|
|
471
479
|
def col_names(cols)
|
|
472
|
-
cols.map { |c| qc(c
|
|
480
|
+
cols.map { |c| qc(column_name(c)) }.join(", ")
|
|
473
481
|
end
|
|
474
482
|
|
|
475
483
|
def column_ref(col, prefix:)
|
data/lib/full_search/search.rb
CHANGED
|
@@ -78,7 +78,15 @@ module FullSearch
|
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
def check_stale_config!
|
|
81
|
-
|
|
81
|
+
if FullSearch::Index.missing_table?(model)
|
|
82
|
+
raise MissingTableError, "FTS table `#{FullSearch::Index.fts_table_name(model)}` does not exist. Run `bin/rails full_search:prepare` to create it."
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
stored = begin
|
|
86
|
+
FullSearch::Index.stored_config_hash(model)
|
|
87
|
+
rescue StandardError
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
82
90
|
return unless stored
|
|
83
91
|
|
|
84
92
|
return if stored == dsl.config_hash
|
data/lib/full_search/version.rb
CHANGED
data/lib/tasks/full_search.rake
CHANGED
|
@@ -17,6 +17,29 @@ def resolve_full_search_models(args)
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
namespace :full_search do
|
|
20
|
+
desc "Create full_search FTS tables and triggers if missing (idempotent, safe for first deploy)"
|
|
21
|
+
task :prepare, [:models] => :environment do |_t, args|
|
|
22
|
+
Rails.application.eager_load!
|
|
23
|
+
|
|
24
|
+
models = if args[:models]
|
|
25
|
+
args[:models].split(",").map do |name|
|
|
26
|
+
klass = begin
|
|
27
|
+
name.singularize.camelize.constantize
|
|
28
|
+
rescue NameError, LoadError
|
|
29
|
+
FullSearch.models.find { |m| m.table_name == name }
|
|
30
|
+
end
|
|
31
|
+
klass
|
|
32
|
+
end.compact
|
|
33
|
+
else
|
|
34
|
+
FullSearch.models
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
FullSearch.setup!
|
|
38
|
+
models.each do |model|
|
|
39
|
+
puts "Prepared #{FullSearch::Index.fts_table_name(model)}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
20
43
|
desc "Rebuild indexes when DSL has changed (checks config hash; safe for production)"
|
|
21
44
|
task :rebuild, [:models] => :environment do |_t, args|
|
|
22
45
|
Rails.application.eager_load!
|