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.
@@ -1,4 +1,4 @@
1
- module MeiliSearch
1
+ module Meilisearch
2
2
  module Rails
3
3
  module Configuration
4
4
  def configuration
@@ -44,11 +44,11 @@ module MeiliSearch
44
44
  def client
45
45
  return black_hole unless active?
46
46
 
47
- ::MeiliSearch::Client.new(
47
+ ::Meilisearch::Client.new(
48
48
  configuration[:meilisearch_url] || 'http://localhost:7700',
49
49
  configuration[:meilisearch_api_key],
50
50
  configuration.slice(:timeout, :max_retries)
51
- .merge(client_agents: MeiliSearch::Rails.qualified_version)
51
+ .merge(client_agents: Meilisearch::Rails.qualified_version)
52
52
  )
53
53
  end
54
54
  end
@@ -1,4 +1,4 @@
1
- module MeiliSearch
1
+ module Meilisearch
2
2
  module Rails
3
3
  class NoBlockGiven < StandardError; end
4
4
 
@@ -6,7 +6,7 @@ module MeiliSearch
6
6
 
7
7
  class NotConfigured < StandardError
8
8
  def message
9
- 'Please configure Meilisearch. Set MeiliSearch::Rails.configuration = ' \
9
+ 'Please configure Meilisearch. Set Meilisearch::Rails.configuration = ' \
10
10
  "{meilisearch_url: 'YOUR_MEILISEARCH_URL', meilisearch_api_key: 'YOUR_API_KEY'}"
11
11
  end
12
12
  end
@@ -0,0 +1,19 @@
1
+ module Meilisearch
2
+ module Rails
3
+ class MSCleanUpJob < ::ActiveJob::Base
4
+ queue_as :meilisearch
5
+
6
+ def perform(documents)
7
+ documents.each do |document|
8
+ index = Meilisearch::Rails.client.index(document[:index_uid])
9
+
10
+ if document[:synchronous]
11
+ index.delete_document(document[:primary_key]).await
12
+ else
13
+ index.delete_document(document[:primary_key])
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,4 +1,4 @@
1
- module MeiliSearch
1
+ module Meilisearch
2
2
  module Rails
3
3
  class MSJob < ::ActiveJob::Base
4
4
  queue_as :meilisearch
@@ -0,0 +1,78 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+
3
+ module Meilisearch
4
+ module Rails
5
+ class FederatedSearchResult
6
+ attr_reader :metadata, :hits
7
+
8
+ def initialize(searches, raw_results)
9
+ hits = raw_results.delete('hits')
10
+ @hits = load_hits(hits, searches.to_a)
11
+ @metadata = raw_results
12
+ end
13
+
14
+ include Enumerable
15
+
16
+ delegate :each, :to_a, :to_ary, :empty?, :[], :first, :last, to: :@hits
17
+
18
+ private
19
+
20
+ def load_hits(hits, searches)
21
+ hits_by_pos = hits.group_by { |hit| hit['_federation']['queriesPosition'] }
22
+
23
+ keys_and_records_by_pos = hits_by_pos.to_h do |pos, group_hits|
24
+ search_target, search_opts = searches[pos]
25
+
26
+ scope = if search_opts[:scope]
27
+ search_opts[:scope]
28
+ elsif search_target.instance_of?(Class)
29
+ search_target
30
+ end
31
+
32
+ if scope.present?
33
+ [pos, load_results(scope, group_hits)]
34
+ else
35
+ [pos, [nil, group_hits]]
36
+ end
37
+ end
38
+
39
+ hits.filter_map do |hit|
40
+ hit_cond_key, recs_by_id = keys_and_records_by_pos[hit['_federation']['queriesPosition']]
41
+
42
+ if hit_cond_key.present?
43
+ record = recs_by_id[hit[hit_cond_key.to_s].to_s]
44
+ record&.formatted = hit['_formatted']
45
+ record
46
+ else
47
+ hit
48
+ end
49
+ end
50
+ end
51
+
52
+ def load_results(scope, hits)
53
+ klass = scope.respond_to?(:model) ? scope.model : scope
54
+
55
+ pk_method = klass.ms_primary_key_method
56
+ pk_method = pk_method.in if Utilities.mongo_model?(klass)
57
+
58
+ condition_key = pk_is_virtual?(klass, pk_method) ? klass.primary_key : pk_method
59
+
60
+ hits_by_id = hits.index_by { |hit| hit[condition_key.to_s] }
61
+
62
+ records = scope.where(condition_key => hits_by_id.keys)
63
+
64
+ results_by_id = records.index_by do |record|
65
+ record.send(condition_key).to_s
66
+ end
67
+
68
+ [condition_key, results_by_id]
69
+ end
70
+
71
+ def pk_is_virtual?(model_class, pk_method)
72
+ model_class.columns
73
+ .map(&(Utilities.sequel_model?(model_class) ? :to_s : :name))
74
+ .exclude?(pk_method.to_s)
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,115 @@
1
+ module Meilisearch
2
+ module Rails
3
+ class MultiSearchResult
4
+ attr_reader :metadata
5
+
6
+ def initialize(searches, raw_results)
7
+ @results = {}
8
+ @metadata = {}
9
+
10
+ searches.zip(raw_results['results']).each do |(target, search_options), result|
11
+ results_class = if search_options[:class_name]
12
+ Meilisearch::Rails.logger.warn(
13
+ '[meilisearch-rails] The :class_name option in multi search is deprecated, please use :scope instead.'
14
+ )
15
+
16
+ search_options[:class_name].constantize
17
+ elsif target.instance_of?(Class)
18
+ target
19
+ elsif search_options[:scope]
20
+ search_options[:scope]
21
+ end
22
+
23
+ @results[target] = results_class ? load_results(results_class, result, scope: search_options[:scope]) : result['hits']
24
+
25
+ @metadata[target] = result.except('hits')
26
+ end
27
+ end
28
+
29
+ include Enumerable
30
+
31
+ def each_hit(&block)
32
+ Meilisearch::Rails.logger.warn(
33
+ <<~DEPRECATION
34
+ [meilisearch-rails] Flattening multi search results is deprecated.
35
+ If you do not want the results to be grouped, please use federated search instead.
36
+ DEPRECATION
37
+ )
38
+
39
+ @results.each_value do |results|
40
+ results.each(&block)
41
+ end
42
+ end
43
+
44
+ def each(&block)
45
+ Meilisearch::Rails.logger.info(
46
+ <<~INFO
47
+ [meilisearch-rails] #each on a multi search now iterates through grouped results.
48
+ If you do not want the results to be grouped, please use federated search instead.
49
+ To quickly go back to the old deprecated behavior, use `#each_hit`.
50
+ INFO
51
+ )
52
+
53
+ @results.each(&block)
54
+ end
55
+
56
+ def each_result(&block)
57
+ @results.each(&block)
58
+ end
59
+
60
+ def to_a
61
+ Meilisearch::Rails.logger.warn(
62
+ <<~DEPRECATION
63
+ [meilisearch-rails] Flattening multi search results is deprecated.
64
+ If you do not want the results to be grouped, please use federated search instead.
65
+ DEPRECATION
66
+ )
67
+ @results.values.flatten(1)
68
+ end
69
+ alias to_ary to_a
70
+
71
+ def to_h
72
+ @results
73
+ end
74
+ alias to_hash to_h
75
+
76
+ private
77
+
78
+ def load_results(klass, result, scope:)
79
+ scope ||= klass
80
+
81
+ pk_method = klass.ms_primary_key_method
82
+ pk_method = pk_method.in if Utilities.mongo_model?(klass)
83
+
84
+ condition_key = pk_is_virtual?(klass, pk_method) ? klass.primary_key : pk_method
85
+
86
+ hits_by_id =
87
+ result['hits'].index_by { |hit| hit[condition_key.to_s] }
88
+
89
+ records = scope.where(condition_key => hits_by_id.keys)
90
+
91
+ if records.respond_to? :in_order_of
92
+ records.in_order_of(condition_key, hits_by_id.keys).each do |record|
93
+ record.formatted = hits_by_id[record.send(condition_key).to_s]['_formatted']
94
+ end
95
+ else
96
+ results_by_id = records.index_by do |hit|
97
+ hit.send(condition_key).to_s
98
+ end
99
+
100
+ result['hits'].filter_map do |hit|
101
+ record = results_by_id[hit[condition_key.to_s].to_s]
102
+ record&.formatted = hit['_formatted']
103
+ record
104
+ end
105
+ end
106
+ end
107
+
108
+ def pk_is_virtual?(model_class, pk_method)
109
+ model_class.columns
110
+ .map(&(Utilities.sequel_model?(model_class) ? :to_s : :name))
111
+ .exclude?(pk_method.to_s)
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,104 @@
1
+ require_relative 'multi_search/multi_search_result'
2
+ require_relative 'multi_search/federated_search_result'
3
+
4
+ module Meilisearch
5
+ module Rails
6
+ class << self
7
+ def multi_search(searches)
8
+ search_parameters = searches.map do |(index_target, options)|
9
+ model_class = options[:scope].respond_to?(:model) ? options[:scope].model : options[:scope]
10
+ index_target = options.delete(:index_uid) || model_class || index_target
11
+
12
+ paginate(options) if pagination_enabled?
13
+ normalize(options, index_target)
14
+ end
15
+
16
+ MultiSearchResult.new(searches, client.multi_search(queries: search_parameters))
17
+ end
18
+
19
+ def federated_search(queries:, federation: {})
20
+ if federation.nil?
21
+ Meilisearch::Rails.logger.warn(
22
+ '[meilisearch-rails] In federated_search, `nil` is an invalid `:federation` option. To explicitly use defaults, pass `{}`.'
23
+ )
24
+
25
+ federation = {}
26
+ end
27
+
28
+ queries.map! { |item| [nil, item] } if queries.is_a?(Array)
29
+
30
+ cleaned_queries = queries.filter_map do |(index_target, options)|
31
+ model_class = options[:scope].respond_to?(:model) ? options[:scope].model : options[:scope]
32
+ index_target = options.delete(:index_uid) || index_target || model_class
33
+
34
+ strip_pagination_options(options)
35
+ normalize(options, index_target)
36
+ end
37
+
38
+ raw_results = client.multi_search(queries: cleaned_queries, federation: federation)
39
+
40
+ FederatedSearchResult.new(queries, raw_results)
41
+ end
42
+
43
+ private
44
+
45
+ def normalize(options, index_target)
46
+ index_target = index_uid_from_target(index_target)
47
+
48
+ return nil if index_target.nil?
49
+
50
+ options
51
+ .except(:class_name, :scope)
52
+ .merge!(index_uid: index_target)
53
+ end
54
+
55
+ def index_uid_from_target(index_target)
56
+ case index_target
57
+ when String, Symbol
58
+ index_target
59
+ when Class
60
+ if index_target.respond_to?(:index)
61
+ index_target.index.uid
62
+ else
63
+ Meilisearch::Rails.logger.warn <<~MODEL_NOT_INDEXED
64
+ [meilisearch-rails] This class was passed to a multi/federated search but it does not have an #index: #{index_target}
65
+ [meilisearch-rails] Are you sure it has a `meilisearch` block?
66
+ MODEL_NOT_INDEXED
67
+
68
+ nil
69
+ end
70
+ end
71
+ end
72
+
73
+ def paginate(options)
74
+ %w[page hitsPerPage hits_per_page].each do |key|
75
+ # Deletes hitsPerPage to avoid passing along a meilisearch-ruby warning/exception
76
+ value = options.delete(key) || options.delete(key.to_sym)
77
+ options[key.underscore.to_sym] = value.to_i if value
78
+ end
79
+
80
+ # It is required to activate the finite pagination in Meilisearch v0.30 (or newer),
81
+ # to have at least `hits_per_page` defined or `page` in the search request.
82
+ options[:page] ||= 1
83
+ end
84
+
85
+ def strip_pagination_options(options)
86
+ pagination_options = %w[page hitsPerPage hits_per_page limit offset].select do |key|
87
+ options.delete(key) || options.delete(key.to_sym)
88
+ end
89
+
90
+ return if pagination_options.empty?
91
+
92
+ Meilisearch::Rails.logger.warn <<~WRONG_PAGINATION
93
+ [meilisearch-rails] Pagination options in federated search must apply to whole federation.
94
+ [meilisearch-rails] These options have been removed: #{pagination_options.join(', ')}.
95
+ [meilisearch-rails] Please pass them after queries, in the `federation:` option.
96
+ WRONG_PAGINATION
97
+ end
98
+
99
+ def pagination_enabled?
100
+ Meilisearch::Rails.configuration[:pagination_backend]
101
+ end
102
+ end
103
+ end
104
+ end
@@ -1,6 +1,6 @@
1
1
  require 'singleton'
2
2
 
3
- module MeiliSearch
3
+ module Meilisearch
4
4
  module Rails
5
5
  class NullObject
6
6
  include Singleton
@@ -1,11 +1,11 @@
1
1
  unless defined? Kaminari
2
- raise(MeiliSearch::BadConfiguration,
2
+ raise(Meilisearch::BadConfiguration,
3
3
  "Meilisearch: Please add 'kaminari' to your Gemfile to use kaminari pagination backend")
4
4
  end
5
5
 
6
6
  require 'kaminari/models/array_extension'
7
7
 
8
- module MeiliSearch
8
+ module Meilisearch
9
9
  module Rails
10
10
  module Pagination
11
11
  class Kaminari < ::Kaminari::PaginatableArray
@@ -18,14 +18,20 @@ module MeiliSearch
18
18
  end
19
19
 
20
20
  def self.create(results, total_hits, options = {})
21
- offset = ((options[:page] - 1) * options[:per_page])
21
+ unless Meilisearch::Rails.active?
22
+ total_hits = 0
23
+ options[:page] = 1
24
+ options[:per_page] = 1
25
+ end
26
+
27
+ offset = (options[:page] - 1) * options[:per_page]
22
28
  array = new results, limit: options[:per_page], offset: offset, total_count: total_hits
23
29
 
24
30
  if array.empty? && !results.empty?
25
31
  # since Kaminari 0.16.0, you need to pad the results with nil values so it matches the offset param
26
32
  # otherwise you'll get an empty array: https://github.com/amatsuda/kaminari/commit/29fdcfa8865f2021f710adaedb41b7a7b081e34d
27
33
  results = Array.new(offset) + results
28
- array = new results, offset: offset, limit: options[:per_page], total_count: total_hits
34
+ array = new results, offset: offset, limit: limit, total_count: total_hits
29
35
  end
30
36
 
31
37
  array
@@ -1,15 +1,21 @@
1
1
  begin
2
2
  require 'will_paginate/collection'
3
3
  rescue LoadError
4
- raise(MeiliSearch::BadConfiguration,
5
- "MeiliSearch: Please add 'will_paginate' to your Gemfile to use will_paginate pagination backend")
4
+ raise(Meilisearch::BadConfiguration,
5
+ "Meilisearch: Please add 'will_paginate' to your Gemfile to use will_paginate pagination backend")
6
6
  end
7
7
 
8
- module MeiliSearch
8
+ module Meilisearch
9
9
  module Rails
10
10
  module Pagination
11
11
  class WillPaginate
12
12
  def self.create(results, total_hits, options = {})
13
+ unless Meilisearch::Rails.active?
14
+ total_hits = 0
15
+ options[:page] = 1
16
+ options[:per_page] = 1
17
+ end
18
+
13
19
  ::WillPaginate::Collection.create(options[:page], options[:per_page], total_hits) do |pager|
14
20
  pager.replace results
15
21
  end
@@ -1,11 +1,11 @@
1
- module MeiliSearch
1
+ module Meilisearch
2
2
  module Rails
3
3
  module Pagination
4
4
  autoload :WillPaginate, 'meilisearch/rails/pagination/will_paginate'
5
5
  autoload :Kaminari, 'meilisearch/rails/pagination/kaminari'
6
6
 
7
7
  def self.create(results, total_hits, options = {})
8
- pagination_backend = MeiliSearch::Rails.configuration[:pagination_backend]
8
+ pagination_backend = Meilisearch::Rails.configuration[:pagination_backend]
9
9
 
10
10
  if pagination_backend.nil? || (is_pagy = pagination_backend.to_s == 'pagy')
11
11
  log_pagy_error if is_pagy
@@ -17,12 +17,12 @@ module MeiliSearch
17
17
  end
18
18
 
19
19
  def self.log_pagy_error
20
- MeiliSearch::Rails.logger
21
- .warning('[meilisearch-rails] Remove `pagination_backend: :pagy` from your initializer, `pagy` it is not required for `pagy`')
20
+ Meilisearch::Rails.logger
21
+ .warn('[meilisearch-rails] Remove `pagination_backend: :pagy` from your initializer, `pagy` it is not required for `pagy`')
22
22
  end
23
23
 
24
24
  def self.load_pagination!(pagination_backend, results, total_hits, options)
25
- ::MeiliSearch::Rails::Pagination
25
+ ::Meilisearch::Rails::Pagination
26
26
  .const_get(pagination_backend.to_s.classify)
27
27
  .create(results, total_hits, options)
28
28
  rescue NameError
@@ -1,6 +1,6 @@
1
1
  require 'rails'
2
2
 
3
- module MeiliSearch
3
+ module Meilisearch
4
4
  module Rails
5
5
  class Railtie < ::Rails::Railtie
6
6
  rake_tasks do
@@ -3,26 +3,26 @@ namespace :meilisearch do
3
3
  task reindex: :environment do
4
4
  puts 'Reindexing all Meilisearch models'
5
5
 
6
- MeiliSearch::Rails::Utilities.reindex_all_models
6
+ Meilisearch::Rails::Utilities.reindex_all_models
7
7
  end
8
8
 
9
9
  desc 'Set settings to all indexes'
10
10
  task set_all_settings: :environment do
11
11
  puts 'Set settings in all Meilisearch models'
12
12
 
13
- MeiliSearch::Rails::Utilities.set_settings_all_models
13
+ Meilisearch::Rails::Utilities.set_settings_all_models
14
14
  end
15
15
 
16
16
  desc 'Clear all indexes'
17
17
  task clear_indexes: :environment do
18
18
  puts 'Clearing indexes from all Meilisearch models'
19
19
 
20
- MeiliSearch::Rails::Utilities.clear_all_indexes
20
+ Meilisearch::Rails::Utilities.clear_all_indexes
21
21
  end
22
22
 
23
23
  desc 'Create initializer file'
24
24
  task install: :environment do
25
- puts 'Creating initializer file'
25
+ puts 'Writing initializer file at config/initializers/meilisearch.rb'
26
26
 
27
27
  copy_file "#{__dir__}/../templates/initializer.rb", 'config/initializers/meilisearch.rb'
28
28
  end
@@ -1,4 +1,4 @@
1
- MeiliSearch::Rails.configuration = {
1
+ Meilisearch::Rails.configuration = {
2
2
  meilisearch_url: ENV.fetch('MEILISEARCH_HOST', 'http://localhost:7700'),
3
3
  meilisearch_api_key: ENV.fetch('MEILISEARCH_API_KEY', 'YourMeilisearchAPIKey')
4
4
  }
@@ -1,4 +1,4 @@
1
- module MeiliSearch
1
+ module Meilisearch
2
2
  module Rails
3
3
  module Utilities
4
4
  class << self
@@ -8,7 +8,7 @@ module MeiliSearch
8
8
  elsif ::Rails.application
9
9
  ::Rails.application.eager_load!
10
10
  end
11
- klasses = MeiliSearch::Rails.instance_variable_get(:@included_in)
11
+ klasses = Meilisearch::Rails.instance_variable_get(:@included_in)
12
12
  (klasses + klasses.map(&:descendants).flatten).uniq
13
13
  end
14
14
 
@@ -48,6 +48,14 @@ module MeiliSearch
48
48
  true
49
49
  end
50
50
 
51
+ def mongo_model?(model_class)
52
+ defined?(::Mongoid::Document) && model_class.include?(::Mongoid::Document)
53
+ end
54
+
55
+ def sequel_model?(model_class)
56
+ defined?(::Sequel::Model) && model_class < Sequel::Model
57
+ end
58
+
51
59
  private
52
60
 
53
61
  def constraint_passes?(record, constraint)
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module MeiliSearch
3
+ module Meilisearch
4
4
  module Rails
5
- VERSION = '0.10.2'
5
+ VERSION = '0.16.0'
6
6
 
7
7
  def self.qualified_version
8
8
  "Meilisearch Rails (v#{VERSION})"