Commit 0355e90a authored by Mark Chao's avatar Mark Chao

ES: Decouple other ActiveRecord models

See https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/14428
parent 61a2d19f
...@@ -100,11 +100,7 @@ def instrument_classes(instrumentation) ...@@ -100,11 +100,7 @@ def instrument_classes(instrumentation)
instrumentation.instrument_instance_methods(Gitlab::Elastic::SnippetSearchResults) instrumentation.instrument_instance_methods(Gitlab::Elastic::SnippetSearchResults)
instrumentation.instrument_methods(Gitlab::Elastic::Helper) instrumentation.instrument_methods(Gitlab::Elastic::Helper)
instrumentation.instrument_instance_methods(Elastic::ApplicationSearch) instrumentation.instrument_instance_methods(Elastic::ApplicationVersionedSearch)
instrumentation.instrument_instance_methods(Elastic::IssuesSearch)
instrumentation.instrument_instance_methods(Elastic::MergeRequestsSearch)
instrumentation.instrument_instance_methods(Elastic::MilestonesSearch)
instrumentation.instrument_instance_methods(Elastic::NotesSearch)
instrumentation.instrument_instance_methods(Elastic::ProjectsSearch) instrumentation.instrument_instance_methods(Elastic::ProjectsSearch)
instrumentation.instrument_instance_methods(Elastic::RepositoriesSearch) instrumentation.instrument_instance_methods(Elastic::RepositoriesSearch)
instrumentation.instrument_instance_methods(Elastic::SnippetsSearch) instrumentation.instrument_instance_methods(Elastic::SnippetsSearch)
......
This diff is collapsed.
# frozen_string_literal: true
module Elastic
module IssuesSearch
extend ActiveSupport::Concern
included do
include ApplicationSearch
def as_indexed_json(options = {})
data = {}
# We don't use as_json(only: ...) because it calls all virtual and serialized attributtes
# https://gitlab.com/gitlab-org/gitlab-ee/issues/349
[:id, :iid, :title, :description, :created_at, :updated_at, :state, :project_id, :author_id, :confidential].each do |attr|
data[attr.to_s] = safely_read_attribute_for_elasticsearch(attr)
end
data['assignee_id'] = safely_read_attribute_for_elasticsearch(:assignee_ids)
data.merge(generic_attributes)
end
def self.nested?
true
end
def self.elastic_search(query, options: {})
query_hash =
if query =~ /#(\d+)\z/
iid_query_hash(Regexp.last_match(1))
else
basic_query_hash(%w(title^2 description), query)
end
options[:features] = 'issues'
query_hash = project_ids_filter(query_hash, options)
query_hash = confidentiality_filter(query_hash, options[:current_user])
self.__elasticsearch__.search(query_hash)
end
def self.confidentiality_filter(query_hash, current_user)
return query_hash if current_user && current_user.full_private_access?
filter = if current_user
{
bool: {
should: [
{ term: { confidential: false } },
{
bool: {
must: [
{ term: { confidential: true } },
{
bool: {
should: [
{ term: { author_id: current_user.id } },
{ term: { assignee_id: current_user.id } },
{ terms: { project_id: current_user.authorized_projects(Gitlab::Access::REPORTER).pluck(:id) } }
]
}
}
]
}
}
]
}
}
else
{ term: { confidential: false } }
end
query_hash[:query][:bool][:filter] << filter
query_hash
end
end
end
end
...@@ -4,13 +4,7 @@ module Elastic ...@@ -4,13 +4,7 @@ module Elastic
module ProjectsSearch module ProjectsSearch
extend ActiveSupport::Concern extend ActiveSupport::Concern
TRACKED_FEATURE_SETTINGS = %w( include ApplicationVersionedSearch
issues_access_level
merge_requests_access_level
snippets_access_level
wiki_access_level
repository_access_level
).freeze
INDEXED_ASSOCIATIONS = [ INDEXED_ASSOCIATIONS = [
:issues, :issues,
...@@ -21,97 +15,10 @@ module Elastic ...@@ -21,97 +15,10 @@ module Elastic
].freeze ].freeze
included do included do
include ApplicationSearch
def use_elasticsearch? def use_elasticsearch?
::Gitlab::CurrentSettings.elasticsearch_indexes_project?(self) ::Gitlab::CurrentSettings.elasticsearch_indexes_project?(self)
end end
def as_indexed_json(options = {})
# We don't use as_json(only: ...) because it calls all virtual and serialized attributtes
# https://gitlab.com/gitlab-org/gitlab-ee/issues/349
data = {}
[
:id,
:name,
:path,
:description,
:namespace_id,
:created_at,
:updated_at,
:archived,
:visibility_level,
:last_activity_at,
:name_with_namespace,
:path_with_namespace
].each do |attr|
data[attr.to_s] = safely_read_attribute_for_elasticsearch(attr)
end
# Set it as a parent in our `project => child` JOIN field
data['join_field'] = es_type
# ES6 is now single-type per index, so we implement our own typing
data['type'] = 'project'
TRACKED_FEATURE_SETTINGS.each do |feature|
data[feature] = project_feature.public_send(feature) # rubocop:disable GitlabSecurity/PublicSend
end
data
end
def self.elastic_search(query, options: {})
options[:in] = %w(name^10 name_with_namespace^2 path_with_namespace path^9 description)
query_hash = basic_query_hash(options[:in], query)
filters = []
if options[:namespace_id]
filters << {
terms: {
namespace_id: [options[:namespace_id]].flatten
}
}
end
if options[:non_archived]
filters << {
terms: {
archived: [!options[:non_archived]].flatten
}
}
end
if options[:visibility_levels]
filters << {
terms: {
visibility_level: [options[:visibility_levels]].flatten
}
}
end
if options[:project_ids]
filters << {
bool: project_ids_query(options[:current_user], options[:project_ids], options[:public_and_internal_projects])
}
end
query_hash[:query][:bool][:filter] = filters
query_hash[:sort] = [:_score]
self.__elasticsearch__.search(query_hash)
end
def self.indexed_association_classes
INDEXED_ASSOCIATIONS.map do |association_name|
reflect_on_association(association_name).klass
end
end
def each_indexed_association def each_indexed_association
INDEXED_ASSOCIATIONS.each do |association_name| INDEXED_ASSOCIATIONS.each do |association_name|
association = self.association(association_name) association = self.association(association_name)
......
...@@ -11,7 +11,7 @@ module EE ...@@ -11,7 +11,7 @@ module EE
WEIGHT_ANY = 'Any'.freeze WEIGHT_ANY = 'Any'.freeze
WEIGHT_NONE = 'None'.freeze WEIGHT_NONE = 'None'.freeze
include Elastic::IssuesSearch include Elastic::ApplicationVersionedSearch
scope :order_weight_desc, -> { reorder ::Gitlab::Database.nulls_last_order('weight', 'DESC') } scope :order_weight_desc, -> { reorder ::Gitlab::Database.nulls_last_order('weight', 'DESC') }
scope :order_weight_asc, -> { reorder ::Gitlab::Database.nulls_last_order('weight') } scope :order_weight_asc, -> { reorder ::Gitlab::Database.nulls_last_order('weight') }
......
...@@ -5,7 +5,7 @@ module EE ...@@ -5,7 +5,7 @@ module EE
extend ActiveSupport::Concern extend ActiveSupport::Concern
prepended do prepended do
include Elastic::NotesSearch include Elastic::ApplicationVersionedSearch
end end
end end
end end
...@@ -11,7 +11,7 @@ module EE ...@@ -11,7 +11,7 @@ module EE
include FromUnion include FromUnion
prepended do prepended do
include Elastic::MergeRequestsSearch include Elastic::ApplicationVersionedSearch
has_many :reviews, inverse_of: :merge_request has_many :reviews, inverse_of: :merge_request
has_many :approvals, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent has_many :approvals, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
......
...@@ -5,7 +5,7 @@ module EE ...@@ -5,7 +5,7 @@ module EE
extend ActiveSupport::Concern extend ActiveSupport::Concern
prepended do prepended do
include Elastic::MilestonesSearch include Elastic::ApplicationVersionedSearch
has_many :boards has_many :boards
end end
......
...@@ -7,7 +7,7 @@ module EE ...@@ -7,7 +7,7 @@ module EE
prepended do prepended do
include ::ObjectStorage::BackgroundMove include ::ObjectStorage::BackgroundMove
include Elastic::NotesSearch include Elastic::ApplicationVersionedSearch
belongs_to :review, inverse_of: :notes belongs_to :review, inverse_of: :notes
......
# frozen_string_literal: true
module Elastic
module Latest
class IssueClassProxy < ApplicationClassProxy
def nested?
true
end
def elastic_search(query, options: {})
query_hash =
if query =~ /#(\d+)\z/
iid_query_hash(Regexp.last_match(1))
else
basic_query_hash(%w(title^2 description), query)
end
options[:features] = 'issues'
query_hash = project_ids_filter(query_hash, options)
query_hash = confidentiality_filter(query_hash, options[:current_user])
search(query_hash)
end
private
def confidentiality_filter(query_hash, current_user)
return query_hash if current_user && current_user.full_private_access?
filter =
if current_user
{
bool: {
should: [
{ term: { confidential: false } },
{
bool: {
must: [
{ term: { confidential: true } },
{
bool: {
should: [
{ term: { author_id: current_user.id } },
{ term: { assignee_id: current_user.id } },
{ terms: { project_id: current_user.authorized_projects(Gitlab::Access::REPORTER).pluck_primary_key } }
]
}
}
]
}
}
]
}
}
else
{ term: { confidential: false } }
end
query_hash[:query][:bool][:filter] << filter
query_hash
end
end
end
end
# frozen_string_literal: true
module Elastic
module Latest
class IssueInstanceProxy < ApplicationInstanceProxy
def as_indexed_json(options = {})
data = {}
# We don't use as_json(only: ...) because it calls all virtual and serialized attributtes
# https://gitlab.com/gitlab-org/gitlab-ee/issues/349
[:id, :iid, :title, :description, :created_at, :updated_at, :state, :project_id, :author_id, :confidential].each do |attr|
data[attr.to_s] = safely_read_attribute_for_elasticsearch(attr)
end
data['assignee_id'] = safely_read_attribute_for_elasticsearch(:assignee_ids)
data.merge(generic_attributes)
end
end
end
end
# frozen_string_literal: true
module Elastic
module Latest
class MergeRequestClassProxy < ApplicationClassProxy
def nested?
true
end
def elastic_search(query, options: {})
query_hash =
if query =~ /\!(\d+)\z/
iid_query_hash(Regexp.last_match(1))
else
basic_query_hash(%w(title^2 description), query)
end
options[:features] = 'merge_requests'
query_hash = project_ids_filter(query_hash, options)
search(query_hash)
end
end
end
end
# frozen_string_literal: true # frozen_string_literal: true
module Elastic module Elastic
module MergeRequestsSearch module Latest
extend ActiveSupport::Concern class MergeRequestInstanceProxy < ApplicationInstanceProxy
included do
include ApplicationSearch
def as_indexed_json(options = {}) def as_indexed_json(options = {})
# We don't use as_json(only: ...) because it calls all virtual and serialized attributtes # We don't use as_json(only: ...) because it calls all virtual and serialized attributtes
# https://gitlab.com/gitlab-org/gitlab-ee/issues/349 # https://gitlab.com/gitlab-org/gitlab-ee/issues/349
...@@ -32,28 +28,6 @@ module Elastic ...@@ -32,28 +28,6 @@ module Elastic
data.merge(generic_attributes) data.merge(generic_attributes)
end end
def es_parent
"project_#{target_project_id}"
end
def self.nested?
true
end
def self.elastic_search(query, options: {})
query_hash =
if query =~ /\!(\d+)\z/
iid_query_hash(Regexp.last_match(1))
else
basic_query_hash(%w(title^2 description), query)
end
options[:features] = 'merge_requests'
query_hash = project_ids_filter(query_hash, options)
self.__elasticsearch__.search(query_hash)
end
end end
end end
end end
# frozen_string_literal: true
module Elastic
module Latest
class MilestoneClassProxy < ApplicationClassProxy
def nested?
true
end
def elastic_search(query, options: {})
options[:in] = %w(title^2 description)
query_hash = basic_query_hash(options[:in], query)
query_hash = project_ids_filter(query_hash, options)
search(query_hash)
end
end
end
end
# frozen_string_literal: true # frozen_string_literal: true
module Elastic module Elastic
module MilestonesSearch module Latest
extend ActiveSupport::Concern class MilestoneInstanceProxy < ApplicationInstanceProxy
included do
include ApplicationSearch
def as_indexed_json(options = {}) def as_indexed_json(options = {})
# We don't use as_json(only: ...) because it calls all virtual and serialized attributtes # We don't use as_json(only: ...) because it calls all virtual and serialized attributtes
# https://gitlab.com/gitlab-org/gitlab-ee/issues/349 # https://gitlab.com/gitlab-org/gitlab-ee/issues/349
...@@ -18,20 +14,6 @@ module Elastic ...@@ -18,20 +14,6 @@ module Elastic
data.merge(generic_attributes) data.merge(generic_attributes)
end end
def self.nested?
true
end
def self.elastic_search(query, options: {})
options[:in] = %w(title^2 description)
query_hash = basic_query_hash(options[:in], query)
query_hash = project_ids_filter(query_hash, options)
self.__elasticsearch__.search(query_hash)
end
end end
end end
end end
# frozen_string_literal: true # frozen_string_literal: true
module Elastic module Elastic
module NotesSearch module Latest
extend ActiveSupport::Concern class NoteClassProxy < ApplicationClassProxy
included do
include ApplicationSearch
def es_type def es_type
'note' 'note'
end end
def as_indexed_json(options = {}) def nested?
data = {}
# We don't use as_json(only: ...) because it calls all virtual and serialized attributtes
# https://gitlab.com/gitlab-org/gitlab-ee/issues/349
[:id, :note, :project_id, :noteable_type, :noteable_id, :created_at, :updated_at].each do |attr|
data[attr.to_s] = safely_read_attribute_for_elasticsearch(attr)
end
if noteable.is_a?(Issue)
data['issue'] = {
assignee_id: noteable.assignee_ids,
author_id: noteable.author_id,
confidential: noteable.confidential
}
end
data.merge(generic_attributes)
end
def self.nested?
true true
end end
def self.elastic_search(query, options: {}) def elastic_search(query, options: {})
options[:in] = ['note'] options[:in] = ['note']
query_hash = basic_query_hash(%w[note], query) query_hash = basic_query_hash(%w[note], query)
...@@ -49,11 +25,13 @@ module Elastic ...@@ -49,11 +25,13 @@ module Elastic
query_hash[:highlight] = highlight_options(options[:in]) query_hash[:highlight] = highlight_options(options[:in])
self.__elasticsearch__.search(query_hash) search(query_hash)
end end
def self.confidentiality_filter(query_hash, current_user) private
return query_hash if current_user && current_user.full_private_access?
def confidentiality_filter(query_hash, current_user)
return query_hash if current_user&.full_private_access?
filter = { filter = {
bool: { bool: {
...@@ -74,7 +52,7 @@ module Elastic ...@@ -74,7 +52,7 @@ module Elastic
should: [ should: [
{ term: { "issue.author_id" => current_user.id } }, { term: { "issue.author_id" => current_user.id } },
{ term: { "issue.assignee_id" => current_user.id } }, { term: { "issue.assignee_id" => current_user.id } },
{ terms: { "project_id" => current_user.authorized_projects(Gitlab::Access::REPORTER).pluck(:id) } } { terms: { "project_id" => current_user.authorized_projects(Gitlab::Access::REPORTER).pluck_primary_key } }
] ]
} }
} }
......
# frozen_string_literal: true
module Elastic
module Latest
class NoteInstanceProxy < ApplicationInstanceProxy
delegate :noteable, to: :target
def as_indexed_json(options = {})
data = {}
# We don't use as_json(only: ...) because it calls all virtual and serialized attributtes
# https://gitlab.com/gitlab-org/gitlab-ee/issues/349
[:id, :note, :project_id, :noteable_type, :noteable_id, :created_at, :updated_at].each do |attr|
data[attr.to_s] = safely_read_attribute_for_elasticsearch(attr)
end
if noteable.is_a?(Issue)
data['issue'] = {
assignee_id: noteable.assignee_ids,
author_id: noteable.author_id,
confidential: noteable.confidential
}
end
data.merge(generic_attributes)
end
end
end
end
# frozen_string_literal: true
module Elastic
module Latest
class ProjectClassProxy < ApplicationClassProxy
def elastic_search(query, options: {})
options[:in] = %w(name^10 name_with_namespace^2 path_with_namespace path^9 description)
query_hash = basic_query_hash(options[:in], query)
filters = []
if options[:namespace_id]
filters << {
terms: {
namespace_id: [options[:namespace_id]].flatten
}
}
end
if options[:non_archived]
filters << {
terms: {
archived: [!options[:non_archived]].flatten
}
}
end
if options[:visibility_levels]
filters << {
terms: {
visibility_level: [options[:visibility_levels]].flatten
}
}
end
if options[:project_ids]
filters << {
bool: project_ids_query(options[:current_user], options[:project_ids], options[:public_and_internal_projects])
}
end
query_hash[:query][:bool][:filter] = filters
query_hash[:sort] = [:_score]
search(query_hash)
end
end
end
end
# frozen_string_literal: true
module Elastic
module Latest
class ProjectInstanceProxy < ApplicationInstanceProxy
TRACKED_FEATURE_SETTINGS = %w(
issues_access_level
merge_requests_access_level
snippets_access_level
wiki_access_level
repository_access_level
).freeze
def as_indexed_json(options = {})
# We don't use as_json(only: ...) because it calls all virtual and serialized attributtes
# https://gitlab.com/gitlab-org/gitlab-ee/issues/349
data = {}
[
:id,
:name,
:path,
:description,
:namespace_id,
:created_at,
:updated_at,
:archived,
:visibility_level,
:last_activity_at,
:name_with_namespace,
:path_with_namespace
].each do |attr|
data[attr.to_s] = safely_read_attribute_for_elasticsearch(attr)
end
# Set it as a parent in our `project => child` JOIN field
data['join_field'] = es_type
# ES6 is now single-type per index, so we implement our own typing
data['type'] = 'project'
TRACKED_FEATURE_SETTINGS.each do |feature|
data[feature] = target.project_feature.public_send(feature) # rubocop:disable GitlabSecurity/PublicSend
end
data
end
end
end
end
# frozen_string_literal: true
module Elastic
module V12p1
IssueClassProxy = Elastic::Latest::IssueClassProxy
end
end
# frozen_string_literal: true
module Elastic
module V12p1
IssueInstanceProxy = Elastic::Latest::IssueInstanceProxy
end
end
# frozen_string_literal: true
module Elastic
module V12p1
MergeRequestClassProxy = Elastic::Latest::MergeRequestClassProxy
end
end
# frozen_string_literal: true
module Elastic
module V12p1
MergeRequestInstanceProxy = Elastic::Latest::MergeRequestInstanceProxy
end
end
# frozen_string_literal: true
module Elastic
module V12p1
MilestoneClassProxy = Elastic::Latest::MilestoneClassProxy
end
end
# frozen_string_literal: true
module Elastic
module V12p1
MilestoneInstanceProxy = Elastic::Latest::MilestoneInstanceProxy
end
end
# frozen_string_literal: true
module Elastic
module V12p1
NoteClassProxy = Elastic::Latest::NoteClassProxy
end
end
# frozen_string_literal: true
module Elastic
module V12p1
NoteInstanceProxy = Elastic::Latest::NoteInstanceProxy
end
end
# frozen_string_literal: true
module Elastic
module V12p1
ProjectClassProxy = Elastic::Latest::ProjectClassProxy
end
end
# frozen_string_literal: true
module Elastic
module V12p1
ProjectInstanceProxy = Elastic::Latest::ProjectInstanceProxy
end
end
...@@ -199,7 +199,7 @@ module Gitlab ...@@ -199,7 +199,7 @@ module Gitlab
def milestones def milestones
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 ApplicationClassProxy#project_ids_query
# Otherwise it will ignore project_ids and return milestones # Otherwise it will ignore project_ids and return milestones
# from projects with milestones disabled. # from projects with milestones disabled.
options = base_options options = base_options
......
...@@ -89,7 +89,7 @@ describe Issue, :elastic do ...@@ -89,7 +89,7 @@ describe Issue, :elastic do
expected_hash['assignee_id'] = [assignee.id] expected_hash['assignee_id'] = [assignee.id]
expect(issue.as_indexed_json).to eq(expected_hash) expect(issue.__elasticsearch__.as_indexed_json).to eq(expected_hash)
end end
it_behaves_like 'no results when the user cannot read cross project' do it_behaves_like 'no results when the user cannot read cross project' do
......
...@@ -61,7 +61,7 @@ describe MergeRequest, :elastic do ...@@ -61,7 +61,7 @@ describe MergeRequest, :elastic do
'type' => merge_request.es_type 'type' => merge_request.es_type
}) })
expect(merge_request.as_indexed_json).to eq(expected_hash) expect(merge_request.__elasticsearch__.as_indexed_json).to eq(expected_hash)
end end
it_behaves_like 'no results when the user cannot read cross project' do it_behaves_like 'no results when the user cannot read cross project' do
......
...@@ -53,7 +53,7 @@ describe Milestone, :elastic do ...@@ -53,7 +53,7 @@ describe Milestone, :elastic do
'type' => milestone.es_type 'type' => milestone.es_type
}) })
expect(milestone.as_indexed_json).to eq(expected_hash) expect(milestone.__elasticsearch__.as_indexed_json).to eq(expected_hash)
end end
it_behaves_like 'no results when the user cannot read cross project' do it_behaves_like 'no results when the user cannot read cross project' do
......
...@@ -88,7 +88,7 @@ describe Note, :elastic do ...@@ -88,7 +88,7 @@ describe Note, :elastic do
type type
) )
expect(note.as_indexed_json.keys).to eq(expected_hash_keys) expect(note.__elasticsearch__.as_indexed_json.keys).to eq(expected_hash_keys)
end end
it "does not create ElasticIndexerWorker job for system messages" do it "does not create ElasticIndexerWorker job for system messages" do
...@@ -105,7 +105,7 @@ describe Note, :elastic do ...@@ -105,7 +105,7 @@ describe Note, :elastic do
Note.subclasses.each do |note_class| Note.subclasses.each do |note_class|
expect(note_class.index_name).to eq(Note.index_name) expect(note_class.index_name).to eq(Note.index_name)
expect(note_class.document_type).to eq(Note.document_type) expect(note_class.document_type).to eq(Note.document_type)
expect(note_class.mappings.to_hash).to eq(Note.mappings.to_hash) expect(note_class.__elasticsearch__.mappings.to_hash).to eq(Note.__elasticsearch__.mappings.to_hash)
end end
end end
......
...@@ -159,6 +159,6 @@ describe Project, :elastic do ...@@ -159,6 +159,6 @@ describe Project, :elastic do
expected_hash['name_with_namespace'] = project.full_name expected_hash['name_with_namespace'] = project.full_name
expected_hash['path_with_namespace'] = project.full_path expected_hash['path_with_namespace'] = project.full_path
expect(project.as_indexed_json).to eq(expected_hash) expect(project.__elasticsearch__.as_indexed_json).to eq(expected_hash)
end end
end end
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