diff --git a/Gemfile.lock b/Gemfile.lock index 0074728e..b6cabe54 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -98,7 +98,7 @@ GEM amq-protocol (~> 2.3.0) bunny-mock (1.7.0) bunny (>= 1.7) - byebug (10.0.1) + byebug (10.0.2) charlock_holmes (0.7.6) chewy (5.0.0) activesupport (>= 4.0) @@ -126,8 +126,8 @@ GEM responders warden (~> 1.2.3) diff-lcs (1.3) - docile (1.3.0) - dotenv (2.2.1) + docile (1.3.1) + dotenv (2.4.0) dry-configurable (0.7.0) concurrent-ruby (~> 1.0) dry-container (0.6.0) @@ -182,7 +182,7 @@ GEM fasterer (0.4.1) colorize (~> 0.7) ruby_parser (~> 3.11.0) - ffi (1.9.23) + ffi (1.9.25) filelock (1.1.1) formatador (0.2.5) fuubar (2.3.1) @@ -365,14 +365,14 @@ GEM activemodel (>= 4.0.0) railties (>= 4.0.0) sequel (>= 3.28, < 6.0) - sequel_pg (1.8.1) + sequel_pg (1.8.2) pg (>= 0.18.0) sequel (>= 4.34.0) sequel_postgresql_triggers (1.4.0) sequel serverengine (2.0.6) sigdump (~> 0.2.2) - sexp_processor (4.10.1) + sexp_processor (4.11.0) sigdump (0.2.4) simplecov (0.16.1) docile (~> 1.1) @@ -403,7 +403,7 @@ GEM thread_safe (~> 0.1) unicode-display_width (1.4.0) url (0.3.2) - uuid (2.3.8) + uuid (2.3.9) macaddr (~> 1.0) warden (1.2.7) rack (>= 1.0) diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index c6ae8140..cb424bc3 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -83,7 +83,7 @@ description 'The query string' end - resolve(->(_root, _arguments, _context) { :ok }) + resolve ->(_root, arguments, _context) { arguments } end field :version, !Types::VersionType do diff --git a/app/graphql/types/search_result_type.rb b/app/graphql/types/search_result_type.rb index 2f24584b..0f14be0c 100644 --- a/app/graphql/types/search_result_type.rb +++ b/app/graphql/types/search_result_type.rb @@ -14,8 +14,6 @@ description 'Limit search to certain categories' end - resolve(lambda do |_root, arguments, _context| - SearchResult.new(categories: arguments['categories']) - end) + resolve SearchResolver.new end end diff --git a/lib/search_resolver.rb b/lib/search_resolver.rb new file mode 100644 index 00000000..633449d6 --- /dev/null +++ b/lib/search_resolver.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require 'ostruct' + +# Returns a search result to the GraphQL API +class SearchResolver + def call(root, arguments, _context) + result = search_index(root[:query], + map_categories_to_indices(arguments[:categories])) + OpenStruct.new( + entries: map_entries_to_models(result), + count: OpenStruct.new( + all: result.size, + organizational_units: organizational_units_count(result), + repositories: repositories_count(result) + ) + ) + end + + def search_index(query, indices) + indices.map do |index| + index.query(multi_match: {query: query, + fuzziness: 'auto', + fields: %i( + display_name + slug + name + description + )}).entries + end.flatten + end + + def map_categories_to_indices(categories) + if categories.blank? + [::Index::RepositoryIndex::Repository, + ::Index::OrganizationIndex::Organization, + ::Index::UserIndex::User] + else + reduce_categories(categories) + end + end + + # rubocop:disable Metrics/MethodLength + def reduce_categories(categories) + # rubocop:enable Metrics/MethodLength + categories.reduce([]) do |indices, category| + case category + when 'organizationalUnits' + indices + [::Index::OrganizationIndex::Organization, + ::Index::UserIndex::User] + when 'repositories' + indices + [::Index::RepositoryIndex::Repository] + else + indices + end + end + end + + def organizational_units_count(result) + result.count do |element| + elem = element._data['_index'] + elem == 'organization' || elem == 'user' + end + end + + def repositories_count(result) + result.count do |element| + element._data['_index'] == 'repository' + end + end + + # rubocop:disable Metrics/MethodLength, Metrics/AbcSize + def map_entries_to_models(result) + # rubocop:enable Metrics/MethodLength, Metrics/AbcSize + result.map do |element| + OpenStruct.new( + ranking: element._data['_score'], + entry: + if element._data['_type'] == 'user' + User.first(slug: element.attributes['slug']) + elsif element._data['_type'] == 'repository' + Repository.first(slug: element.attributes['slug']) + else + Organization.first(slug: element.attributes['slug']) + end + ) + end + end +end diff --git a/spec/graphql/queries/search_query_spec.rb b/spec/graphql/queries/search_query_spec.rb index 00808da0..f60f1a13 100644 --- a/spec/graphql/queries/search_query_spec.rb +++ b/spec/graphql/queries/search_query_spec.rb @@ -15,11 +15,37 @@ end RSpec.describe 'Search query' do - let(:user) { create :user } before do - create :user - 3.times { create :organization } - 5.times { create :repository, owner: user } + ::Index::UserIndex.purge + ::Index::OrganizationIndex.purge + ::Index::RepositoryIndex.purge + + ada = create :user, display_name: 'Ada' + ::Index::UserIndex.import(ada) + adc = create :user, display_name: 'Adc' + ::Index::UserIndex.import(adc) + bob = create :user, display_name: 'Bob' + ::Index::UserIndex.import(bob) + + ::Index::OrganizationIndex.import(create(:organization, + display_name: 'Ada')) + ::Index::OrganizationIndex.import(create(:organization, + display_name: 'Bda Organization')) + ::Index::OrganizationIndex.import(create(:organization, + display_name: 'Abc_Organization')) + + ::Index::RepositoryIndex.import(create(:repository, + name: 'Ada/repository', + owner: ada)) + ::Index::RepositoryIndex.import(create(:repository, + name: 'Bob/repository', + owner: bob)) + ::Index::RepositoryIndex.import(create(:repository, + name: 'Adc/repository', + owner: adc)) + ::Index::RepositoryIndex.import(create(:repository, + name: 'Bob/AdaRepository', + owner: bob)) end let(:context) { {} } @@ -71,11 +97,11 @@ context 'with global scope' do let(:scope) { 'global' } context 'no categories' do - let(:variables) { {'query' => ''} } - let(:expected_num_entries) { 10 } - let(:expected_count_all) { 10 } - let(:expected_count_organizational_units) { 5 } - let(:expected_count_repositories) { 5 } + let(:variables) { {'query' => 'Ada'} } + let(:expected_num_entries) { 6 } + let(:expected_count_all) { 6 } + let(:expected_count_organizational_units) { 4 } + let(:expected_count_repositories) { 2 } include_examples 'number of entries' @@ -83,25 +109,26 @@ repositories = search_result['entries'].select do |e| e['entry']['__typename'] == 'Repository' end - expect(repositories.length).to eq(5) + expect(repositories.length).to eq(expected_count_repositories) end it 'returns the organizational units' do organizational_units = search_result['entries'].select do |e| %w(User Organization).include?(e['entry']['__typename']) end - expect(organizational_units.length).to eq(5) + expect(organizational_units.length). + to eq(expected_count_organizational_units) end end context 'category: all' do let(:variables) do - {'query' => '', 'categories' => %w(repositories organizationalUnits)} + {'query' => 'Ada', 'categories' => %w(repositories organizationalUnits)} end - let(:expected_num_entries) { 10 } - let(:expected_count_all) { 10 } - let(:expected_count_organizational_units) { 5 } - let(:expected_count_repositories) { 5 } + let(:expected_num_entries) { 6 } + let(:expected_count_all) { 6 } + let(:expected_count_organizational_units) { 4 } + let(:expected_count_repositories) { 2 } include_examples 'number of entries' @@ -109,23 +136,25 @@ repositories = search_result['entries'].select do |e| e['entry']['__typename'] == 'Repository' end - expect(repositories.length).to eq(5) + expect(repositories.length). + to eq(expected_count_repositories) end it 'returns the organizational units' do organizational_units = search_result['entries'].select do |e| %w(User Organization).include?(e['entry']['__typename']) end - expect(organizational_units.length).to eq(5) + expect(organizational_units.length). + to eq(expected_count_organizational_units) end end context 'category: repositories' do - let(:variables) { {'query' => '', 'categories' => %w(repositories)} } - let(:expected_num_entries) { 5 } - let(:expected_count_all) { 10 } - let(:expected_count_organizational_units) { 5 } - let(:expected_count_repositories) { 5 } + let(:variables) { {'query' => 'Ada', 'categories' => %w(repositories)} } + let(:expected_num_entries) { 2 } + let(:expected_count_all) { 2 } + let(:expected_count_organizational_units) { 0 } + let(:expected_count_repositories) { 2 } include_examples 'number of entries' @@ -133,26 +162,26 @@ repositories = search_result['entries'].select do |e| e['entry']['__typename'] == 'Repository' end - expect(repositories.length).to eq(5) + expect(repositories.length).to eq(expected_count_repositories) end end context 'category: organizationalUnits' do let(:variables) do - {'query' => '', 'categories' => %w(organizationalUnits)} + {'query' => 'Ada', 'categories' => %w(organizationalUnits)} end - let(:expected_num_entries) { 5 } - let(:expected_count_all) { 10 } - let(:expected_count_organizational_units) { 5 } - let(:expected_count_repositories) { 5 } + let(:expected_num_entries) { 4 } + let(:expected_count_all) { 4 } + let(:expected_count_organizational_units) { 4 } + let(:expected_count_repositories) { 0 } include_examples 'number of entries' - it 'returns the organizational units' do organizational_units = search_result['entries'].select do |e| %w(User Organization).include?(e['entry']['__typename']) end - expect(organizational_units.length).to eq(5) + expect(organizational_units.length). + to eq(expected_count_organizational_units) end end end