PageRenderTime 25ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/gitlab/search_results.rb

https://gitlab.com/realsatomic/gitlab
Ruby | 258 lines | 177 code | 50 blank | 31 comment | 11 complexity | c37494218a1e96da92daf0a2c6c1dfc1 MD5 | raw file
  1. # frozen_string_literal: true
  2. module Gitlab
  3. class SearchResults
  4. COUNT_LIMIT = 100
  5. COUNT_LIMIT_MESSAGE = "#{COUNT_LIMIT - 1}+"
  6. DEFAULT_PAGE = 1
  7. DEFAULT_PER_PAGE = 20
  8. SCOPE_ONLY_SORT = {
  9. popularity_asc: %w[issues],
  10. popularity_desc: %w[issues]
  11. }.freeze
  12. attr_reader :current_user, :query, :order_by, :sort, :filters
  13. # Limit search results by passed projects
  14. # It allows us to search only for projects user has access to
  15. attr_reader :limit_projects
  16. # Whether a custom filter is used to restrict scope of projects.
  17. # If the default filter (which lists all projects user has access to)
  18. # is used, we can skip it when filtering merge requests and optimize the
  19. # query
  20. attr_reader :default_project_filter
  21. def initialize(current_user, query, limit_projects = nil, order_by: nil, sort: nil, default_project_filter: false, filters: {})
  22. @current_user = current_user
  23. @query = query
  24. @limit_projects = limit_projects || Project.all
  25. @default_project_filter = default_project_filter
  26. @order_by = order_by
  27. @sort = sort
  28. @filters = filters
  29. end
  30. def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE, without_count: true, preload_method: nil)
  31. should_preload = preload_method.present?
  32. collection = collection_for(scope)
  33. if collection.nil?
  34. should_preload = false
  35. collection = Kaminari.paginate_array([])
  36. end
  37. collection = collection.public_send(preload_method) if should_preload # rubocop:disable GitlabSecurity/PublicSend
  38. collection = collection.page(page).per(per_page)
  39. without_count ? collection.without_count : collection
  40. end
  41. def formatted_count(scope)
  42. case scope
  43. when 'projects'
  44. formatted_limited_count(limited_projects_count)
  45. when 'issues'
  46. formatted_limited_count(limited_issues_count)
  47. when 'merge_requests'
  48. formatted_limited_count(limited_merge_requests_count)
  49. when 'milestones'
  50. formatted_limited_count(limited_milestones_count)
  51. when 'users'
  52. formatted_limited_count(limited_users_count)
  53. end
  54. end
  55. def formatted_limited_count(count)
  56. if count >= COUNT_LIMIT
  57. COUNT_LIMIT_MESSAGE
  58. else
  59. count.to_s
  60. end
  61. end
  62. def limited_projects_count
  63. @limited_projects_count ||= limited_count(projects)
  64. end
  65. def limited_issues_count
  66. return @limited_issues_count if @limited_issues_count
  67. # By default getting limited count (e.g. 1000+) is fast on issuable
  68. # collections except for issues, where filtering both not confidential
  69. # and confidential issues user has access to, is too complex.
  70. # It's faster to try to fetch all public issues first, then only
  71. # if necessary try to fetch all issues.
  72. sum = limited_count(issues(public_only: true))
  73. @limited_issues_count = sum < count_limit ? limited_count(issues) : sum
  74. end
  75. def limited_merge_requests_count
  76. @limited_merge_requests_count ||= limited_count(merge_requests)
  77. end
  78. def limited_milestones_count
  79. @limited_milestones_count ||= limited_count(milestones)
  80. end
  81. def limited_users_count
  82. @limited_users_count ||= limited_count(users)
  83. end
  84. def count_limit
  85. COUNT_LIMIT
  86. end
  87. def users
  88. return User.none unless Ability.allowed?(current_user, :read_users_list)
  89. UsersFinder.new(current_user, search: query).execute
  90. end
  91. # highlighting is only performed by Elasticsearch backed results
  92. def highlight_map(scope)
  93. {}
  94. end
  95. # aggregations are only performed by Elasticsearch backed results
  96. def aggregations(scope)
  97. []
  98. end
  99. private
  100. def collection_for(scope)
  101. case scope
  102. when 'projects'
  103. projects
  104. when 'issues'
  105. issues
  106. when 'merge_requests'
  107. merge_requests
  108. when 'milestones'
  109. milestones
  110. when 'users'
  111. users
  112. end
  113. end
  114. # rubocop: disable CodeReuse/ActiveRecord
  115. def apply_sort(results, scope: nil)
  116. # Due to different uses of sort param we prefer order_by when
  117. # present
  118. sort_by = ::Gitlab::Search::SortOptions.sort_and_direction(order_by, sort)
  119. # Reset sort to default if the chosen one is not supported by scope
  120. sort_by = nil if SCOPE_ONLY_SORT[sort_by] && !SCOPE_ONLY_SORT[sort_by].include?(scope)
  121. case sort_by
  122. when :created_at_asc
  123. results.reorder('created_at ASC')
  124. when :created_at_desc
  125. results.reorder('created_at DESC')
  126. when :updated_at_asc
  127. results.reorder('updated_at ASC')
  128. when :updated_at_desc
  129. results.reorder('updated_at DESC')
  130. when :popularity_asc
  131. results.reorder('upvotes_count ASC')
  132. when :popularity_desc
  133. results.reorder('upvotes_count DESC')
  134. else
  135. results.reorder('created_at DESC')
  136. end
  137. end
  138. # rubocop: enable CodeReuse/ActiveRecord
  139. def projects
  140. limit_projects.search(query)
  141. end
  142. def issues(finder_params = {})
  143. issues = IssuesFinder.new(current_user, issuable_params.merge(finder_params)).execute
  144. unless default_project_filter
  145. issues = issues.in_projects(project_ids_relation)
  146. end
  147. apply_sort(issues, scope: 'issues')
  148. end
  149. # rubocop: disable CodeReuse/ActiveRecord
  150. def milestones
  151. milestones = Milestone.search(query)
  152. milestones = filter_milestones_by_project(milestones)
  153. milestones.reorder('updated_at DESC')
  154. end
  155. # rubocop: enable CodeReuse/ActiveRecord
  156. def merge_requests
  157. merge_requests = MergeRequestsFinder.new(current_user, issuable_params).execute
  158. unless default_project_filter
  159. merge_requests = merge_requests.of_projects(project_ids_relation)
  160. end
  161. apply_sort(merge_requests, scope: 'merge_requests')
  162. end
  163. def default_scope
  164. 'projects'
  165. end
  166. # Filter milestones by authorized projects.
  167. # For performance reasons project_id is being plucked
  168. # to be used on a smaller query.
  169. #
  170. # rubocop: disable CodeReuse/ActiveRecord
  171. def filter_milestones_by_project(milestones)
  172. project_ids =
  173. milestones.where(project_id: project_ids_relation)
  174. .select(:project_id).distinct
  175. .pluck(:project_id)
  176. return Milestone.none if project_ids.nil?
  177. authorized_project_ids_relation =
  178. Project.where(id: project_ids).ids_with_issuables_available_for(current_user)
  179. milestones.where(project_id: authorized_project_ids_relation)
  180. end
  181. # rubocop: enable CodeReuse/ActiveRecord
  182. # rubocop: disable CodeReuse/ActiveRecord
  183. def project_ids_relation
  184. limit_projects.select(:id).reorder(nil)
  185. end
  186. # rubocop: enable CodeReuse/ActiveRecord
  187. def issuable_params
  188. {}.tap do |params|
  189. params[:sort] = 'updated_desc'
  190. if query =~ /#(\d+)\z/
  191. params[:iids] = Regexp.last_match(1)
  192. else
  193. params[:search] = query
  194. end
  195. params[:state] = filters[:state] if filters.key?(:state)
  196. if [true, false].include?(filters[:confidential])
  197. params[:confidential] = filters[:confidential]
  198. end
  199. end
  200. end
  201. # rubocop: disable CodeReuse/ActiveRecord
  202. def limited_count(relation)
  203. relation.reorder(nil).limit(count_limit).size
  204. end
  205. # rubocop: enable CodeReuse/ActiveRecord
  206. end
  207. end
  208. Gitlab::SearchResults.prepend_mod_with('Gitlab::SearchResults')