-
Notifications
You must be signed in to change notification settings - Fork 47
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
321: Support Multi-Search r=ellnix a=ellnix # Pull Request ## Related issue Fixes #254 I thought I'd make a draft PR to discuss and review API decisions. I discussed the method signature of a theoretical `multi_search` in #254 (comment), if there is no problem I will proceed with this one: ```ruby MeiliSearch::Rails.multi_search( 'book_production' => {q: 'paper', class_name: 'Book', **book_options}, # Index with a model Product => {q: 'thing', **product_options}, # Model with implied index 'blurbs' => { q: 'happy' }, # Index not backed by a model, results will be simple hashes **other_searches ) ``` Initially I expected that the return type would be a simple array, however this might not be ideal since it - does not provide a performant way to use only the results of a single search - does not provide a way to access search metadata [provided by meilisearch](https://www.meilisearch.com/docs/reference/api/multi_search#response) I am thinking of either a simple hash or a hash-like class with convenience methods. Co-authored-by: ellnix <[email protected]>
- Loading branch information
Showing
6 changed files
with
386 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
require_relative 'multi_search/result' | ||
|
||
module MeiliSearch | ||
module Rails | ||
class << self | ||
def multi_search(searches) | ||
search_parameters = searches.map do |(index_target, options)| | ||
paginate(options) if pagination_enabled? | ||
normalize(options, index_target) | ||
end | ||
|
||
MultiSearchResult.new(searches, client.multi_search(search_parameters)) | ||
end | ||
|
||
private | ||
|
||
def normalize(options, index_target) | ||
options | ||
.except(:class_name) | ||
.merge!(index_uid: index_uid_from_target(index_target)) | ||
end | ||
|
||
def index_uid_from_target(index_target) | ||
case index_target | ||
when String, Symbol | ||
index_target | ||
else | ||
index_target.index.uid | ||
end | ||
end | ||
|
||
def paginate(options) | ||
%w[page hitsPerPage hits_per_page].each do |key| | ||
# Deletes hitsPerPage to avoid passing along a meilisearch-ruby warning/exception | ||
value = options.delete(key) || options.delete(key.to_sym) | ||
options[key.underscore.to_sym] = value.to_i if value | ||
end | ||
|
||
# It is required to activate the finite pagination in Meilisearch v0.30 (or newer), | ||
# to have at least `hits_per_page` defined or `page` in the search request. | ||
options[:page] ||= 1 | ||
end | ||
|
||
def pagination_enabled? | ||
MeiliSearch::Rails.configuration[:pagination_backend] | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
module MeiliSearch | ||
module Rails | ||
class MultiSearchResult | ||
attr_reader :metadata | ||
|
||
def initialize(searches, raw_results) | ||
@results = {} | ||
@metadata = {} | ||
|
||
searches.zip(raw_results['results']).each do |(index_target, search_options), result| | ||
index_target = search_options[:class_name].constantize if search_options[:class_name] | ||
|
||
@results[index_target] = case index_target | ||
when String, Symbol | ||
result['hits'] | ||
else | ||
load_results(index_target, result) | ||
end | ||
|
||
@metadata[index_target] = result.except('hits') | ||
end | ||
end | ||
|
||
include Enumerable | ||
|
||
def each_hit(&block) | ||
@results.each do |_index_target, results| | ||
results.each(&block) | ||
end | ||
end | ||
alias each each_hit | ||
|
||
def each_result | ||
@results.each | ||
end | ||
|
||
def to_a | ||
@results.values.flatten(1) | ||
end | ||
alias to_ary to_a | ||
|
||
def to_h | ||
@results | ||
end | ||
alias to_hash to_h | ||
|
||
private | ||
|
||
def load_results(klass, result) | ||
pk_method = klass.ms_primary_key_method | ||
pk_method = pk_method.in if Utilities.mongo_model?(klass) | ||
|
||
condition_key = pk_is_virtual?(klass, pk_method) ? klass.primary_key : pk_method | ||
|
||
hits_by_id = | ||
result['hits'].index_by { |hit| hit[condition_key.to_s] } | ||
|
||
records = klass.where(condition_key => hits_by_id.keys) | ||
|
||
if records.respond_to? :in_order_of | ||
records.in_order_of(condition_key, hits_by_id.keys).each do |record| | ||
record.formatted = hits_by_id[record.send(condition_key).to_s]['_formatted'] | ||
end | ||
else | ||
results_by_id = records.index_by do |hit| | ||
hit.send(condition_key).to_s | ||
end | ||
|
||
result['hits'].filter_map do |hit| | ||
record = results_by_id[hit[condition_key.to_s].to_s] | ||
record&.formatted = hit['_formatted'] | ||
record | ||
end | ||
end | ||
end | ||
|
||
def pk_is_virtual?(model_class, pk_method) | ||
model_class.columns | ||
.map(&(Utilities.sequel_model?(model_class) ? :to_s : :name)) | ||
.exclude?(pk_method.to_s) | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
require 'spec_helper' | ||
|
||
describe MeiliSearch::Rails::MultiSearchResult do # rubocop:todo RSpec/FilePath | ||
let(:raw_results) do | ||
{ | ||
'results' => [ | ||
{ 'indexUid' => 'books_index', | ||
'hits' => [{ 'name' => 'Steve Jobs', 'id' => '3', 'author' => 'Walter Isaacson', 'premium' => nil, 'released' => nil, 'genre' => nil }], | ||
'query' => 'Steve', 'processingTimeMs' => 0, 'limit' => 20, 'offset' => 0, 'estimatedTotalHits' => 1 }, | ||
{ 'indexUid' => 'products_index', | ||
'hits' => [{ 'id' => '4', 'href' => 'ebay', 'name' => 'palm pixi plus' }], | ||
'query' => 'palm', 'processingTimeMs' => 0, 'limit' => 1, 'offset' => 0, 'estimatedTotalHits' => 2 }, | ||
{ 'indexUid' => 'color_index', | ||
'hits' => [ | ||
{ 'name' => 'black', 'id' => '5', 'short_name' => 'bla', 'hex' => 0 }, | ||
{ 'name' => 'blue', 'id' => '4', 'short_name' => 'blu', 'hex' => 255 } | ||
], | ||
'query' => 'bl', 'processingTimeMs' => 0, 'limit' => 20, 'offset' => 0, 'estimatedTotalHits' => 2 } | ||
] | ||
} | ||
end | ||
|
||
it 'is enumerable' do | ||
expect(described_class).to include(Enumerable) | ||
end | ||
|
||
context 'with index name keys' do | ||
subject(:result) { described_class.new(searches, raw_results) } | ||
|
||
let(:searches) do | ||
{ | ||
'books_index' => { q: 'Steve' }, | ||
'products_index' => { q: 'palm', limit: 1 }, | ||
'color_index' => { q: 'bl' } | ||
} | ||
end | ||
|
||
it 'enumerates through the hits' do | ||
expect(result).to contain_exactly( | ||
a_hash_including('author' => 'Walter Isaacson', 'name' => 'Steve Jobs'), | ||
a_hash_including('name' => 'palm pixi plus'), | ||
a_hash_including('name' => 'blue', 'short_name' => 'blu'), | ||
a_hash_including('name' => 'black', 'short_name' => 'bla') | ||
) | ||
end | ||
|
||
it 'enumerates through the hits of each result with #each_result' do | ||
expect(result.each_result).to be_an(Enumerator) | ||
expect(result.each_result).to contain_exactly( | ||
['books_index', contain_exactly( | ||
a_hash_including('author' => 'Walter Isaacson', 'name' => 'Steve Jobs') | ||
)], | ||
['products_index', contain_exactly( | ||
a_hash_including('name' => 'palm pixi plus') | ||
)], | ||
['color_index', contain_exactly( | ||
a_hash_including('name' => 'blue', 'short_name' => 'blu'), | ||
a_hash_including('name' => 'black', 'short_name' => 'bla') | ||
)] | ||
) | ||
end | ||
|
||
describe '#to_a' do | ||
it 'returns the hits' do | ||
expect(result.to_a).to contain_exactly( | ||
a_hash_including('author' => 'Walter Isaacson', 'name' => 'Steve Jobs'), | ||
a_hash_including('name' => 'palm pixi plus'), | ||
a_hash_including('name' => 'blue', 'short_name' => 'blu'), | ||
a_hash_including('name' => 'black', 'short_name' => 'bla') | ||
) | ||
end | ||
|
||
it 'aliases as #to_ary' do | ||
expect(result.method(:to_ary).original_name).to eq :to_a | ||
end | ||
end | ||
|
||
describe '#to_h' do | ||
it 'returns a hash of indexes and hits' do | ||
expect(result.to_h).to match( | ||
'books_index' => contain_exactly( | ||
a_hash_including('author' => 'Walter Isaacson', 'name' => 'Steve Jobs') | ||
), | ||
'products_index' => contain_exactly( | ||
a_hash_including('name' => 'palm pixi plus') | ||
), | ||
'color_index' => contain_exactly( | ||
a_hash_including('name' => 'blue', 'short_name' => 'blu'), | ||
a_hash_including('name' => 'black', 'short_name' => 'bla') | ||
) | ||
) | ||
end | ||
|
||
it 'is aliased as #to_hash' do | ||
expect(result.method(:to_hash).original_name).to eq :to_h | ||
end | ||
end | ||
|
||
describe '#metadata' do | ||
it 'returns search metadata for each result' do | ||
expect(result.metadata).to match( | ||
'books_index' => { | ||
'indexUid' => 'books_index', | ||
'query' => 'Steve', 'processingTimeMs' => 0, 'limit' => 20, 'offset' => 0, 'estimatedTotalHits' => 1 | ||
}, | ||
'products_index' => { | ||
'indexUid' => 'products_index', | ||
'query' => 'palm', 'processingTimeMs' => 0, 'limit' => 1, 'offset' => 0, 'estimatedTotalHits' => 2 | ||
}, | ||
'color_index' => { | ||
'indexUid' => 'color_index', | ||
'query' => 'bl', 'processingTimeMs' => 0, 'limit' => 20, 'offset' => 0, 'estimatedTotalHits' => 2 | ||
} | ||
) | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.