full_search 0.2.0 → 0.3.1

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.
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FullSearch
4
+ module VirtualTablesPatch
5
+ def virtual_tables
6
+ super.reject { |_, options| options.first.nil? }
7
+ end
8
+ end
9
+
10
+ module SchemaDumperPatch
11
+ private
12
+
13
+ def virtual_tables(stream)
14
+ virtual_tables = @connection.virtual_tables.reject { |name, _| ignored?(name) }
15
+
16
+ if virtual_tables.any?
17
+ stream.puts
18
+ stream.puts " # Virtual tables defined in this database."
19
+ stream.puts " # Note that virtual tables may not work with other database engines. Be careful if changing database."
20
+ virtual_tables.sort.each do |table_name, options|
21
+ module_name, arguments = options
22
+ next if module_name.nil? || arguments.nil?
23
+
24
+ stream.puts " create_virtual_table #{table_name.inspect}, #{module_name.inspect}, #{arguments.split(", ").inspect}"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ ActiveSupport.on_load(:active_record) do
32
+ begin
33
+ require "active_record/connection_adapters/sqlite3_adapter"
34
+ rescue LoadError
35
+ next
36
+ end
37
+
38
+ if defined?(ActiveRecord::ConnectionAdapters::SQLite3Adapter)
39
+ ActiveRecord::ConnectionAdapters::SQLite3Adapter.prepend(FullSearch::VirtualTablesPatch)
40
+ end
41
+
42
+ if defined?(ActiveRecord::ConnectionAdapters::SQLite3::SchemaDumper)
43
+ ActiveRecord::ConnectionAdapters::SQLite3::SchemaDumper.prepend(FullSearch::SchemaDumperPatch)
44
+ end
45
+ end
@@ -16,14 +16,20 @@ module FullSearch
16
16
  @matching_strategy = matching_strategy
17
17
  end
18
18
 
19
+ MIN_TERM_LENGTH = 3
20
+
19
21
  def relation
22
+ validate_filter_keys!
20
23
  validate_required_filters!
24
+ check_stale_config!
25
+
26
+ return model.none if dsl.tokenize == "trigram" && query.length < MIN_TERM_LENGTH && !dsl.typo_tolerance?
21
27
 
22
28
  parsed = QueryParser.parse(query)
23
29
  exact_ids = ExactMatch.ids_for(model, query, filters)
24
30
  primary_ids = fts_match_ids(parsed)
25
- fallback_ids = dsl.typo_tolerance? && matching_strategy != "all" ? trigram_match_ids(parsed, primary_ids) : []
26
- fuzzy_ids = dsl.typo_tolerance? && matching_strategy != "all" && primary_ids.empty? && fallback_ids.empty? ? fuzzy_match_ids(parsed) : []
31
+ fallback_ids = (dsl.typo_tolerance? && matching_strategy != "all") ? trigram_match_ids(parsed, primary_ids) : []
32
+ fuzzy_ids = (dsl.typo_tolerance? && matching_strategy != "all" && primary_ids.empty? && fallback_ids.empty?) ? fuzzy_match_ids(parsed) : []
27
33
 
28
34
  all_ids = (exact_ids + primary_ids + fallback_ids + fuzzy_ids).uniq
29
35
  return model.none if all_ids.empty?
@@ -50,6 +56,15 @@ module FullSearch
50
56
  model.full_search_dsl
51
57
  end
52
58
 
59
+ def validate_filter_keys!
60
+ allowed = dsl.filters.map(&:name).to_set
61
+ filters.each_key do |key|
62
+ unless allowed.include?(key.to_s)
63
+ raise UnknownFilterError, "Unknown filter: #{key}"
64
+ end
65
+ end
66
+ end
67
+
53
68
  def validate_required_filters!
54
69
  dsl.filters.each do |filter|
55
70
  next unless filter.required
@@ -57,21 +72,36 @@ module FullSearch
57
72
  end
58
73
  end
59
74
 
75
+ def check_stale_config!
76
+ stored = FullSearch::Index.stored_config_hash(model)
77
+ return unless stored
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
86
+ end
87
+ end
88
+
60
89
  def fts_match_ids(parsed)
61
90
  return [] if query.empty?
62
91
 
63
92
  match_expr = QueryParser.to_match_expression(parsed)
64
- fts_table = FullSearch::Index.fts_table_name(model)
93
+ fts_table = qt(FullSearch::Index.fts_table_name(model))
94
+ tbl = qt(model.table_name)
65
95
 
66
96
  sql = <<~SQL
67
- SELECT #{model.table_name}.id
97
+ SELECT #{tbl}.id
68
98
  FROM #{fts_table}
69
- JOIN #{model.table_name} ON #{model.table_name}.id = #{fts_table}.rowid
70
- WHERE #{fts_table} MATCH #{connection.quote(match_expr)}
99
+ JOIN #{tbl} ON #{tbl}.id = #{fts_table}.rowid
100
+ WHERE #{fts_table} MATCH #{q(match_expr)}
71
101
  SQL
72
102
 
73
103
  filter_conditions = filters.map do |name, value|
74
- "AND #{fts_table}.#{name} = #{connection.quote(value)}"
104
+ "AND #{fts_table}.#{qc(name)} = #{q(value)}"
75
105
  end.join(" ")
76
106
 
77
107
  connection.execute("#{sql} #{filter_conditions}").map { |r| r["id"] }
@@ -83,24 +113,25 @@ module FullSearch
83
113
  match_expr = QueryParser.to_match_expression(parsed)
84
114
  return [] if match_expr.empty?
85
115
 
86
- term = parsed.last rescue nil
116
+ term = parsed.last
87
117
  return [] if term.nil?
88
118
 
89
119
  if term.length < dsl.typo_tolerance_min_term_length.to_i
90
120
  return like_prefix_ids(term)
91
121
  end
92
122
 
93
- trigram_table = FullSearch::Index.trigram_table_name(model)
123
+ trigram_table = qt(FullSearch::Index.trigram_table_name(model))
124
+ tbl = qt(model.table_name)
94
125
 
95
126
  sql = <<~SQL
96
- SELECT #{model.table_name}.id
127
+ SELECT #{tbl}.id
97
128
  FROM #{trigram_table}
98
- JOIN #{model.table_name} ON #{model.table_name}.id = #{trigram_table}.rowid
99
- WHERE #{trigram_table} MATCH #{connection.quote(match_expr)}
129
+ JOIN #{tbl} ON #{tbl}.id = #{trigram_table}.rowid
130
+ WHERE #{trigram_table} MATCH #{q(match_expr)}
100
131
  SQL
101
132
 
102
133
  filter_conditions = filters.map do |name, value|
103
- "AND #{trigram_table}.#{name} = #{connection.quote(value)}"
134
+ "AND #{trigram_table}.#{qc(name)} = #{q(value)}"
104
135
  end.join(" ")
105
136
 
106
137
  connection.execute("#{sql} #{filter_conditions}").map { |r| r["id"] }
@@ -109,53 +140,59 @@ module FullSearch
109
140
  def like_prefix_ids(term)
110
141
  column_fields = dsl.fields.select { |f| f.source.nil? }
111
142
  source_fields = dsl.fields.select { |f| f.source }
143
+ tbl = qt(model.table_name)
144
+
145
+ soft_delete_clause = ""
146
+ if dsl.soft_delete_column && !include_soft_deleted
147
+ soft_delete_clause = "AND #{tbl}.#{qc(dsl.soft_delete_column)} IS NULL"
148
+ end
112
149
 
113
150
  ids = []
114
151
 
115
152
  if column_fields.any?
116
153
  like_conditions = column_fields.map do |field|
117
- "#{connection.quote_table_name(model.table_name)}.#{connection.quote_column_name(field.name)} LIKE #{connection.quote("#{term}%")}"
154
+ "#{tbl}.#{qc(field.name)} LIKE #{q("#{term}%")}"
118
155
  end.join(" OR ")
119
156
 
120
- sql = <<~SQL
121
- SELECT #{model.table_name}.id
122
- FROM #{model.table_name}
123
- WHERE (#{like_conditions})
124
- SQL
125
-
126
157
  filter_conditions = filters.map do |name, value|
127
- "AND #{model.table_name}.#{name} = #{connection.quote(value)}"
158
+ "AND #{tbl}.#{qc(name)} = #{q(value)}"
128
159
  end.join(" ")
129
160
 
130
- ids = connection.execute("#{sql} #{filter_conditions}").map { |r| r["id"] }
161
+ sql = <<~SQL
162
+ SELECT #{tbl}.id
163
+ FROM #{tbl}
164
+ WHERE (#{like_conditions}) #{filter_conditions} #{soft_delete_clause}
165
+ SQL
166
+
167
+ ids = connection.execute(sql).map { |r| r["id"] }
131
168
  return ids if ids.any?
132
169
  end
133
170
 
134
171
  if source_fields.any?
135
- fts_table = FullSearch::Index.fts_table_name(model)
172
+ fts_table = qt(FullSearch::Index.fts_table_name(model))
136
173
  like_conditions = source_fields.map do |field|
137
- "#{fts_table}.#{field.name} LIKE #{connection.quote("#{term}%")}"
174
+ "#{fts_table}.#{qc(field.name)} LIKE #{q("#{term}%")}"
138
175
  end.join(" OR ")
139
176
 
177
+ filter_conditions = filters.map do |name, value|
178
+ "AND #{fts_table}.#{qc(name)} = #{q(value)}"
179
+ end.join(" ")
180
+
140
181
  sql = <<~SQL
141
- SELECT #{model.table_name}.id
182
+ SELECT #{tbl}.id
142
183
  FROM #{fts_table}
143
- JOIN #{model.table_name} ON #{model.table_name}.id = #{fts_table}.rowid
144
- WHERE (#{like_conditions})
184
+ JOIN #{tbl} ON #{tbl}.id = #{fts_table}.rowid
185
+ WHERE (#{like_conditions}) #{filter_conditions} #{soft_delete_clause}
145
186
  SQL
146
187
 
147
- filter_conditions = filters.map do |name, value|
148
- "AND #{fts_table}.#{name} = #{connection.quote(value)}"
149
- end.join(" ")
150
-
151
- ids = connection.execute("#{sql} #{filter_conditions}").map { |r| r["id"] }
188
+ ids = connection.execute(sql).map { |r| r["id"] }
152
189
  end
153
190
 
154
191
  ids
155
192
  end
156
193
 
157
194
  def fuzzy_match_ids(parsed)
158
- term = parsed.last rescue nil
195
+ term = parsed.last
159
196
  return [] if term.nil?
160
197
 
161
198
  term_str = term.is_a?(Array) ? extract_last_term_string(term) : term.to_s
@@ -168,22 +205,28 @@ module FullSearch
168
205
  return [] if column_fields.empty?
169
206
 
170
207
  register_levenshtein!
208
+ tbl = qt(model.table_name)
209
+
210
+ soft_delete_clause = ""
211
+ if dsl.soft_delete_column && !include_soft_deleted
212
+ soft_delete_clause = "AND #{tbl}.#{qc(dsl.soft_delete_column)} IS NULL"
213
+ end
214
+
215
+ filter_conditions = filters.map do |name, value|
216
+ "AND #{tbl}.#{qc(name)} = #{q(value)}"
217
+ end.join(" ")
171
218
 
172
219
  conditions = column_fields.map do |field|
173
- "levenshtein(LOWER(#{connection.quote_table_name(model.table_name)}.#{connection.quote_column_name(field.name)}), #{connection.quote(term_str.downcase)}) <= #{max_typos}"
220
+ "levenshtein(LOWER(#{tbl}.#{qc(field.name)}), #{q(term_str.downcase)}) <= #{max_typos}"
174
221
  end.join(" OR ")
175
222
 
176
223
  sql = <<~SQL
177
- SELECT #{model.table_name}.id
178
- FROM #{model.table_name}
179
- WHERE (#{conditions})
224
+ SELECT #{tbl}.id
225
+ FROM #{tbl}
226
+ WHERE (#{conditions}) #{filter_conditions} #{soft_delete_clause}
180
227
  SQL
181
228
 
182
- filter_conditions = filters.map do |name, value|
183
- "AND #{model.table_name}.#{name} = #{connection.quote(value)}"
184
- end.join(" ")
185
-
186
- connection.execute("#{sql} #{filter_conditions}").map { |r| r["id"] }
229
+ connection.execute(sql).map { |r| r["id"] }
187
230
  end
188
231
 
189
232
  def max_allowed_typos(length)
@@ -203,65 +246,38 @@ module FullSearch
203
246
 
204
247
  raw = connection.raw_connection
205
248
  raw.create_function("levenshtein", 2) do |func, s1, s2|
206
- func.result = damerau_levenshtein(s1.to_s, s2.to_s)
249
+ func.result = Distance.damerau_levenshtein(s1.to_s, s2.to_s)
207
250
  end
208
251
  @levenshtein_registered = true
209
252
  end
210
253
 
211
- def damerau_levenshtein(a, b)
212
- a_len = a.length
213
- b_len = b.length
214
- return a_len if b_len == 0
215
- return b_len if a_len == 0
216
-
217
- d = Array.new(a_len + 1) { Array.new(b_len + 1, 0) }
218
- (0..a_len).each { |i| d[i][0] = i }
219
- (0..b_len).each { |j| d[0][j] = j }
220
-
221
- (1..a_len).each do |i|
222
- (1..b_len).each do |j|
223
- cost = a[i - 1] == b[j - 1] ? 0 : 1
224
- d[i][j] = [
225
- d[i - 1][j] + 1,
226
- d[i][j - 1] + 1,
227
- d[i - 1][j - 1] + cost
228
- ].min
229
-
230
- if i > 1 && j > 1 && a[i - 1] == b[j - 2] && a[i - 2] == b[j - 1]
231
- d[i][j] = [d[i][j], d[i - 2][j - 2] + 1].min
232
- end
233
- end
234
- end
235
-
236
- d[a_len][b_len]
237
- end
238
-
239
254
  def apply_ranking(rel, all_ids, exact_ids)
240
255
  return rel if all_ids.empty?
241
256
 
242
257
  order_parts = []
258
+ tbl = qt(model.table_name)
243
259
 
244
260
  if exact_ids.any?
245
- order_parts << "CASE #{model.table_name}.id #{exact_ids.map { |id| "WHEN #{id} THEN 0" }.join(" ")} ELSE 1 END"
261
+ order_parts << "CASE #{tbl}.id #{exact_ids.map { |id| "WHEN #{q(id)} THEN 0" }.join(" ")} ELSE 1 END"
246
262
  end
247
263
 
248
- fts_table = FullSearch::Index.fts_table_name(model)
264
+ fts_table = qt(FullSearch::Index.fts_table_name(model))
249
265
  match_expr = QueryParser.to_match_expression(QueryParser.parse(query))
250
266
 
251
267
  rank_subquery = <<~SQL
252
268
  SELECT rowid, rank
253
269
  FROM #{fts_table}
254
- WHERE #{fts_table} MATCH #{connection.quote(match_expr)}
270
+ WHERE #{fts_table} MATCH #{q(match_expr)}
255
271
  SQL
256
272
 
257
273
  rel = rel
258
- .select("#{model.table_name}.*, fts_rank.rank AS full_search_rank")
259
- .joins("LEFT JOIN (#{rank_subquery}) AS fts_rank ON fts_rank.rowid = #{model.table_name}.id")
274
+ .select("#{tbl}.*, fts_rank.rank AS full_search_rank")
275
+ .joins("LEFT JOIN (#{rank_subquery}) AS fts_rank ON fts_rank.rowid = #{tbl}.id")
260
276
 
261
277
  order_parts << "COALESCE(fts_rank.rank, 1)"
262
278
 
263
279
  dsl.rank_bys.each do |rank_by|
264
- col = "#{connection.quote_table_name(model.table_name)}.#{connection.quote_column_name(rank_by.column)}"
280
+ col = "#{tbl}.#{qc(rank_by.column)}"
265
281
  order_parts << "#{col} #{rank_by.direction.to_s.upcase} NULLS LAST"
266
282
  end
267
283
 
@@ -269,7 +285,11 @@ module FullSearch
269
285
  end
270
286
 
271
287
  def connection
288
+ model.connection
289
+ rescue NoMethodError
272
290
  ActiveRecord::Base.connection
273
291
  end
292
+
293
+ include Quoting
274
294
  end
275
295
  end
@@ -2,11 +2,24 @@
2
2
 
3
3
  module FullSearch
4
4
  module SoftDelete
5
- def self.delete_transition_sql(model)
5
+ def self.active_update_clause(model)
6
6
  dsl = model.full_search_dsl
7
7
  return nil unless dsl&.soft_delete_column
8
8
 
9
- "WHEN new.#{dsl.soft_delete_column} IS NULL"
9
+ col = model.connection.quote_column_name(dsl.soft_delete_column)
10
+ "WHEN new.#{col} IS NULL"
11
+ end
12
+
13
+ def self.soft_delete_remove_clause(model)
14
+ dsl = model.full_search_dsl
15
+ return nil unless dsl&.soft_delete_column
16
+
17
+ col = model.connection.quote_column_name(dsl.soft_delete_column)
18
+ "WHEN old.#{col} IS NULL AND new.#{col} IS NOT NULL"
19
+ end
20
+
21
+ def self.delete_transition_sql(model)
22
+ active_update_clause(model)
10
23
  end
11
24
  end
12
25
  end
@@ -5,23 +5,25 @@ module FullSearch
5
5
  MIN_SQLITE_VERSION = [3, 34].freeze
6
6
 
7
7
  class << self
8
- def supported?
9
- major, minor = sqlite_version_parts
8
+ def supported?(model = nil)
9
+ conn = model&.connection || ActiveRecord::Base.connection
10
+ major, minor = sqlite_version_parts(conn)
10
11
  major > MIN_SQLITE_VERSION.first || (major == MIN_SQLITE_VERSION.first && minor >= MIN_SQLITE_VERSION.last)
11
12
  end
12
13
 
13
- def warn_unsupported!
14
- warn "[full_search] SQLite #{sqlite_version} does not support the trigram tokenizer. typo_tolerance requires SQLite >= 3.34."
14
+ def warn_unsupported!(model = nil)
15
+ version = sqlite_version(model&.connection || ActiveRecord::Base.connection)
16
+ warn "[full_search] SQLite #{version} does not support the trigram tokenizer. typo_tolerance requires SQLite >= 3.34."
15
17
  end
16
18
 
17
19
  private
18
20
 
19
- def sqlite_version_parts
20
- sqlite_version.split(".").first(2).map(&:to_i)
21
+ def sqlite_version_parts(conn)
22
+ sqlite_version(conn).split(".").first(2).map(&:to_i)
21
23
  end
22
24
 
23
- def sqlite_version
24
- ActiveRecord::Base.connection.execute("SELECT sqlite_version() AS v").first["v"]
25
+ def sqlite_version(conn)
26
+ conn.execute("SELECT sqlite_version() AS v").first["v"]
25
27
  end
26
28
  end
27
29
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FullSearch
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.1"
5
5
  end
data/lib/full_search.rb CHANGED
@@ -6,6 +6,8 @@ require "active_support/concern"
6
6
 
7
7
  require "full_search/version"
8
8
  require "full_search/config"
9
+ require "full_search/distance"
10
+ require "full_search/quoting"
9
11
  require "full_search/errors"
10
12
  require "full_search/tokenizer"
11
13
  require "full_search/dsl"
@@ -22,6 +24,7 @@ require "full_search/reindex_job"
22
24
  require "full_search/optimize_job"
23
25
  require "full_search/test_helpers"
24
26
  require "full_search/multi_search"
27
+ require "full_search/schema_dumper_patch"
25
28
 
26
29
  ActiveSupport.on_load(:active_record) do
27
30
  include FullSearch::Model
@@ -30,7 +33,15 @@ end
30
33
  module FullSearch
31
34
  class << self
32
35
  def models
33
- @models ||= []
36
+ @models ||= Set.new
37
+ end
38
+
39
+ def register_model(model)
40
+ models << model
41
+ end
42
+
43
+ def deregister_model(model)
44
+ models.delete(model)
34
45
  end
35
46
 
36
47
  def optimize!
@@ -3,7 +3,12 @@
3
3
  def resolve_full_search_models(args)
4
4
  if args[:models]
5
5
  args[:models].split(",").map do |name|
6
- klass = name.singularize.camelize.constantize rescue nil
6
+ klass = begin
7
+ name.singularize.camelize.constantize
8
+ rescue NameError, LoadError => e
9
+ warn "[full_search] Could not resolve model '#{name}': #{e.message}"
10
+ nil
11
+ end
7
12
  klass || FullSearch.models.find { |m| m.table_name == name }
8
13
  end.compact
9
14
  else
@@ -50,14 +55,17 @@ namespace :full_search do
50
55
  FullSearch.models.each do |model|
51
56
  stored = FullSearch::Index.stored_config_hash(model)
52
57
  current = model.full_search_dsl.config_hash
53
- status = stored == current ? "ok" : "stale"
58
+ status = (stored == current) ? "ok" : "stale"
54
59
  source_fields = model.full_search_dsl.fields.select(&:source).map(&:name)
55
60
 
56
61
  drift_info = if source_fields.any?
62
+ conn = ActiveRecord::Base.connection
63
+ qc = ->(name) { conn.quote_column_name(name) }
64
+ qt = ->(name) { conn.quote_table_name(name) }
57
65
  empty_count = ActiveRecord::Base.connection.execute(<<~SQL).first["c"]
58
66
  SELECT COUNT(*) AS c
59
- FROM #{FullSearch::Index.fts_table_name(model)}
60
- WHERE #{source_fields.map { |c| "#{c} = ''" }.join(' OR ')}
67
+ FROM #{qt.call(FullSearch::Index.fts_table_name(model))}
68
+ WHERE #{source_fields.map { |c| "#{qc.call(c)} = ''" }.join(" OR ")}
61
69
  SQL
62
70
  " | empty source fields: #{empty_count}"
63
71
  else
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.2.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben D'Angelo
@@ -16,6 +16,9 @@ dependencies:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
18
  version: '8.0'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '9'
19
22
  type: :runtime
20
23
  prerelease: false
21
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -23,6 +26,9 @@ dependencies:
23
26
  - - ">="
24
27
  - !ruby/object:Gem::Version
25
28
  version: '8.0'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '9'
26
32
  - !ruby/object:Gem::Dependency
27
33
  name: railties
28
34
  requirement: !ruby/object:Gem::Requirement
@@ -30,6 +36,9 @@ dependencies:
30
36
  - - ">="
31
37
  - !ruby/object:Gem::Version
32
38
  version: '8.0'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '9'
33
42
  type: :runtime
34
43
  prerelease: false
35
44
  version_requirements: !ruby/object:Gem::Requirement
@@ -37,6 +46,9 @@ dependencies:
37
46
  - - ">="
38
47
  - !ruby/object:Gem::Version
39
48
  version: '8.0'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '9'
40
52
  - !ruby/object:Gem::Dependency
41
53
  name: sqlite3
42
54
  requirement: !ruby/object:Gem::Requirement
@@ -51,20 +63,6 @@ dependencies:
51
63
  - - ">="
52
64
  - !ruby/object:Gem::Version
53
65
  version: '2.0'
54
- - !ruby/object:Gem::Dependency
55
- name: ostruct
56
- requirement: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - ">="
59
- - !ruby/object:Gem::Version
60
- version: '0.6'
61
- type: :runtime
62
- prerelease: false
63
- version_requirements: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - ">="
66
- - !ruby/object:Gem::Version
67
- version: '0.6'
68
66
  - !ruby/object:Gem::Dependency
69
67
  name: minitest
70
68
  requirement: !ruby/object:Gem::Requirement
@@ -93,6 +91,20 @@ dependencies:
93
91
  - - "~>"
94
92
  - !ruby/object:Gem::Version
95
93
  version: '13.0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: ostruct
96
+ requirement: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '0.6'
101
+ type: :development
102
+ prerelease: false
103
+ version_requirements: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0.6'
96
108
  - !ruby/object:Gem::Dependency
97
109
  name: irb
98
110
  requirement: !ruby/object:Gem::Requirement
@@ -107,6 +119,20 @@ dependencies:
107
119
  - - ">="
108
120
  - !ruby/object:Gem::Version
109
121
  version: '0'
122
+ - !ruby/object:Gem::Dependency
123
+ name: standard
124
+ requirement: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - "~>"
127
+ - !ruby/object:Gem::Version
128
+ version: '1.40'
129
+ type: :development
130
+ prerelease: false
131
+ version_requirements: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - "~>"
134
+ - !ruby/object:Gem::Version
135
+ version: '1.40'
110
136
  description: Declarative full-text search for Rails apps backed by SQLite FTS5
111
137
  executables: []
112
138
  extensions: []
@@ -116,6 +142,7 @@ files:
116
142
  - lib/full_search.rb
117
143
  - lib/full_search/callbacks.rb
118
144
  - lib/full_search/config.rb
145
+ - lib/full_search/distance.rb
119
146
  - lib/full_search/dsl.rb
120
147
  - lib/full_search/errors.rb
121
148
  - lib/full_search/exact_match.rb
@@ -125,8 +152,10 @@ files:
125
152
  - lib/full_search/multi_search.rb
126
153
  - lib/full_search/optimize_job.rb
127
154
  - lib/full_search/query_parser.rb
155
+ - lib/full_search/quoting.rb
128
156
  - lib/full_search/railtie.rb
129
157
  - lib/full_search/reindex_job.rb
158
+ - lib/full_search/schema_dumper_patch.rb
130
159
  - lib/full_search/search.rb
131
160
  - lib/full_search/soft_delete.rb
132
161
  - lib/full_search/test_helpers.rb
@@ -147,12 +176,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
147
176
  requirements:
148
177
  - - ">="
149
178
  - !ruby/object:Gem::Version
150
- version: 4.0.0
179
+ version: 3.1.0
151
180
  required_rubygems_version: !ruby/object:Gem::Requirement
152
181
  requirements:
153
182
  - - ">="
154
183
  - !ruby/object:Gem::Version
155
- version: '0'
184
+ version: '2.0'
156
185
  requirements: []
157
186
  rubygems_version: 4.0.15
158
187
  specification_version: 4