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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed7b0998cc6d4014ab815a6152ba07562da34c98b3cca3aa3886e63fb45acc5b
4
- data.tar.gz: f29430f7ac4d5a3175e1eb46c803aa3a9b9ce68e3e930f2f75ecfd8ad03242d7
3
+ metadata.gz: 01b6db287587e57e3bc4f7226eff10257303eadf30c6a6a64e53be1e2e52afb5
4
+ data.tar.gz: 01ec5e01cf2d479245338c5a7ab23f778a9f953c78c7a0a1ab4f428a4472c98c
5
5
  SHA512:
6
- metadata.gz: cce0189e77ec25883ad52dae321dea4f99cb8f1f245f331b5cdeb94f6c3554c675e84b0214e4036ca1c01fc53f22e41594324729d39688c8e48d0bcd791356f3
7
- data.tar.gz: a9d3f31295d082892dc5cf797f2d38a9cb8151629f9e6d2c0ac0ad1b8dcfeb5e732b740fb91d138d0b3a7f1db513c72c36aa61a1410d2b052bd57bda0790c5dd
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")
@@ -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
- :default_async_reindex, :default_async_source_reindex,
7
- :default_tokenizer, :auto_rebuild_on_stale_query
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
@@ -20,9 +20,9 @@ module FullSearch
20
20
  end
21
21
 
22
22
  def field(name, weight: 1, source: nil, reindex_on: nil,
23
- async: FullSearch.config.default_async_reindex,
24
- async_source: FullSearch.config.default_async_source_reindex,
25
- as: nil, version: nil)
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
@@ -8,5 +8,6 @@ module FullSearch
8
8
  class InvalidFieldError < Error; end
9
9
  class NotConfiguredError < Error; end
10
10
  class UnsupportedDatabaseError < Error; end
11
+ class MissingTableError < Error; end
11
12
  class InvalidQueryError < Error; end
12
13
  end
@@ -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.name)} UNINDEXED" : qc(c.name) }.join(", ")
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.name) }.join(", ")})
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.name)} UNINDEXED" : qc(c.name) }.join(", ")
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.name) }.join(", ")})
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.name) }.join(", ")
480
+ cols.map { |c| qc(column_name(c)) }.join(", ")
473
481
  end
474
482
 
475
483
  def column_ref(col, prefix:)
@@ -78,7 +78,15 @@ module FullSearch
78
78
  end
79
79
 
80
80
  def check_stale_config!
81
- stored = FullSearch::Index.stored_config_hash(model)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FullSearch
4
- VERSION = "0.3.4"
4
+ VERSION = "0.3.6"
5
5
  end
@@ -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!
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.3.4
4
+ version: 0.3.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben D'Angelo