gitlab-org--gitlab-foss/lib/gitlab/search_results.rb
Jan Provaznik 090ca9c33e Use limit for search count queries
Search query is especially slow if a user searches a generic string
which matches many records, in such case search can take tens of
seconds or time out. To speed up the search query, we search only for
first 1000 records, if there is >1000 matching records we just display
"1000+" instead of precise total count supposing that with such amount
the exact count is not so important for the user.

Because for issues even limited search was not fast enough, 2-phase
approach is used for issues: first we use simpler/faster query to get
all public issues, if this exceeds the limit, we just return the limit.
If the amount of matching results is lower than limit, we re-run more
complex search query (which includes also confidential issues).
Re-running the complex query should be fast enough in such case because the
amount of matching issues is lower than limit.

Because exact total_count is now limited, this patch also switches to
to "prev/next" pagination.

Related #40540
2018-01-23 22:33:42 +01:00

164 lines
4.4 KiB
Ruby

module Gitlab
class SearchResults
class FoundBlob
attr_reader :id, :filename, :basename, :ref, :startline, :data
def initialize(opts = {})
@id = opts.fetch(:id, nil)
@filename = opts.fetch(:filename, nil)
@basename = opts.fetch(:basename, nil)
@ref = opts.fetch(:ref, nil)
@startline = opts.fetch(:startline, nil)
@data = opts.fetch(:data, nil)
end
def path
filename
end
def no_highlighting?
false
end
end
attr_reader :current_user, :query
# Limit search results by passed projects
# It allows us to search only for projects user has access to
attr_reader :limit_projects
# Whether a custom filter is used to restrict scope of projects.
# If the default filter (which lists all projects user has access to)
# is used, we can skip it when filtering merge requests and optimize the
# query
attr_reader :default_project_filter
def initialize(current_user, limit_projects, query, default_project_filter: false)
@current_user = current_user
@limit_projects = limit_projects || Project.all
@query = query
@default_project_filter = default_project_filter
end
def objects(scope, page = nil, without_count = true)
collection = case scope
when 'projects'
projects.page(page).per(per_page)
when 'issues'
issues.page(page).per(per_page)
when 'merge_requests'
merge_requests.page(page).per(per_page)
when 'milestones'
milestones.page(page).per(per_page)
else
Kaminari.paginate_array([]).page(page).per(per_page)
end
without_count ? collection.without_count : collection
end
def projects_count
@projects_count ||= projects.count
end
def issues_count
@issues_count ||= issues.count
end
def merge_requests_count
@merge_requests_count ||= merge_requests.count
end
def milestones_count
@milestones_count ||= milestones.count
end
def limited_projects_count
@limited_projects_count ||= projects.limit(count_limit).count
end
def limited_issues_count
return @limited_issues_count if @limited_issues_count
# By default getting limited count (e.g. 1000+) is fast on issuable
# collections except for issues, where filtering both not confidential
# and confidential issues user has access to, is too complex.
# It's faster to try to fetch all public issues first, then only
# if necessary try to fetch all issues.
sum = issues(public_only: true).limit(count_limit).count
@limited_issues_count = sum < count_limit ? issues.limit(count_limit).count : sum
end
def limited_merge_requests_count
@limited_merge_requests_count ||= merge_requests.limit(count_limit).count
end
def limited_milestones_count
@limited_milestones_count ||= milestones.limit(count_limit).count
end
def single_commit_result?
false
end
def count_limit
1001
end
private
def projects
limit_projects.search(query)
end
def issues(finder_params = {})
issues = IssuesFinder.new(current_user, finder_params).execute
unless default_project_filter
issues = issues.where(project_id: project_ids_relation)
end
issues =
if query =~ /#(\d+)\z/
issues.where(iid: $1)
else
issues.full_search(query)
end
issues.reorder('updated_at DESC')
end
def milestones
milestones = Milestone.where(project_id: project_ids_relation)
milestones = milestones.search(query)
milestones.reorder('updated_at DESC')
end
def merge_requests
merge_requests = MergeRequestsFinder.new(current_user).execute
unless default_project_filter
merge_requests = merge_requests.in_projects(project_ids_relation)
end
merge_requests =
if query =~ /[#!](\d+)\z/
merge_requests.where(iid: $1)
else
merge_requests.full_search(query)
end
merge_requests.reorder('updated_at DESC')
end
def default_scope
'projects'
end
def per_page
20
end
def project_ids_relation
limit_projects.select(:id).reorder(nil)
end
end
end