full_search 0.1.3 → 0.3.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 +115 -9
- data/lib/full_search/callbacks.rb +26 -8
- data/lib/full_search/config.rb +2 -2
- data/lib/full_search/distance.rb +33 -0
- data/lib/full_search/dsl.rb +29 -12
- data/lib/full_search/errors.rb +2 -0
- data/lib/full_search/exact_match.rb +7 -4
- data/lib/full_search/highlighter.rb +175 -80
- data/lib/full_search/index.rb +134 -44
- data/lib/full_search/model.rb +23 -4
- data/lib/full_search/multi_search.rb +4 -1
- data/lib/full_search/optimize_job.rb +13 -0
- data/lib/full_search/query_parser.rb +101 -88
- data/lib/full_search/quoting.rb +11 -0
- data/lib/full_search/railtie.rb +2 -8
- data/lib/full_search/search.rb +161 -30
- data/lib/full_search/soft_delete.rb +15 -2
- data/lib/full_search/typo.rb +10 -8
- data/lib/full_search/version.rb +1 -1
- data/lib/full_search.rb +16 -1
- data/lib/generators/full_search/install/templates/full_search.rb +1 -1
- data/lib/tasks/full_search.rake +50 -14
- metadata +55 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 50bb1c53fe9f326612a2413a441a37acfe48f25bf4730786de3631c7230b2572
|
|
4
|
+
data.tar.gz: ab53bbd57f8fd9cb464af29d37aa70cd0efb18a2835eff1b501d61a14d0de43f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ab98601c786c02808246cf4657ef7c3912e36e0fbc429417c0ff564a3ec1e2575216fda8d64ee2ab22db32c707bcbce0915fcc3d57038059f1dfe05bde48963e
|
|
7
|
+
data.tar.gz: 52564a1ce083d5aa0f6cb624f8f9aa4a0bb0ac13a85216f14a857a5c2c2c744d37fa4370272679c3bd13c76eb6ec732814463cfa143bb714654243968c6c800c
|
data/README.md
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
# full_search
|
|
2
2
|
|
|
3
|
+
[](https://github.com/bendangelo/full_search/actions/workflows/ci.yml)
|
|
4
|
+
|
|
3
5
|
SQLite FTS5 full-text search for Rails/ActiveRecord. A lightweight, self-contained alternative to `pg_search` for apps already running on SQLite.
|
|
4
6
|
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Ruby 3.1+
|
|
10
|
+
- Rails 8.0+
|
|
11
|
+
- SQLite 3.34+ (if using typo tolerance)
|
|
12
|
+
|
|
13
|
+
## When to use
|
|
14
|
+
|
|
15
|
+
`full_search` is designed for apps with **100,000 records or fewer per table** that want full-text search without running a separate service. If you're on SQLite and need keyword search, phrase matching, typo-tolerant substring queries, or result highlighting, this gem gives you full-text search suitable for small to medium SQLite-backed apps with zero infrastructure — no Elasticsearch, no Meilisearch, no Sidekiq queue.
|
|
16
|
+
|
|
5
17
|
## Installation
|
|
6
18
|
|
|
7
19
|
Add to your Gemfile:
|
|
@@ -30,12 +42,12 @@ end
|
|
|
30
42
|
```
|
|
31
43
|
|
|
32
44
|
```ruby
|
|
33
|
-
Customer.
|
|
45
|
+
Customer.search("sam", filters: { account_id: 1 }).page(params[:page])
|
|
34
46
|
```
|
|
35
47
|
|
|
36
48
|
## Features
|
|
37
49
|
|
|
38
|
-
- Declarative `full_search` DSL
|
|
50
|
+
- Declarative `full_search` DSL with `search` / `full_search` query methods
|
|
39
51
|
- SQLite FTS5 backed
|
|
40
52
|
- Required-filter enforcement for multi-tenant apps
|
|
41
53
|
- Exact-match queries for encrypted identifiers
|
|
@@ -44,14 +56,102 @@ Customer.full_search("sam", filters: { account_id: 1 }).page(params[:page])
|
|
|
44
56
|
- Phrase, exclusion, and OR query operators
|
|
45
57
|
- FTS5 `highlight()` support for result snippets
|
|
46
58
|
- Opt-in trigram typo/substring fallback (requires SQLite >= 3.34)
|
|
47
|
-
|
|
59
|
+
|
|
60
|
+
### Per-model operations
|
|
61
|
+
|
|
62
|
+
Once a model declares `full_search`, you can call these class methods:
|
|
63
|
+
|
|
64
|
+
| Method | What it does |
|
|
65
|
+
|--------|-------------|
|
|
66
|
+
| `Customer.rebuild!` | Force-drop and recreate the model's FTS table, backfill from the source table, and reinstall triggers. |
|
|
67
|
+
| `Customer.reindex!` | Re-evaluates computed `source:` fields for every record and updates the FTS table in-place. Table structure is untouched. |
|
|
68
|
+
| `Customer.optimize!` | Runs FTS5 `optimize` on the model's index to merge b-tree segments. |
|
|
69
|
+
|
|
70
|
+
## Index management
|
|
71
|
+
|
|
72
|
+
FTS indexes are SQLite virtual tables (`customers_fts`, `vehicles_fts`, etc.) that mirror your model tables. They stay in sync via database triggers on `INSERT` / `UPDATE` / `DELETE`.
|
|
73
|
+
|
|
74
|
+
### Rebuild vs reindex
|
|
75
|
+
|
|
76
|
+
These two operations are often confused:
|
|
77
|
+
|
|
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
|
+
- **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
|
+
|
|
81
|
+
> **Note on rebuild locking:** The `lock_rebuilds` option prevents concurrent rebuilds
|
|
82
|
+
> within the same process/connection. For multi-process or multi-host deployments,
|
|
83
|
+
> run `full_search:rebuild` from a single deployment step.
|
|
84
|
+
|
|
85
|
+
### Auto-rebuild on app load
|
|
86
|
+
|
|
87
|
+
When `auto_rebuild_schema` is enabled (default in the generated initializer), the railtie hooks into Rails `after_initialize` and:
|
|
88
|
+
|
|
89
|
+
1. Creates missing FTS tables for every model using `full_search`
|
|
90
|
+
2. Compares each table's stored config hash against the current DSL
|
|
91
|
+
3. Rebuilds the index automatically when the DSL changes (new fields, different tokenizer, etc.)
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
# config/initializers/full_search.rb
|
|
95
|
+
FullSearch.configure do |config|
|
|
96
|
+
config.auto_rebuild_schema = true
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### In production
|
|
101
|
+
|
|
102
|
+
Auto-rebuild runs on every Rails process boot (web, worker, console). For zero-downtime deploys where old processes still serve traffic, or if you prefer explicit control, set `auto_rebuild_schema` to `false` and run the rebuild task manually:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# Rebuild only indexes whose DSL has changed (fast, safe for production)
|
|
106
|
+
bin/rails full_search:rebuild
|
|
107
|
+
|
|
108
|
+
# Target specific models by table name
|
|
109
|
+
bin/rails 'full_search:rebuild[customers,vehicles]'
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The rebuild task checks each model's stored config hash against the current DSL and only drops/recreates indexes when they differ. For a full forced reset (drops and recreates all FTS tables regardless):
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
bin/rails full_search:reset
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Rake tasks
|
|
119
|
+
|
|
120
|
+
| Task | Description |
|
|
121
|
+
|------|-------------|
|
|
122
|
+
| `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. |
|
|
123
|
+
| `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. |
|
|
124
|
+
| `full_search:optimize` | Run FTS5 [`optimize`](https://www.sqlite.org/fts5.html#the_optimize_command) to merge b-tree segments. Useful after bulk updates. |
|
|
125
|
+
| `full_search:status` | Show each model's index status (`ok` / `stale`) and count of empty sourced fields. |
|
|
126
|
+
|
|
127
|
+
## Background jobs
|
|
128
|
+
|
|
129
|
+
### Scheduled optimization
|
|
130
|
+
|
|
131
|
+
FTS5 b-tree segments accumulate over time as rows are inserted, updated, and deleted. Running `optimize` periodically merges these segments, keeping queries fast. For most apps, once a day during a low-traffic window is sufficient. Apps with heavy write volume may benefit from every few hours.
|
|
132
|
+
|
|
133
|
+
The gem ships `FullSearch::OptimizeJob` ready to use. Schedule it with Solid Queue's `recurring.yml`:
|
|
134
|
+
|
|
135
|
+
```yaml
|
|
136
|
+
# config/recurring.yml
|
|
137
|
+
full_search_optimize:
|
|
138
|
+
class: FullSearch::OptimizeJob
|
|
139
|
+
schedule: daily at 4am
|
|
140
|
+
description: "Merge FTS5 b-tree segments for full_search indexes"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
If you're not on Solid Queue, most job frameworks support recurring schedules via gems like `sidekiq-cron` or `whenever`.
|
|
144
|
+
|
|
145
|
+
### Config hash drift detection
|
|
146
|
+
|
|
147
|
+
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`).
|
|
48
148
|
|
|
49
149
|
## Query operators
|
|
50
150
|
|
|
51
151
|
```ruby
|
|
52
|
-
Customer.
|
|
53
|
-
Customer.
|
|
54
|
-
Customer.
|
|
152
|
+
Customer.search('"Sam Smith"', filters: { account_id: 1 }) # phrase
|
|
153
|
+
Customer.search("honda -civic", filters: { account_id: 1 }) # exclusion
|
|
154
|
+
Customer.search("honda OR toyota", filters: { account_id: 1 }) # OR
|
|
55
155
|
```
|
|
56
156
|
|
|
57
157
|
## Highlighting
|
|
@@ -66,7 +166,7 @@ end
|
|
|
66
166
|
```
|
|
67
167
|
|
|
68
168
|
```ruby
|
|
69
|
-
Customer.
|
|
169
|
+
Customer.search("sam", filters: { account_id: 1 }, highlight: true)
|
|
70
170
|
# each result has #full_search_snippet
|
|
71
171
|
```
|
|
72
172
|
|
|
@@ -81,9 +181,15 @@ class Customer < ApplicationRecord
|
|
|
81
181
|
end
|
|
82
182
|
```
|
|
83
183
|
|
|
84
|
-
`typo_tolerance` uses an FTS5 trigram shadow table as a fallback when the primary index returns no results. It is substring
|
|
184
|
+
`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.
|
|
85
185
|
|
|
86
186
|
## Known limitations
|
|
87
187
|
|
|
88
188
|
- 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.
|
|
89
|
-
- Per-model only;
|
|
189
|
+
- Per-model only; `multi_search` returns grouped results, not a unified ranked list
|
|
190
|
+
- No built-in multi-model aggregator
|
|
191
|
+
|
|
192
|
+
## Security
|
|
193
|
+
|
|
194
|
+
If you discover a security vulnerability, please email the maintainer directly at
|
|
195
|
+
the address listed in the gemspec. Do not file a public issue.
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module FullSearch
|
|
4
4
|
module Callbacks
|
|
5
5
|
def self.install!(model)
|
|
6
|
+
return if model.instance_variable_get(:@__full_search_callbacks_installed)
|
|
7
|
+
|
|
6
8
|
dsl = model.full_search_dsl
|
|
7
9
|
|
|
8
10
|
source_fields = dsl.fields.select(&:source)
|
|
@@ -27,6 +29,12 @@ module FullSearch
|
|
|
27
29
|
FullSearch::Callbacks.reindex_dependents!(record, model, field)
|
|
28
30
|
end
|
|
29
31
|
end
|
|
32
|
+
|
|
33
|
+
model.instance_variable_set(:@__full_search_callbacks_installed, true)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.reset_installed_flag!(model)
|
|
37
|
+
model.instance_variable_set(:@__full_search_callbacks_installed, false)
|
|
30
38
|
end
|
|
31
39
|
|
|
32
40
|
def self.reindex_record!(record)
|
|
@@ -46,22 +54,24 @@ module FullSearch
|
|
|
46
54
|
return unless field&.source
|
|
47
55
|
|
|
48
56
|
value = record.instance_exec(&field.source)
|
|
49
|
-
table = FullSearch::Index.fts_table_name(record.class)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
"UPDATE #{table} SET #{field.name} = #{
|
|
57
|
+
table = qt(FullSearch::Index.fts_table_name(record.class))
|
|
58
|
+
conn = ActiveRecord::Base.connection
|
|
59
|
+
conn.execute(
|
|
60
|
+
"UPDATE #{table} SET #{qc(field.name)} = #{q(value.to_s)} WHERE rowid = #{q(record.id)}"
|
|
53
61
|
)
|
|
54
62
|
end
|
|
55
63
|
|
|
56
64
|
def self.remove_record!(record)
|
|
57
|
-
table = FullSearch::Index.fts_table_name(record.class)
|
|
58
|
-
ActiveRecord::Base.connection
|
|
65
|
+
table = qt(FullSearch::Index.fts_table_name(record.class))
|
|
66
|
+
conn = ActiveRecord::Base.connection
|
|
67
|
+
conn.execute("DELETE FROM #{table} WHERE rowid = #{q(record.id)}")
|
|
59
68
|
end
|
|
60
69
|
|
|
61
70
|
def self.reindex_dependents!(parent_record, dependent_model, field)
|
|
62
71
|
fk = association_key(dependent_model, field.reindex_on)
|
|
63
|
-
|
|
64
|
-
|
|
72
|
+
conn = ActiveRecord::Base.connection
|
|
73
|
+
sql = "SELECT id FROM #{qt(dependent_model.table_name)} WHERE #{qc(fk)} = #{q(parent_record.id)}"
|
|
74
|
+
dependent_ids = conn.execute(sql).map { |r| r["id"] }
|
|
65
75
|
|
|
66
76
|
dependent_ids.each do |dep_id|
|
|
67
77
|
if field.async
|
|
@@ -73,6 +83,14 @@ module FullSearch
|
|
|
73
83
|
end
|
|
74
84
|
end
|
|
75
85
|
|
|
86
|
+
class << self
|
|
87
|
+
include FullSearch::Quoting
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def self.connection
|
|
91
|
+
ActiveRecord::Base.connection
|
|
92
|
+
end
|
|
93
|
+
|
|
76
94
|
def self.associated_class(model, association_name)
|
|
77
95
|
reflection = model.reflect_on_association(association_name.to_sym)
|
|
78
96
|
reflection&.klass
|
data/lib/full_search/config.rb
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module FullSearch
|
|
4
4
|
class Config
|
|
5
|
-
attr_accessor :
|
|
5
|
+
attr_accessor :auto_rebuild_schema, :stale_query_behavior, :lock_rebuilds, :default_async_reindex, :default_tokenizer
|
|
6
6
|
|
|
7
7
|
def initialize
|
|
8
|
-
@
|
|
8
|
+
@auto_rebuild_schema = false
|
|
9
9
|
@stale_query_behavior = :raise
|
|
10
10
|
@lock_rebuilds = true
|
|
11
11
|
@default_async_reindex = true
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FullSearch
|
|
4
|
+
module Distance
|
|
5
|
+
def self.damerau_levenshtein(a, b)
|
|
6
|
+
a_len = a.length
|
|
7
|
+
b_len = b.length
|
|
8
|
+
return a_len if b_len == 0
|
|
9
|
+
return b_len if a_len == 0
|
|
10
|
+
|
|
11
|
+
d = Array.new(a_len + 1) { Array.new(b_len + 1, 0) }
|
|
12
|
+
(0..a_len).each { |i| d[i][0] = i }
|
|
13
|
+
(0..b_len).each { |j| d[0][j] = j }
|
|
14
|
+
|
|
15
|
+
(1..a_len).each do |i|
|
|
16
|
+
(1..b_len).each do |j|
|
|
17
|
+
cost = (a[i - 1] == b[j - 1]) ? 0 : 1
|
|
18
|
+
d[i][j] = [
|
|
19
|
+
d[i - 1][j] + 1,
|
|
20
|
+
d[i][j - 1] + 1,
|
|
21
|
+
d[i - 1][j - 1] + cost
|
|
22
|
+
].min
|
|
23
|
+
|
|
24
|
+
if i > 1 && j > 1 && a[i - 1] == b[j - 2] && a[i - 2] == b[j - 1]
|
|
25
|
+
d[i][j] = [d[i][j], d[i - 2][j - 2] + 1].min
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
d[a_len][b_len]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/full_search/dsl.rb
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module FullSearch
|
|
4
4
|
class Dsl
|
|
5
|
-
attr_reader :fields, :exact_matches, :filters, :model_class, :
|
|
5
|
+
attr_reader :fields, :exact_matches, :filters, :model_class, :highlight_config, :rank_bys
|
|
6
6
|
|
|
7
|
-
Field = Data.define(:name, :weight, :source, :reindex_on, :async)
|
|
8
|
-
ExactMatch = Data.define(:name, :source)
|
|
7
|
+
Field = Data.define(:name, :weight, :source, :reindex_on, :async, :as, :version)
|
|
8
|
+
ExactMatch = Data.define(:name, :source, :version)
|
|
9
9
|
Filter = Data.define(:name, :required)
|
|
10
10
|
RankBy = Data.define(:column, :direction)
|
|
11
11
|
|
|
@@ -19,32 +19,49 @@ module FullSearch
|
|
|
19
19
|
@soft_delete_column = nil
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
def field(name, weight: 1, source: nil, reindex_on: nil, async: FullSearch.config.default_async_reindex)
|
|
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
24
|
raise InvalidFieldError, "Invalid field name: #{name.inspect}"
|
|
25
25
|
end
|
|
26
|
-
|
|
26
|
+
if as && !valid_name?(as)
|
|
27
|
+
raise InvalidFieldError, "Invalid field alias (as): #{as.inspect}"
|
|
28
|
+
end
|
|
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 }
|
|
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)
|
|
27
33
|
end
|
|
28
34
|
|
|
29
|
-
def exact_match(name, source: -> { public_send(name) })
|
|
35
|
+
def exact_match(name, source: -> { public_send(name) }, version: nil)
|
|
30
36
|
unless valid_name?(name)
|
|
31
37
|
raise InvalidFieldError, "Invalid exact_match name: #{name.inspect}"
|
|
32
38
|
end
|
|
33
|
-
|
|
39
|
+
str = name.to_s
|
|
40
|
+
raise InvalidFieldError, "Duplicate exact_match name: #{name.inspect}" if exact_matches.any? { |e| e.name == str }
|
|
41
|
+
@exact_matches << ExactMatch.new(name: str, source: source, version: version)
|
|
34
42
|
end
|
|
35
43
|
|
|
36
44
|
def rank_by(column, direction = :desc)
|
|
37
45
|
unless valid_name?(column)
|
|
38
46
|
raise InvalidFieldError, "Invalid rank_by column: #{column.inspect}"
|
|
39
47
|
end
|
|
40
|
-
|
|
48
|
+
dir = direction.to_s.downcase
|
|
49
|
+
unless %w[asc desc].include?(dir)
|
|
50
|
+
raise InvalidFieldError, "Invalid rank_by direction: #{direction.inspect}. Use :asc or :desc."
|
|
51
|
+
end
|
|
52
|
+
str = column.to_s
|
|
53
|
+
raise InvalidFieldError, "Duplicate rank_by column: #{column.inspect}" if rank_bys.any? { |r| r.column == str }
|
|
54
|
+
@rank_bys << RankBy.new(column: str, direction: dir.to_sym)
|
|
41
55
|
end
|
|
42
56
|
|
|
43
57
|
def filter(name, required: false)
|
|
44
58
|
unless valid_name?(name)
|
|
45
59
|
raise InvalidFieldError, "Invalid filter name: #{name.inspect}"
|
|
46
60
|
end
|
|
47
|
-
|
|
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 }
|
|
64
|
+
@filters << Filter.new(name: str, required: required)
|
|
48
65
|
end
|
|
49
66
|
|
|
50
67
|
def soft_delete_column(name = :_no_arg_)
|
|
@@ -64,7 +81,7 @@ module FullSearch
|
|
|
64
81
|
end
|
|
65
82
|
|
|
66
83
|
def highlight(open_tag: "<mark>", close_tag: "</mark>")
|
|
67
|
-
@highlight_config = {
|
|
84
|
+
@highlight_config = {open_tag: open_tag, close_tag: close_tag}
|
|
68
85
|
end
|
|
69
86
|
|
|
70
87
|
def typo_tolerance(enabled = true, min_term_length: nil)
|
|
@@ -88,8 +105,8 @@ module FullSearch
|
|
|
88
105
|
soft_delete_column,
|
|
89
106
|
typo_tolerance?,
|
|
90
107
|
typo_tolerance_min_term_length,
|
|
91
|
-
fields.map { |f| [f.name, f.weight, f.source.nil? ? "column" : "
|
|
92
|
-
exact_matches.map { |e| [e.name] },
|
|
108
|
+
fields.map { |f| [f.name, f.weight, f.source.nil? ? "column" : "proc:#{f.version}", f.reindex_on, f.async, f.as] },
|
|
109
|
+
exact_matches.map { |e| [e.name, "proc:#{e.version}"] },
|
|
93
110
|
filters.map { |f| [f.name, f.required] },
|
|
94
111
|
rank_bys.map { |r| [r.column, r.direction] }
|
|
95
112
|
].inspect)
|
data/lib/full_search/errors.rb
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
module FullSearch
|
|
4
4
|
class Error < StandardError; end
|
|
5
5
|
class MissingRequiredFilterError < Error; end
|
|
6
|
+
class UnknownFilterError < Error; end
|
|
6
7
|
class ConfigChangedError < Error; end
|
|
7
8
|
class InvalidFieldError < Error; end
|
|
8
9
|
class NotConfiguredError < Error; end
|
|
9
10
|
class UnsupportedDatabaseError < Error; end
|
|
11
|
+
class InvalidQueryError < Error; end
|
|
10
12
|
end
|
|
@@ -12,11 +12,14 @@ module FullSearch
|
|
|
12
12
|
relation = model.all
|
|
13
13
|
filters.each { |name, value| relation = relation.where(name => value) }
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
ids = []
|
|
16
|
+
relation.find_each do |record|
|
|
17
|
+
dsl.exact_matches.each do |em|
|
|
18
|
+
value = record.instance_exec(&em.source)
|
|
19
|
+
ids << record.id if value.to_s.casecmp?(cleaned)
|
|
20
|
+
end
|
|
17
21
|
end
|
|
18
|
-
|
|
19
|
-
relation.where(conditions.reduce(:or)).pluck(:id)
|
|
22
|
+
ids.uniq
|
|
20
23
|
end
|
|
21
24
|
end
|
|
22
25
|
end
|