From 3b2a8e42cd2040521e655e5e004f1212366e2042 Mon Sep 17 00:00:00 2001 From: ellnix <103502144+ellnix@users.noreply.github.com> Date: Fri, 9 Feb 2024 09:57:39 +0100 Subject: [PATCH 1/8] Write multi-search specs --- spec/multi_search/result_spec.rb | 6 ++ spec/multi_search_spec.rb | 95 ++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 spec/multi_search/result_spec.rb create mode 100644 spec/multi_search_spec.rb diff --git a/spec/multi_search/result_spec.rb b/spec/multi_search/result_spec.rb new file mode 100644 index 00000000..ef664395 --- /dev/null +++ b/spec/multi_search/result_spec.rb @@ -0,0 +1,6 @@ +require 'spec_helper' + +describe 'MeiliSearch::Rails::MultiSearchResult' do + # TODO: Write specs +end + diff --git a/spec/multi_search_spec.rb b/spec/multi_search_spec.rb new file mode 100644 index 00000000..e536ee69 --- /dev/null +++ b/spec/multi_search_spec.rb @@ -0,0 +1,95 @@ +require 'spec_helper' + +describe 'multi-search' do + let!(:palmpre) { Product.create!(name: 'palmpre', href: 'ebay', tags: ['discontinued', 'worst phone ever']) } + let!(:palm_pixi_plus) { Product.create!(name: 'palm pixi plus', href: 'ebay', tags: ['terrible']) } + let!(:lg_vortex) { Product.create!(name: 'lg vortex', href: 'ebay', tags: ['decent']) } + + before { Product.reindex! } + + let!(:steve_jobs) { Book.create! name: 'Steve Jobs', author: 'Walter Isaacson' } + let!(:moby_dick) { Book.create! name: 'Moby Dick', author: 'Herman Melville' } + + let!(:blue) { Color.create!(name: 'blue', short_name: 'blu', hex: 0x0000FF) } + let!(:black) { Color.create!(name: 'black', short_name: 'bla', hex: 0x000000) } + let!(:green) { Color.create!(name: 'green', short_name: 'gre', hex: 0x00FF00) } + + after do + [Book, Color, Product].each do |klass| + klass.delete_all + klass.index.delete_all_documents + end + end + + context 'with class keys' do + it 'returns ORM records' do + results = MeiliSearch::Rails.multi_search( + Book => { q: 'Steve' }, + Product => { q: 'palm', limit: 1 }, + Color => { q: 'bl' } + ) + + expect(results).to contain_exactly( + steve_jobs, palm_pixi_plus, blue, black + ) + end + end + + context 'with index name keys' do + it 'returns hashes' do + results = MeiliSearch::Rails.multi_search( + Book.index.uid => { q: 'Steve' }, + Product.index.uid.to_sym => { q: 'palm', limit: 1 }, + Color.index.uid => { q: 'bl' } + ) + + 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 + + context 'when class_name is specified' do + it 'returns ORM records' do + results = MeiliSearch::Rails.multi_search( + Book.index.uid => { q: 'Steve', class_name: 'Book' }, + Product.index.uid.to_sym => { q: 'palm', limit: 1, class_name: 'Product' }, + Color.index.uid => { q: 'bl', class_name: 'Color' } + ) + + expect(results).to contain_exactly( + steve_jobs, palm_pixi_plus, blue, black + ) + end + + it 'throws error if class cannot be found' do + expect do + results = MeiliSearch::Rails.multi_search( + Book.index.uid => { q: 'Steve', class_name: 'Book' }, + Product.index.uid.to_sym => { q: 'palm', limit: 1, class_name: 'ProductOfCapitalism' }, + Color.index.uid => { q: 'bl', class_name: 'Color' } + ) + end.to raise_error(NameError) + end + end + end + + context 'with a mixture of symbol and class keys' do + it 'returns a mixture of ORM records and hashes' do + results = MeiliSearch::Rails.multi_search( + Book => { q: 'Steve' }, + Product.index.uid => { q: 'palm', limit: 1, class_name: 'Product' }, + Color.index.uid => { q: 'bl' } + ) + + expect(results).to contain_exactly( + steve_jobs, palm_pixi_plus, + a_hash_including('name' => 'blue', 'short_name' => 'blu'), + a_hash_including('name' => 'black', 'short_name' => 'bla'), + ) + end + end +end + From 502fc3004e333f4a26fefaedf7602149f47fa84b Mon Sep 17 00:00:00 2001 From: ellnix <103502144+ellnix@users.noreply.github.com> Date: Fri, 9 Feb 2024 17:47:49 +0100 Subject: [PATCH 2/8] Write simple multi-search implementation --- lib/meilisearch-rails.rb | 11 ++-- lib/meilisearch/rails/multi_search.rb | 73 +++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 lib/meilisearch/rails/multi_search.rb diff --git a/lib/meilisearch-rails.rb b/lib/meilisearch-rails.rb index 1d9a298e..20d727a1 100644 --- a/lib/meilisearch-rails.rb +++ b/lib/meilisearch-rails.rb @@ -3,6 +3,7 @@ require 'meilisearch/rails/version' require 'meilisearch/rails/utilities' require 'meilisearch/rails/errors' +require 'meilisearch/rails/multi_search' if defined? Rails begin @@ -760,6 +761,11 @@ def ms_must_reindex?(document) false end + def ms_primary_key_method(options = nil) + options ||= meilisearch_options + options[:primary_key] || options[:id] || :id + end + protected def ms_ensure_init(options = meilisearch_options, settings = meilisearch_settings, user_configuration = settings.to_settings) @@ -814,11 +820,6 @@ def ms_configurations @configurations end - def ms_primary_key_method(options = nil) - options ||= meilisearch_options - options[:primary_key] || options[:id] || :id - end - def ms_primary_key_of(doc, options = nil) doc.send(ms_primary_key_method(options)).to_s end diff --git a/lib/meilisearch/rails/multi_search.rb b/lib/meilisearch/rails/multi_search.rb new file mode 100644 index 00000000..d4f4ce7d --- /dev/null +++ b/lib/meilisearch/rails/multi_search.rb @@ -0,0 +1,73 @@ +module MeiliSearch + module Rails + class << self + def multi_search(searches) + search_parameters = searches.map do |(index_target, options)| + index_uid = case index_target + when String, Symbol + index_target + else + index_target.index.uid + end + + options.except(:class_name).merge(index_uid: index_uid) + end + + raw_results = client.multi_search(search_parameters)['results'] + + searches.zip(raw_results).flat_map do |(index_target, search_options), result| + if search_options[:class_name] + index_target = search_options[:class_name].constantize + end + + case index_target + when String, Symbol + result['hits'] + else + load_results(index_target, result) + end + end + end + + private + + def load_results(klass, result) + pk_method = if defined?(::Mongoid::Document) && klass.include?(::Mongoid::Document) + klass.ms_primary_key_method.in + else + klass.ms_primary_key_method + end + + ms_pk = klass.meilisearch_options[:primary_key] || IndexSettings::DEFAULT_PRIMARY_KEY + + db_is_sequel = defined?(::Sequel::Model) && klass < Sequel::Model + pk_is_virtual = klass.columns.map(&(db_is_sequel ? :to_s : :name)).exclude?(pk_method.to_s) + + condition_key = pk_is_virtual ? klass.primary_key : pk_method + + hits_by_id = + result['hits'].index_by { |hit| hit[pk_is_virtual ? condition_key : ms_pk.to_s] } + + records = klass.where(condition_key => hits_by_id.keys) + + if records.respond_to? :in_order_of + records.in_order_of(pk_method, hits_by_id.keys).each do |record| + record.formatted = hits_by_id[record.send(pk_method).to_s]['_formatted'] + end + else + results_by_id = records.index_by do |hit| + hit.send(pk_method).to_s + end + + result['hits'].filter_map do |hit| + record = results_by_id[hit[ms_pk.to_s].to_s] + if record + record.formatted = hit['_formatted'] + record + end + end + end + end + end + end +end From edd61ac471bd76864a4ec51cc22951bdc5e1e217 Mon Sep 17 00:00:00 2001 From: ellnix <103502144+ellnix@users.noreply.github.com> Date: Fri, 9 Feb 2024 17:58:49 +0100 Subject: [PATCH 3/8] Fix context leak in multi-search spec --- spec/multi_search_spec.rb | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/spec/multi_search_spec.rb b/spec/multi_search_spec.rb index e536ee69..4c215558 100644 --- a/spec/multi_search_spec.rb +++ b/spec/multi_search_spec.rb @@ -1,6 +1,16 @@ require 'spec_helper' describe 'multi-search' do + def reset_indexes + [Book, Color, Product].each do |klass| + klass.delete_all + klass.index.delete_all_documents + end + end + + before(:all) { reset_indexes } + after { reset_indexes } + let!(:palmpre) { Product.create!(name: 'palmpre', href: 'ebay', tags: ['discontinued', 'worst phone ever']) } let!(:palm_pixi_plus) { Product.create!(name: 'palm pixi plus', href: 'ebay', tags: ['terrible']) } let!(:lg_vortex) { Product.create!(name: 'lg vortex', href: 'ebay', tags: ['decent']) } @@ -14,13 +24,6 @@ let!(:black) { Color.create!(name: 'black', short_name: 'bla', hex: 0x000000) } let!(:green) { Color.create!(name: 'green', short_name: 'gre', hex: 0x00FF00) } - after do - [Book, Color, Product].each do |klass| - klass.delete_all - klass.index.delete_all_documents - end - end - context 'with class keys' do it 'returns ORM records' do results = MeiliSearch::Rails.multi_search( From d3dd92ae02298dcebc343d96f14f1ca035da94e5 Mon Sep 17 00:00:00 2001 From: ellnix <103502144+ellnix@users.noreply.github.com> Date: Fri, 9 Feb 2024 19:04:02 +0100 Subject: [PATCH 4/8] Refactor multi-search with rubocop --- lib/meilisearch/rails/multi_search.rb | 10 +++------ spec/multi_search/result_spec.rb | 3 +-- spec/multi_search_spec.rb | 29 ++++++++++++++------------- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/lib/meilisearch/rails/multi_search.rb b/lib/meilisearch/rails/multi_search.rb index d4f4ce7d..ed76427f 100644 --- a/lib/meilisearch/rails/multi_search.rb +++ b/lib/meilisearch/rails/multi_search.rb @@ -16,9 +16,7 @@ def multi_search(searches) raw_results = client.multi_search(search_parameters)['results'] searches.zip(raw_results).flat_map do |(index_target, search_options), result| - if search_options[:class_name] - index_target = search_options[:class_name].constantize - end + index_target = search_options[:class_name].constantize if search_options[:class_name] case index_target when String, Symbol @@ -61,10 +59,8 @@ def load_results(klass, result) result['hits'].filter_map do |hit| record = results_by_id[hit[ms_pk.to_s].to_s] - if record - record.formatted = hit['_formatted'] - record - end + record&.formatted = hit['_formatted'] + record end end end diff --git a/spec/multi_search/result_spec.rb b/spec/multi_search/result_spec.rb index ef664395..ad3430dc 100644 --- a/spec/multi_search/result_spec.rb +++ b/spec/multi_search/result_spec.rb @@ -1,6 +1,5 @@ require 'spec_helper' -describe 'MeiliSearch::Rails::MultiSearchResult' do +describe 'MeiliSearch::Rails::MultiSearchResult' do # rubocop:todo RSpec/EmptyExampleGroup # TODO: Write specs end - diff --git a/spec/multi_search_spec.rb b/spec/multi_search_spec.rb index 4c215558..2090692e 100644 --- a/spec/multi_search_spec.rb +++ b/spec/multi_search_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'multi-search' do +describe 'multi-search' do # rubocop:todo RSpec/DescribeClass def reset_indexes [Book, Color, Product].each do |klass| klass.delete_all @@ -8,21 +8,23 @@ def reset_indexes end end - before(:all) { reset_indexes } + before(:all) { reset_indexes } # rubocop:todo RSpec/BeforeAfterAll + after { reset_indexes } - let!(:palmpre) { Product.create!(name: 'palmpre', href: 'ebay', tags: ['discontinued', 'worst phone ever']) } let!(:palm_pixi_plus) { Product.create!(name: 'palm pixi plus', href: 'ebay', tags: ['terrible']) } - let!(:lg_vortex) { Product.create!(name: 'lg vortex', href: 'ebay', tags: ['decent']) } - - before { Product.reindex! } - let!(:steve_jobs) { Book.create! name: 'Steve Jobs', author: 'Walter Isaacson' } - let!(:moby_dick) { Book.create! name: 'Moby Dick', author: 'Herman Melville' } - let!(:blue) { Color.create!(name: 'blue', short_name: 'blu', hex: 0x0000FF) } let!(:black) { Color.create!(name: 'black', short_name: 'bla', hex: 0x000000) } - let!(:green) { Color.create!(name: 'green', short_name: 'gre', hex: 0x00FF00) } + + before do + Product.create! name: 'lg vortex', href: 'ebay', tags: ['decent'] + Product.create! name: 'palmpre', href: 'ebay', tags: ['discontinued', 'worst phone ever'] + Product.reindex! + + Color.create! name: 'green', short_name: 'gre', hex: 0x00FF00 + Book.create! name: 'Moby Dick', author: 'Herman Melville' + end context 'with class keys' do it 'returns ORM records' do @@ -50,7 +52,7 @@ def reset_indexes 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'), + a_hash_including('name' => 'black', 'short_name' => 'bla') ) end @@ -69,7 +71,7 @@ def reset_indexes it 'throws error if class cannot be found' do expect do - results = MeiliSearch::Rails.multi_search( + MeiliSearch::Rails.multi_search( Book.index.uid => { q: 'Steve', class_name: 'Book' }, Product.index.uid.to_sym => { q: 'palm', limit: 1, class_name: 'ProductOfCapitalism' }, Color.index.uid => { q: 'bl', class_name: 'Color' } @@ -90,9 +92,8 @@ def reset_indexes expect(results).to contain_exactly( steve_jobs, palm_pixi_plus, a_hash_including('name' => 'blue', 'short_name' => 'blu'), - a_hash_including('name' => 'black', 'short_name' => 'bla'), + a_hash_including('name' => 'black', 'short_name' => 'bla') ) end end end - From e67fe2522f3c6b61d2f298abe04e8f52a541c8df Mon Sep 17 00:00:00 2001 From: ellnix <103502144+ellnix@users.noreply.github.com> Date: Mon, 12 Feb 2024 16:30:39 +0100 Subject: [PATCH 5/8] Add MultiSearchResult --- lib/meilisearch/rails/multi_search.rb | 51 +------- lib/meilisearch/rails/multi_search/result.rb | 86 ++++++++++++++ lib/meilisearch/rails/utilities.rb | 8 ++ spec/multi_search/result_spec.rb | 115 ++++++++++++++++++- 4 files changed, 210 insertions(+), 50 deletions(-) create mode 100644 lib/meilisearch/rails/multi_search/result.rb diff --git a/lib/meilisearch/rails/multi_search.rb b/lib/meilisearch/rails/multi_search.rb index ed76427f..64d2abc4 100644 --- a/lib/meilisearch/rails/multi_search.rb +++ b/lib/meilisearch/rails/multi_search.rb @@ -1,3 +1,5 @@ +require_relative 'multi_search/result' + module MeiliSearch module Rails class << self @@ -15,54 +17,7 @@ def multi_search(searches) raw_results = client.multi_search(search_parameters)['results'] - searches.zip(raw_results).flat_map do |(index_target, search_options), result| - index_target = search_options[:class_name].constantize if search_options[:class_name] - - case index_target - when String, Symbol - result['hits'] - else - load_results(index_target, result) - end - end - end - - private - - def load_results(klass, result) - pk_method = if defined?(::Mongoid::Document) && klass.include?(::Mongoid::Document) - klass.ms_primary_key_method.in - else - klass.ms_primary_key_method - end - - ms_pk = klass.meilisearch_options[:primary_key] || IndexSettings::DEFAULT_PRIMARY_KEY - - db_is_sequel = defined?(::Sequel::Model) && klass < Sequel::Model - pk_is_virtual = klass.columns.map(&(db_is_sequel ? :to_s : :name)).exclude?(pk_method.to_s) - - condition_key = pk_is_virtual ? klass.primary_key : pk_method - - hits_by_id = - result['hits'].index_by { |hit| hit[pk_is_virtual ? condition_key : ms_pk.to_s] } - - records = klass.where(condition_key => hits_by_id.keys) - - if records.respond_to? :in_order_of - records.in_order_of(pk_method, hits_by_id.keys).each do |record| - record.formatted = hits_by_id[record.send(pk_method).to_s]['_formatted'] - end - else - results_by_id = records.index_by do |hit| - hit.send(pk_method).to_s - end - - result['hits'].filter_map do |hit| - record = results_by_id[hit[ms_pk.to_s].to_s] - record&.formatted = hit['_formatted'] - record - end - end + MultiSearchResult.new(searches, raw_results) end end end diff --git a/lib/meilisearch/rails/multi_search/result.rb b/lib/meilisearch/rails/multi_search/result.rb new file mode 100644 index 00000000..20118f7e --- /dev/null +++ b/lib/meilisearch/rails/multi_search/result.rb @@ -0,0 +1,86 @@ +module MeiliSearch + module Rails + class MultiSearchResult + attr_reader :metadata + + def initialize(searches, raw_results) + @results = {} + @metadata = {} + + searches.zip(raw_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 + @results.each do |_index_target, results| + results.each { |res| yield res } + end + end + alias_method :each, :each_hit + + def each_result + @results.each + end + + def to_a + @results.values.flatten(1) + end + alias_method :to_ary, :to_a + + def to_h + @results + end + alias_method :to_hash, :to_h + + private + + def load_results(klass, result) + pk_method = klass.ms_primary_key_method + pk_method = pk_method.in if Utilities.is_mongo_model?(klass) + + ms_pk = klass.meilisearch_options[:primary_key] || IndexSettings::DEFAULT_PRIMARY_KEY + + 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.is_sequel_model?(model_class) ? :to_s : :name)) + .exclude?(pk_method.to_s) + end + end + end +end diff --git a/lib/meilisearch/rails/utilities.rb b/lib/meilisearch/rails/utilities.rb index 377eca3d..e15bdc91 100644 --- a/lib/meilisearch/rails/utilities.rb +++ b/lib/meilisearch/rails/utilities.rb @@ -48,6 +48,14 @@ def indexable?(record, options) true end + def is_mongo_model?(model_class) + defined?(::Mongoid::Document) && model_class.include?(::Mongoid::Document) + end + + def is_sequel_model?(model_class) + defined?(::Sequel::Model) && model_class < Sequel::Model + end + private def constraint_passes?(record, constraint) diff --git a/spec/multi_search/result_spec.rb b/spec/multi_search/result_spec.rb index ad3430dc..74372cf3 100644 --- a/spec/multi_search/result_spec.rb +++ b/spec/multi_search/result_spec.rb @@ -1,5 +1,116 @@ require 'spec_helper' -describe 'MeiliSearch::Rails::MultiSearchResult' do # rubocop:todo RSpec/EmptyExampleGroup - # TODO: Write specs +describe MeiliSearch::Rails::MultiSearchResult do + it 'is enumerable' do + expect(described_class).to include(Enumerable) + end + + let(:raw_results) do + [ + { '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 + + 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(subject.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 From ef6b0199fe694300a8316e891f1b20e4b96bd4f9 Mon Sep 17 00:00:00 2001 From: ellnix <103502144+ellnix@users.noreply.github.com> Date: Mon, 12 Feb 2024 16:37:50 +0100 Subject: [PATCH 6/8] Fix linter problems --- lib/meilisearch/rails/multi_search/result.rb | 18 +++++----- lib/meilisearch/rails/utilities.rb | 4 +-- spec/multi_search/result_spec.rb | 38 ++++++++++---------- 3 files changed, 29 insertions(+), 31 deletions(-) diff --git a/lib/meilisearch/rails/multi_search/result.rb b/lib/meilisearch/rails/multi_search/result.rb index 20118f7e..72d9fb44 100644 --- a/lib/meilisearch/rails/multi_search/result.rb +++ b/lib/meilisearch/rails/multi_search/result.rb @@ -23,12 +23,12 @@ def initialize(searches, raw_results) include Enumerable - def each_hit + def each_hit(&block) @results.each do |_index_target, results| - results.each { |res| yield res } + results.each(&block) end end - alias_method :each, :each_hit + alias each each_hit def each_result @results.each @@ -37,20 +37,18 @@ def each_result def to_a @results.values.flatten(1) end - alias_method :to_ary, :to_a + alias to_ary to_a def to_h @results end - alias_method :to_hash, :to_h + 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.is_mongo_model?(klass) - - ms_pk = klass.meilisearch_options[:primary_key] || IndexSettings::DEFAULT_PRIMARY_KEY + pk_method = pk_method.in if Utilities.mongo_model?(klass) condition_key = pk_is_virtual?(klass, pk_method) ? klass.primary_key : pk_method @@ -78,8 +76,8 @@ def load_results(klass, result) def pk_is_virtual?(model_class, pk_method) model_class.columns - .map(&(Utilities.is_sequel_model?(model_class) ? :to_s : :name)) - .exclude?(pk_method.to_s) + .map(&(Utilities.sequel_model?(model_class) ? :to_s : :name)) + .exclude?(pk_method.to_s) end end end diff --git a/lib/meilisearch/rails/utilities.rb b/lib/meilisearch/rails/utilities.rb index e15bdc91..bd918b7b 100644 --- a/lib/meilisearch/rails/utilities.rb +++ b/lib/meilisearch/rails/utilities.rb @@ -48,11 +48,11 @@ def indexable?(record, options) true end - def is_mongo_model?(model_class) + def mongo_model?(model_class) defined?(::Mongoid::Document) && model_class.include?(::Mongoid::Document) end - def is_sequel_model?(model_class) + def sequel_model?(model_class) defined?(::Sequel::Model) && model_class < Sequel::Model end diff --git a/spec/multi_search/result_spec.rb b/spec/multi_search/result_spec.rb index 74372cf3..ff63dc11 100644 --- a/spec/multi_search/result_spec.rb +++ b/spec/multi_search/result_spec.rb @@ -1,30 +1,27 @@ require 'spec_helper' -describe MeiliSearch::Rails::MultiSearchResult do - it 'is enumerable' do - expect(described_class).to include(Enumerable) - end - +describe MeiliSearch::Rails::MultiSearchResult do # rubocop:todo RSpec/FilePath let(:raw_results) do [ { '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 - }, + '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 - }, + '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 - } + '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) } @@ -48,13 +45,16 @@ 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')) ] + ['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 @@ -69,7 +69,7 @@ end it 'aliases as #to_ary' do - expect(subject.method(:to_ary).original_name).to eq :to_a + expect(result.method(:to_ary).original_name).to eq :to_a end end From b0248afc5309c6aff2535021c584dbd9bc726213 Mon Sep 17 00:00:00 2001 From: ellnix <103502144+ellnix@users.noreply.github.com> Date: Fri, 23 Feb 2024 17:49:31 +0100 Subject: [PATCH 7/8] Implement multi-search pagination and refactor --- lib/meilisearch/rails/multi_search.rb | 46 +++++++++++++++----- lib/meilisearch/rails/multi_search/result.rb | 2 +- spec/multi_search/result_spec.rb | 30 +++++++------ spec/multi_search_spec.rb | 25 +++++++++-- 4 files changed, 74 insertions(+), 29 deletions(-) diff --git a/lib/meilisearch/rails/multi_search.rb b/lib/meilisearch/rails/multi_search.rb index 64d2abc4..4126a40e 100644 --- a/lib/meilisearch/rails/multi_search.rb +++ b/lib/meilisearch/rails/multi_search.rb @@ -5,19 +5,45 @@ module Rails class << self def multi_search(searches) search_parameters = searches.map do |(index_target, options)| - index_uid = case index_target - when String, Symbol - index_target - else - index_target.index.uid - end - - options.except(:class_name).merge(index_uid: index_uid) + paginate(options) if pagination_enabled? + normalize(options, index_target) end - raw_results = client.multi_search(search_parameters)['results'] + 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 - MultiSearchResult.new(searches, raw_results) + def pagination_enabled? + MeiliSearch::Rails.configuration[:pagination_backend] end end end diff --git a/lib/meilisearch/rails/multi_search/result.rb b/lib/meilisearch/rails/multi_search/result.rb index 72d9fb44..593856ab 100644 --- a/lib/meilisearch/rails/multi_search/result.rb +++ b/lib/meilisearch/rails/multi_search/result.rb @@ -7,7 +7,7 @@ def initialize(searches, raw_results) @results = {} @metadata = {} - searches.zip(raw_results).each do |(index_target, search_options), result| + 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 diff --git a/spec/multi_search/result_spec.rb b/spec/multi_search/result_spec.rb index ff63dc11..99c35a0f 100644 --- a/spec/multi_search/result_spec.rb +++ b/spec/multi_search/result_spec.rb @@ -2,20 +2,22 @@ describe MeiliSearch::Rails::MultiSearchResult do # rubocop:todo RSpec/FilePath let(:raw_results) do - [ - { '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 } - ] + { + '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 diff --git a/spec/multi_search_spec.rb b/spec/multi_search_spec.rb index 2090692e..552bf76d 100644 --- a/spec/multi_search_spec.rb +++ b/spec/multi_search_spec.rb @@ -4,13 +4,11 @@ def reset_indexes [Book, Color, Product].each do |klass| klass.delete_all - klass.index.delete_all_documents + klass.clear_index! end end - before(:all) { reset_indexes } # rubocop:todo RSpec/BeforeAfterAll - - after { reset_indexes } + before { reset_indexes } let!(:palm_pixi_plus) { Product.create!(name: 'palm pixi plus', href: 'ebay', tags: ['terrible']) } let!(:steve_jobs) { Book.create! name: 'Steve Jobs', author: 'Walter Isaacson' } @@ -96,4 +94,23 @@ def reset_indexes ) end end + + context 'with pagination' do + it 'it properly paginates each search' do + MeiliSearch::Rails.configuration[:pagination_backend] = :kaminari + + results = MeiliSearch::Rails.multi_search( + Book => { q: 'Steve' }, + Product => { q: 'palm', page: 1, hits_per_page: 1 }, + Color.index.uid => { q: 'bl', page: 1, 'hitsPerPage' => '1' } + ) + + expect(results).to contain_exactly( + steve_jobs, palm_pixi_plus, + a_hash_including('name' => 'black', 'short_name' => 'bla') + ) + + MeiliSearch::Rails.configuration[:pagination_backend] = nil + end + end end From 9c186c92bc5d91a62a33b8b43a60a4f13967dc09 Mon Sep 17 00:00:00 2001 From: ellnix <103502144+ellnix@users.noreply.github.com> Date: Fri, 23 Feb 2024 18:08:38 +0100 Subject: [PATCH 8/8] Fix linter problems --- lib/meilisearch/rails/multi_search.rb | 1 - spec/multi_search_spec.rb | 23 ++++++++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/meilisearch/rails/multi_search.rb b/lib/meilisearch/rails/multi_search.rb index 4126a40e..1f9b63bd 100644 --- a/lib/meilisearch/rails/multi_search.rb +++ b/lib/meilisearch/rails/multi_search.rb @@ -12,7 +12,6 @@ def multi_search(searches) MultiSearchResult.new(searches, client.multi_search(search_parameters)) end - private def normalize(options, index_target) diff --git a/spec/multi_search_spec.rb b/spec/multi_search_spec.rb index 552bf76d..c8f9ed17 100644 --- a/spec/multi_search_spec.rb +++ b/spec/multi_search_spec.rb @@ -4,26 +4,31 @@ def reset_indexes [Book, Color, Product].each do |klass| klass.delete_all - klass.clear_index! + klass.clear_index!(true) end end - before { reset_indexes } - - let!(:palm_pixi_plus) { Product.create!(name: 'palm pixi plus', href: 'ebay', tags: ['terrible']) } - let!(:steve_jobs) { Book.create! name: 'Steve Jobs', author: 'Walter Isaacson' } - let!(:blue) { Color.create!(name: 'blue', short_name: 'blu', hex: 0x0000FF) } - let!(:black) { Color.create!(name: 'black', short_name: 'bla', hex: 0x000000) } - before do + reset_indexes + + Product.create! name: 'palm pixi plus', href: 'ebay', tags: ['terrible'] Product.create! name: 'lg vortex', href: 'ebay', tags: ['decent'] Product.create! name: 'palmpre', href: 'ebay', tags: ['discontinued', 'worst phone ever'] Product.reindex! + Color.create! name: 'blue', short_name: 'blu', hex: 0x0000FF + Color.create! name: 'black', short_name: 'bla', hex: 0x000000 Color.create! name: 'green', short_name: 'gre', hex: 0x00FF00 + + Book.create! name: 'Steve Jobs', author: 'Walter Isaacson' Book.create! name: 'Moby Dick', author: 'Herman Melville' end + let!(:palm_pixi_plus) { Product.find_by name: 'palm pixi plus' } + let!(:steve_jobs) { Book.find_by name: 'Steve Jobs' } + let!(:blue) { Color.find_by name: 'blue' } + let!(:black) { Color.find_by name: 'black' } + context 'with class keys' do it 'returns ORM records' do results = MeiliSearch::Rails.multi_search( @@ -96,7 +101,7 @@ def reset_indexes end context 'with pagination' do - it 'it properly paginates each search' do + it 'properly paginates each search' do MeiliSearch::Rails.configuration[:pagination_backend] = :kaminari results = MeiliSearch::Rails.multi_search(