Commit d9cb907c authored by Mario de la Ossa's avatar Mario de la Ossa

Avoid loading objects from DB in ES results

We already have most of the information we want to display in
Elasticsearch results inside ES itself, so instead of finding (and
returning!) the full ES object and only using the ID to get the database
object, we instead use a Lite object that quacks like the original
and uses the info from Elasticsearch.

We do this for:
- Projects
- Issues
- Merge Requests
- Milestones
parent ab9cf52e
...@@ -25,6 +25,7 @@ class SearchController < ApplicationController ...@@ -25,6 +25,7 @@ class SearchController < ApplicationController
@show_snippets = search_service.show_snippets? @show_snippets = search_service.show_snippets?
@search_results = search_service.search_results @search_results = search_service.search_results
@search_objects = search_service.search_objects @search_objects = search_service.search_objects
@display_options = search_service.display_options
render_commits if @scope == 'commits' render_commits if @scope == 'commits'
eager_load_user_status if @scope == 'users' eager_load_user_status if @scope == 'users'
......
...@@ -72,6 +72,8 @@ class ProjectFeature < ApplicationRecord ...@@ -72,6 +72,8 @@ class ProjectFeature < ApplicationRecord
default_value_for :wiki_access_level, value: ENABLED, allows_nil: false default_value_for :wiki_access_level, value: ENABLED, allows_nil: false
default_value_for :repository_access_level, value: ENABLED, allows_nil: false default_value_for :repository_access_level, value: ENABLED, allows_nil: false
scope :for_project_id, -> (project) { where(project: project) }
def feature_available?(feature, user) def feature_available?(feature, user)
# This feature might not be behind a feature flag at all, so default to true # This feature might not be behind a feature flag at all, so default to true
return false unless ::Feature.enabled?(feature, user, default_enabled: true) return false unless ::Feature.enabled?(feature, user, default_enabled: true)
......
...@@ -52,6 +52,10 @@ class SearchService ...@@ -52,6 +52,10 @@ class SearchService
@search_objects ||= search_results.objects(scope, params[:page]) @search_objects ||= search_results.objects(scope, params[:page])
end end
def display_options
@display_options ||= search_results.display_options(scope)
end
private private
def search_service def search_service
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
.search-results .search-results
- 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 }.merge(@display_options)
- else - else
- locals = { projects: blob_projects(@search_objects) } if %w[blobs wiki_blobs].include?(@scope) - locals = { projects: blob_projects(@search_objects) } if %w[blobs wiki_blobs].include?(@scope)
= render partial: "search/results/#{@scope.singularize}", collection: @search_objects, locals: locals = render partial: "search/results/#{@scope.singularize}", collection: @search_objects, locals: locals
......
.search-result-row .search-result-row
%h4 %h4
= confidential_icon(issue) = confidential_icon(issue)
= link_to [issue.project.namespace.becomes(Namespace), issue.project, issue] do = link_to namespace_project_issue_path(issue.project.namespace.becomes(Namespace), issue.project, issue) do
%span.term.str-truncated= issue.title %span.term.str-truncated= issue.title
- if issue.closed? - if issue.closed?
%span.badge.badge-danger.prepend-left-5= _("Closed") %span.badge.badge-danger.prepend-left-5= _("Closed")
......
.search-result-row .search-result-row
%h4 %h4
= link_to [merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request] do = link_to namespace_project_merge_request_path(merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request) do
%span.term.str-truncated= merge_request.title %span.term.str-truncated= merge_request.title
- if merge_request.merged? - if merge_request.merged?
%span.badge.badge-primary.prepend-left-5= _("Merged") %span.badge.badge-primary.prepend-left-5= _("Merged")
......
.search-result-row .search-result-row
%h4 %h4
= link_to [milestone.project.namespace.becomes(Namespace), milestone.project, milestone] do = link_to namespace_project_milestone_path(milestone.project.namespace.becomes(Namespace), milestone.project, milestone) do
%span.term.str-truncated= milestone.title %span.term.str-truncated= milestone.title
- if milestone.description.present? - if milestone.description.present?
......
...@@ -315,7 +315,7 @@ module Elastic ...@@ -315,7 +315,7 @@ module Elastic
self.import(options) self.import(options)
end end
def basic_query_hash(fields, query) def basic_query_hash(fields, query, page: nil, per_page: nil)
query_hash = if query.present? query_hash = if query.present?
{ {
query: { query: {
...@@ -344,6 +344,9 @@ module Elastic ...@@ -344,6 +344,9 @@ module Elastic
} }
end end
query_hash[:size] = per_page if per_page
query_hash[:from] = per_page * (page - 1) if per_page && page
query_hash[:sort] = [ query_hash[:sort] = [
{ updated_at: { order: :desc } }, { updated_at: { order: :desc } },
:_score :_score
......
...@@ -30,7 +30,7 @@ module Elastic ...@@ -30,7 +30,7 @@ module Elastic
if query =~ /#(\d+)\z/ if query =~ /#(\d+)\z/
iid_query_hash(Regexp.last_match(1)) iid_query_hash(Regexp.last_match(1))
else else
basic_query_hash(%w(title^2 description), query) basic_query_hash(%w(title^2 description), query, page: options[:page], per_page: options[:per_page])
end end
options[:features] = 'issues' options[:features] = 'issues'
......
...@@ -46,7 +46,7 @@ module Elastic ...@@ -46,7 +46,7 @@ module Elastic
if query =~ /\!(\d+)\z/ if query =~ /\!(\d+)\z/
iid_query_hash(Regexp.last_match(1)) iid_query_hash(Regexp.last_match(1))
else else
basic_query_hash(%w(title^2 description), query) basic_query_hash(%w(title^2 description), query, page: options[:page], per_page: options[:per_page])
end end
options[:features] = 'merge_requests' options[:features] = 'merge_requests'
......
...@@ -12,7 +12,7 @@ module Elastic ...@@ -12,7 +12,7 @@ module Elastic
# https://gitlab.com/gitlab-org/gitlab-ee/issues/349 # https://gitlab.com/gitlab-org/gitlab-ee/issues/349
data = {} data = {}
[:id, :title, :description, :project_id, :created_at, :updated_at].each do |attr| [:id, :iid, :title, :description, :project_id, :created_at, :updated_at].each do |attr|
data[attr.to_s] = safely_read_attribute_for_elasticsearch(attr) data[attr.to_s] = safely_read_attribute_for_elasticsearch(attr)
end end
...@@ -26,7 +26,7 @@ module Elastic ...@@ -26,7 +26,7 @@ module Elastic
def self.elastic_search(query, options: {}) def self.elastic_search(query, options: {})
options[:in] = %w(title^2 description) options[:in] = %w(title^2 description)
query_hash = basic_query_hash(options[:in], query) query_hash = basic_query_hash(options[:in], query, page: options[:page], per_page: options[:per_page])
query_hash = project_ids_filter(query_hash, options) query_hash = project_ids_filter(query_hash, options)
......
...@@ -65,7 +65,7 @@ module Elastic ...@@ -65,7 +65,7 @@ module Elastic
def self.elastic_search(query, options: {}) def self.elastic_search(query, options: {})
options[:in] = %w(name^10 name_with_namespace^2 path_with_namespace path^9 description) options[:in] = %w(name^10 name_with_namespace^2 path_with_namespace path^9 description)
query_hash = basic_query_hash(options[:in], query) query_hash = basic_query_hash(options[:in], query, page: options[:page], per_page: options[:per_page])
filters = [] filters = []
......
---
title: Avoid loading database objects for Elasticsearch results
merge_request: 12691
author:
type: performance
# frozen_string_literal: true
module Elasticsearch
class LiteProject
include Gitlab::Utils::StrongMemoize
attr_accessor :id, :name, :path, :description, :namespace_id, :create_at, :updated_at, :archived,
:visibility_level, :last_activity_at, :name_with_namespace, :path_with_namespace,
:issues_access_level, :merge_requests_access_level, :snippets_access_level, :wiki_access_level,
:repository_access_level
attr_accessor :pipeline_status, :commit, :creator
alias_attribute :last_activity_date, :last_activity_at
def initialize(raw_project_hash)
raw_project_hash.each do |key, value|
value = value.to_datetime if key =~ /_at$/
self.instance_variable_set(:"@#{key}", value)
end
end
# only used for routing and results display, so we trick Rails here
def namespace
# can't use an OpenStruct because to_param is a defined method on it
Struct.new(:to_param, :human_name).new(
path_with_namespace.sub(/\/#{path}$/, ''),
name_with_namespace.sub(/\s+\/\s+#{name}$/, '')
)
end
def route
# Creates an object that has a `cache_key` attribute set to nil
Struct.new(:cache_key).new(nil)
end
def pending_delete?
false
end
def cache_key
"lite_projects/#{id}-#{updated_at.utc.to_s(:number)}"
end
def to_param
path&.to_s
end
def model_name
OpenStruct.new(param_key: 'project')
end
def banzai_render_context(field)
return unless field == :description
{ pipeline: :description, project: self }
end
def default_issues_tracker?
true
end
# rubocop: disable CodeReuse/ActiveRecord
# This method is required by the OpenMergeRequestsCountService
def merge_requests
strong_memoize(:merge_requests) do
MergeRequest.opened.where(project_id: self.id)
end
end
# rubocop: enable CodeReuse/ActiveRecord
def forks_count
Projects::ForksCountService.new(self).count
end
def open_issues_count(current_user = nil)
Projects::OpenIssuesCountService.new(self, current_user).count
end
def open_merge_requests_count
Projects::OpenMergeRequestsCountService.new(self).count
end
end
end
# frozen_string_literal: true
module Elasticsearch
module ResultObjects
# Here we refine our models to learn how to load from Elasticsearch without hitting the database
# In order to use these methods in another class you first have to `using Elasticsearch::ResultObjects`
# and you can then use the model objects directly
ES_ATTRIBUTES = %i(join_field type).freeze
refine ::Project.singleton_class do
def load_from_elasticsearch(es_response, current_user: nil)
es_response.results.response.map(&:_source).then do |projects|
projects.map do |project|
::Elasticsearch::LiteProject.new(project)
end
end
end
end
refine ::Issue.singleton_class do
def load_from_elasticsearch(es_response, current_user:)
es_response.results.response.map(&:_source).then do |issues|
projects = ::ProjectsFinder.new(
current_user: current_user,
project_ids_relation: issues.map(&:project_id)
).execute.index_by(&:id)
issues.map do |issue|
issue[:project] = projects[issue.delete(:project_id)]
issue[:assignee_ids] = issue.delete(:assignee_id)
issue.except!(*ES_ATTRIBUTES)
new(issue)
end
end
end
end
refine ::MergeRequest.singleton_class do
def load_from_elasticsearch(es_response, current_user:)
es_response.results.response.map(&:_source).then do |merge_requests|
# rubocop: disable CodeReuse/ActiveRecord
projects = ::ProjectsFinder.new(
current_user: current_user,
project_ids_relation: merge_requests.map(&:target_project_id)
).execute.includes(:route, namespace: [:route]).index_by(&:id)
# rubocop: enable CodeReuse/ActiveRecord
merge_requests.map do |merge_request|
merge_request[:target_project] = projects[merge_request.delete(:target_project_id)]
merge_request.except!(*ES_ATTRIBUTES)
new(merge_request)
end
end
end
end
refine ::Milestone.singleton_class do
def load_from_elasticsearch(es_response, current_user:)
es_response.results.response.map(&:_source).then do |milestones|
projects = ::ProjectsFinder.new(
current_user: current_user,
project_ids_relation: milestones.map(&:project_id)
).execute.index_by(&:id)
milestones.map do |milestone|
milestone[:project] = projects[milestone.delete(:project_id)]
milestone.except!(*ES_ATTRIBUTES)
new(milestone)
end
end
end
end
end
end
...@@ -5,6 +5,8 @@ module Gitlab ...@@ -5,6 +5,8 @@ module Gitlab
class SearchResults class SearchResults
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
using Elasticsearch::ResultObjects
attr_reader :current_user, :query, :public_and_internal_projects attr_reader :current_user, :query, :public_and_internal_projects
# Limit search results by passed project ids # Limit search results by passed project ids
...@@ -25,13 +27,13 @@ module Gitlab ...@@ -25,13 +27,13 @@ module Gitlab
def objects(scope, page = nil) def objects(scope, page = nil)
case scope case scope
when 'projects' when 'projects'
projects.page(page).per(per_page).records projects(page: page, per_page: per_page)
when 'issues' when 'issues'
issues.page(page).per(per_page).records issues(page: page, per_page: per_page)
when 'merge_requests' when 'merge_requests'
merge_requests.page(page).per(per_page).records merge_requests(page: page, per_page: per_page)
when 'milestones' when 'milestones'
milestones.page(page).per(per_page).records milestones(page: page, per_page: per_page)
when 'blobs' when 'blobs'
blobs.page(page).per(per_page) blobs.page(page).per(per_page)
when 'wiki_blobs' when 'wiki_blobs'
...@@ -45,6 +47,17 @@ module Gitlab ...@@ -45,6 +47,17 @@ module Gitlab
end end
end end
def display_options(scope)
case scope
when 'projects'
{
stars: false
}
else
{}
end
end
def generic_search_results def generic_search_results
@generic_search_results ||= Gitlab::SearchResults.new(current_user, limit_projects, query) @generic_search_results ||= Gitlab::SearchResults.new(current_user, limit_projects, query)
end end
...@@ -143,19 +156,40 @@ module Gitlab ...@@ -143,19 +156,40 @@ module Gitlab
} }
end end
def projects def paginate_array(collection, total_count, page, per_page)
offset = per_page * (page - 1)
Kaminari.paginate_array(collection, total_count: total_count, limit: per_page, offset: offset)
end
def search(model, query, options, page: 1, per_page: 20)
page = (page || 1).to_i
response = model.elastic_search(
query,
options: options.merge(page: page, per_page: per_page)
)
results = model.load_from_elasticsearch(response, current_user: current_user)
paginate_array(results, response.total_count, page, per_page)
end
# See the comment for #commits for more info on why we memoize this way
def projects(page: 1, per_page: 20)
strong_memoize(:projects) do strong_memoize(:projects) do
Project.elastic_search(query, options: base_options) search(Project, query, base_options, page: page, per_page: per_page)
end end
end end
def issues # See the comment for #commits for more info on why we memoize this way
def issues(page: 1, per_page: 20)
strong_memoize(:issues) do strong_memoize(:issues) do
Issue.elastic_search(query, options: base_options) search(Issue, query, base_options, page: page, per_page: per_page)
end end
end end
def milestones # See the comment for #commits for more info on why we memoize this way
def milestones(page: 1, per_page: 20)
strong_memoize(:milestones) do strong_memoize(:milestones) do
# Must pass 'issues' and 'merge_requests' to check # Must pass 'issues' and 'merge_requests' to check
# if any of the features is available for projects in Elastic::ApplicationSearch#project_ids_query # if any of the features is available for projects in Elastic::ApplicationSearch#project_ids_query
...@@ -164,17 +198,18 @@ module Gitlab ...@@ -164,17 +198,18 @@ module Gitlab
options = base_options options = base_options
options[:features] = [:issues, :merge_requests] options[:features] = [:issues, :merge_requests]
Milestone.elastic_search(query, options: options) search(Milestone, query, options, page: page, per_page: per_page)
end end
end end
def merge_requests # See the comment for #commits for more info on why we memoize this way
def merge_requests(page: 1, per_page: 20)
strong_memoize(:merge_requests) do strong_memoize(:merge_requests) do
options = base_options.merge(project_ids: non_guest_project_ids) search(MergeRequest, query, base_options.merge(project_ids: non_guest_project_ids), page: page, per_page: per_page)
MergeRequest.elastic_search(query, options: options)
end end
end end
# See the comment for #commits for more info on why we memoize this way
def blobs def blobs
return Kaminari.paginate_array([]) if query.blank? return Kaminari.paginate_array([]) if query.blank?
...@@ -191,6 +226,7 @@ module Gitlab ...@@ -191,6 +226,7 @@ module Gitlab
end end
end end
# See the comment for #commits for more info on why we memoize this way
def wiki_blobs def wiki_blobs
return Kaminari.paginate_array([]) if query.blank? return Kaminari.paginate_array([]) if query.blank?
......
require 'spec_helper' require 'spec_helper'
describe 'Global elastic search' do describe 'Global elastic search', :elastic do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, :repository, :wiki_repo, namespace: user.namespace) } let(:project) { create(:project, :repository, :wiki_repo, namespace: user.namespace) }
before do before do
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
project.add_maintainer(user) project.add_maintainer(user)
sign_in(user) sign_in(user)
end end
after do after do
Gitlab::Elastic::Helper.delete_index
stub_ee_application_setting(elasticsearch_search: false, elasticsearch_indexing: false) stub_ee_application_setting(elasticsearch_search: false, elasticsearch_indexing: false)
end end
shared_examples 'a pure Elasticsearch result' do
it 'avoids N+1 database queries' do
create(object, creation_args)
Gitlab::Elastic::Helper.refresh_index
control_count = ActiveRecord::QueryRecorder.new { visit path }.count
create_list(object, 10, creation_args)
Gitlab::Elastic::Helper.refresh_index
control_count = control_count + (10 * query_count_multiplier) + 1
expect { visit path }.not_to exceed_query_limit(control_count)
end
end
describe 'I do not overload the database' do
context 'searching issues' do
let(:object) { :issue }
let(:creation_args) { { project: project, title: 'initial' } }
let(:path) { search_path(search: 'initial', scope: 'issues') }
let(:query_count_multiplier) { 0 }
it_behaves_like 'a pure Elasticsearch result'
end
context 'searching projects' do
let(:object) { :project }
let(:creation_args) { { namespace: user.namespace } }
let(:path) { search_path(search: 'project*', scope: 'projects') }
# Each Project requires 4 extra queries: one for each "count" (forks, open MRs, open Issues) and one for access level
let(:query_count_multiplier) { 4 }
it_behaves_like 'a pure Elasticsearch result'
end
context 'searching merge requests' do
it 'avoids N+1 database queries' do
path = search_path(search: 'initial', scope: 'merge_requests')
create(:merge_request, title: 'initial', source_project: project)
Gitlab::Elastic::Helper.refresh_index
control_count = ActiveRecord::QueryRecorder.new { visit path }.count
merge_requests = create_list(:merge_request, 10, title: 'initial')
merge_requests.each { |mr| mr.target_project.add_maintainer(user) }
Gitlab::Elastic::Helper.refresh_index
# Each MR loaded has a Route load that has been tricky to track down
expect { visit path }.not_to exceed_query_limit(control_count + 11)
end
end
context 'searching milestones' do
let(:object) { :milestone }
let(:creation_args) { { project: project } }
let(:path) { search_path(search: 'milestone*', scope: 'milestones') }
let(:query_count_multiplier) { 0 }
it_behaves_like 'a pure Elasticsearch result'
end
end
describe 'I search through the issues and I see pagination' do describe 'I search through the issues and I see pagination' do
before do before do
create_list(:issue, 21, project: project, title: 'initial') create_list(:issue, 21, project: project, title: 'initial')
......
...@@ -162,31 +162,31 @@ describe Gitlab::Elastic::ProjectSearchResults do ...@@ -162,31 +162,31 @@ describe Gitlab::Elastic::ProjectSearchResults do
it 'does not list project confidential issues for non project members' do it 'does not list project confidential issues for non project members' do
results = described_class.new(non_member, query, project.id) results = described_class.new(non_member, query, project.id)
issues = results.objects('issues') issues = results.objects('issues').map(&:id)
expect(issues).to include issue expect(issues).to include issue.id
expect(issues).not_to include security_issue_1 expect(issues).not_to include security_issue_1.id
expect(issues).not_to include security_issue_2 expect(issues).not_to include security_issue_2.id
expect(results.issues_count).to eq 1 expect(results.issues_count).to eq 1
end end
it 'lists project confidential issues for author' do it 'lists project confidential issues for author' do
results = described_class.new(author, query, project.id) results = described_class.new(author, query, project.id)
issues = results.objects('issues') issues = results.objects('issues').map(&:id)
expect(issues).to include issue expect(issues).to include issue.id
expect(issues).to include security_issue_1 expect(issues).to include security_issue_1.id
expect(issues).not_to include security_issue_2 expect(issues).not_to include security_issue_2.id
expect(results.issues_count).to eq 2 expect(results.issues_count).to eq 2
end end
it 'lists project confidential issues for assignee' do it 'lists project confidential issues for assignee' do
results = described_class.new(assignee, query, project.id) results = described_class.new(assignee, query, project.id)
issues = results.objects('issues') issues = results.objects('issues').map(&:id)
expect(issues).to include issue expect(issues).to include issue.id
expect(issues).not_to include security_issue_1 expect(issues).not_to include security_issue_1.id
expect(issues).to include security_issue_2 expect(issues).to include security_issue_2.id
expect(results.issues_count).to eq 2 expect(results.issues_count).to eq 2
end end
...@@ -194,11 +194,11 @@ describe Gitlab::Elastic::ProjectSearchResults do ...@@ -194,11 +194,11 @@ describe Gitlab::Elastic::ProjectSearchResults do
project.add_developer(member) project.add_developer(member)
results = described_class.new(member, query, project.id) results = described_class.new(member, query, project.id)
issues = results.objects('issues') issues = results.objects('issues').map(&:id)
expect(issues).to include issue expect(issues).to include issue.id
expect(issues).to include security_issue_1 expect(issues).to include security_issue_1.id
expect(issues).to include security_issue_2 expect(issues).to include security_issue_2.id
expect(results.issues_count).to eq 3 expect(results.issues_count).to eq 3
end end
...@@ -206,21 +206,21 @@ describe Gitlab::Elastic::ProjectSearchResults do ...@@ -206,21 +206,21 @@ describe Gitlab::Elastic::ProjectSearchResults do
project.add_guest(member) project.add_guest(member)
results = described_class.new(member, query, project.id) results = described_class.new(member, query, project.id)
issues = results.objects('issues') issues = results.objects('issues').map(&:id)
expect(issues).to include issue expect(issues).to include issue.id
expect(issues).not_to include security_issue_1 expect(issues).not_to include security_issue_1.id
expect(issues).not_to include security_issue_2 expect(issues).not_to include security_issue_2.id
expect(results.issues_count).to eq 1 expect(results.issues_count).to eq 1
end end
it 'lists all project issues for admin' do it 'lists all project issues for admin' do
results = described_class.new(admin, query, project.id) results = described_class.new(admin, query, project.id)
issues = results.objects('issues') issues = results.objects('issues').map(&:id)
expect(issues).to include issue expect(issues).to include issue.id
expect(issues).to include security_issue_1 expect(issues).to include security_issue_1.id
expect(issues).to include security_issue_2 expect(issues).to include security_issue_2.id
expect(results.issues_count).to eq 3 expect(results.issues_count).to eq 3
end end
end end
......
...@@ -95,11 +95,11 @@ describe Gitlab::Elastic::SearchResults, :elastic do ...@@ -95,11 +95,11 @@ describe Gitlab::Elastic::SearchResults, :elastic do
it 'lists found issues' do it 'lists found issues' do
results = described_class.new(user, 'hello world', limit_project_ids) results = described_class.new(user, 'hello world', limit_project_ids)
issues = results.objects('issues') issue_ids = results.objects('issues').map(&:id)
expect(issue_ids).to include(@issue_1.id, @issue_2.id)
expect(issue_ids).not_to include(@issue_3.id)
expect(issues).to include @issue_1
expect(issues).to include @issue_2
expect(issues).not_to include @issue_3
expect(results.issues_count).to eq 2 expect(results.issues_count).to eq 2
end end
...@@ -112,11 +112,11 @@ describe Gitlab::Elastic::SearchResults, :elastic do ...@@ -112,11 +112,11 @@ describe Gitlab::Elastic::SearchResults, :elastic do
it 'lists issue when search by a valid iid' do it 'lists issue when search by a valid iid' do
results = described_class.new(user, '#2', limit_project_ids) results = described_class.new(user, '#2', limit_project_ids)
issues = results.objects('issues') issue_ids = results.objects('issues').map(&:id)
expect(issue_ids).to include @issue_2.id
expect(issue_ids).not_to include(@issue_1.id, @issue_3.id)
expect(issues).not_to include @issue_1
expect(issues).to include @issue_2
expect(issues).not_to include @issue_3
expect(results.issues_count).to eq 1 expect(results.issues_count).to eq 1
end end
...@@ -154,53 +154,56 @@ describe Gitlab::Elastic::SearchResults, :elastic do ...@@ -154,53 +154,56 @@ describe Gitlab::Elastic::SearchResults, :elastic do
it 'does not list confidential issues for guests' do it 'does not list confidential issues for guests' do
results = described_class.new(nil, query, limit_project_ids) results = described_class.new(nil, query, limit_project_ids)
issues = results.objects('issues') issue_ids = results.objects('issues').map(&:id)
expect(issues).to include @issue expect(issue_ids).to include @issue.id
expect(issues).not_to include @security_issue_1 expect(issue_ids).not_to include(@security_issue_1.id,
expect(issues).not_to include @security_issue_2 @security_issue_2.id,
expect(issues).not_to include @security_issue_3 @security_issue_3.id,
expect(issues).not_to include @security_issue_4 @security_issue_4.id,
expect(issues).not_to include @security_issue_5 @security_issue_5.id)
expect(results.issues_count).to eq 1 expect(results.issues_count).to eq 1
end end
it 'does not list confidential issues for non project members' do it 'does not list confidential issues for non project members' do
results = described_class.new(non_member, query, limit_project_ids) results = described_class.new(non_member, query, limit_project_ids)
issues = results.objects('issues') issue_ids = results.objects('issues').map(&:id)
expect(issues).to include @issue expect(issue_ids).to include @issue.id
expect(issues).not_to include @security_issue_1 expect(issue_ids).not_to include(@security_issue_1.id,
expect(issues).not_to include @security_issue_2 @security_issue_2.id,
expect(issues).not_to include @security_issue_3 @security_issue_3.id,
expect(issues).not_to include @security_issue_4 @security_issue_4.id,
expect(issues).not_to include @security_issue_5 @security_issue_5.id)
expect(results.issues_count).to eq 1 expect(results.issues_count).to eq 1
end end
it 'lists confidential issues for author' do it 'lists confidential issues for author' do
results = described_class.new(author, query, limit_project_ids) results = described_class.new(author, query, limit_project_ids)
issues = results.objects('issues') issue_ids = results.objects('issues').map(&:id)
expect(issues).to include @issue expect(issue_ids).to include(@issue.id,
expect(issues).to include @security_issue_1 @security_issue_1.id,
expect(issues).not_to include @security_issue_2 @security_issue_3.id)
expect(issues).to include @security_issue_3 expect(issue_ids).not_to include(@security_issue_2.id,
expect(issues).not_to include @security_issue_4 @security_issue_4.id,
expect(issues).not_to include @security_issue_5 @security_issue_5.id)
expect(results.issues_count).to eq 3 expect(results.issues_count).to eq 3
end end
it 'lists confidential issues for assignee' do it 'lists confidential issues for assignee' do
results = described_class.new(assignee, query, limit_project_ids) results = described_class.new(assignee, query, limit_project_ids)
issues = results.objects('issues') issue_ids = results.objects('issues').map(&:id)
expect(issues).to include @issue expect(issue_ids).to include(@issue.id,
expect(issues).not_to include @security_issue_1 @security_issue_2.id,
expect(issues).to include @security_issue_2 @security_issue_4.id)
expect(issues).not_to include @security_issue_3 expect(issue_ids).not_to include(@security_issue_1.id,
expect(issues).to include @security_issue_4 @security_issue_3.id,
expect(issues).not_to include @security_issue_5 @security_issue_5.id)
expect(results.issues_count).to eq 3 expect(results.issues_count).to eq 3
end end
...@@ -209,27 +212,28 @@ describe Gitlab::Elastic::SearchResults, :elastic do ...@@ -209,27 +212,28 @@ describe Gitlab::Elastic::SearchResults, :elastic do
project_2.add_developer(member) project_2.add_developer(member)
results = described_class.new(member, query, limit_project_ids) results = described_class.new(member, query, limit_project_ids)
issues = results.objects('issues') issue_ids = results.objects('issues').map(&:id)
expect(issues).to include @issue expect(issue_ids).to include(@issue.id,
expect(issues).to include @security_issue_1 @security_issue_1.id,
expect(issues).to include @security_issue_2 @security_issue_2.id,
expect(issues).to include @security_issue_3 @security_issue_3.id)
expect(issues).not_to include @security_issue_4 expect(issue_ids).not_to include(@security_issue_4.id,
expect(issues).not_to include @security_issue_5 @security_issue_5.id)
expect(results.issues_count).to eq 4 expect(results.issues_count).to eq 4
end end
it 'lists all issues for admin' do it 'lists all issues for admin' do
results = described_class.new(admin, query, limit_project_ids) results = described_class.new(admin, query, limit_project_ids)
issues = results.objects('issues') issues = results.objects('issues').map(&:id)
expect(issues).to include @issue expect(issues).to include(@issue.id,
expect(issues).to include @security_issue_1 @security_issue_1.id,
expect(issues).to include @security_issue_2 @security_issue_2.id,
expect(issues).to include @security_issue_3 @security_issue_3.id,
expect(issues).to include @security_issue_4 @security_issue_4.id)
expect(issues).not_to include @security_issue_5 expect(issues).not_to include @security_issue_5.id
expect(results.issues_count).to eq 5 expect(results.issues_count).to eq 5
end end
end end
...@@ -239,53 +243,55 @@ describe Gitlab::Elastic::SearchResults, :elastic do ...@@ -239,53 +243,55 @@ describe Gitlab::Elastic::SearchResults, :elastic do
it 'does not list confidential issues for guests' do it 'does not list confidential issues for guests' do
results = described_class.new(nil, query, limit_project_ids) results = described_class.new(nil, query, limit_project_ids)
issues = results.objects('issues') issues = results.objects('issues').map(&:id)
expect(issues).to include @issue expect(issues).to include @issue.id
expect(issues).not_to include @security_issue_1 expect(issues).not_to include(@security_issue_1.id,
expect(issues).not_to include @security_issue_2 @security_issue_2.id,
expect(issues).not_to include @security_issue_3 @security_issue_3.id,
expect(issues).not_to include @security_issue_4 @security_issue_4.id,
expect(issues).not_to include @security_issue_5 @security_issue_5.id)
expect(results.issues_count).to eq 1 expect(results.issues_count).to eq 1
end end
it 'does not list confidential issues for non project members' do it 'does not list confidential issues for non project members' do
results = described_class.new(non_member, query, limit_project_ids) results = described_class.new(non_member, query, limit_project_ids)
issues = results.objects('issues') issues = results.objects('issues').map(&:id)
expect(issues).to include @issue expect(issues).to include @issue.id
expect(issues).not_to include @security_issue_1 expect(issues).not_to include(@security_issue_1.id,
expect(issues).not_to include @security_issue_2 @security_issue_2.id,
expect(issues).not_to include @security_issue_3 @security_issue_3.id,
expect(issues).not_to include @security_issue_4 @security_issue_4.id,
expect(issues).not_to include @security_issue_5 @security_issue_5.id)
expect(results.issues_count).to eq 1 expect(results.issues_count).to eq 1
end end
it 'lists confidential issues for author' do it 'lists confidential issues for author' do
results = described_class.new(author, query, limit_project_ids) results = described_class.new(author, query, limit_project_ids)
issues = results.objects('issues') issue_ids = results.objects('issues').map(&:id)
expect(issues).to include @issue expect(issue_ids).to include(@issue.id,
expect(issues).not_to include @security_issue_1 @security_issue_3.id)
expect(issues).not_to include @security_issue_2 expect(issue_ids).not_to include(@security_issue_1.id,
expect(issues).to include @security_issue_3 @security_issue_2.id,
expect(issues).not_to include @security_issue_4 @security_issue_4.id,
expect(issues).not_to include @security_issue_5 @security_issue_5.id)
expect(results.issues_count).to eq 2 expect(results.issues_count).to eq 2
end end
it 'lists confidential issues for assignee' do it 'lists confidential issues for assignee' do
results = described_class.new(assignee, query, limit_project_ids) results = described_class.new(assignee, query, limit_project_ids)
issues = results.objects('issues') issue_ids = results.objects('issues').map(&:id)
expect(issues).to include @issue expect(issue_ids).to include(@issue.id,
expect(issues).not_to include @security_issue_1 @security_issue_4.id)
expect(issues).not_to include @security_issue_2 expect(issue_ids).not_to include(@security_issue_1.id,
expect(issues).not_to include @security_issue_3 @security_issue_2.id,
expect(issues).to include @security_issue_4 @security_issue_3.id,
expect(issues).not_to include @security_issue_5 @security_issue_5.id)
expect(results.issues_count).to eq 2 expect(results.issues_count).to eq 2
end end
...@@ -294,27 +300,28 @@ describe Gitlab::Elastic::SearchResults, :elastic do ...@@ -294,27 +300,28 @@ describe Gitlab::Elastic::SearchResults, :elastic do
project_3.add_developer(member) project_3.add_developer(member)
results = described_class.new(member, query, limit_project_ids) results = described_class.new(member, query, limit_project_ids)
issues = results.objects('issues') issue_ids = results.objects('issues').map(&:id)
expect(issues).to include @issue expect(issue_ids).to include(@issue.id,
expect(issues).not_to include @security_issue_1 @security_issue_3.id,
expect(issues).not_to include @security_issue_2 @security_issue_4.id)
expect(issues).to include @security_issue_3 expect(issue_ids).not_to include(@security_issue_1.id,
expect(issues).to include @security_issue_4 @security_issue_2.id,
expect(issues).not_to include @security_issue_5 @security_issue_5.id)
expect(results.issues_count).to eq 3 expect(results.issues_count).to eq 3
end end
it 'lists all issues for admin' do it 'lists all issues for admin' do
results = described_class.new(admin, query, limit_project_ids) results = described_class.new(admin, query, limit_project_ids)
issues = results.objects('issues') issue_ids = results.objects('issues').map(&:id)
expect(issues).to include @issue expect(issue_ids).to include(@issue.id,
expect(issues).not_to include @security_issue_1 @security_issue_3.id,
expect(issues).not_to include @security_issue_2 @security_issue_4.id)
expect(issues).to include @security_issue_3 expect(issue_ids).not_to include(@security_issue_1.id,
expect(issues).to include @security_issue_4 @security_issue_2.id,
expect(issues).not_to include @security_issue_5 @security_issue_5.id)
expect(results.issues_count).to eq 3 expect(results.issues_count).to eq 3
end end
end end
...@@ -351,11 +358,10 @@ describe Gitlab::Elastic::SearchResults, :elastic do ...@@ -351,11 +358,10 @@ describe Gitlab::Elastic::SearchResults, :elastic do
it 'lists found merge requests' do it 'lists found merge requests' do
results = described_class.new(user, 'hello world', limit_project_ids) results = described_class.new(user, 'hello world', limit_project_ids)
merge_requests = results.objects('merge_requests') merge_requests = results.objects('merge_requests').map(&:id)
expect(merge_requests).to include @merge_request_1 expect(merge_requests).to include(@merge_request_1.id, @merge_request_2.id)
expect(merge_requests).to include @merge_request_2 expect(merge_requests).not_to include @merge_request_3.id
expect(merge_requests).not_to include @merge_request_3
expect(results.merge_requests_count).to eq 2 expect(results.merge_requests_count).to eq 2
end end
...@@ -368,11 +374,10 @@ describe Gitlab::Elastic::SearchResults, :elastic do ...@@ -368,11 +374,10 @@ describe Gitlab::Elastic::SearchResults, :elastic do
it 'lists merge request when search by a valid iid' do it 'lists merge request when search by a valid iid' do
results = described_class.new(user, '#2', limit_project_ids) results = described_class.new(user, '#2', limit_project_ids)
merge_requests = results.objects('merge_requests') merge_requests = results.objects('merge_requests').map(&:id)
expect(merge_requests).not_to include @merge_request_1 expect(merge_requests).to include @merge_request_2.id
expect(merge_requests).to include @merge_request_2 expect(merge_requests).not_to include(@merge_request_1.id, @merge_request_3.id)
expect(merge_requests).not_to include @merge_request_3
expect(results.merge_requests_count).to eq 1 expect(results.merge_requests_count).to eq 1
end end
...@@ -678,18 +683,17 @@ describe Gitlab::Elastic::SearchResults, :elastic do ...@@ -678,18 +683,17 @@ describe Gitlab::Elastic::SearchResults, :elastic do
# Authenticated search # Authenticated search
results = described_class.new(user, 'project', limit_project_ids) results = described_class.new(user, 'project', limit_project_ids)
issues = results.objects('issues') issues = results.objects('issues').map(&:id)
expect(issues).to include(issue_1.id, issue_3.id, issue_4.id)
expect(issues).to include issue_1
expect(issues).to include issue_3
expect(issues).to include issue_4
expect(results.issues_count).to eq 3 expect(results.issues_count).to eq 3
# Unauthenticated search # Unauthenticated search
results = described_class.new(nil, 'project', []) results = described_class.new(nil, 'project', [])
issues = results.objects('issues') issues = results.objects('issues').map(&:id)
expect(issues).to include issue_4 expect(issues).to include issue_4.id
expect(results.issues_count).to eq 1 expect(results.issues_count).to eq 1
end end
end end
...@@ -821,18 +825,17 @@ describe Gitlab::Elastic::SearchResults, :elastic do ...@@ -821,18 +825,17 @@ describe Gitlab::Elastic::SearchResults, :elastic do
# Authenticated search # Authenticated search
results = described_class.new(user, 'project', limit_project_ids) results = described_class.new(user, 'project', limit_project_ids)
milestones = results.objects('projects') projects = results.objects('projects').map(&:id)
expect(projects).to include(internal_project.id, private_project2.id, public_project.id)
expect(milestones).to include internal_project
expect(milestones).to include private_project2
expect(milestones).to include public_project
expect(results.projects_count).to eq 3 expect(results.projects_count).to eq 3
# Unauthenticated search # Unauthenticated search
results = described_class.new(nil, 'project', []) results = described_class.new(nil, 'project', [])
projects = results.objects('projects') projects = results.objects('projects').map(&:id)
expect(projects).to include public_project expect(projects).to include public_project.id
expect(results.projects_count).to eq 1 expect(results.projects_count).to eq 1
end end
end end
...@@ -848,18 +851,17 @@ describe Gitlab::Elastic::SearchResults, :elastic do ...@@ -848,18 +851,17 @@ describe Gitlab::Elastic::SearchResults, :elastic do
# Authenticated search # Authenticated search
results = described_class.new(user, 'project', limit_project_ids) results = described_class.new(user, 'project', limit_project_ids)
merge_requests = results.objects('merge_requests') merge_requests = results.objects('merge_requests').map(&:id)
expect(merge_requests).to include(merge_request_1.id, merge_request_3.id, merge_request_4.id)
expect(merge_requests).to include merge_request_1
expect(merge_requests).to include merge_request_3
expect(merge_requests).to include merge_request_4
expect(results.merge_requests_count).to eq 3 expect(results.merge_requests_count).to eq 3
# Unauthenticated search # Unauthenticated search
results = described_class.new(nil, 'project', []) results = described_class.new(nil, 'project', [])
merge_requests = results.objects('merge_requests') merge_requests = results.objects('merge_requests').map(&:id)
expect(merge_requests).to include merge_request_4 expect(merge_requests).to include merge_request_4.id
expect(results.merge_requests_count).to eq 1 expect(results.merge_requests_count).to eq 1
end end
end end
......
...@@ -39,6 +39,7 @@ describe Milestone, :elastic do ...@@ -39,6 +39,7 @@ describe Milestone, :elastic do
expected_hash = milestone.attributes.extract!( expected_hash = milestone.attributes.extract!(
'id', 'id',
'iid',
'title', 'title',
'description', 'description',
'project_id', 'project_id',
......
...@@ -40,18 +40,18 @@ describe Search::GroupService, :elastic do ...@@ -40,18 +40,18 @@ describe Search::GroupService, :elastic do
end end
context 'finding projects by name' do context 'finding projects by name' do
subject { results.objects('projects') } subject { results.objects('projects').map(&:id) }
context 'in parent group' do context 'in parent group' do
let(:search_group) { nested_group.parent } let(:search_group) { nested_group.parent }
it { is_expected.to match_array([project1, project2, project3]) } it { is_expected.to match_array([project1.id, project2.id, project3.id]) }
end end
context 'in subgroup' do context 'in subgroup' do
let(:search_group) { nested_group } let(:search_group) { nested_group }
it { is_expected.to match_array([project1, project2]) } it { is_expected.to match_array([project1.id, project2.id]) }
end end
end end
end end
......
...@@ -85,6 +85,10 @@ module Gitlab ...@@ -85,6 +85,10 @@ module Gitlab
UsersFinder.new(current_user, search: query).execute UsersFinder.new(current_user, search: query).execute
end end
def display_options(_scope)
{}
end
private private
def projects def projects
......
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