Skip to content

Commit f4847d5

Browse files
committed
WIP: Add federated search
1 parent 44542b6 commit f4847d5

File tree

3 files changed

+131
-5
lines changed

3 files changed

+131
-5
lines changed

lib/meilisearch/rails/multi_search.rb

Lines changed: 55 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
@@ -9,23 +10,58 @@ def multi_search(searches)
910
normalize(options, index_target)
1011
end
1112

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

1538
private
1639

1740
def normalize(options, index_target)
41+
index_target = index_uid_from_target(index_target)
42+
43+
return nil if index_target.nil?
44+
1845
options
1946
.except(:class_name)
20-
.merge!(index_uid: index_uid_from_target(index_target))
47+
.merge!(index_uid: index_target)
2148
end
2249

2350
def index_uid_from_target(index_target)
2451
case index_target
2552
when String, Symbol
2653
index_target
27-
else
28-
index_target.index.uid
54+
when Class
55+
if index_target.respond_to?(:index)
56+
index_target.index.uid
57+
else
58+
Meilisearch::Rails.logger.warn <<~MODEL_NOT_INDEXED
59+
[meilisearch-rails] This class was passed to a multi/federated search but it does not have an #index: #{index_target}
60+
[meilisearch-rails] Are you sure it has a `meilisearch` block?
61+
MODEL_NOT_INDEXED
62+
63+
nil
64+
end
2965
end
3066
end
3167

@@ -41,6 +77,20 @@ def paginate(options)
4177
options[:page] ||= 1
4278
end
4379

80+
def strip_pagination_options(options)
81+
pagination_options = %w[page hitsPerPage hits_per_page limit].select do |key|
82+
options.delete(key) || options.delete(key.to_sym)
83+
end
84+
85+
return if pagination_options.empty?
86+
87+
Meilisearch::Rails.logger.warn <<~WRONG_PAGINATION
88+
[meilisearch-rails] Pagination options in federated search must apply to whole federation.
89+
[meilisearch-rails] These options have been removed: #{pagination_options.join(', ')}.
90+
[meilisearch-rails] Please pass them after queries, in the `federation:` option.
91+
WRONG_PAGINATION
92+
end
93+
4494
def pagination_enabled?
4595
Meilisearch::Rails.configuration[:pagination_backend]
4696
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, 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.

0 commit comments

Comments
 (0)