meilisearch-rails 0.10.2 → 0.16.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.
@@ -3,6 +3,7 @@ require 'meilisearch/rails/null_object'
3
3
  require 'meilisearch/rails/version'
4
4
  require 'meilisearch/rails/utilities'
5
5
  require 'meilisearch/rails/errors'
6
+ require 'meilisearch/rails/multi_search'
6
7
 
7
8
  if defined? Rails
8
9
  begin
@@ -19,7 +20,13 @@ end
19
20
 
20
21
  require 'logger'
21
22
 
22
- module MeiliSearch
23
+ # Workaround for the soft deprecation of MeiliSearch (old spelling)
24
+ # The regular `const_get` method does not seem to work
25
+ # too well with autoload and thus does not pull in methods
26
+ # like `client` which are obviously vital.
27
+ MeiliSearch::Rails = Meilisearch::Rails
28
+
29
+ module Meilisearch
23
30
  module Rails
24
31
  autoload :Configuration, 'meilisearch/rails/configuration'
25
32
  extend Configuration
@@ -66,6 +73,7 @@ module MeiliSearch
66
73
  pagination
67
74
  faceting
68
75
  typo_tolerance
76
+ proximity_precision
69
77
  ].freeze
70
78
 
71
79
  CAMELIZE_OPTIONS = %i[pagination faceting typo_tolerance].freeze
@@ -82,6 +90,22 @@ module MeiliSearch
82
90
  def initialize(options, &block)
83
91
  @options = options
84
92
  instance_exec(&block) if block_given?
93
+ warn_searchable_missing_attributes
94
+ end
95
+
96
+ def warn_searchable_missing_attributes
97
+ searchables = get_setting(:searchable_attributes)&.map { |searchable| searchable.to_s.split('.').first }
98
+ attrs = get_setting(:attributes)&.map { |k, _| k.to_s }
99
+
100
+ if searchables.present? && attrs.present?
101
+ (searchables - attrs).each do |missing_searchable|
102
+ warning = <<~WARNING
103
+ [meilisearch-rails] #{missing_searchable} declared in searchable_attributes but not in attributes. \
104
+ Please add it to attributes if it should be searchable.
105
+ WARNING
106
+ Meilisearch::Rails.logger.warn(warning)
107
+ end
108
+ end
85
109
  end
86
110
 
87
111
  def use_serializer(serializer)
@@ -249,14 +273,15 @@ module MeiliSearch
249
273
  # lazy load the ActiveJob class to ensure the
250
274
  # queue is initialized before using it
251
275
  autoload :MSJob, 'meilisearch/rails/ms_job'
276
+ autoload :MSCleanUpJob, 'meilisearch/rails/ms_clean_up_job'
252
277
  end
253
278
 
254
- # this class wraps an MeiliSearch::Index document ensuring all raised exceptions
279
+ # this class wraps an Meilisearch::Index document ensuring all raised exceptions
255
280
  # are correctly logged or thrown depending on the `raise_on_failure` option
256
281
  class SafeIndex
257
282
  def initialize(index_uid, raise_on_failure, options)
258
- client = MeiliSearch::Rails.client
259
- primary_key = options[:primary_key] || MeiliSearch::Rails::IndexSettings::DEFAULT_PRIMARY_KEY
283
+ client = Meilisearch::Rails.client
284
+ primary_key = options[:primary_key] || Meilisearch::Rails::IndexSettings::DEFAULT_PRIMARY_KEY
260
285
  @raise_on_failure = raise_on_failure.nil? || raise_on_failure
261
286
 
262
287
  SafeIndex.log_or_throw(nil, @raise_on_failure) do
@@ -266,7 +291,7 @@ module MeiliSearch
266
291
  @index = client.index(index_uid)
267
292
  end
268
293
 
269
- ::MeiliSearch::Index.instance_methods(false).each do |m|
294
+ ::Meilisearch::Index.instance_methods(false).each do |m|
270
295
  define_method(m) do |*args, &block|
271
296
  if m == :update_settings
272
297
  args[0].delete(:attributes_to_highlight) if args[0][:attributes_to_highlight]
@@ -275,13 +300,22 @@ module MeiliSearch
275
300
  end
276
301
 
277
302
  SafeIndex.log_or_throw(m, @raise_on_failure) do
278
- return MeiliSearch::Rails.black_hole unless MeiliSearch::Rails.active?
303
+ return Meilisearch::Rails.black_hole unless Meilisearch::Rails.active?
279
304
 
280
305
  @index.send(m, *args, &block)
281
306
  end
282
307
  end
283
308
  end
284
309
 
310
+ # Maually define facet_search due to complications with **opts in ruby 2.*
311
+ def facet_search(*args, **opts)
312
+ SafeIndex.log_or_throw(:facet_search, @raise_on_failure) do
313
+ return Meilisearch::Rails.black_hole unless Meilisearch::Rails.active?
314
+
315
+ @index.facet_search(*args, **opts)
316
+ end
317
+ end
318
+
285
319
  # special handling of wait_for_task to handle null task_id
286
320
  def wait_for_task(task_uid)
287
321
  return if task_uid.nil? && !@raise_on_failure # ok
@@ -295,8 +329,8 @@ module MeiliSearch
295
329
  def settings(*args)
296
330
  SafeIndex.log_or_throw(:settings, @raise_on_failure) do
297
331
  @index.settings(*args)
298
- rescue ::MeiliSearch::ApiError => e
299
- return {} if e.code == 404 # not fatal
332
+ rescue ::Meilisearch::ApiError => e
333
+ return {} if e.code == 'index_not_found' # not fatal
300
334
 
301
335
  raise e
302
336
  end
@@ -304,11 +338,11 @@ module MeiliSearch
304
338
 
305
339
  def self.log_or_throw(method, raise_on_failure, &block)
306
340
  yield
307
- rescue ::MeiliSearch::TimeoutError, ::MeiliSearch::ApiError => e
341
+ rescue ::Meilisearch::TimeoutError, ::Meilisearch::ApiError => e
308
342
  raise e if raise_on_failure
309
343
 
310
344
  # log the error
311
- MeiliSearch::Rails.logger.info("[meilisearch-rails] #{e.message}")
345
+ Meilisearch::Rails.logger.info("[meilisearch-rails] #{e.message}")
312
346
  # return something
313
347
  case method.to_s
314
348
  when 'search'
@@ -321,7 +355,7 @@ module MeiliSearch
321
355
  end
322
356
  end
323
357
 
324
- # these are the class methods added when MeiliSearch is included
358
+ # these are the class methods added when Meilisearch is included
325
359
  module ClassMethods
326
360
  def self.extended(base)
327
361
  class << base
@@ -351,7 +385,7 @@ module MeiliSearch
351
385
  attr_accessor :formatted
352
386
 
353
387
  if options.key?(:per_environment)
354
- raise BadConfiguration, ':per_environment option should be defined globally on MeiliSearch::Rails.configuration block.'
388
+ raise BadConfiguration, ':per_environment option should be defined globally on Meilisearch::Rails.configuration block.'
355
389
  end
356
390
 
357
391
  if options[:synchronous] == true
@@ -373,7 +407,11 @@ module MeiliSearch
373
407
 
374
408
  proc = if options[:enqueue] == true
375
409
  proc do |record, remove|
376
- MSJob.perform_later(record, remove ? 'ms_remove_from_index!' : 'ms_index!')
410
+ if remove
411
+ MSCleanUpJob.perform_later(record.ms_entries)
412
+ else
413
+ MSJob.perform_later(record, 'ms_index!')
414
+ end
377
415
  end
378
416
  elsif options[:enqueue].respond_to?(:call)
379
417
  options[:enqueue]
@@ -383,7 +421,7 @@ module MeiliSearch
383
421
  raise ArgumentError, "Invalid `enqueue` option: #{options[:enqueue]}"
384
422
  end
385
423
  meilisearch_options[:enqueue] = proc do |record, remove|
386
- proc.call(record, remove) unless ms_without_auto_index_scope
424
+ proc.call(record, remove) if ::Meilisearch::Rails.active? && !ms_without_auto_index_scope
387
425
  end
388
426
  end
389
427
  unless options[:auto_index] == false
@@ -445,11 +483,9 @@ module MeiliSearch
445
483
  end
446
484
  end
447
485
  elsif respond_to?(:after_destroy)
448
- after_destroy { |searchable| searchable.ms_enqueue_remove_from_index!(ms_synchronous?) }
486
+ after_commit(on: :destroy) { |searchable| searchable.ms_enqueue_remove_from_index!(ms_synchronous?) }
449
487
  end
450
488
  end
451
-
452
- warn_searchable_missing_attributes
453
489
  end
454
490
 
455
491
  def ms_without_auto_index(&block)
@@ -469,7 +505,7 @@ module MeiliSearch
469
505
  Thread.current["ms_without_auto_index_scope_for_#{model_name}"]
470
506
  end
471
507
 
472
- def ms_reindex!(batch_size = MeiliSearch::Rails::IndexSettings::DEFAULT_BATCH_SIZE, synchronous = false)
508
+ def ms_reindex!(batch_size = Meilisearch::Rails::IndexSettings::DEFAULT_BATCH_SIZE, synchronous = false)
473
509
  return if ms_without_auto_index_scope
474
510
 
475
511
  ms_configurations.each do |options, settings|
@@ -526,7 +562,8 @@ module MeiliSearch
526
562
  def ms_index!(document, synchronous = false)
527
563
  return if ms_without_auto_index_scope
528
564
 
529
- ms_configurations.each do |options, settings|
565
+ # MS tasks to be returned
566
+ ms_configurations.map do |options, settings|
530
567
  next if ms_indexing_disabled?(options)
531
568
 
532
569
  primary_key = ms_primary_key_of(document, options)
@@ -538,20 +575,32 @@ module MeiliSearch
538
575
  doc = doc.merge ms_pk(options) => primary_key
539
576
 
540
577
  if synchronous || options[:synchronous]
541
- index.add_documents!(doc)
578
+ index.add_documents(doc).await
542
579
  else
543
580
  index.add_documents(doc)
544
581
  end
545
582
  elsif ms_conditional_index?(options) && primary_key.present?
546
583
  # remove non-indexable documents
547
584
  if synchronous || options[:synchronous]
548
- index.delete_document!(primary_key)
585
+ index.delete_document(primary_key).await
549
586
  else
550
587
  index.delete_document(primary_key)
551
588
  end
552
589
  end
590
+ end.compact
591
+ end
592
+
593
+ def ms_entries_for(document:, synchronous:)
594
+ primary_key = ms_primary_key_of(document)
595
+ raise ArgumentError, 'Cannot index a record without a primary key' if primary_key.blank?
596
+
597
+ ms_configurations.filter_map do |options, settings|
598
+ {
599
+ synchronous: synchronous || options[:synchronous],
600
+ index_uid: ms_index_uid(options),
601
+ primary_key: primary_key
602
+ }.with_indifferent_access unless ms_indexing_disabled?(options)
553
603
  end
554
- nil
555
604
  end
556
605
 
557
606
  def ms_remove_from_index!(document, synchronous = false)
@@ -565,7 +614,7 @@ module MeiliSearch
565
614
 
566
615
  index = ms_ensure_init(options, settings)
567
616
  if synchronous || options[:synchronous]
568
- index.delete_document!(primary_key)
617
+ index.delete_document(primary_key).await
569
618
  else
570
619
  index.delete_document(primary_key)
571
620
  end
@@ -578,8 +627,8 @@ module MeiliSearch
578
627
  next if ms_indexing_disabled?(options)
579
628
 
580
629
  index = ms_ensure_init(options, settings)
581
- synchronous || options[:synchronous] ? index.delete_all_documents! : index.delete_all_documents
582
- @ms_indexes[MeiliSearch::Rails.active?][settings] = nil
630
+ synchronous || options[:synchronous] ? index.delete_all_documents.await : index.delete_all_documents
631
+ @ms_indexes[Meilisearch::Rails.active?][settings] = nil
583
632
  end
584
633
  nil
585
634
  end
@@ -623,7 +672,7 @@ module MeiliSearch
623
672
  end
624
673
 
625
674
  def ms_search(query, params = {})
626
- if MeiliSearch::Rails.configuration[:pagination_backend]
675
+ if Meilisearch::Rails.configuration[:pagination_backend]
627
676
  %i[page hitsPerPage hits_per_page].each do |key|
628
677
  params[key.to_s.underscore.to_sym] = params[key].to_i if params.key?(key)
629
678
  end
@@ -647,6 +696,8 @@ module MeiliSearch
647
696
  # respond with a valid database column. The blocks below prevent that from happening.
648
697
  has_virtual_column_as_pk = if defined?(::Sequel::Model) && self < Sequel::Model
649
698
  meilisearch_options[:type].columns.map(&:to_s).exclude?(condition_key.to_s)
699
+ elsif Utilities.mongo_model?(self)
700
+ fields.keys.exclude?(condition_key.to_s)
650
701
  else
651
702
  meilisearch_options[:type].columns.map(&:name).map(&:to_s).exclude?(condition_key.to_s)
652
703
  end
@@ -693,7 +744,7 @@ module MeiliSearch
693
744
 
694
745
  def ms_index_uid(options = nil)
695
746
  options ||= meilisearch_options
696
- global_options ||= MeiliSearch::Rails.configuration
747
+ global_options ||= Meilisearch::Rails.configuration
697
748
 
698
749
  name = options[:index_uid] || model_name.to_s.gsub('::', '_')
699
750
  name = "#{name}_#{::Rails.env}" if global_options[:per_environment]
@@ -733,35 +784,45 @@ module MeiliSearch
733
784
  false
734
785
  end
735
786
 
787
+ def ms_primary_key_method(options = nil)
788
+ options ||= meilisearch_options
789
+ options[:primary_key] || options[:id] || (Utilities.mongo_model?(self) ? :_id : :id)
790
+ end
791
+
736
792
  protected
737
793
 
738
- def ms_ensure_init(options = nil, settings = nil, index_settings = nil)
794
+ def ms_ensure_init(options = meilisearch_options, settings = meilisearch_settings, user_configuration = settings.to_settings)
739
795
  raise ArgumentError, 'No `meilisearch` block found in your model.' if meilisearch_settings.nil?
740
796
 
741
797
  @ms_indexes ||= { true => {}, false => {} }
742
798
 
743
- options ||= meilisearch_options
744
- settings ||= meilisearch_settings
799
+ @ms_indexes[Meilisearch::Rails.active?][settings] ||= SafeIndex.new(ms_index_uid(options), meilisearch_options[:raise_on_failure], meilisearch_options)
745
800
 
746
- return @ms_indexes[MeiliSearch::Rails.active?][settings] if @ms_indexes[MeiliSearch::Rails.active?][settings]
801
+ update_settings_if_changed(@ms_indexes[Meilisearch::Rails.active?][settings], options, user_configuration)
747
802
 
748
- @ms_indexes[MeiliSearch::Rails.active?][settings] = SafeIndex.new(ms_index_uid(options), meilisearch_options[:raise_on_failure], meilisearch_options)
803
+ @ms_indexes[Meilisearch::Rails.active?][settings]
804
+ end
749
805
 
750
- current_settings = @ms_indexes[MeiliSearch::Rails.active?][settings].settings(getVersion: 1) rescue nil # if the index doesn't exist
806
+ private
751
807
 
752
- index_settings ||= settings.to_settings
753
- index_settings = options[:primary_settings].to_settings.merge(index_settings) if options[:inherit]
808
+ def update_settings_if_changed(index, options, user_configuration)
809
+ server_state = index.settings
810
+ user_configuration = options[:primary_settings].to_settings.merge(user_configuration) if options[:inherit]
754
811
 
755
- options[:check_settings] = true if options[:check_settings].nil?
812
+ config = user_configuration.except(:attributes_to_highlight, :attributes_to_crop, :crop_length)
756
813
 
757
- if !ms_indexing_disabled?(options) && options[:check_settings] && meilisearch_settings_changed?(current_settings, index_settings)
758
- @ms_indexes[MeiliSearch::Rails.active?][settings].update_settings(index_settings)
814
+ if !skip_checking_settings?(options) && meilisearch_settings_changed?(server_state, config)
815
+ index.update_settings(user_configuration)
759
816
  end
817
+ end
760
818
 
761
- @ms_indexes[MeiliSearch::Rails.active?][settings]
819
+ def skip_checking_settings?(options)
820
+ ms_indexing_disabled?(options) || ms_checking_disabled?(options)
762
821
  end
763
822
 
764
- private
823
+ def ms_checking_disabled?(options)
824
+ options[:check_settings] == false
825
+ end
765
826
 
766
827
  def ms_configurations
767
828
  raise ArgumentError, 'No `meilisearch` block found in your model.' if meilisearch_settings.nil?
@@ -782,11 +843,6 @@ module MeiliSearch
782
843
  @configurations
783
844
  end
784
845
 
785
- def ms_primary_key_method(options = nil)
786
- options ||= meilisearch_options
787
- options[:primary_key] || options[:id] || :id
788
- end
789
-
790
846
  def ms_primary_key_of(doc, options = nil)
791
847
  doc.send(ms_primary_key_method(options)).to_s
792
848
  end
@@ -797,22 +853,25 @@ module MeiliSearch
797
853
  end
798
854
 
799
855
  def ms_pk(options = nil)
800
- options[:primary_key] || MeiliSearch::Rails::IndexSettings::DEFAULT_PRIMARY_KEY
856
+ options[:primary_key] || Meilisearch::Rails::IndexSettings::DEFAULT_PRIMARY_KEY
801
857
  end
802
858
 
803
- def meilisearch_settings_changed?(prev, current)
804
- return true if prev.nil?
859
+ def meilisearch_settings_changed?(server_state, user_configuration)
860
+ return true if server_state.nil?
861
+
862
+ user_configuration.transform_keys! { |key| key.to_s.camelize(:lower) }
805
863
 
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
864
+ user_configuration.any? do |key, user|
865
+ server = server_state[key]
866
+
867
+ if user.is_a?(Hash) && server.is_a?(Hash)
868
+ meilisearch_settings_changed?(server, user)
869
+ elsif user.is_a?(Array) && server.is_a?(Array)
870
+ user.map(&:to_s).sort! != server.map(&:to_s).sort!
871
+ else
872
+ user.to_s != server.to_s
813
873
  end
814
874
  end
815
- false
816
875
  end
817
876
 
818
877
  def ms_conditional_index?(options = nil)
@@ -864,19 +923,6 @@ module MeiliSearch
864
923
  # We don't know if the attribute has changed, so conservatively assume it has
865
924
  true
866
925
  end
867
-
868
- def warn_searchable_missing_attributes
869
- searchables = meilisearch_settings.get_setting(:searchable_attributes)
870
- attrs = meilisearch_settings.get_setting(:attributes)&.keys
871
-
872
- if searchables.present? && attrs.present?
873
- (searchables.map(&:to_s) - attrs.map(&:to_s)).each do |missing_searchable|
874
- MeiliSearch::Rails.logger.warn(
875
- "[meilisearch-rails] #{name}##{missing_searchable} declared in searchable_attributes but not in attributes. Please add it to attributes if it should be searchable."
876
- )
877
- end
878
- end
879
- end
880
926
  end
881
927
 
882
928
  # these are the instance methods included
@@ -923,6 +969,10 @@ module MeiliSearch
923
969
  @ms_synchronous
924
970
  end
925
971
 
972
+ def ms_entries(synchronous = false)
973
+ self.class.ms_entries_for(document: self, synchronous: synchronous || ms_synchronous?)
974
+ end
975
+
926
976
  private
927
977
 
928
978
  def ms_mark_synchronous
@@ -5,7 +5,7 @@ require 'meilisearch/rails/version'
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = 'meilisearch-rails'
8
- s.version = MeiliSearch::Rails::VERSION
8
+ s.version = Meilisearch::Rails::VERSION
9
9
 
10
10
  s.authors = ['Meili']
11
11
  s.email = 'bonjour@meilisearch.com'
@@ -32,7 +32,8 @@ Gem::Specification.new do |s|
32
32
  'Rakefile'
33
33
  ]
34
34
 
35
- s.required_ruby_version = '>= 2.6.0'
35
+ s.required_ruby_version = '>= 3.0.0'
36
36
 
37
- s.add_dependency 'meilisearch', '~> 0.26.0'
37
+ s.add_dependency 'meilisearch', '~> 0.32.0'
38
+ s.add_dependency 'mutex_m', '~> 0.2'
38
39
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: meilisearch-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.2
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Meili
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-12-11 00:00:00.000000000 Z
11
+ date: 2025-05-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: meilisearch
@@ -16,14 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.26.0
19
+ version: 0.32.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.26.0
26
+ version: 0.32.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: mutex_m
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.2'
27
41
  description: Meilisearch integration for Ruby on Rails. See https://github.com/meilisearch/meilisearch
28
42
  email: bonjour@meilisearch.com
29
43
  executables: []
@@ -40,7 +54,11 @@ files:
40
54
  - lib/meilisearch-rails.rb
41
55
  - lib/meilisearch/rails/configuration.rb
42
56
  - lib/meilisearch/rails/errors.rb
57
+ - lib/meilisearch/rails/ms_clean_up_job.rb
43
58
  - lib/meilisearch/rails/ms_job.rb
59
+ - lib/meilisearch/rails/multi_search.rb
60
+ - lib/meilisearch/rails/multi_search/federated_search_result.rb
61
+ - lib/meilisearch/rails/multi_search/multi_search_result.rb
44
62
  - lib/meilisearch/rails/null_object.rb
45
63
  - lib/meilisearch/rails/pagination.rb
46
64
  - lib/meilisearch/rails/pagination/kaminari.rb
@@ -55,7 +73,7 @@ homepage: https://github.com/meilisearch/meilisearch-rails
55
73
  licenses:
56
74
  - MIT
57
75
  metadata: {}
58
- post_install_message:
76
+ post_install_message:
59
77
  rdoc_options: []
60
78
  require_paths:
61
79
  - lib
@@ -63,15 +81,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
63
81
  requirements:
64
82
  - - ">="
65
83
  - !ruby/object:Gem::Version
66
- version: 2.6.0
84
+ version: 3.0.0
67
85
  required_rubygems_version: !ruby/object:Gem::Requirement
68
86
  requirements:
69
87
  - - ">="
70
88
  - !ruby/object:Gem::Version
71
89
  version: '0'
72
90
  requirements: []
73
- rubygems_version: 3.1.6
74
- signing_key:
91
+ rubygems_version: 3.4.19
92
+ signing_key:
75
93
  specification_version: 4
76
94
  summary: Meilisearch integration for Ruby on Rails.
77
95
  test_files: []