diff --git a/README.md b/README.md index 3558b540..ee1cb552 100644 --- a/README.md +++ b/README.md @@ -241,22 +241,7 @@ multi_search_results = MeiliSearch::Rails.multi_search( ) ``` -You can iterate through the results with `.each` or `.each_result`: - -```erb -<% multi_search_results.each do |record| %> -

<%= record.title %>

-

<%= record.author %>

-<% end %> - -

Harry Potter and the Philosopher's Stone

-

J. K. Rowling

-

Harry Potter and the Chamber of Secrets

-

J. K. Rowling

-

Attack on Titan

-

Iseyama

-``` - +Use `#each_result` to loop through pairs of your provided keys and the results: ```erb <% multi_search_results.each_result do |klass, results| %>

<%= klass.name.pluralize %>

@@ -280,6 +265,59 @@ You can iterate through the results with `.each` or `.each_result`: ``` +Records are loaded when the keys are models, or when `:class_name` option is passed: + +```ruby +multi_search_results = MeiliSearch::Rails.multi_search( + 'books' => { q: 'Harry', class_name: 'Book' }, + 'mangas' => { q: 'Attack', class_name: 'Manga' } +) +``` + +Otherwise, hashes are returned. + +The index to search is inferred from the model if the key is a model, if the key is a string the key is assumed to be the index unless the `:index_name` option is passed: + +```ruby +multi_search_results = MeiliSearch::Rails.multi_search( + 'western' => { q: 'Harry', class_name: 'Book', index_name: 'books_production' }, + 'japanese' => { q: 'Attack', class_name: 'Manga', index_name: 'mangas_production' } +) +``` + +### Multi search the same index + +You can search the same index multiple times by specifying `:index_name`: + +```ruby +query = 'hero' +multi_search_results = MeiliSearch::Rails.multi_search( + 'Isekai Manga' => { q: query, class_name: 'Manga', filters: 'genre:isekai', index_name: 'mangas_production' } + 'Shounen Manga' => { q: query, class_name: 'Manga', filters: 'genre:shounen', index_name: 'mangas_production' } + 'Steampunk Manga' => { q: query, class_name: 'Manga', filters: 'genre:steampunk', index_name: 'mangas_production' } +) +``` + +### Deprecated #each + +**DEPRECATED:** You used to be able to iterate through a flattened collection with `.each`: + +```erb +<% multi_search_results.each do |record| %> +

<%= record.title %>

+

<%= record.author %>

+<% end %> + +

Harry Potter and the Philosopher's Stone

+

J. K. Rowling

+

Harry Potter and the Chamber of Secrets

+

J. K. Rowling

+

Attack on Titan

+

Iseyama

+``` + +But this has been deprecated in favor of **federated search**. + See the [official multi search documentation](https://www.meilisearch.com/docs/reference/api/multi_search). ## 🪛 Options diff --git a/lib/meilisearch/rails/multi_search.rb b/lib/meilisearch/rails/multi_search.rb index 1f9b63bd..cfbbad51 100644 --- a/lib/meilisearch/rails/multi_search.rb +++ b/lib/meilisearch/rails/multi_search.rb @@ -5,6 +5,8 @@ module Rails class << self def multi_search(searches) search_parameters = searches.map do |(index_target, options)| + index_target = options.delete(:index_name) || index_target + paginate(options) if pagination_enabled? normalize(options, index_target) end diff --git a/lib/meilisearch/rails/multi_search/result.rb b/lib/meilisearch/rails/multi_search/result.rb index e7938fcc..dd99ab2f 100644 --- a/lib/meilisearch/rails/multi_search/result.rb +++ b/lib/meilisearch/rails/multi_search/result.rb @@ -7,34 +7,57 @@ 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] + searches.zip(raw_results['results']).each do |(target, search_options), result| + results_class = if search_options[:class_name] + search_options[:class_name].constantize + elsif target.instance_of?(Class) + target + end - @results[index_target] = case index_target - when String, Symbol - result['hits'] - else - load_results(index_target, result) - end + @results[target] = results_class ? load_results(results_class, result) : result['hits'] - @metadata[index_target] = result.except('hits') + @metadata[target] = result.except('hits') end end include Enumerable def each_hit(&block) - @results.each do |_index_target, results| + MeiliSearch::Rails.logger.warn( + <<~DEPRECATION + [meilisearch-rails] Flattening multi search results is deprecated. + If you do not want the results to be grouped, please use federated search instead. + DEPRECATION + ) + + @results.each_value do |results| results.each(&block) end end - alias each each_hit + + def each(&block) + MeiliSearch::Rails.logger.info( + <<~INFO + [meilisearch-rails] #each on a multi search now iterates through grouped results. + If you do not want the results to be grouped, please use federated search instead. + To quickly go back to the old deprecated behavior, use `#each_hit`. + INFO + ) + + @results.each(&block) + end def each_result(&block) @results.each(&block) end def to_a + MeiliSearch::Rails.logger.warn( + <<~DEPRECATION + [meilisearch-rails] Flattening multi search results is deprecated. + If you do not want the results to be grouped, please use federated search instead. + DEPRECATION + ) @results.values.flatten(1) end alias to_ary to_a diff --git a/spec/multi_search/result_spec.rb b/spec/multi_search/result_spec.rb index 99c35a0f..791770ce 100644 --- a/spec/multi_search/result_spec.rb +++ b/spec/multi_search/result_spec.rb @@ -60,7 +60,57 @@ ) end + describe '#each' do + let(:logger) { instance_double('Logger', info: nil) } + + before do + allow(MeiliSearch::Rails).to receive(:logger).and_return(logger) + end + + it 'has the same behavior as #each_result' do + expect(result.each).to be_an(Enumerator) + expect(result.each).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 + + it 'warns about changed behavior' do + result.each(&:to_s) + + expect(logger).to have_received(:info).with(a_string_including('#each on a multi search now iterates through grouped results.')) + end + end + + describe '#each_hit' do + let(:logger) { instance_double('Logger', warn: nil) } + + before do + allow(MeiliSearch::Rails).to receive(:logger).and_return(logger) + end + + it 'warns about deprecation' do + result.each_hit(&:to_s) + + expect(logger).to have_received(:warn).with(a_string_including('Flattening multi search')) + end + end + describe '#to_a' do + let(:logger) { instance_double('Logger', warn: nil) } + + before do + allow(MeiliSearch::Rails).to receive(:logger).and_return(logger) + end + it 'returns the hits' do expect(result.to_a).to contain_exactly( a_hash_including('author' => 'Walter Isaacson', 'name' => 'Steve Jobs'), @@ -73,6 +123,12 @@ it 'aliases as #to_ary' do expect(result.method(:to_ary).original_name).to eq :to_a end + + it 'warns about deprecation' do + result.to_a + + expect(logger).to have_received(:warn).with(a_string_including('Flattening multi search')) + end end describe '#to_h' do diff --git a/spec/multi_search_spec.rb b/spec/multi_search_spec.rb index 14ca7755..49dba2df 100644 --- a/spec/multi_search_spec.rb +++ b/spec/multi_search_spec.rb @@ -1,4 +1,7 @@ require 'spec_helper' +require 'support/models/book' +require 'support/models/product' +require 'support/models/color' describe 'multi-search' do def reset_indexes @@ -43,6 +46,65 @@ def reset_indexes end end + context 'with arbitrary keys' do + context 'when index_name is not present' do + it 'assumes key is index and errors' do + expect do + MeiliSearch::Rails.multi_search( + 'test_group' => { q: 'Steve' } + ) + end.to raise_error(MeiliSearch::ApiError) + end + end + + context 'when :index_name is present' do + it 'searches the correct index' do + results = MeiliSearch::Rails.multi_search( + 'books' => { q: 'Steve', index_name: Book.index.uid }, + 'products' => { q: 'palm', index_name: Product.index.uid, limit: 1 }, + 'colors' => { q: 'bl', index_name: Color.index.uid } + ) + + expect(results).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 'allows searching the same index n times' do + index_name = Color.index.uid + + results = MeiliSearch::Rails.multi_search( + 'dark_colors' => { q: 'black', index_name: index_name }, + 'bright_colors' => { q: 'blue', index_name: index_name }, + 'nature_colors' => { q: 'green', index_name: index_name } + ) + + expect(results).to contain_exactly( + a_hash_including('name' => 'blue', 'short_name' => 'blu'), + a_hash_including('name' => 'black', 'short_name' => 'bla'), + a_hash_including('name' => 'green', 'short_name' => 'gre') + ) + end + + context 'when :class_name is also present' do + it 'loads results from the correct models' do + results = MeiliSearch::Rails.multi_search( + 'books' => { q: 'Steve', index_name: Book.index.uid, class_name: 'Book' }, + 'products' => { q: 'palm', limit: 1, index_name: Product.index.uid, class_name: 'Product' }, + 'colors' => { q: 'bl', index_name: Color.index.uid, class_name: 'Color' } + ) + + expect(results).to contain_exactly( + steve_jobs, palm_pixi_plus, blue, black + ) + end + end + end + end + context 'with index name keys' do it 'returns hashes' do results = MeiliSearch::Rails.multi_search(