typesense-rails 1.0.0.rc1
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 +7 -0
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +42 -0
- data/Gemfile.lock +260 -0
- data/LICENSE +22 -0
- data/README.md +239 -0
- data/Rakefile +18 -0
- data/lib/typesense/config.rb +30 -0
- data/lib/typesense/import_job.rb +21 -0
- data/lib/typesense/pagination/kaminari.rb +39 -0
- data/lib/typesense/pagination/pagy.rb +29 -0
- data/lib/typesense/pagination/will_paginate.rb +17 -0
- data/lib/typesense/pagination.rb +20 -0
- data/lib/typesense/railtie.rb +12 -0
- data/lib/typesense/tasks/typesense.rake +17 -0
- data/lib/typesense/typesense_job.rb +9 -0
- data/lib/typesense/utilities.rb +47 -0
- data/lib/typesense/version.rb +3 -0
- data/lib/typesense-rails.rb +996 -0
- data/spec/integration_spec.rb +1178 -0
- data/spec/spec_helper.rb +54 -0
- data/typesense-rails.gemspec +69 -0
- metadata +164 -0
@@ -0,0 +1,996 @@
|
|
1
|
+
require "typesense"
|
2
|
+
require "typesense/version"
|
3
|
+
require "typesense/utilities"
|
4
|
+
require "rails/all"
|
5
|
+
|
6
|
+
if defined? Rails
|
7
|
+
begin
|
8
|
+
require "typesense/railtie"
|
9
|
+
rescue LoadError
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
begin
|
14
|
+
require "active_job"
|
15
|
+
rescue LoadError
|
16
|
+
# no queue support, fine
|
17
|
+
end
|
18
|
+
|
19
|
+
require "logger"
|
20
|
+
Rails.logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
|
21
|
+
Rails.logger.level = Logger::INFO
|
22
|
+
|
23
|
+
module Typesense
|
24
|
+
class NotConfigured < StandardError; end
|
25
|
+
class BadConfiguration < StandardError; end
|
26
|
+
class NoBlockGiven < StandardError; end
|
27
|
+
|
28
|
+
autoload :Config, "typesense/config"
|
29
|
+
extend Config
|
30
|
+
|
31
|
+
autoload :Pagination, "typesense/pagination"
|
32
|
+
|
33
|
+
class << self
|
34
|
+
attr_reader :included_in
|
35
|
+
|
36
|
+
def included(klass)
|
37
|
+
@included_in ||= []
|
38
|
+
@included_in << klass
|
39
|
+
@included_in.uniq!
|
40
|
+
|
41
|
+
klass.class_eval do
|
42
|
+
extend ClassMethods
|
43
|
+
include InstanceMethods
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class IndexSettings
|
49
|
+
DEFAULT_BATCH_SIZE = 250
|
50
|
+
|
51
|
+
OPTIONS = [
|
52
|
+
:multi_way_synonyms,
|
53
|
+
:one_way_synonyms,
|
54
|
+
:predefined_fields,
|
55
|
+
:fields,
|
56
|
+
:default_sorting_field,
|
57
|
+
:symbols_to_index,
|
58
|
+
:token_separators,
|
59
|
+
:enable_nested_fields,
|
60
|
+
:metadata,
|
61
|
+
]
|
62
|
+
OPTIONS.each do |k|
|
63
|
+
define_method k do |v|
|
64
|
+
instance_variable_set("@#{k}", v)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def initialize(options, &block)
|
69
|
+
@options = options
|
70
|
+
instance_exec(&block) if block_given?
|
71
|
+
end
|
72
|
+
|
73
|
+
def use_serializer(serializer)
|
74
|
+
@serializer = serializer
|
75
|
+
end
|
76
|
+
|
77
|
+
def attribute(*names, &block)
|
78
|
+
raise ArgumentError.new("Cannot pass multiple attribute names if block given") if block_given? and names.length > 1
|
79
|
+
@attributes ||= {}
|
80
|
+
names.flatten.each do |name|
|
81
|
+
@attributes[name.to_s] = block_given? ? Proc.new { |o| o.instance_eval(&block) } : Proc.new { |o| o.send(name) }
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
alias :attributes :attribute
|
86
|
+
|
87
|
+
def add_attribute(*names, &block)
|
88
|
+
raise ArgumentError.new("Cannot pass multiple attribute names if block given") if block_given? and names.length > 1
|
89
|
+
@additional_attributes ||= {}
|
90
|
+
names.each do |name|
|
91
|
+
@additional_attributes[name.to_s] = block_given? ? Proc.new { |o| o.instance_eval(&block) } : Proc.new { |o| o.send(name) }
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
alias :add_attributes :add_attribute
|
96
|
+
|
97
|
+
def mongoid?(object)
|
98
|
+
defined?(::Mongoid::Document) && object.class.include?(::Mongoid::Document)
|
99
|
+
end
|
100
|
+
|
101
|
+
def sequel?(object)
|
102
|
+
defined?(::Sequel) && object.class < ::Sequel::Model
|
103
|
+
end
|
104
|
+
|
105
|
+
def active_record?(object)
|
106
|
+
!mongoid?(object) && !sequel?(object)
|
107
|
+
end
|
108
|
+
|
109
|
+
def get_default_attributes(object)
|
110
|
+
if mongoid?(object)
|
111
|
+
# work-around mongoid 2.4's unscoped method, not accepting a block
|
112
|
+
object.attributes
|
113
|
+
elsif sequel?(object)
|
114
|
+
object.to_hash
|
115
|
+
else
|
116
|
+
object.class.unscoped do
|
117
|
+
object.attributes
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def get_attribute_names(object)
|
123
|
+
get_attributes(object).keys
|
124
|
+
end
|
125
|
+
|
126
|
+
def attributes_to_hash(attributes, object)
|
127
|
+
if attributes
|
128
|
+
Hash[attributes.map { |name, value| [name.to_s, value.call(object)] }]
|
129
|
+
else
|
130
|
+
{}
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def get_attributes(object)
|
135
|
+
# If a serializer is set, we ignore attributes
|
136
|
+
# everything should be done via the serializer
|
137
|
+
if not @serializer.nil?
|
138
|
+
attributes = @serializer.new(object).attributes
|
139
|
+
elsif @attributes.nil? || @attributes.length.zero?
|
140
|
+
attributes = get_default_attributes(object)
|
141
|
+
# no `attribute ...` have been configured, use the default attributes of the model
|
142
|
+
elsif active_record?(object)
|
143
|
+
# at least 1 `attribute ...` has been configured, therefore use ONLY the one configured
|
144
|
+
object.class.unscoped do
|
145
|
+
attributes = attributes_to_hash(@attributes, object)
|
146
|
+
end
|
147
|
+
else
|
148
|
+
attributes = attributes_to_hash(@attributes, object)
|
149
|
+
end
|
150
|
+
|
151
|
+
attributes.merge!(attributes_to_hash(@additional_attributes, object)) if @additional_attributes
|
152
|
+
|
153
|
+
if @options[:sanitize]
|
154
|
+
sanitizer = begin
|
155
|
+
::HTML::FullSanitizer.new
|
156
|
+
rescue NameError
|
157
|
+
# from rails 4.2
|
158
|
+
::Rails::Html::FullSanitizer.new
|
159
|
+
end
|
160
|
+
attributes = sanitize_attributes(attributes, sanitizer)
|
161
|
+
end
|
162
|
+
|
163
|
+
attributes = encode_attributes(attributes) if @options[:force_utf8_encoding] && Object.const_defined?(:RUBY_VERSION) && RUBY_VERSION.to_f > 1.8
|
164
|
+
|
165
|
+
attributes
|
166
|
+
end
|
167
|
+
|
168
|
+
def sanitize_attributes(v, sanitizer)
|
169
|
+
case v
|
170
|
+
when String
|
171
|
+
sanitizer.sanitize(v)
|
172
|
+
when Hash
|
173
|
+
v.each { |key, value| v[key] = sanitize_attributes(value, sanitizer) }
|
174
|
+
when Array
|
175
|
+
v.map { |x| sanitize_attributes(x, sanitizer) }
|
176
|
+
else
|
177
|
+
v
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def encode_attributes(v)
|
182
|
+
case v
|
183
|
+
when String
|
184
|
+
v.dup.force_encoding("utf-8")
|
185
|
+
when Hash
|
186
|
+
v.each { |key, value| v[key] = encode_attributes(value) }
|
187
|
+
when Array
|
188
|
+
v.map { |x| encode_attributes(x) }
|
189
|
+
else
|
190
|
+
v
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def get_setting(name)
|
195
|
+
instance_variable_get("@#{name}")
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# Default queueing system
|
200
|
+
if defined?(::ActiveJob::Base)
|
201
|
+
# lazy load the ActiveJob class to ensure the
|
202
|
+
# queue is initialized before using it
|
203
|
+
autoload :TypesenseJob, "typesense/typesense_job"
|
204
|
+
autoload :ImportJob, "typesense/import_job"
|
205
|
+
end
|
206
|
+
|
207
|
+
# these are the class methods added when Typesense is included
|
208
|
+
module ClassMethods
|
209
|
+
def self.extended(base)
|
210
|
+
class << base
|
211
|
+
alias_method :without_auto_index, :typesense_without_auto_index unless method_defined? :without_auto_index
|
212
|
+
alias_method :reindex!, :typesense_reindex! unless method_defined? :reindex!
|
213
|
+
alias_method :reindex, :typesense_reindex unless method_defined? :reindex
|
214
|
+
alias_method :index_objects, :typesense_index_objects unless method_defined? :index_objects
|
215
|
+
alias_method :index!, :typesense_index! unless method_defined? :index!
|
216
|
+
alias_method :remove_from_index!, :typesense_remove_from_index! unless method_defined? :remove_from_index!
|
217
|
+
alias_method :clear_index!, :typesense_clear_index! unless method_defined? :clear_index!
|
218
|
+
alias_method :search, :typesense_search unless method_defined? :search
|
219
|
+
alias_method :raw_search, :typesense_raw_search unless method_defined? :raw_search
|
220
|
+
alias_method :index, :typesense_index unless method_defined? :index
|
221
|
+
alias_method :index_name, :typesense_collection_name unless method_defined? :index_name
|
222
|
+
alias_method :collection_name, :typesense_collection_name unless method_defined? :collection_name
|
223
|
+
alias_method :must_reindex?, :typesense_must_reindex? unless method_defined? :must_reindex?
|
224
|
+
alias_method :create_collection, :typesense_create_collection unless method_defined? :create_collection
|
225
|
+
alias_method :upsert_alias, :typesense_upsert_alias unless method_defined? :upsert_alias
|
226
|
+
alias_method :get_collection, :typesense_get_collection unless method_defined? :get_collection
|
227
|
+
alias_method :num_documents, :typesense_num_documents unless method_defined? :num_documents
|
228
|
+
alias_method :get_alias, :typesense_get_alias unless method_defined? :get_alias
|
229
|
+
alias_method :upsert_document, :typesense_upsert_document unless method_defined? :upsert_document
|
230
|
+
alias_method :import_documents, :typesense_import_documents unless method_defined? :import_documents
|
231
|
+
alias_method :retrieve_document, :typesense_retrieve_document unless method_defined? :retrieve_document
|
232
|
+
alias_method :delete_document, :typesense_delete_document unless method_defined? :delete_document
|
233
|
+
alias_method :delete_collection, :typesense_delete_collection unless method_defined? :delete_collection
|
234
|
+
alias_method :delete_by_query, :typesense_delete_by_query unless method_defined? :delete_by_query
|
235
|
+
alias_method :search_collection, :typesense_search_collection unless method_defined? :search_collection
|
236
|
+
end
|
237
|
+
|
238
|
+
base.cattr_accessor :typesense_options, :typesense_settings, :typesense_client
|
239
|
+
end
|
240
|
+
|
241
|
+
def collection_name_with_timestamp(options)
|
242
|
+
"#{typesense_collection_name(options)}_#{Time.now.to_i}"
|
243
|
+
end
|
244
|
+
|
245
|
+
def typesense_create_collection(collection_name, settings = nil)
|
246
|
+
fields = settings.get_setting(:predefined_fields) || settings.get_setting(:fields)
|
247
|
+
default_sorting_field = settings.get_setting(:default_sorting_field)
|
248
|
+
multi_way_synonyms = settings.get_setting(:multi_way_synonyms)
|
249
|
+
one_way_synonyms = settings.get_setting(:one_way_synonyms)
|
250
|
+
symbols_to_index = settings.get_setting(:symbols_to_index)
|
251
|
+
token_separators = settings.get_setting(:token_separators)
|
252
|
+
enable_nested_fields = settings.get_setting(:enable_nested_fields)
|
253
|
+
metadata = settings.get_setting(:metadata)
|
254
|
+
typesense_client.collections.create(
|
255
|
+
{ "name" => collection_name }
|
256
|
+
.merge(
|
257
|
+
if fields
|
258
|
+
{ "fields" => fields }
|
259
|
+
else
|
260
|
+
{ "fields" => [
|
261
|
+
{ "name" => ".*",
|
262
|
+
"type" => "auto" },
|
263
|
+
] }
|
264
|
+
end,
|
265
|
+
default_sorting_field ? { "default_sorting_field" => default_sorting_field } : {},
|
266
|
+
symbols_to_index ? { "symbols_to_index" => symbols_to_index } : {},
|
267
|
+
token_separators ? { "token_separators" => token_separators } : {},
|
268
|
+
enable_nested_fields ? { "enable_nested_fields" => enable_nested_fields } : {},
|
269
|
+
metadata ? { "metadata" => metadata } : {}
|
270
|
+
)
|
271
|
+
)
|
272
|
+
Rails.logger.info "Collection '#{collection_name}' created!"
|
273
|
+
|
274
|
+
typesense_multi_way_synonyms(collection_name, multi_way_synonyms) if multi_way_synonyms
|
275
|
+
|
276
|
+
typesense_one_way_synonyms(collection_name, one_way_synonyms) if one_way_synonyms
|
277
|
+
end
|
278
|
+
|
279
|
+
def typesense_upsert_alias(collection_name, alias_name)
|
280
|
+
typesense_client.aliases.upsert(alias_name, { "collection_name" => collection_name })
|
281
|
+
end
|
282
|
+
|
283
|
+
def typesense_get_collection(collection)
|
284
|
+
typesense_client.collections[collection].retrieve
|
285
|
+
rescue Typesense::Error::ObjectNotFound
|
286
|
+
nil
|
287
|
+
end
|
288
|
+
|
289
|
+
def typesense_num_documents(collection)
|
290
|
+
typesense_client.collections[collection].retrieve["num_documents"]
|
291
|
+
end
|
292
|
+
|
293
|
+
def typesense_get_alias(alias_name)
|
294
|
+
typesense_client.aliases[alias_name].retrieve
|
295
|
+
end
|
296
|
+
|
297
|
+
def typesense_upsert_document(object, collection, dirtyvalues = nil)
|
298
|
+
raise ArgumentError, "Object is required" unless object
|
299
|
+
|
300
|
+
typesense_client.collections[collection].documents.upsert(object, dirty_values: dirtyvalues) if dirtyvalues
|
301
|
+
typesense_client.collections[collection].documents.upsert(object)
|
302
|
+
end
|
303
|
+
|
304
|
+
def typesense_import_documents(jsonl_object, action, collection, batch_size: nil)
|
305
|
+
raise ArgumentError, "JSONL object is required" unless jsonl_object
|
306
|
+
import_options = { action: action }
|
307
|
+
import_options[:batch_size] = batch_size if batch_size
|
308
|
+
|
309
|
+
typesense_client.collections[collection].documents.import(jsonl_object, import_options)
|
310
|
+
end
|
311
|
+
|
312
|
+
def typesense_retrieve_document(object_id, collection = nil)
|
313
|
+
if collection
|
314
|
+
typesense_client.collections[collection].documents[object_id].retrieve
|
315
|
+
else
|
316
|
+
collection_obj = typesense_ensure_init
|
317
|
+
typesense_client.collections[collection_obj[:alias_name]].documents[object_id].retrieve
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
def typesense_delete_document(object_id, collection)
|
322
|
+
typesense_client.collections[collection].documents[object_id].delete
|
323
|
+
end
|
324
|
+
|
325
|
+
def typesense_delete_by_query(collection, query)
|
326
|
+
typesense_client.collections[collection].documents.delete(filter_by: query)
|
327
|
+
end
|
328
|
+
|
329
|
+
def typesense_delete_collection(collection)
|
330
|
+
typesense_client.collections[collection].delete
|
331
|
+
end
|
332
|
+
|
333
|
+
def typesense_search_collection(search_parameters, collection)
|
334
|
+
typesense_client.collections[collection].documents.search(search_parameters)
|
335
|
+
end
|
336
|
+
|
337
|
+
def typesense_multi_way_synonyms(collection, synonyms)
|
338
|
+
synonyms.each do |synonym_hash|
|
339
|
+
synonym_hash.each do |synonym_name, synonym|
|
340
|
+
typesense_client.collections[collection].synonyms.upsert(
|
341
|
+
synonym_name,
|
342
|
+
{ "synonyms" => synonym }
|
343
|
+
)
|
344
|
+
end
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
def typesense_one_way_synonyms(collection, synonyms)
|
349
|
+
synonyms.each do |synonym_hash|
|
350
|
+
synonym_hash.each do |synonym_name, synonym|
|
351
|
+
typesense_client.collections[collection].synonyms.upsert(
|
352
|
+
synonym_name,
|
353
|
+
synonym
|
354
|
+
)
|
355
|
+
end
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
def typesense(options = {}, &block)
|
360
|
+
self.typesense_settings = IndexSettings.new(options, &block)
|
361
|
+
self.typesense_options = { type: typesense_full_const_get(model_name.to_s) }.merge(options) # :per_page => typesense_settings.get_setting(:hitsPerPage) || 10, :page => 1
|
362
|
+
self.typesense_client ||= Typesense.client
|
363
|
+
attr_accessor :highlight_result, :snippet_result
|
364
|
+
|
365
|
+
if options[:enqueue]
|
366
|
+
proc = if options[:enqueue] == true
|
367
|
+
proc do |record, remove|
|
368
|
+
typesenseJob.perform_later(record, remove ? "typesense_remove_from_index!" : "typesense_index!")
|
369
|
+
end
|
370
|
+
elsif options[:enqueue].respond_to?(:call)
|
371
|
+
options[:enqueue]
|
372
|
+
elsif options[:enqueue].is_a?(Symbol)
|
373
|
+
proc { |record, remove| send(options[:enqueue], record, remove) }
|
374
|
+
else
|
375
|
+
raise ArgumentError, "Invalid `enqueue` option: #{options[:enqueue]}"
|
376
|
+
end
|
377
|
+
typesense_options[:enqueue] = proc do |record, remove|
|
378
|
+
proc.call(record, remove) unless typesense_without_auto_index_scope
|
379
|
+
end
|
380
|
+
end
|
381
|
+
unless options[:auto_index] == false
|
382
|
+
if defined?(::Sequel) && self < Sequel::Model
|
383
|
+
class_eval do
|
384
|
+
copy_after_validation = instance_method(:after_validation)
|
385
|
+
copy_before_save = instance_method(:before_save)
|
386
|
+
|
387
|
+
define_method(:after_validation) do |*args|
|
388
|
+
super(*args)
|
389
|
+
copy_after_validation.bind(self).call
|
390
|
+
typesense_mark_must_reindex
|
391
|
+
end
|
392
|
+
|
393
|
+
define_method(:before_save) do |*args|
|
394
|
+
copy_before_save.bind(self).call
|
395
|
+
typesense_mark_for_auto_indexing
|
396
|
+
super(*args)
|
397
|
+
end
|
398
|
+
|
399
|
+
sequel_version = Gem::Version.new(Sequel.version)
|
400
|
+
if sequel_version >= Gem::Version.new("4.0.0") && sequel_version < Gem::Version.new("5.0.0")
|
401
|
+
copy_after_commit = instance_method(:after_commit)
|
402
|
+
define_method(:after_commit) do |*args|
|
403
|
+
super(*args)
|
404
|
+
copy_after_commit.bind(self).call
|
405
|
+
typesense_perform_index_tasks
|
406
|
+
end
|
407
|
+
else
|
408
|
+
copy_after_save = instance_method(:after_save)
|
409
|
+
define_method(:after_save) do |*args|
|
410
|
+
super(*args)
|
411
|
+
copy_after_save.bind(self).call
|
412
|
+
db.after_commit do
|
413
|
+
typesense_perform_index_tasks
|
414
|
+
end
|
415
|
+
end
|
416
|
+
end
|
417
|
+
end
|
418
|
+
else
|
419
|
+
after_validation :typesense_mark_must_reindex if respond_to?(:after_validation)
|
420
|
+
before_save :typesense_mark_for_auto_indexing if respond_to?(:before_save)
|
421
|
+
if respond_to?(:after_commit)
|
422
|
+
after_commit :typesense_perform_index_tasks
|
423
|
+
elsif respond_to?(:after_save)
|
424
|
+
after_save :typesense_perform_index_tasks
|
425
|
+
end
|
426
|
+
end
|
427
|
+
end
|
428
|
+
unless options[:auto_remove] == false
|
429
|
+
if defined?(::Sequel) && self < Sequel::Model
|
430
|
+
class_eval do
|
431
|
+
copy_after_destroy = instance_method(:after_destroy)
|
432
|
+
|
433
|
+
define_method(:after_destroy) do |*args|
|
434
|
+
copy_after_destroy.bind(self).call
|
435
|
+
typesense_enqueue_remove_from_index!
|
436
|
+
super(*args)
|
437
|
+
end
|
438
|
+
end
|
439
|
+
elsif respond_to?(:after_destroy)
|
440
|
+
after_destroy { |searchable| searchable.typesense_enqueue_remove_from_index! }
|
441
|
+
end
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
def typesense_without_auto_index
|
446
|
+
self.typesense_without_auto_index_scope = true
|
447
|
+
begin
|
448
|
+
yield
|
449
|
+
ensure
|
450
|
+
self.typesense_without_auto_index_scope = false
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
def typesense_without_auto_index_scope=(value)
|
455
|
+
Thread.current["typesense_without_auto_index_scope_for_#{model_name}"] = value
|
456
|
+
end
|
457
|
+
|
458
|
+
def typesense_without_auto_index_scope
|
459
|
+
Thread.current["typesense_without_auto_index_scope_for_#{model_name}"]
|
460
|
+
end
|
461
|
+
|
462
|
+
def typesense_reindex!(batch_size = Typesense::IndexSettings::DEFAULT_BATCH_SIZE)
|
463
|
+
# typesense_reindex!: Reindexes all objects in database(does not remove deleted objects from the collection)
|
464
|
+
return if typesense_without_auto_index_scope
|
465
|
+
|
466
|
+
api_response = nil
|
467
|
+
|
468
|
+
typesense_configurations.each do |options, settings|
|
469
|
+
next if typesense_indexing_disabled?(options)
|
470
|
+
|
471
|
+
collection_obj = typesense_ensure_init(options, settings)
|
472
|
+
|
473
|
+
typesense_find_in_batches(batch_size) do |group|
|
474
|
+
if typesense_conditional_index?(options)
|
475
|
+
# delete non-indexable objects
|
476
|
+
ids = group.reject { |o| typesense_indexable?(o, options) }.map { |o| typesense_object_id_of(o, options) }
|
477
|
+
delete_by_query(collection_obj[:alias_name], "id: [#{ids.reject(&:blank?).join(",")}]")
|
478
|
+
|
479
|
+
group = group.select { |o| typesense_indexable?(o, options) }
|
480
|
+
end
|
481
|
+
documents = group.map do |o|
|
482
|
+
attributes = settings.get_attributes(o)
|
483
|
+
attributes = attributes.to_hash unless attributes.instance_of?(Hash)
|
484
|
+
# convert to JSON object
|
485
|
+
attributes.merge!("id" => typesense_object_id_of(o, options)).to_json
|
486
|
+
end
|
487
|
+
|
488
|
+
jsonl_object = documents.join("\n")
|
489
|
+
api_response = import_documents(jsonl_object, "upsert", collection_obj[:alias_name])
|
490
|
+
end
|
491
|
+
end
|
492
|
+
api_response
|
493
|
+
end
|
494
|
+
|
495
|
+
def typesense_reindex(batch_size = Typesense::IndexSettings::DEFAULT_BATCH_SIZE)
|
496
|
+
# typesense_reindex: Reindexes whole database using alias(removes deleted objects from collection)
|
497
|
+
return if typesense_without_auto_index_scope
|
498
|
+
|
499
|
+
typesense_configurations.each do |options, settings|
|
500
|
+
next if typesense_indexing_disabled?(options)
|
501
|
+
|
502
|
+
begin
|
503
|
+
master_index = typesense_ensure_init(options, settings, false)
|
504
|
+
delete_collection(master_index[:alias_name])
|
505
|
+
rescue ArgumentError
|
506
|
+
@typesense_indexes[settings] = { collection_name: "", alias_name: typesense_collection_name(options) }
|
507
|
+
master_index = @typesense_indexes[settings]
|
508
|
+
end
|
509
|
+
|
510
|
+
# init temporary index
|
511
|
+
src_index_name = collection_name_with_timestamp(options)
|
512
|
+
tmp_options = options.merge({ index_name: src_index_name })
|
513
|
+
tmp_options.delete(:per_environment) # already included in the temporary index_name
|
514
|
+
tmp_settings = settings.dup
|
515
|
+
|
516
|
+
create_collection(src_index_name, settings)
|
517
|
+
|
518
|
+
typesense_find_in_batches(batch_size) do |group|
|
519
|
+
if typesense_conditional_index?(options)
|
520
|
+
# select only indexable objects
|
521
|
+
group = group.select { |o| typesense_indexable?(o, tmp_options) }
|
522
|
+
end
|
523
|
+
documents = group.map do |o|
|
524
|
+
tmp_settings.get_attributes(o).merge!("id" => typesense_object_id_of(o, tmp_options)).to_json
|
525
|
+
end
|
526
|
+
jsonl_object = documents.join("\n")
|
527
|
+
import_documents(jsonl_object, "upsert", src_index_name)
|
528
|
+
end
|
529
|
+
|
530
|
+
upsert_alias(src_index_name, master_index[:alias_name])
|
531
|
+
master_index[:collection_name] = src_index_name
|
532
|
+
end
|
533
|
+
nil
|
534
|
+
end
|
535
|
+
|
536
|
+
def typesense_index_objects_async(objects, batch_size = Typesense::IndexSettings::DEFAULT_BATCH_SIZE)
|
537
|
+
typesense_configurations.each do |options, settings|
|
538
|
+
next if typesense_indexing_disabled?(options)
|
539
|
+
collection_obj = typesense_ensure_init(options, settings)
|
540
|
+
documents = objects.map do |o|
|
541
|
+
settings.get_attributes(o).merge!("id" => typesense_object_id_of(o, options)).to_json
|
542
|
+
end
|
543
|
+
jsonl_object = documents.join("\n")
|
544
|
+
ImportJob.perform(jsonl_object, collection_obj[:alias_name], batch_size)
|
545
|
+
Rails.logger.info "#{objects.length} objects enqueued for import into #{collection_obj[:collection_name]}"
|
546
|
+
end
|
547
|
+
nil
|
548
|
+
end
|
549
|
+
|
550
|
+
def typesense_index_objects(objects, batch_size = Typesense::IndexSettings::DEFAULT_BATCH_SIZE)
|
551
|
+
typesense_configurations.each do |options, settings|
|
552
|
+
next if typesense_indexing_disabled?(options)
|
553
|
+
|
554
|
+
collection_obj = typesense_ensure_init(options, settings)
|
555
|
+
documents = objects.map do |o|
|
556
|
+
settings.get_attributes(o).merge!("id" => typesense_object_id_of(o, options)).to_json
|
557
|
+
end
|
558
|
+
jsonl_object = documents.join("\n")
|
559
|
+
import_documents(jsonl_object, "upsert", collection_obj[:alias_name], batch_size: batch_size)
|
560
|
+
Rails.logger.info "#{objects.length} objects upserted into #{collection_obj[:collection_name]}!"
|
561
|
+
end
|
562
|
+
nil
|
563
|
+
end
|
564
|
+
|
565
|
+
def typesense_index!(object)
|
566
|
+
# typesense_index!: Creates a document for the object and retrieves it.
|
567
|
+
return if typesense_without_auto_index_scope
|
568
|
+
|
569
|
+
api_response = nil
|
570
|
+
|
571
|
+
typesense_configurations.each do |options, settings|
|
572
|
+
next if typesense_indexing_disabled?(options)
|
573
|
+
|
574
|
+
object_id = typesense_object_id_of(object, options)
|
575
|
+
collection_obj = typesense_ensure_init(options, settings)
|
576
|
+
|
577
|
+
if typesense_indexable?(object, options)
|
578
|
+
raise ArgumentError, "Cannot index a record with a blank objectID" if object_id.blank?
|
579
|
+
|
580
|
+
object = settings.get_attributes(object).merge!("id" => object_id)
|
581
|
+
|
582
|
+
if options[:dirty_values]
|
583
|
+
api_response = upsert_document(object, collection_obj[:alias_name], options[:dirty_values])
|
584
|
+
else
|
585
|
+
api_response = upsert_document(object, collection_obj[:alias_name])
|
586
|
+
end
|
587
|
+
elsif typesense_conditional_index?(options) && !object_id.blank?
|
588
|
+
begin
|
589
|
+
api_response = delete_document(object_id, collection_obj[:collection_name])
|
590
|
+
rescue Typesense::Error::ObjectNotFound => e
|
591
|
+
Rails.logger.error "Object not found in index: #{e.message}"
|
592
|
+
end
|
593
|
+
end
|
594
|
+
end
|
595
|
+
|
596
|
+
api_response
|
597
|
+
end
|
598
|
+
|
599
|
+
def typesense_remove_from_index!(object)
|
600
|
+
# typesense_remove_from_index: Removes specified object from the collection of given model.
|
601
|
+
return if typesense_without_auto_index_scope
|
602
|
+
|
603
|
+
object_id = typesense_object_id_of(object)
|
604
|
+
raise ArgumentError, "Cannot index a record with a blank objectID" if object_id.blank?
|
605
|
+
|
606
|
+
typesense_configurations.each do |options, settings|
|
607
|
+
next if typesense_indexing_disabled?(options)
|
608
|
+
|
609
|
+
collection_obj = typesense_ensure_init(options, settings, false)
|
610
|
+
|
611
|
+
begin
|
612
|
+
delete_document(object_id, collection_obj[:alias_name])
|
613
|
+
rescue Typesense::Error::ObjectNotFound => e
|
614
|
+
Rails.logger.error "Object #{object_id} could not be removed from #{collection_obj[:collection_name]} collection! Use reindex to update the collection."
|
615
|
+
end
|
616
|
+
Rails.logger.info "Removed document with object id '#{object_id}' from #{collection_obj[:collection_name]}"
|
617
|
+
end
|
618
|
+
nil
|
619
|
+
end
|
620
|
+
|
621
|
+
def typesense_clear_index!
|
622
|
+
# typesense_clear_index!: Delete collection of given model.
|
623
|
+
typesense_configurations.each do |options, settings|
|
624
|
+
next if typesense_indexing_disabled?(options)
|
625
|
+
|
626
|
+
collection_obj = typesense_ensure_init(options, settings, false)
|
627
|
+
|
628
|
+
delete_collection(collection_obj[:alias_name])
|
629
|
+
Rails.logger.info "Deleted #{collection_obj[:alias_name]} collection!"
|
630
|
+
@typesense_indexes[settings] = nil
|
631
|
+
end
|
632
|
+
nil
|
633
|
+
end
|
634
|
+
|
635
|
+
def typesense_raw_search(q, query_by, params = {})
|
636
|
+
# typesense_raw_search: JSON output of search.
|
637
|
+
params[:page] = params[:page] ? params[:page].to_i : 1
|
638
|
+
collection_obj = typesense_index # index_name)
|
639
|
+
search_collection(params.merge({ q: q, query_by: query_by }), collection_obj[:alias_name])
|
640
|
+
end
|
641
|
+
|
642
|
+
module AdditionalMethods
|
643
|
+
def self.extended(base)
|
644
|
+
class << base
|
645
|
+
alias_method :raw_answer, :typesense_raw_answer unless method_defined? :raw_answer
|
646
|
+
alias_method :facets, :typesense_facets unless method_defined? :facets
|
647
|
+
end
|
648
|
+
end
|
649
|
+
|
650
|
+
def typesense_raw_answer
|
651
|
+
@typesense_json
|
652
|
+
end
|
653
|
+
|
654
|
+
def typesense_facets
|
655
|
+
@typesense_json["facet_counts"]
|
656
|
+
end
|
657
|
+
|
658
|
+
private
|
659
|
+
|
660
|
+
def typesense_init_raw_answer(json)
|
661
|
+
@typesense_json = json
|
662
|
+
end
|
663
|
+
end
|
664
|
+
|
665
|
+
def typesense_search(q, query_by, params = {})
|
666
|
+
# typsense_search: Searches and returns matching objects from the database.
|
667
|
+
|
668
|
+
json = typesense_raw_search(q, query_by, params)
|
669
|
+
hit_ids = json["hits"].map { |hit| hit["document"]["id"] }
|
670
|
+
|
671
|
+
condition_key = if defined?(::Mongoid::Document) && include?(::Mongoid::Document)
|
672
|
+
typesense_object_id_method.in
|
673
|
+
else
|
674
|
+
typesense_object_id_method
|
675
|
+
end
|
676
|
+
|
677
|
+
results_by_id = typesense_options[:type].where(condition_key => hit_ids).index_by do |hit|
|
678
|
+
typesense_object_id_of(hit)
|
679
|
+
end
|
680
|
+
|
681
|
+
results = json["hits"].map do |hit|
|
682
|
+
o = results_by_id[hit["document"]["id"].to_s]
|
683
|
+
next unless o
|
684
|
+
|
685
|
+
o.highlight_result = hit["highlights"]
|
686
|
+
o.snippet_result = hit["highlights"].map do |highlight|
|
687
|
+
highlight["snippet"]
|
688
|
+
end
|
689
|
+
o
|
690
|
+
end.compact
|
691
|
+
|
692
|
+
total_hits = json["found"]
|
693
|
+
res = Typesense::Pagination.create(results, total_hits,
|
694
|
+
typesense_options.merge({ page: json["page"].to_i, per_page: json["request_params"]["per_page"] }))
|
695
|
+
res.extend(AdditionalMethods)
|
696
|
+
res.send(:typesense_init_raw_answer, json)
|
697
|
+
res
|
698
|
+
end
|
699
|
+
|
700
|
+
def typesense_index(name = nil)
|
701
|
+
# typesense_index: Creates collection and its alias.
|
702
|
+
if name
|
703
|
+
typesense_configurations.each do |o, s|
|
704
|
+
return typesense_ensure_init(o, s) if o[:collection_name].to_s == name.to_s || o[:index_name].to_s == name.to_s
|
705
|
+
end
|
706
|
+
raise ArgumentError, "Invalid index/replica name: #{name}"
|
707
|
+
end
|
708
|
+
typesense_ensure_init
|
709
|
+
end
|
710
|
+
|
711
|
+
def typesense_collection_name(options = nil)
|
712
|
+
options ||= typesense_options
|
713
|
+
name = options[:collection_name] || options[:index_name] || model_name.to_s.gsub("::", "_")
|
714
|
+
name = "#{name}_#{Rails.env}" if options[:per_environment]
|
715
|
+
name
|
716
|
+
end
|
717
|
+
|
718
|
+
def typesense_must_reindex?(object)
|
719
|
+
# use +typesense_dirty?+ method if implemented
|
720
|
+
return object.send(:typesense_dirty?) if object.respond_to?(:typesense_dirty?)
|
721
|
+
|
722
|
+
# Loop over each index to see if a attribute used in records has changed
|
723
|
+
typesense_configurations.each do |options, settings|
|
724
|
+
next if typesense_indexing_disabled?(options)
|
725
|
+
return true if typesense_object_id_changed?(object, options)
|
726
|
+
|
727
|
+
settings.get_attribute_names(object).each do |k|
|
728
|
+
return true if typesense_attribute_changed?(object, k)
|
729
|
+
# return true if !object.respond_to?(changed_method) || object.send(changed_method)
|
730
|
+
end
|
731
|
+
[options[:if], options[:unless]].each do |condition|
|
732
|
+
case condition
|
733
|
+
when nil
|
734
|
+
when String, Symbol
|
735
|
+
return true if typesense_attribute_changed?(object, condition)
|
736
|
+
else
|
737
|
+
# if the :if, :unless condition is a anything else,
|
738
|
+
# we have no idea whether we should reindex or not
|
739
|
+
# let's always reindex then
|
740
|
+
return true
|
741
|
+
end
|
742
|
+
end
|
743
|
+
end
|
744
|
+
# By default, we don't reindex
|
745
|
+
false
|
746
|
+
end
|
747
|
+
|
748
|
+
protected
|
749
|
+
|
750
|
+
def typesense_ensure_init(options = nil, settings = nil, create = true)
|
751
|
+
raise ArgumentError, "No `typesense` block found in your model." if typesense_settings.nil?
|
752
|
+
|
753
|
+
@typesense_indexes ||= {}
|
754
|
+
|
755
|
+
options ||= typesense_options
|
756
|
+
settings ||= typesense_settings
|
757
|
+
|
758
|
+
return @typesense_indexes[settings] if @typesense_indexes[settings] && get_collection(@typesense_indexes[settings][:alias_name])
|
759
|
+
|
760
|
+
alias_name = typesense_collection_name(options)
|
761
|
+
collection = get_collection(alias_name)
|
762
|
+
|
763
|
+
if collection
|
764
|
+
collection_name = collection["name"]
|
765
|
+
else
|
766
|
+
collection_name = self.collection_name_with_timestamp(options)
|
767
|
+
raise ArgumentError, "#{collection_name} is not found in your model." unless create
|
768
|
+
|
769
|
+
create_collection(collection_name, settings)
|
770
|
+
upsert_alias(collection_name, alias_name)
|
771
|
+
end
|
772
|
+
@typesense_indexes[settings] = { collection_name: collection_name, alias_name: alias_name }
|
773
|
+
|
774
|
+
@typesense_indexes[settings]
|
775
|
+
end
|
776
|
+
|
777
|
+
private
|
778
|
+
|
779
|
+
def typesense_configurations
|
780
|
+
raise ArgumentError, "No `typesense` block found in your model." if typesense_settings.nil?
|
781
|
+
|
782
|
+
if @configurations.nil?
|
783
|
+
@configurations = {}
|
784
|
+
@configurations[typesense_options] = typesense_settings
|
785
|
+
end
|
786
|
+
@configurations
|
787
|
+
end
|
788
|
+
|
789
|
+
def typesense_object_id_method(options = nil)
|
790
|
+
options ||= typesense_options
|
791
|
+
options[:id] || options[:object_id] || :id
|
792
|
+
end
|
793
|
+
|
794
|
+
def typesense_object_id_of(o, options = nil)
|
795
|
+
o.send(typesense_object_id_method(options)).to_s
|
796
|
+
end
|
797
|
+
|
798
|
+
def typesense_object_id_changed?(o, options = nil)
|
799
|
+
changed = typesense_attribute_changed?(o, typesense_object_id_method(options))
|
800
|
+
changed.nil? ? false : changed
|
801
|
+
end
|
802
|
+
|
803
|
+
def typesense_settings_changed?(prev, current)
|
804
|
+
return true if prev.nil?
|
805
|
+
|
806
|
+
current.each do |k, v|
|
807
|
+
prev_v = prev[k.to_s]
|
808
|
+
if v.is_a?(Array) && prev_v.is_a?(Array)
|
809
|
+
# compare array of strings, avoiding symbols VS strings comparison
|
810
|
+
return true if v.map(&:to_s) != prev_v.map(&:to_s)
|
811
|
+
elsif prev_v != v
|
812
|
+
return true
|
813
|
+
end
|
814
|
+
end
|
815
|
+
false
|
816
|
+
end
|
817
|
+
|
818
|
+
def typesense_full_const_get(name)
|
819
|
+
list = name.split("::")
|
820
|
+
list.shift if list.first.blank?
|
821
|
+
obj = Object.const_defined?(:RUBY_VERSION) && RUBY_VERSION.to_f < 1.9 ? Object : self
|
822
|
+
list.each do |x|
|
823
|
+
# This is required because const_get tries to look for constants in the
|
824
|
+
# ancestor chain, but we only want constants that are HERE
|
825
|
+
obj = obj.const_defined?(x) ? obj.const_get(x) : obj.const_missing(x)
|
826
|
+
end
|
827
|
+
obj
|
828
|
+
end
|
829
|
+
|
830
|
+
def typesense_conditional_index?(options = nil)
|
831
|
+
options ||= typesense_options
|
832
|
+
options[:if].present? || options[:unless].present?
|
833
|
+
end
|
834
|
+
|
835
|
+
def typesense_indexable?(object, options = nil)
|
836
|
+
options ||= typesense_options
|
837
|
+
if_passes = options[:if].blank? || typesense_constraint_passes?(object, options[:if])
|
838
|
+
unless_passes = options[:unless].blank? || !typesense_constraint_passes?(object, options[:unless])
|
839
|
+
if_passes && unless_passes
|
840
|
+
end
|
841
|
+
|
842
|
+
def typesense_constraint_passes?(object, constraint)
|
843
|
+
case constraint
|
844
|
+
when Symbol
|
845
|
+
object.send(constraint)
|
846
|
+
when String
|
847
|
+
object.send(constraint.to_sym)
|
848
|
+
when Enumerable
|
849
|
+
# All constraints must pass
|
850
|
+
constraint.all? { |inner_constraint| typesense_constraint_passes?(object, inner_constraint) }
|
851
|
+
else
|
852
|
+
raise ArgumentError, "Unknown constraint type: #{constraint} (#{constraint.class})" unless constraint.respond_to?(:call)
|
853
|
+
|
854
|
+
constraint.call(object)
|
855
|
+
end
|
856
|
+
end
|
857
|
+
|
858
|
+
def typesense_indexing_disabled?(options = nil)
|
859
|
+
options ||= typesense_options
|
860
|
+
constraint = options[:disable_indexing] || options["disable_indexing"]
|
861
|
+
case constraint
|
862
|
+
when nil
|
863
|
+
return false
|
864
|
+
when true, false
|
865
|
+
return constraint
|
866
|
+
when String, Symbol
|
867
|
+
return send(constraint)
|
868
|
+
else
|
869
|
+
return constraint.call if constraint.respond_to?(:call) # Proc
|
870
|
+
end
|
871
|
+
raise ArgumentError, "Unknown constraint type: #{constraint} (#{constraint.class})"
|
872
|
+
end
|
873
|
+
|
874
|
+
def typesense_find_in_batches(batch_size, &block)
|
875
|
+
if (defined?(::ActiveRecord) && ancestors.include?(::ActiveRecord::Base)) || respond_to?(:find_in_batches)
|
876
|
+
find_in_batches(batch_size: batch_size, &block)
|
877
|
+
elsif defined?(::Sequel) && self < Sequel::Model
|
878
|
+
dataset.extension(:pagination).each_page(batch_size, &block)
|
879
|
+
else
|
880
|
+
# don't worry, mongoid has its own underlying cursor/streaming mechanism
|
881
|
+
items = []
|
882
|
+
all.each do |item|
|
883
|
+
items << item
|
884
|
+
if items.length % batch_size.zero?
|
885
|
+
yield items
|
886
|
+
items = []
|
887
|
+
end
|
888
|
+
end
|
889
|
+
yield items unless items.empty?
|
890
|
+
end
|
891
|
+
end
|
892
|
+
|
893
|
+
def typesense_attribute_changed?(object, attr_name)
|
894
|
+
# if one of two method is implemented, we return its result
|
895
|
+
# true/false means whether it has changed or not
|
896
|
+
# +#{attr_name}_changed?+ always defined for automatic attributes but deprecated after Rails 5.2
|
897
|
+
# +will_save_change_to_#{attr_name}?+ should be use instead for Rails 5.2+, also defined for automatic attributes.
|
898
|
+
# If none of the method are defined, it's a dynamic attribute
|
899
|
+
|
900
|
+
method_name = "#{attr_name}_changed?"
|
901
|
+
if object.respond_to?(method_name)
|
902
|
+
# If +#{attr_name}_changed?+ respond we want to see if the method is user defined or if it's automatically
|
903
|
+
# defined by Rails.
|
904
|
+
# If it's user-defined, we call it.
|
905
|
+
# If it's automatic we check ActiveRecord version to see if this method is deprecated
|
906
|
+
# and try to call +will_save_change_to_#{attr_name}?+ instead.
|
907
|
+
# See: https://github.com/typesense/typesense-rails/pull/338
|
908
|
+
# This feature is not compatible with Ruby 1.8
|
909
|
+
# In this case, we always call #{attr_name}_changed?
|
910
|
+
return object.send(method_name) if Object.const_defined?(:RUBY_VERSION) && RUBY_VERSION.to_f < 1.9
|
911
|
+
return object.send(method_name) unless automatic_changed_method?(object, method_name) && automatic_changed_method_deprecated?
|
912
|
+
end
|
913
|
+
|
914
|
+
return object.send("will_save_change_to_#{attr_name}?") if object.respond_to?("will_save_change_to_#{attr_name}?")
|
915
|
+
|
916
|
+
# We don't know if the attribute has changed, so conservatively assume it has
|
917
|
+
true
|
918
|
+
end
|
919
|
+
|
920
|
+
def automatic_changed_method?(object, method_name)
|
921
|
+
unless object.respond_to?(method_name)
|
922
|
+
raise ArgumentError,
|
923
|
+
"Method #{method_name} doesn't exist on #{object.class.name}"
|
924
|
+
end
|
925
|
+
|
926
|
+
file = object.method(method_name).source_location[0]
|
927
|
+
file.end_with?("active_model/attribute_methods.rb")
|
928
|
+
end
|
929
|
+
|
930
|
+
def automatic_changed_method_deprecated?
|
931
|
+
(defined?(::ActiveRecord) && ActiveRecord::VERSION::MAJOR >= 5 && ActiveRecord::VERSION::MINOR >= 1) ||
|
932
|
+
(defined?(::ActiveRecord) && ActiveRecord::VERSION::MAJOR > 5)
|
933
|
+
end
|
934
|
+
end
|
935
|
+
|
936
|
+
# these are the instance methods included
|
937
|
+
module InstanceMethods
|
938
|
+
def self.included(base)
|
939
|
+
base.instance_eval do
|
940
|
+
alias_method :index!, :typesense_index! unless method_defined? :index!
|
941
|
+
alias_method :remove_from_index!, :typesense_remove_from_index! unless method_defined? :remove_from_index!
|
942
|
+
end
|
943
|
+
end
|
944
|
+
|
945
|
+
def typesense_index!
|
946
|
+
self.class.typesense_index!(self)
|
947
|
+
end
|
948
|
+
|
949
|
+
def typesense_remove_from_index!
|
950
|
+
self.class.typesense_remove_from_index!(self)
|
951
|
+
end
|
952
|
+
|
953
|
+
def typesense_enqueue_remove_from_index!
|
954
|
+
if typesense_options[:enqueue]
|
955
|
+
typesense_options[:enqueue].call(self, true) unless self.class.send(:typesense_indexing_disabled?,
|
956
|
+
typesense_options)
|
957
|
+
else
|
958
|
+
typesense_remove_from_index!
|
959
|
+
end
|
960
|
+
end
|
961
|
+
|
962
|
+
def typesense_enqueue_index!
|
963
|
+
if typesense_options[:enqueue]
|
964
|
+
typesense_options[:enqueue].call(self, false) unless self.class.send(:typesense_indexing_disabled?,
|
965
|
+
typesense_options)
|
966
|
+
else
|
967
|
+
typesense_index!
|
968
|
+
end
|
969
|
+
end
|
970
|
+
|
971
|
+
private
|
972
|
+
|
973
|
+
def typesense_mark_for_auto_indexing
|
974
|
+
@typesense_auto_indexing = true
|
975
|
+
end
|
976
|
+
|
977
|
+
def typesense_mark_must_reindex
|
978
|
+
# typesense_must_reindex flag is reset after every commit as part. If we must reindex at any point in
|
979
|
+
# a stransaction, keep flag set until it is explicitly unset
|
980
|
+
@typesense_must_reindex ||= if defined?(::Sequel) && is_a?(Sequel::Model)
|
981
|
+
new? || self.class.typesense_must_reindex?(self)
|
982
|
+
else
|
983
|
+
new_record? || self.class.typesense_must_reindex?(self)
|
984
|
+
end
|
985
|
+
true
|
986
|
+
end
|
987
|
+
|
988
|
+
def typesense_perform_index_tasks
|
989
|
+
return if !@typesense_auto_indexing || @typesense_must_reindex == false
|
990
|
+
|
991
|
+
typesense_enqueue_index!
|
992
|
+
remove_instance_variable(:@typesense_auto_indexing) if instance_variable_defined?(:@typesense_auto_indexing)
|
993
|
+
remove_instance_variable(:@typesense_must_reindex) if instance_variable_defined?(:@typesense_must_reindex)
|
994
|
+
end
|
995
|
+
end
|
996
|
+
end
|