Skip to content

Commit fc4d29a

Browse files
committed
Add federated search
1 parent b5d918f commit fc4d29a

File tree

4 files changed

+133
-6
lines changed

4 files changed

+133
-6
lines changed

lib/meilisearch/rails/multi_search.rb

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
require_relative 'multi_search/result'
1+
require_relative 'multi_search/multi_search_result'
2+
require_relative 'multi_search/federated_search_result'
23

34
module Meilisearch
45
module Rails
@@ -11,23 +12,59 @@ def multi_search(searches)
1112
normalize(options, index_target)
1213
end
1314

14-
MultiSearchResult.new(searches, client.multi_search(search_parameters))
15+
MultiSearchResult.new(searches, client.multi_search(queries: search_parameters))
16+
end
17+
18+
def federated_search(queries:, federation: {})
19+
if federation.nil?
20+
Meilisearch::Rails.logger.warn(
21+
'[meilisearch-rails] In federated_search, `nil` is an invalid `:federation` option. To explicitly use defaults, pass `{}`.'
22+
)
23+
24+
federation = {}
25+
end
26+
27+
queries.map! { |item| [nil, item] } if queries.is_a?(Array)
28+
29+
cleaned_queries = queries.filter_map do |(index_target, options)|
30+
index_target = options.delete(:index_uid) || index_target || options[:class_name]&.constantize
31+
32+
strip_pagination_options(options)
33+
normalize(options, index_target)
34+
end
35+
36+
raw_results = client.multi_search(queries: cleaned_queries, federation: federation)
37+
38+
FederatedSearchResult.new(queries, raw_results)
1539
end
1640

1741
private
1842

1943
def normalize(options, index_target)
44+
index_target = index_uid_from_target(index_target)
45+
46+
return nil if index_target.nil?
47+
2048
options
2149
.except(:class_name)
22-
.merge!(index_uid: index_uid_from_target(index_target))
50+
.merge!(index_uid: index_target)
2351
end
2452

2553
def index_uid_from_target(index_target)
2654
case index_target
2755
when String, Symbol
2856
index_target
29-
else
30-
index_target.index.uid
57+
when Class
58+
if index_target.respond_to?(:index)
59+
index_target.index.uid
60+
else
61+
Meilisearch::Rails.logger.warn <<~MODEL_NOT_INDEXED
62+
[meilisearch-rails] This class was passed to a multi/federated search but it does not have an #index: #{index_target}
63+
[meilisearch-rails] Are you sure it has a `meilisearch` block?
64+
MODEL_NOT_INDEXED
65+
66+
nil
67+
end
3168
end
3269
end
3370

@@ -43,6 +80,20 @@ def paginate(options)
4380
options[:page] ||= 1
4481
end
4582

83+
def strip_pagination_options(options)
84+
pagination_options = %w[page hitsPerPage hits_per_page limit offset].select do |key|
85+
options.delete(key) || options.delete(key.to_sym)
86+
end
87+
88+
return if pagination_options.empty?
89+
90+
Meilisearch::Rails.logger.warn <<~WRONG_PAGINATION
91+
[meilisearch-rails] Pagination options in federated search must apply to whole federation.
92+
[meilisearch-rails] These options have been removed: #{pagination_options.join(', ')}.
93+
[meilisearch-rails] Please pass them after queries, in the `federation:` option.
94+
WRONG_PAGINATION
95+
end
96+
4697
def pagination_enabled?
4798
Meilisearch::Rails.configuration[:pagination_backend]
4899
end
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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+
klass = if search_opts[:class_name]
27+
search_opts[:class_name].constantize
28+
elsif search_target.instance_of?(Class)
29+
search_target
30+
end
31+
32+
if klass.present?
33+
[pos, load_results(klass, 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(klass, hits)
53+
pk_method = klass.ms_primary_key_method
54+
pk_method = pk_method.in if Utilities.mongo_model?(klass)
55+
56+
condition_key = pk_is_virtual?(klass, pk_method) ? klass.primary_key : pk_method
57+
58+
hits_by_id = hits.index_by { |hit| hit[condition_key.to_s] }
59+
60+
records = klass.where(condition_key => hits_by_id.keys)
61+
62+
results_by_id = records.index_by do |record|
63+
record.send(condition_key).to_s
64+
end
65+
66+
[condition_key, results_by_id]
67+
end
68+
69+
def pk_is_virtual?(model_class, pk_method)
70+
model_class.columns
71+
.map(&(Utilities.sequel_model?(model_class) ? :to_s : :name))
72+
.exclude?(pk_method.to_s)
73+
end
74+
end
75+
end
76+
end
File renamed without changes.

meilisearch-rails.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@ Gem::Specification.new do |s|
3434

3535
s.required_ruby_version = '>= 3.0.0'
3636

37-
s.add_dependency 'meilisearch', '~> 0.30.0'
37+
s.add_dependency 'meilisearch', '~> 0.31.0'
3838
s.add_dependency 'mutex_m', '~> 0.2'
3939
end

0 commit comments

Comments
 (0)