full_search 0.3.5 → 0.3.7
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 +101 -0
- data/lib/full_search/config.rb +2 -1
- data/lib/full_search/dsl.rb +13 -4
- data/lib/full_search/errors.rb +1 -0
- data/lib/full_search/exact_match.rb +2 -1
- data/lib/full_search/index.rb +18 -7
- data/lib/full_search/index_cache.rb +24 -0
- data/lib/full_search/model.rb +2 -2
- data/lib/full_search/multi_search.rb +3 -3
- data/lib/full_search/search.rb +19 -5
- data/lib/full_search/version.rb +1 -1
- data/lib/full_search.rb +1 -0
- data/lib/generators/full_search/install/install_generator.rb +13 -0
- data/lib/tasks/full_search.rake +23 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d25d24145bf1aef1508210eb9d6879b21218ed8760ef6a61812706720a12ac17
|
|
4
|
+
data.tar.gz: 94b71b2b86db367ef8fde640550ab7b5945b50829d618c5e4eb41a4d9a6e0d65
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4074fd91514f5f209cbf6d5f4b2fdd36eed20ccfd894e0ccaac330e25bf7e0cda0c2c689833f2fcf947df768cf0282e88d717f1a001797e901ee0ebb18ab6e1a
|
|
7
|
+
data.tar.gz: f16291785a67d4e9cec7e48fe407a3c4735e21711752feea5ff2ad88573ef8010732feb8eaa9a1f1c40bfb14c14f476dd5f242fb9e624ba68f1a4ec18ca077ba
|
data/README.md
CHANGED
|
@@ -29,6 +29,13 @@ bundle install
|
|
|
29
29
|
bin/rails generate full_search:install
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
This creates the initializer and automatically runs `full_search:prepare` to create FTS tables.
|
|
33
|
+
Use `--skip-prepare` if your database isn't ready yet:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
bin/rails generate full_search:install --skip-prepare
|
|
37
|
+
```
|
|
38
|
+
|
|
32
39
|
## Usage
|
|
33
40
|
|
|
34
41
|
```ruby
|
|
@@ -96,6 +103,34 @@ end
|
|
|
96
103
|
|
|
97
104
|
This is enforced at the database level — the FTS virtual table only contains rows matching the condition, and triggers skip inserts/updates for non-matching records.
|
|
98
105
|
|
|
106
|
+
### Exact match
|
|
107
|
+
|
|
108
|
+
For exact-match lookups on encrypted identifiers or fields that need precise matching outside the FTS tokenizer, use `exact_match`:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
class Customer < ApplicationRecord
|
|
112
|
+
full_search do
|
|
113
|
+
field :first_name, weight: 5
|
|
114
|
+
filter :account_id, required: true
|
|
115
|
+
|
|
116
|
+
exact_match :customer_number
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
Customer.search("CUST-001", filters: { account_id: 1 })
|
|
123
|
+
# same query but only returns exact matches on customer_number
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
For case- or punctuation-normalized SQL exact matches, pass a `normalize` lambda that transforms the query the same way the SQL expression transforms the column:
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
exact_match :license_plate,
|
|
130
|
+
sql: "UPPER(REPLACE(REPLACE(license_plate, ' ', ''), '-', ''))",
|
|
131
|
+
normalize: ->(q) { q.to_s.upcase.gsub(/[ -]/, "") }
|
|
132
|
+
```
|
|
133
|
+
|
|
99
134
|
### Per-model operations
|
|
100
135
|
|
|
101
136
|
Once a model declares `full_search`, you can call these class methods:
|
|
@@ -125,6 +160,22 @@ These two operations are often confused:
|
|
|
125
160
|
> within the same process/connection. For multi-process or multi-host deployments,
|
|
126
161
|
> run `full_search:rebuild` from a single deployment step.
|
|
127
162
|
|
|
163
|
+
### First-deploy setup
|
|
164
|
+
|
|
165
|
+
After `db:prepare` creates your application tables, FTS virtual tables like `customers_fts` do not yet exist. Run `full_search:prepare` to create them:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
bin/rails full_search:prepare
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
For Docker-based deployments, add this to your entrypoint script after `db:prepare:with_data`:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
./bin/rails full_search:prepare
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
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.
|
|
178
|
+
|
|
128
179
|
### Auto-rebuild on app load
|
|
129
180
|
|
|
130
181
|
When `auto_rebuild_schema` is enabled (default in the generated initializer), the railtie hooks into Rails `after_initialize` and:
|
|
@@ -162,6 +213,7 @@ bin/rails full_search:reset
|
|
|
162
213
|
|
|
163
214
|
| Task | Description |
|
|
164
215
|
|------|-------------|
|
|
216
|
+
| `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. |
|
|
165
217
|
| `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. |
|
|
166
218
|
| `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. |
|
|
167
219
|
| `full_search:optimize` | Run FTS5 [`optimize`](https://www.sqlite.org/fts5.html#the_optimize_command) to merge b-tree segments. Useful after bulk updates. |
|
|
@@ -223,6 +275,17 @@ end
|
|
|
223
275
|
|
|
224
276
|
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.
|
|
225
277
|
|
|
278
|
+
### Missing table error
|
|
279
|
+
|
|
280
|
+
When the FTS table does not exist and a search is attempted, `full_search` raises `MissingTableError` with a clear message:
|
|
281
|
+
|
|
282
|
+
```
|
|
283
|
+
FullSearch::MissingTableError: FTS table `customers_fts` does not exist.
|
|
284
|
+
Run `bin/rails full_search:prepare` to create it.
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
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.
|
|
288
|
+
|
|
226
289
|
## Query operators
|
|
227
290
|
|
|
228
291
|
```ruby
|
|
@@ -260,6 +323,44 @@ end
|
|
|
260
323
|
|
|
261
324
|
`typo_tolerance` uses an FTS5 trigram shadow table as a fallback when the primary index returns no results. It is substring/trigram fallback, not edit-distance correction, and requires SQLite >= 3.34.
|
|
262
325
|
|
|
326
|
+
You can disable the expensive `LIKE 'term%'` fallback for very short queries:
|
|
327
|
+
|
|
328
|
+
```ruby
|
|
329
|
+
full_search do
|
|
330
|
+
typo_tolerance
|
|
331
|
+
min_like_prefix_length 3
|
|
332
|
+
end
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## Multi-search
|
|
336
|
+
|
|
337
|
+
Search across multiple models in a single call with `FullSearch.multi_search`:
|
|
338
|
+
|
|
339
|
+
```ruby
|
|
340
|
+
results = FullSearch.multi_search(
|
|
341
|
+
query: "Honda",
|
|
342
|
+
groups: [
|
|
343
|
+
{key: :customers, label: "Customers", model: Customer, filters: {account_id: 1}},
|
|
344
|
+
{key: :vehicles, label: "Vehicles", model: Vehicle, filters: {account_id: 1}}
|
|
345
|
+
]
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
results[:customers] #=> [#<Customer...>, ...]
|
|
349
|
+
results[:vehicles] #=> [#<Vehicle...>, ...]
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Pass `includes:` to eager-load associations for each group:
|
|
353
|
+
|
|
354
|
+
```ruby
|
|
355
|
+
FullSearch.multi_search(
|
|
356
|
+
query: "Honda",
|
|
357
|
+
groups: [
|
|
358
|
+
{key: :vehicles, label: "Vehicles", model: Vehicle,
|
|
359
|
+
filters: {account_id: 1}, includes: [:customer]}
|
|
360
|
+
]
|
|
361
|
+
)
|
|
362
|
+
```
|
|
363
|
+
|
|
263
364
|
## Known limitations
|
|
264
365
|
|
|
265
366
|
- 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/config.rb
CHANGED
|
@@ -4,7 +4,7 @@ module FullSearch
|
|
|
4
4
|
class Config
|
|
5
5
|
attr_accessor :auto_rebuild_schema, :stale_query_behavior, :lock_rebuilds,
|
|
6
6
|
:default_async_reindex, :default_async_source_reindex,
|
|
7
|
-
:default_tokenizer, :auto_rebuild_on_stale_query
|
|
7
|
+
:default_tokenizer, :auto_rebuild_on_stale_query, :min_like_prefix_length
|
|
8
8
|
|
|
9
9
|
def initialize
|
|
10
10
|
@auto_rebuild_schema = false
|
|
@@ -14,6 +14,7 @@ module FullSearch
|
|
|
14
14
|
@default_async_source_reindex = true
|
|
15
15
|
@default_tokenizer = "unicode61"
|
|
16
16
|
@auto_rebuild_on_stale_query = false
|
|
17
|
+
@min_like_prefix_length = 3
|
|
17
18
|
end
|
|
18
19
|
end
|
|
19
20
|
|
data/lib/full_search/dsl.rb
CHANGED
|
@@ -5,7 +5,7 @@ module FullSearch
|
|
|
5
5
|
attr_reader :fields, :exact_matches, :filters, :model_class, :highlight_config, :rank_bys, :index_if_sql
|
|
6
6
|
|
|
7
7
|
Field = Data.define(:name, :weight, :source, :reindex_on, :async, :async_source, :as, :version)
|
|
8
|
-
ExactMatch = Data.define(:name, :source, :sql, :version)
|
|
8
|
+
ExactMatch = Data.define(:name, :source, :sql, :version, :normalize)
|
|
9
9
|
Filter = Data.define(:name, :required)
|
|
10
10
|
RankBy = Data.define(:column, :direction)
|
|
11
11
|
|
|
@@ -39,13 +39,13 @@ module FullSearch
|
|
|
39
39
|
)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
def exact_match(name, source: -> { public_send(name) }, sql: nil, version: nil)
|
|
42
|
+
def exact_match(name, source: -> { public_send(name) }, sql: nil, normalize: nil, version: nil)
|
|
43
43
|
unless valid_name?(name)
|
|
44
44
|
raise InvalidFieldError, "#{model_class.name}: invalid exact_match name #{name.inspect}"
|
|
45
45
|
end
|
|
46
46
|
str = name.to_s
|
|
47
47
|
raise InvalidFieldError, "#{model_class.name}: duplicate exact_match name #{name.inspect}" if exact_matches.any? { |e| e.name == str }
|
|
48
|
-
@exact_matches << ExactMatch.new(name: str, source: source, sql: sql&.to_s, version: version)
|
|
48
|
+
@exact_matches << ExactMatch.new(name: str, source: source, sql: sql&.to_s, normalize: normalize, version: version)
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
def rank_by(column, direction = :desc)
|
|
@@ -99,6 +99,14 @@ module FullSearch
|
|
|
99
99
|
@index_if_sql.present?
|
|
100
100
|
end
|
|
101
101
|
|
|
102
|
+
def min_like_prefix_length(value = :_no_arg_)
|
|
103
|
+
if value == :_no_arg_
|
|
104
|
+
@min_like_prefix_length ||= FullSearch.config.min_like_prefix_length
|
|
105
|
+
else
|
|
106
|
+
@min_like_prefix_length = value.to_i
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
102
110
|
def typo_tolerance(enabled = true, min_term_length: nil)
|
|
103
111
|
@typo_tolerance = enabled
|
|
104
112
|
@typo_tolerance_min_term_length = min_term_length || 3
|
|
@@ -124,7 +132,8 @@ module FullSearch
|
|
|
124
132
|
fields.map { |f| [f.name, f.weight, f.source.nil? ? "column" : "proc:#{f.version}", f.reindex_on, f.async, f.async_source, f.as] },
|
|
125
133
|
exact_matches.map { |e| [e.name, "proc:#{e.version}", e.sql] },
|
|
126
134
|
filters.map { |f| [f.name, f.required] },
|
|
127
|
-
rank_bys.map { |r| [r.column, r.direction] }
|
|
135
|
+
rank_bys.map { |r| [r.column, r.direction] },
|
|
136
|
+
min_like_prefix_length
|
|
128
137
|
].inspect)
|
|
129
138
|
end
|
|
130
139
|
|
data/lib/full_search/errors.rb
CHANGED
|
@@ -19,7 +19,8 @@ module FullSearch
|
|
|
19
19
|
filters.each { |name, value| base = base.where(name => value) }
|
|
20
20
|
|
|
21
21
|
conditions = sql_matches.map do |em|
|
|
22
|
-
|
|
22
|
+
value = em.normalize ? em.normalize.call(cleaned) : cleaned
|
|
23
|
+
"(#{em.sql}) = #{model.connection.quote(value)}"
|
|
23
24
|
end.join(" OR ")
|
|
24
25
|
|
|
25
26
|
ids += base.where(conditions).pluck(:id)
|
data/lib/full_search/index.rb
CHANGED
|
@@ -55,6 +55,7 @@ module FullSearch
|
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
def rebuild!(model)
|
|
58
|
+
IndexCache.clear!
|
|
58
59
|
sqlite!(model)
|
|
59
60
|
|
|
60
61
|
dsl = model.full_search_dsl
|
|
@@ -83,6 +84,7 @@ module FullSearch
|
|
|
83
84
|
end
|
|
84
85
|
|
|
85
86
|
def rebuild_if_needed!(model)
|
|
87
|
+
IndexCache.clear!
|
|
86
88
|
sqlite!(model)
|
|
87
89
|
|
|
88
90
|
dsl = model.full_search_dsl
|
|
@@ -137,6 +139,7 @@ module FullSearch
|
|
|
137
139
|
end
|
|
138
140
|
|
|
139
141
|
def drop!(model)
|
|
142
|
+
IndexCache.clear!
|
|
140
143
|
verified_tables.delete(model.table_name)
|
|
141
144
|
sqlite!(model)
|
|
142
145
|
drop_triggers!(model)
|
|
@@ -168,10 +171,16 @@ module FullSearch
|
|
|
168
171
|
end
|
|
169
172
|
|
|
170
173
|
def stored_config_hash(model)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
174
|
+
IndexCache.fetch("stored_config_hash:#{model.table_name}") do
|
|
175
|
+
row = connection.execute(
|
|
176
|
+
"SELECT config_hash FROM full_search_index_versions WHERE table_name=#{q(model.table_name)}"
|
|
177
|
+
).first
|
|
178
|
+
row&.[]("config_hash")
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def missing_table?(model)
|
|
183
|
+
!table_exists?(model)
|
|
175
184
|
end
|
|
176
185
|
|
|
177
186
|
private
|
|
@@ -203,9 +212,11 @@ module FullSearch
|
|
|
203
212
|
end
|
|
204
213
|
|
|
205
214
|
def table_exists?(model)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
215
|
+
IndexCache.fetch("table_exists:#{model.table_name}") do
|
|
216
|
+
connection.execute(
|
|
217
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name=#{q(fts_table_name(model))} LIMIT 1"
|
|
218
|
+
).any?
|
|
219
|
+
end
|
|
209
220
|
end
|
|
210
221
|
|
|
211
222
|
def create_metadata_table!
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FullSearch
|
|
4
|
+
class IndexCache
|
|
5
|
+
def self.with_cache
|
|
6
|
+
already_cached = !Thread.current[:full_search_index_cache].nil?
|
|
7
|
+
Thread.current[:full_search_index_cache] ||= {}
|
|
8
|
+
yield
|
|
9
|
+
ensure
|
|
10
|
+
Thread.current[:full_search_index_cache] = nil unless already_cached
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.clear!
|
|
14
|
+
Thread.current[:full_search_index_cache] = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.fetch(key)
|
|
18
|
+
cache = Thread.current[:full_search_index_cache]
|
|
19
|
+
return yield unless cache
|
|
20
|
+
return cache[key] if cache.key?(key)
|
|
21
|
+
cache[key] = yield
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/full_search/model.rb
CHANGED
|
@@ -5,7 +5,7 @@ module FullSearch
|
|
|
5
5
|
extend ActiveSupport::Concern
|
|
6
6
|
|
|
7
7
|
class_methods do
|
|
8
|
-
def full_search(query_or_options = nil, filters: {}, include_soft_deleted: false, limit: nil, offset: nil, highlight: false, highlight_fields: false, matching_strategy: nil, per_strategy_limit: nil, &block)
|
|
8
|
+
def full_search(query_or_options = nil, filters: {}, include_soft_deleted: false, limit: nil, offset: nil, highlight: false, highlight_fields: false, matching_strategy: nil, per_strategy_limit: nil, scope: nil, includes: nil, &block)
|
|
9
9
|
if block_given? || query_or_options.is_a?(Hash)
|
|
10
10
|
@full_search_dsl ||= FullSearch::Dsl.new(self)
|
|
11
11
|
@full_search_dsl.tokenize(query_or_options[:tokenize]) if query_or_options.is_a?(Hash) && query_or_options.key?(:tokenize)
|
|
@@ -16,7 +16,7 @@ module FullSearch
|
|
|
16
16
|
FullSearch.register_model(self)
|
|
17
17
|
@full_search_dsl
|
|
18
18
|
else
|
|
19
|
-
FullSearch::Search.new(self, query_or_options, filters: filters, include_soft_deleted: include_soft_deleted, limit: limit, offset: offset, highlight: highlight, highlight_fields: highlight_fields, matching_strategy: matching_strategy, per_strategy_limit: per_strategy_limit).relation
|
|
19
|
+
FullSearch::Search.new(self, query_or_options, filters: filters, include_soft_deleted: include_soft_deleted, limit: limit, offset: offset, highlight: highlight, highlight_fields: highlight_fields, matching_strategy: matching_strategy, per_strategy_limit: per_strategy_limit, scope: scope, includes: includes).relation
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
|
|
@@ -29,11 +29,11 @@ module FullSearch
|
|
|
29
29
|
highlight: group[:highlight],
|
|
30
30
|
highlight_fields: group[:highlight_fields],
|
|
31
31
|
matching_strategy: group[:matching_strategy],
|
|
32
|
-
per_strategy_limit: group[:per_strategy_limit]
|
|
32
|
+
per_strategy_limit: group[:per_strategy_limit],
|
|
33
|
+
scope: group[:scope],
|
|
34
|
+
includes: group[:includes]
|
|
33
35
|
)
|
|
34
36
|
|
|
35
|
-
relation = group[:scope].call(relation) if group[:scope]
|
|
36
|
-
|
|
37
37
|
records = relation.to_a
|
|
38
38
|
has_more = records.size > limit
|
|
39
39
|
records = records.first(limit) if has_more
|
data/lib/full_search/search.rb
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module FullSearch
|
|
4
4
|
class Search
|
|
5
|
-
attr_reader :model, :query, :filters, :include_soft_deleted, :limit, :offset, :highlight, :highlight_fields, :matching_strategy, :per_strategy_limit
|
|
5
|
+
attr_reader :model, :query, :filters, :include_soft_deleted, :limit, :offset, :highlight, :highlight_fields, :matching_strategy, :per_strategy_limit, :scope, :includes
|
|
6
6
|
|
|
7
|
-
def initialize(model, query, filters:, include_soft_deleted:, limit:, offset:, highlight: false, highlight_fields: false, matching_strategy: nil, per_strategy_limit: nil)
|
|
7
|
+
def initialize(model, query, filters:, include_soft_deleted:, limit:, offset:, highlight: false, highlight_fields: false, matching_strategy: nil, per_strategy_limit: nil, scope: nil, includes: nil)
|
|
8
8
|
@model = model
|
|
9
9
|
@query = query.to_s.strip
|
|
10
10
|
@filters = filters
|
|
@@ -15,6 +15,8 @@ module FullSearch
|
|
|
15
15
|
@highlight_fields = highlight_fields
|
|
16
16
|
@matching_strategy = matching_strategy
|
|
17
17
|
@per_strategy_limit = per_strategy_limit
|
|
18
|
+
@scope = scope
|
|
19
|
+
@includes = includes
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
MIN_TERM_LENGTH = 3
|
|
@@ -37,10 +39,11 @@ module FullSearch
|
|
|
37
39
|
|
|
38
40
|
rel = model.where(id: all_ids)
|
|
39
41
|
rel = rel.where(model.arel_table[dsl.soft_delete_column].eq(nil)) if dsl.soft_delete_column && !include_soft_deleted
|
|
42
|
+
rel = apply_ranking(rel, all_ids, exact_ids)
|
|
43
|
+
rel = scope.call(rel) if scope
|
|
40
44
|
rel = rel.limit(limit) if limit
|
|
41
45
|
rel = rel.offset(offset) if offset
|
|
42
|
-
|
|
43
|
-
rel = apply_ranking(rel, all_ids, exact_ids)
|
|
46
|
+
rel = rel.includes(includes) if includes
|
|
44
47
|
|
|
45
48
|
if highlight
|
|
46
49
|
records = rel.to_a
|
|
@@ -78,7 +81,15 @@ module FullSearch
|
|
|
78
81
|
end
|
|
79
82
|
|
|
80
83
|
def check_stale_config!
|
|
81
|
-
|
|
84
|
+
if FullSearch::Index.missing_table?(model)
|
|
85
|
+
raise MissingTableError, "FTS table `#{FullSearch::Index.fts_table_name(model)}` does not exist. Run `bin/rails full_search:prepare` to create it."
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
stored = begin
|
|
89
|
+
FullSearch::Index.stored_config_hash(model)
|
|
90
|
+
rescue
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
82
93
|
return unless stored
|
|
83
94
|
|
|
84
95
|
return if stored == dsl.config_hash
|
|
@@ -129,6 +140,7 @@ module FullSearch
|
|
|
129
140
|
return [] if term.nil?
|
|
130
141
|
|
|
131
142
|
if term.length < dsl.typo_tolerance_min_term_length.to_i
|
|
143
|
+
return [] if term.length < dsl.min_like_prefix_length
|
|
132
144
|
return like_prefix_ids(term, candidate_limit: candidate_limit)
|
|
133
145
|
end
|
|
134
146
|
|
|
@@ -154,6 +166,8 @@ module FullSearch
|
|
|
154
166
|
end
|
|
155
167
|
|
|
156
168
|
def like_prefix_ids(term, candidate_limit: nil)
|
|
169
|
+
return [] if term.to_s.length < dsl.min_like_prefix_length
|
|
170
|
+
|
|
157
171
|
column_fields = dsl.fields.select { |f| f.source.nil? }
|
|
158
172
|
source_fields = dsl.fields.select { |f| f.source }
|
|
159
173
|
tbl = qt(model.table_name)
|
data/lib/full_search/version.rb
CHANGED
data/lib/full_search.rb
CHANGED
|
@@ -7,9 +7,22 @@ module FullSearch
|
|
|
7
7
|
class InstallGenerator < Rails::Generators::Base
|
|
8
8
|
source_root File.expand_path("templates", __dir__)
|
|
9
9
|
|
|
10
|
+
class_option :skip_prepare, type: :boolean, default: false,
|
|
11
|
+
desc: "Skip automatic full_search:prepare"
|
|
12
|
+
|
|
10
13
|
def create_initializer
|
|
11
14
|
template "full_search.rb", "config/initializers/full_search.rb"
|
|
12
15
|
end
|
|
16
|
+
|
|
17
|
+
def prepare_indexes
|
|
18
|
+
return if options[:skip_prepare]
|
|
19
|
+
|
|
20
|
+
say "Running full_search:prepare to create FTS tables..."
|
|
21
|
+
rake("full_search:prepare")
|
|
22
|
+
rescue => e
|
|
23
|
+
say "Skipping full_search:prepare — #{e.message}", :yellow
|
|
24
|
+
say "Run `bin/rails full_search:prepare` after your database is ready.", :yellow
|
|
25
|
+
end
|
|
13
26
|
end
|
|
14
27
|
end
|
|
15
28
|
end
|
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!
|
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
|
+
version: 0.3.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ben D'Angelo
|
|
@@ -150,6 +150,7 @@ files:
|
|
|
150
150
|
- lib/full_search/exact_match.rb
|
|
151
151
|
- lib/full_search/highlighter.rb
|
|
152
152
|
- lib/full_search/index.rb
|
|
153
|
+
- lib/full_search/index_cache.rb
|
|
153
154
|
- lib/full_search/model.rb
|
|
154
155
|
- lib/full_search/multi_search.rb
|
|
155
156
|
- lib/full_search/optimize_job.rb
|