Commit 18fc240c authored by Mario de la Ossa's avatar Mario de la Ossa

Avoid Project N+1 on Elasticsearch Code results

We were loading a project per blob result on the elasticsearch search
page. Now we load all projects in the current 'page' in a single
transaction in order to avoid multiple round-trips to the database.
parent d59aa8da
...@@ -33,10 +33,15 @@ module SearchHelper ...@@ -33,10 +33,15 @@ module SearchHelper
"Showing #{from} - #{to} of #{count} #{scope.humanize(capitalize: false)} for \"#{term}\"" "Showing #{from} - #{to} of #{count} #{scope.humanize(capitalize: false)} for \"#{term}\""
end end
def find_project_for_result_blob(result) def find_project_for_result_blob(projects, result)
@project @project
end end
# Used in EE
def blob_projects(results)
nil
end
def parse_search_result(result) def parse_search_result(result)
result result
end end
......
...@@ -22,6 +22,8 @@ ...@@ -22,6 +22,8 @@
- if @scope == 'projects' - if @scope == 'projects'
.term .term
= render 'shared/projects/list', projects: @search_objects, pipeline_status: false = render 'shared/projects/list', projects: @search_objects, pipeline_status: false
- elsif %w[blobs wiki_blobs].include?(@scope)
= render partial: 'search/results/blob', collection: @search_objects, locals: { projects: blob_projects(@search_objects) }
- else - else
= render partial: "search/results/#{@scope.singularize}", collection: @search_objects = render partial: "search/results/#{@scope.singularize}", collection: @search_objects
......
- project = find_project_for_result_blob(blob) - project = find_project_for_result_blob(projects, blob)
- return unless project - return unless project
- blob = parse_search_result(blob) - blob = parse_search_result(blob)
......
- project = find_project_for_result_blob(wiki_blob) - project = find_project_for_result_blob(projects, wiki_blob)
- wiki_blob = parse_search_result(wiki_blob) - wiki_blob = parse_search_result(wiki_blob)
- wiki_blob_link = project_wiki_path(project, wiki_blob.basename) - wiki_blob_link = project_wiki_path(project, wiki_blob.basename)
......
...@@ -11,11 +11,20 @@ module EE ...@@ -11,11 +11,20 @@ module EE
end end
override :find_project_for_result_blob override :find_project_for_result_blob
# rubocop: disable CodeReuse/ActiveRecord def find_project_for_result_blob(projects, result)
def find_project_for_result_blob(result) return super if result.is_a?(::Gitlab::Search::FoundBlob)
super || ::Project.find_by(id: result.dig('_source', 'join_field', 'parent')&.split('_')&.last)
super || projects&.find { |project| project.id == blob_project_id(result) }
end
override :blob_projects
def blob_projects(results)
return super if results.first.is_a?(::Gitlab::Search::FoundBlob)
project_ids = results.map(&method(:blob_project_id))
::ProjectsFinder.new(current_user: current_user, project_ids_relation: project_ids).execute
end end
# rubocop: enable CodeReuse/ActiveRecord
override :parse_search_result override :parse_search_result
def parse_search_result(result) def parse_search_result(result)
...@@ -41,5 +50,9 @@ module EE ...@@ -41,5 +50,9 @@ module EE
type == :issues && (context == :dashboard || type == :issues && (context == :dashboard ||
context.feature_available?(:multiple_issue_assignees)) context.feature_available?(:multiple_issue_assignees))
end end
def blob_project_id(blob_result)
blob_result.dig('_source', 'join_field', 'parent')&.split('_')&.last.to_i
end
end end
end end
---
title: Avoid N+1 when loading Code search results with Elasticsearch enabled
merge_request: 10394
author:
type: security
...@@ -49,17 +49,12 @@ describe SearchHelper do ...@@ -49,17 +49,12 @@ describe SearchHelper do
end end
end end
describe '#parse_search_result_from_elastic' do describe '#parse_search_result with elastic enabled', :elastic do
let(:user) { create(:user) } let(:user) { create(:user) }
before do before do
allow(self).to receive(:current_user).and_return(user)
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true) stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
Gitlab::Elastic::Helper.create_empty_index
end
after do
Gitlab::Elastic::Helper.delete_index
stub_ee_application_setting(elasticsearch_search: false, elasticsearch_indexing: false)
end end
it "returns parsed result" do it "returns parsed result" do
...@@ -82,28 +77,37 @@ describe SearchHelper do ...@@ -82,28 +77,37 @@ describe SearchHelper do
expect(parsed_result.startline).to eq(2) expect(parsed_result.startline).to eq(2)
expect(parsed_result.data).to include("Popen") expect(parsed_result.data).to include("Popen")
end end
end
describe '#blob_projects', :elastic do
let(:user) { create(:user) }
before do
allow(self).to receive(:current_user).and_return(user)
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
end
it 'does not return project that does not exist' do def es_blob_search
Gitlab::Elastic::Helper.create_empty_index Repository.search(
'def popen',
type: :blob,
options: { highlight: true }
)[:blobs][:results]
end
@project_2 = create :project, :repository it 'returns all projects in the result page without causing an N+1' do
control_count = ActiveRecord::QueryRecorder.new { blob_projects(es_blob_search) }.count
@project_2.repository.create_file( projects = create_list :project, 3, :repository, :public
user, projects.each { |project| project.repository.index_blobs }
'thing.txt',
' function application.js ',
message: 'Find me',
branch_name: 'master')
@project_2.repository.index_blobs
Gitlab::Elastic::Helper.refresh_index Gitlab::Elastic::Helper.refresh_index
@project_2.destroy
blob = { _source: { join_field: { parent: @project_2.es_id } } }.as_json
result = find_project_for_result_blob(blob) # So we can access it outside the following block
result_projects = nil
expect(result).to be(nil) expect { result_projects = blob_projects(es_blob_search) }.not_to exceed_query_limit(control_count)
expect(result_projects).to match_array(projects)
end end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment