full_search 0.3.2 → 0.3.3

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: 540d8604b539de87bf3d8448c33319a80853132d5d1911d8d0c12bbc35f87ea0
4
- data.tar.gz: 63d7304396d67181ed0b35d35b37996801b901c7c9998d08945b6be88754d09f
3
+ metadata.gz: 8c3e974821a38f57a0d71c6a2ad0260942773836c3404c9b3665bb9cc0a6d305
4
+ data.tar.gz: 841b5bb5c5086942f47bbf7626455d4a27d4261778dfc845e22d2d5262d0ff41
5
5
  SHA512:
6
- metadata.gz: aeb01d6b88bcf617d83f273dbdbb3ace5bb1f12fa8094ad978d96f88f31ab058d12000e6964454d8f6630bd1f6fe70b6debe5ed3937445e8e07ac8de091e0fd7
7
- data.tar.gz: ba24e0621fcf6ebb6857cc820a96b0a2a9fde6c43953602dd9a295592fdb5e162a8e63dd91bcdca17ec8e63d913d08fd475b77402dac2a7f0b17f232996cfabd
6
+ metadata.gz: 88f20b95e78a0de07571c06560df3c9e45ac5927106dc600e6d8f4a9702611ee776d0f2c2edec9c9cfd5a26aacc5e91c727ae156043c46767ca995c4228f37b5
7
+ data.tar.gz: 8ba992243ac6ae15fb755d0817c75e6f8e41ec1cb4bad856c75cc9b7fe9ee3f760da92b97fcdbd1093677eaf83e93701c1f917ac82f26c650274e3891de74f6f
data/README.md CHANGED
@@ -78,6 +78,10 @@ These two operations are often confused:
78
78
  - **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.
79
79
  - **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; only Ruby-evaluated `source:` blocks need an explicit reindex.
80
80
 
81
+ ### Setup lifecycle
82
+
83
+ `full_search` evaluates the DSL block and installs callbacks when the model class loads, but it does **not** create the FTS table until Rails `after_initialize` (or until you call `FullSearch.setup!` or `FullSearch::Index.rebuild!` manually). This guarantees that `source:` blocks run against fully-loaded model definitions, avoiding load-order issues where associations or methods defined later in the class body are not yet available.
84
+
81
85
  > **Note on rebuild locking:** The `lock_rebuilds` option prevents concurrent rebuilds
82
86
  > within the same process/connection. For multi-process or multi-host deployments,
83
87
  > run `full_search:rebuild` from a single deployment step.
@@ -146,6 +150,18 @@ If you're not on Solid Queue, most job frameworks support recurring schedules vi
146
150
 
147
151
  Every FTS table stores a SHA256 digest of its DSL configuration in the `full_search_index_versions` table. When the DSL changes (e.g., adding a field), the stored hash no longer matches, and `rebuild!` is triggered automatically (if `auto_rebuild_schema` is true) to bring the index in line with the new definition. If `auto_rebuild_schema` is false, queries raise `ConfigChangedError` when the hash doesn't match (or log a warning and fall back if configured with `:log_and_fallback`).
148
152
 
153
+ ### Query-time auto-rebuild in local environments
154
+
155
+ By default, the generated initializer also enables `auto_rebuild_on_stale_query` in local environments:
156
+
157
+ ```ruby
158
+ FullSearch.configure do |config|
159
+ config.auto_rebuild_on_stale_query = Rails.env.local?
160
+ end
161
+ ```
162
+
163
+ 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.
164
+
149
165
  ## Query operators
150
166
 
151
167
  ```ruby
@@ -53,7 +53,7 @@ module FullSearch
53
53
  field = dsl.fields.find { |f| f.name == field_name }
54
54
  return unless field&.source
55
55
 
56
- value = record.instance_exec(&field.source)
56
+ value = FullSearch::Model.evaluate_source(record, field)
57
57
  table = qt(FullSearch::Index.fts_table_name(record.class))
58
58
  conn = ActiveRecord::Base.connection
59
59
  conn.execute(
@@ -2,7 +2,7 @@
2
2
 
3
3
  module FullSearch
4
4
  class Config
5
- attr_accessor :auto_rebuild_schema, :stale_query_behavior, :lock_rebuilds, :default_async_reindex, :default_tokenizer
5
+ attr_accessor :auto_rebuild_schema, :stale_query_behavior, :lock_rebuilds, :default_async_reindex, :default_tokenizer, :auto_rebuild_on_stale_query
6
6
 
7
7
  def initialize
8
8
  @auto_rebuild_schema = false
@@ -10,6 +10,7 @@ module FullSearch
10
10
  @lock_rebuilds = true
11
11
  @default_async_reindex = true
12
12
  @default_tokenizer = "unicode61"
13
+ @auto_rebuild_on_stale_query = false
13
14
  end
14
15
  end
15
16
 
@@ -21,46 +21,46 @@ module FullSearch
21
21
 
22
22
  def field(name, weight: 1, source: nil, reindex_on: nil, async: FullSearch.config.default_async_reindex, as: nil, version: nil)
23
23
  unless valid_name?(name)
24
- raise InvalidFieldError, "Invalid field name: #{name.inspect}"
24
+ raise InvalidFieldError, "#{model_class.name}: invalid field name #{name.inspect}"
25
25
  end
26
26
  if as && !valid_name?(as)
27
- raise InvalidFieldError, "Invalid field alias (as): #{as.inspect}"
27
+ raise InvalidFieldError, "#{model_class.name}: invalid field alias (as): #{as.inspect}"
28
28
  end
29
29
  str = name.to_s
30
- raise InvalidFieldError, "Duplicate field name: #{name.inspect}" if fields.any? { |f| f.name == str }
31
- raise InvalidFieldError, "Field name conflicts with filter: #{name.inspect}" if filters.any? { |f| f.name == str }
30
+ raise InvalidFieldError, "#{model_class.name}: duplicate field name #{name.inspect}" if fields.any? { |f| f.name == str }
31
+ raise InvalidFieldError, "#{model_class.name}: field name #{name.inspect} conflicts with existing filter" if filters.any? { |f| f.name == str }
32
32
  @fields << Field.new(name: str, weight: weight.to_i, source: source, reindex_on: reindex_on&.to_s, async: async, as: as&.to_s, version: version)
33
33
  end
34
34
 
35
35
  def exact_match(name, source: -> { public_send(name) }, version: nil)
36
36
  unless valid_name?(name)
37
- raise InvalidFieldError, "Invalid exact_match name: #{name.inspect}"
37
+ raise InvalidFieldError, "#{model_class.name}: invalid exact_match name #{name.inspect}"
38
38
  end
39
39
  str = name.to_s
40
- raise InvalidFieldError, "Duplicate exact_match name: #{name.inspect}" if exact_matches.any? { |e| e.name == str }
40
+ raise InvalidFieldError, "#{model_class.name}: duplicate exact_match name #{name.inspect}" if exact_matches.any? { |e| e.name == str }
41
41
  @exact_matches << ExactMatch.new(name: str, source: source, version: version)
42
42
  end
43
43
 
44
44
  def rank_by(column, direction = :desc)
45
45
  unless valid_name?(column)
46
- raise InvalidFieldError, "Invalid rank_by column: #{column.inspect}"
46
+ raise InvalidFieldError, "#{model_class.name}: invalid rank_by column #{column.inspect}"
47
47
  end
48
48
  dir = direction.to_s.downcase
49
49
  unless %w[asc desc].include?(dir)
50
- raise InvalidFieldError, "Invalid rank_by direction: #{direction.inspect}. Use :asc or :desc."
50
+ raise InvalidFieldError, "#{model_class.name}: invalid rank_by direction #{direction.inspect}. Use :asc or :desc."
51
51
  end
52
52
  str = column.to_s
53
- raise InvalidFieldError, "Duplicate rank_by column: #{column.inspect}" if rank_bys.any? { |r| r.column == str }
53
+ raise InvalidFieldError, "#{model_class.name}: duplicate rank_by column #{column.inspect}" if rank_bys.any? { |r| r.column == str }
54
54
  @rank_bys << RankBy.new(column: str, direction: dir.to_sym)
55
55
  end
56
56
 
57
57
  def filter(name, required: false)
58
58
  unless valid_name?(name)
59
- raise InvalidFieldError, "Invalid filter name: #{name.inspect}"
59
+ raise InvalidFieldError, "#{model_class.name}: invalid filter name #{name.inspect}"
60
60
  end
61
61
  str = name.to_s
62
- raise InvalidFieldError, "Duplicate filter name: #{name.inspect}" if filters.any? { |f| f.name == str }
63
- raise InvalidFieldError, "Filter name conflicts with field: #{name.inspect}" if fields.any? { |f| f.name == str }
62
+ raise InvalidFieldError, "#{model_class.name}: duplicate filter name #{name.inspect}" if filters.any? { |f| f.name == str }
63
+ raise InvalidFieldError, "#{model_class.name}: filter name #{name.inspect} conflicts with existing field" if fields.any? { |f| f.name == str }
64
64
  @filters << Filter.new(name: str, required: required)
65
65
  end
66
66
 
@@ -11,7 +11,6 @@ module FullSearch
11
11
  @full_search_dsl.tokenize(query_or_options[:tokenize]) if query_or_options.is_a?(Hash) && query_or_options.key?(:tokenize)
12
12
  @full_search_dsl.instance_eval(&block) if block_given?
13
13
  FullSearch::Callbacks.install!(self)
14
- FullSearch::Index.ensure_table!(self)
15
14
  include InstanceMethods
16
15
 
17
16
  FullSearch.register_model(self)
@@ -48,6 +47,16 @@ module FullSearch
48
47
  end
49
48
  end
50
49
 
50
+ def self.evaluate_source(record, field)
51
+ if field.source
52
+ record.instance_exec(&field.source)
53
+ else
54
+ record.public_send(field.name)
55
+ end
56
+ rescue NameError => e
57
+ raise e.class, "#{record.class.name}: full_search source block for field #{field.name.inspect} failed: #{e.message}"
58
+ end
59
+
51
60
  module InstanceMethods
52
61
  attr_accessor :full_search_snippet, :full_search_highlight_fields
53
62
 
@@ -56,7 +65,7 @@ module FullSearch
56
65
  field = dsl.fields.find { |f| f.name == field_name.to_s || f.as == field_name.to_s }
57
66
  return nil unless field
58
67
 
59
- field.source ? instance_exec(&field.source) : public_send(field.name)
68
+ FullSearch::Model.evaluate_source(self, field)
60
69
  end
61
70
  end
62
71
  end
@@ -76,13 +76,18 @@ module FullSearch
76
76
  stored = FullSearch::Index.stored_config_hash(model)
77
77
  return unless stored
78
78
 
79
- if stored != dsl.config_hash
80
- case FullSearch.config.stale_query_behavior
81
- when :raise
82
- raise ConfigChangedError, "FTS index for #{model.table_name} is stale; run full_search:rebuild"
83
- when :log_and_fallback
84
- Rails.logger.warn("[full_search] FTS index for #{model.table_name} is stale; results may be incomplete")
85
- end
79
+ return if stored == dsl.config_hash
80
+
81
+ if FullSearch.config.auto_rebuild_on_stale_query
82
+ FullSearch::Index.rebuild_if_needed!(model)
83
+ return
84
+ end
85
+
86
+ case FullSearch.config.stale_query_behavior
87
+ when :raise
88
+ raise ConfigChangedError, "FTS index for #{model.table_name} is stale; run full_search:rebuild"
89
+ when :log_and_fallback
90
+ Rails.logger.warn("[full_search] FTS index for #{model.table_name} is stale; results may be incomplete")
86
91
  end
87
92
  end
88
93
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FullSearch
4
- VERSION = "0.3.2"
4
+ VERSION = "0.3.3"
5
5
  end
@@ -3,6 +3,8 @@
3
3
  FullSearch.configure do |config|
4
4
  config.auto_rebuild_schema = Rails.env.local?
5
5
 
6
+ config.auto_rebuild_on_stale_query = Rails.env.local?
7
+
6
8
  config.stale_query_behavior = Rails.env.production? ? :log_and_fallback : :raise
7
9
 
8
10
  config.lock_rebuilds = true
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.2
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben D'Angelo