diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-02-24 06:13:09 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-02-24 06:13:09 +0000 |
commit | 89bfc148f90c410512f9c470ca1e50485b7000b2 (patch) | |
tree | 9ff12f0c281483cd24585ae9fc5acc4be6a90ac9 /app | |
parent | df6d6623faac958bea9787a1cda4259cbcdc1287 (diff) | |
download | gitlab-ce-89bfc148f90c410512f9c470ca1e50485b7000b2.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r-- | app/assets/javascripts/notes/components/discussion_filter.vue | 2 | ||||
-rw-r--r-- | app/controllers/concerns/issuable_collections.rb | 4 | ||||
-rw-r--r-- | app/finders/issuable_finder.rb | 8 | ||||
-rw-r--r-- | app/graphql/resolvers/work_item_resolver.rb | 2 | ||||
-rw-r--r-- | app/graphql/types/work_item_type.rb | 2 | ||||
-rw-r--r-- | app/models/concerns/pg_full_text_searchable.rb | 111 | ||||
-rw-r--r-- | app/models/issue.rb | 14 | ||||
-rw-r--r-- | app/models/issues/search_data.rb | 11 | ||||
-rw-r--r-- | app/models/note.rb | 1 | ||||
-rw-r--r-- | app/policies/project_policy.rb | 2 | ||||
-rw-r--r-- | app/policies/work_item_policy.rb | 11 |
11 files changed, 155 insertions, 13 deletions
diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue index 102afaf308f..d5a7fc36ace 100644 --- a/app/assets/javascripts/notes/components/discussion_filter.vue +++ b/app/assets/javascripts/notes/components/discussion_filter.vue @@ -116,7 +116,7 @@ export default { <gl-dropdown v-if="displayFilters" id="discussion-filter-dropdown" - class="gl-mr-3 full-width-mobile discussion-filter-container js-discussion-filter-container" + class="full-width-mobile discussion-filter-container js-discussion-filter-container" data-qa-selector="discussion_filter_dropdown" :text="currentFilter.title" :disabled="isLoading" diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index 4841225de08..63ff1390d92 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -117,6 +117,10 @@ module IssuableCollections options[:attempt_group_search_optimizations] = true end + if collection_type == 'Issue' && Feature.enabled?(:issues_full_text_search, @project || @group, default_enabled: :yaml) + options[:attempt_full_text_search] = true + end + params.permit(finder_type.valid_params).merge(options) end end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 3e436f30971..416db1c9c7d 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -37,6 +37,7 @@ # attempt_project_search_optimizations: boolean # crm_contact_id: integer # crm_organization_id: integer +# attempt_full_text_search: boolean # class IssuableFinder prepend FinderWithCrossProjectAccess @@ -46,6 +47,7 @@ class IssuableFinder requires_cross_project_access unless: -> { params.project? } + FULL_TEXT_SEARCH_TERM_REGEX = /\A[\p{ASCII}|\p{Latin}]+\z/.freeze NEGATABLE_PARAMS_HELPER_KEYS = %i[project_id scope status include_subgroups].freeze attr_accessor :current_user, :params @@ -331,6 +333,8 @@ class IssuableFinder return items if items.is_a?(ActiveRecord::NullRelation) return items if Feature.enabled?(:disable_anonymous_search, type: :ops) && current_user.nil? + return items.pg_full_text_search(search) if use_full_text_search? + if use_cte_for_search? cte = Gitlab::SQL::CTE.new(klass.table_name, items) @@ -341,6 +345,10 @@ class IssuableFinder end # rubocop: enable CodeReuse/ActiveRecord + def use_full_text_search? + params[:attempt_full_text_search] && params[:search] =~ FULL_TEXT_SEARCH_TERM_REGEX + end + # rubocop: disable CodeReuse/ActiveRecord def by_iids(items) params[:iids].present? ? items.where(iid: params[:iids]) : items diff --git a/app/graphql/resolvers/work_item_resolver.rb b/app/graphql/resolvers/work_item_resolver.rb index b1733d657e4..6eb23916d83 100644 --- a/app/graphql/resolvers/work_item_resolver.rb +++ b/app/graphql/resolvers/work_item_resolver.rb @@ -4,7 +4,7 @@ module Resolvers class WorkItemResolver < BaseResolver include Gitlab::Graphql::Authorize::AuthorizeResource - authorize :read_issue + authorize :read_work_item type Types::WorkItemType, null: true diff --git a/app/graphql/types/work_item_type.rb b/app/graphql/types/work_item_type.rb index 78cef150b11..512b9ef64d2 100644 --- a/app/graphql/types/work_item_type.rb +++ b/app/graphql/types/work_item_type.rb @@ -4,7 +4,7 @@ module Types class WorkItemType < BaseObject graphql_name 'WorkItem' - authorize :read_issue + authorize :read_work_item field :description, GraphQL::Types::String, null: true, description: 'Description of the work item.' diff --git a/app/models/concerns/pg_full_text_searchable.rb b/app/models/concerns/pg_full_text_searchable.rb new file mode 100644 index 00000000000..612a9c5908b --- /dev/null +++ b/app/models/concerns/pg_full_text_searchable.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +# This module adds PG full-text search capabilities to a model. +# A `search_data` association with a `search_vector` column is required. +# +# Declare the fields that will be part of the search vector with their +# corresponding weights. Possible values for weight are A, B, C, or D. +# For example: +# +# include PgFullTextSearchable +# pg_full_text_searchable columns: [{ name: 'title', weight: 'A' }, { name: 'description', weight: 'B' }] +# +# This module sets up an after_commit hook that updates the search data +# when the searchable columns are changed. You will need to implement the +# `#persist_pg_full_text_search_vector` method that does the actual insert or update. +# +# This also adds a `pg_full_text_search` scope so you can do: +# +# Model.pg_full_text_search("some search term") + +module PgFullTextSearchable + extend ActiveSupport::Concern + + LONG_WORDS_REGEX = %r([A-Za-z0-9+/]{50,}).freeze + TSVECTOR_MAX_LENGTH = 1.megabyte.freeze + TEXT_SEARCH_DICTIONARY = 'english' + + def update_search_data! + tsvector_sql_nodes = self.class.pg_full_text_searchable_columns.map do |column, weight| + tsvector_arel_node(column, weight)&.to_sql + end + + persist_pg_full_text_search_vector(Arel.sql(tsvector_sql_nodes.compact.join(' || '))) + rescue ActiveRecord::StatementInvalid => e + raise unless e.cause.is_a?(PG::ProgramLimitExceeded) && e.message.include?('string is too long for tsvector') + + Gitlab::AppJsonLogger.error( + message: 'Error updating search data: string is too long for tsvector', + class: self.class.name, + model_id: self.id + ) + end + + private + + def persist_pg_full_text_search_vector(search_vector) + raise NotImplementedError + end + + def tsvector_arel_node(column, weight) + return if self[column].blank? + + column_text = self[column].gsub(LONG_WORDS_REGEX, ' ') + column_text = column_text[0..(TSVECTOR_MAX_LENGTH - 1)] + column_text = ActiveSupport::Inflector.transliterate(column_text) + + Arel::Nodes::NamedFunction.new( + 'setweight', + [ + Arel::Nodes::NamedFunction.new( + 'to_tsvector', + [Arel::Nodes.build_quoted(TEXT_SEARCH_DICTIONARY), Arel::Nodes.build_quoted(column_text)] + ), + Arel::Nodes.build_quoted(weight) + ] + ) + end + + included do + cattr_reader :pg_full_text_searchable_columns do + {} + end + end + + class_methods do + def pg_full_text_searchable(columns:) + raise 'Full text search columns already defined!' if pg_full_text_searchable_columns.present? + + columns.each do |column| + pg_full_text_searchable_columns[column[:name]] = column[:weight] + end + + # We update this outside the transaction because this could raise an error if the resulting tsvector + # is too long. When that happens, we still persist the create / update but the model will not have a + # search data record. This is fine in most cases because this is a very rare occurrence and only happens + # with strings that are most likely unsearchable anyway. + # + # We also do not want to use a subtransaction here due to: https://gitlab.com/groups/gitlab-org/-/epics/6540 + after_save_commit do + next unless pg_full_text_searchable_columns.keys.any? { |f| saved_changes.has_key?(f) } + + update_search_data! + end + end + + def pg_full_text_search(search_term) + search_data_table = reflect_on_association(:search_data).klass.arel_table + + joins(:search_data).where( + Arel::Nodes::InfixOperation.new( + '@@', + search_data_table[:search_vector], + Arel::Nodes::NamedFunction.new( + 'websearch_to_tsquery', + [Arel::Nodes.build_quoted(TEXT_SEARCH_DICTIONARY), Arel::Nodes.build_quoted(search_term)] + ) + ) + ) + end + end +end diff --git a/app/models/issue.rb b/app/models/issue.rb index 68ea6cb3abc..75727fff2cd 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -24,6 +24,7 @@ class Issue < ApplicationRecord include Todoable include FromUnion include EachBatch + include PgFullTextSearchable extend ::Gitlab::Utils::Override @@ -77,6 +78,7 @@ class Issue < ApplicationRecord end end + has_one :search_data, class_name: 'Issues::SearchData' has_one :issuable_severity has_one :sentry_issue has_one :alert_management_alert, class_name: 'AlertManagement::Alert' @@ -102,6 +104,8 @@ class Issue < ApplicationRecord alias_attribute :external_author, :service_desk_reply_to + pg_full_text_searchable columns: [{ name: 'title', weight: 'A' }, { name: 'description', weight: 'B' }] + scope :in_projects, ->(project_ids) { where(project_id: project_ids) } scope :not_in_projects, ->(project_ids) { where.not(project_id: project_ids) } @@ -233,6 +237,11 @@ class Issue < ApplicationRecord def order_upvotes_asc reorder(upvotes_count: :asc) end + + override :pg_full_text_search + def pg_full_text_search(search_term) + super.where('issue_search_data.project_id = issues.project_id') + end end def next_object_by_relative_position(ignoring: nil, order: :asc) @@ -611,6 +620,11 @@ class Issue < ApplicationRecord private + override :persist_pg_full_text_search_vector + def persist_pg_full_text_search_vector(search_vector) + Issues::SearchData.upsert({ project_id: project_id, issue_id: id, search_vector: search_vector }, unique_by: %i(project_id issue_id)) + end + def spammable_attribute_changed? title_changed? || description_changed? || diff --git a/app/models/issues/search_data.rb b/app/models/issues/search_data.rb new file mode 100644 index 00000000000..0eda292796d --- /dev/null +++ b/app/models/issues/search_data.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Issues + class SearchData < ApplicationRecord + extend SuppressCompositePrimaryKeyWarning + + self.table_name = 'issue_search_data' + + belongs_to :issue + end +end diff --git a/app/models/note.rb b/app/models/note.rb index 3f3fa968393..1d661367599 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -609,7 +609,6 @@ class Note < ApplicationRecord def show_outdated_changes? return false unless for_merge_request? - return false unless Feature.enabled?(:display_outdated_line_diff, noteable.source_project, default_enabled: :yaml) return false unless system? return false unless change_position&.line_range diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 4cc5ed06d61..147ca9c9881 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -264,8 +264,6 @@ class ProjectPolicy < BasePolicy enable :create_work_item end - rule { can?(:update_issue) }.enable :update_work_item - # These abilities are not allowed to admins that are not members of the project, # that's why they are defined separately. rule { guest & can?(:download_code) }.enable :build_download_code diff --git a/app/policies/work_item_policy.rb b/app/policies/work_item_policy.rb index 7ba5102a406..b4723bc7ed8 100644 --- a/app/policies/work_item_policy.rb +++ b/app/policies/work_item_policy.rb @@ -1,12 +1,9 @@ # frozen_string_literal: true -class WorkItemPolicy < BasePolicy - delegate { @subject.project } +class WorkItemPolicy < IssuePolicy + rule { can?(:owner_access) | is_author }.enable :delete_work_item - desc 'User is author of the work item' - condition(:author) do - @user && @user == @subject.author - end + rule { can?(:update_issue) }.enable :update_work_item - rule { can?(:owner_access) | author }.enable :delete_work_item + rule { can?(:read_issue) }.enable :read_work_item end |