/lib/gitlab/search_results.rb
Ruby | 258 lines | 177 code | 50 blank | 31 comment | 11 complexity | c37494218a1e96da92daf0a2c6c1dfc1 MD5 | raw file
- # frozen_string_literal: true
- module Gitlab
- class SearchResults
- COUNT_LIMIT = 100
- COUNT_LIMIT_MESSAGE = "#{COUNT_LIMIT - 1}+"
- DEFAULT_PAGE = 1
- DEFAULT_PER_PAGE = 20
- SCOPE_ONLY_SORT = {
- popularity_asc: %w[issues],
- popularity_desc: %w[issues]
- }.freeze
- attr_reader :current_user, :query, :order_by, :sort, :filters
- # 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, query, limit_projects = nil, order_by: nil, sort: nil, default_project_filter: false, filters: {})
- @current_user = current_user
- @query = query
- @limit_projects = limit_projects || Project.all
- @default_project_filter = default_project_filter
- @order_by = order_by
- @sort = sort
- @filters = filters
- end
- def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE, without_count: true, preload_method: nil)
- should_preload = preload_method.present?
- collection = collection_for(scope)
- if collection.nil?
- should_preload = false
- collection = Kaminari.paginate_array([])
- end
- collection = collection.public_send(preload_method) if should_preload # rubocop:disable GitlabSecurity/PublicSend
- collection = collection.page(page).per(per_page)
- without_count ? collection.without_count : collection
- end
- def formatted_count(scope)
- case scope
- when 'projects'
- formatted_limited_count(limited_projects_count)
- when 'issues'
- formatted_limited_count(limited_issues_count)
- when 'merge_requests'
- formatted_limited_count(limited_merge_requests_count)
- when 'milestones'
- formatted_limited_count(limited_milestones_count)
- when 'users'
- formatted_limited_count(limited_users_count)
- end
- end
- def formatted_limited_count(count)
- if count >= COUNT_LIMIT
- COUNT_LIMIT_MESSAGE
- else
- count.to_s
- end
- end
- def limited_projects_count
- @limited_projects_count ||= limited_count(projects)
- 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 = limited_count(issues(public_only: true))
- @limited_issues_count = sum < count_limit ? limited_count(issues) : sum
- end
- def limited_merge_requests_count
- @limited_merge_requests_count ||= limited_count(merge_requests)
- end
- def limited_milestones_count
- @limited_milestones_count ||= limited_count(milestones)
- end
- def limited_users_count
- @limited_users_count ||= limited_count(users)
- end
- def count_limit
- COUNT_LIMIT
- end
- def users
- return User.none unless Ability.allowed?(current_user, :read_users_list)
- UsersFinder.new(current_user, search: query).execute
- end
- # highlighting is only performed by Elasticsearch backed results
- def highlight_map(scope)
- {}
- end
- # aggregations are only performed by Elasticsearch backed results
- def aggregations(scope)
- []
- end
- private
- def collection_for(scope)
- case scope
- when 'projects'
- projects
- when 'issues'
- issues
- when 'merge_requests'
- merge_requests
- when 'milestones'
- milestones
- when 'users'
- users
- end
- end
- # rubocop: disable CodeReuse/ActiveRecord
- def apply_sort(results, scope: nil)
- # Due to different uses of sort param we prefer order_by when
- # present
- sort_by = ::Gitlab::Search::SortOptions.sort_and_direction(order_by, sort)
- # Reset sort to default if the chosen one is not supported by scope
- sort_by = nil if SCOPE_ONLY_SORT[sort_by] && !SCOPE_ONLY_SORT[sort_by].include?(scope)
- case sort_by
- when :created_at_asc
- results.reorder('created_at ASC')
- when :created_at_desc
- results.reorder('created_at DESC')
- when :updated_at_asc
- results.reorder('updated_at ASC')
- when :updated_at_desc
- results.reorder('updated_at DESC')
- when :popularity_asc
- results.reorder('upvotes_count ASC')
- when :popularity_desc
- results.reorder('upvotes_count DESC')
- else
- results.reorder('created_at DESC')
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
- def projects
- limit_projects.search(query)
- end
- def issues(finder_params = {})
- issues = IssuesFinder.new(current_user, issuable_params.merge(finder_params)).execute
- unless default_project_filter
- issues = issues.in_projects(project_ids_relation)
- end
- apply_sort(issues, scope: 'issues')
- end
- # rubocop: disable CodeReuse/ActiveRecord
- def milestones
- milestones = Milestone.search(query)
- milestones = filter_milestones_by_project(milestones)
- milestones.reorder('updated_at DESC')
- end
- # rubocop: enable CodeReuse/ActiveRecord
- def merge_requests
- merge_requests = MergeRequestsFinder.new(current_user, issuable_params).execute
- unless default_project_filter
- merge_requests = merge_requests.of_projects(project_ids_relation)
- end
- apply_sort(merge_requests, scope: 'merge_requests')
- end
- def default_scope
- 'projects'
- end
- # Filter milestones by authorized projects.
- # For performance reasons project_id is being plucked
- # to be used on a smaller query.
- #
- # rubocop: disable CodeReuse/ActiveRecord
- def filter_milestones_by_project(milestones)
- project_ids =
- milestones.where(project_id: project_ids_relation)
- .select(:project_id).distinct
- .pluck(:project_id)
- return Milestone.none if project_ids.nil?
- authorized_project_ids_relation =
- Project.where(id: project_ids).ids_with_issuables_available_for(current_user)
- milestones.where(project_id: authorized_project_ids_relation)
- end
- # rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
- def project_ids_relation
- limit_projects.select(:id).reorder(nil)
- end
- # rubocop: enable CodeReuse/ActiveRecord
- def issuable_params
- {}.tap do |params|
- params[:sort] = 'updated_desc'
- if query =~ /#(\d+)\z/
- params[:iids] = Regexp.last_match(1)
- else
- params[:search] = query
- end
- params[:state] = filters[:state] if filters.key?(:state)
- if [true, false].include?(filters[:confidential])
- params[:confidential] = filters[:confidential]
- end
- end
- end
- # rubocop: disable CodeReuse/ActiveRecord
- def limited_count(relation)
- relation.reorder(nil).limit(count_limit).size
- end
- # rubocop: enable CodeReuse/ActiveRecord
- end
- end
- Gitlab::SearchResults.prepend_mod_with('Gitlab::SearchResults')