Commit 12a98f61 authored by Micael Bergeron's avatar Micael Bergeron

Use local QueryContext query context instance

This commit changes the way the `QueryContext` (renamed from
`QueryFactory`) is used in the query building process.

With this commit, a `context` method is defined in the root
`ApplicationClassProxy` and used through the whole process.

This simplifies the logic quite a bit and removes the need for a thread
locality.
parent f23cd251
...@@ -5,6 +5,7 @@ module Elastic ...@@ -5,6 +5,7 @@ module Elastic
class ApplicationClassProxy < Elasticsearch::Model::Proxy::ClassMethodsProxy class ApplicationClassProxy < Elasticsearch::Model::Proxy::ClassMethodsProxy
include ClassProxyUtil include ClassProxyUtil
include Elastic::Latest::Routing include Elastic::Latest::Routing
include Elastic::Latest::QueryContext::Aware
def search(query, search_options = {}) def search(query, search_options = {})
es_options = routing_options(search_options) es_options = routing_options(search_options)
...@@ -55,7 +56,7 @@ module Elastic ...@@ -55,7 +56,7 @@ module Elastic
bool: { bool: {
must: [{ must: [{
simple_query_string: { simple_query_string: {
_name: QueryFactory.query_name(self.es_type, :match, :search_terms), _name: context.name(self.es_type, :match, :search_terms),
fields: fields, fields: fields,
query: query, query: query,
default_operator: default_operator default_operator: default_operator
...@@ -64,7 +65,7 @@ module Elastic ...@@ -64,7 +65,7 @@ module Elastic
filter: [{ filter: [{
term: { term: {
type: { type: {
_name: QueryFactory.query_name(:doc, :is_a, self.es_type), _name: context.name(:doc, :is_a, self.es_type),
value: self.es_type value: self.es_type
} }
} }
...@@ -93,8 +94,8 @@ module Elastic ...@@ -93,8 +94,8 @@ module Elastic
query: { query: {
bool: { bool: {
filter: [ filter: [
{ term: { iid: { _name: QueryFactory.query_name(self.es_type, :related, :iid), value: iid } } }, { term: { iid: { _name: context.name(self.es_type, :related, :iid), value: iid } } },
{ term: { type: { _name: QueryFactory.query_name(:doc, :is_a, self.es_type), value: self.es_type } } } { term: { type: { _name: context.name(:doc, :is_a, self.es_type), value: self.es_type } } }
] ]
} }
} }
...@@ -104,7 +105,7 @@ module Elastic ...@@ -104,7 +105,7 @@ module Elastic
# Builds an elasticsearch query that will select child documents from a # Builds an elasticsearch query that will select child documents from a
# set of projects, taking user access rules into account. # set of projects, taking user access rules into account.
def project_ids_filter(query_hash, options) def project_ids_filter(query_hash, options)
QueryFactory.query_context(:project) do |context| context.name(:project) do
project_query = project_ids_query( project_query = project_ids_query(
options[:current_user], options[:current_user],
options[:project_ids], options[:project_ids],
...@@ -162,7 +163,7 @@ module Elastic ...@@ -162,7 +163,7 @@ module Elastic
conditions = pick_projects_by_membership(scoped_project_ids, user, features) conditions = pick_projects_by_membership(scoped_project_ids, user, features)
if public_and_internal_projects if public_and_internal_projects
QueryFactory.query_context(:visibility) do context.name(:visibility) do
# Skip internal projects for anonymous and external users. # Skip internal projects for anonymous and external users.
# Others are given access to all internal projects. # Others are given access to all internal projects.
# #
...@@ -190,23 +191,23 @@ module Elastic ...@@ -190,23 +191,23 @@ module Elastic
def pick_projects_by_membership(project_ids, user, features = nil) def pick_projects_by_membership(project_ids, user, features = nil)
if features.nil? if features.nil?
if project_ids == :any if project_ids == :any
return [{ term: { visibility_level: { _name: QueryFactory.query_name(:any), value: Project::PRIVATE } } }] return [{ term: { visibility_level: { _name: context.name(:any), value: Project::PRIVATE } } }]
else else
return [{ terms: { _name: QueryFactory.query_name(:membership, :id), id: project_ids } }] return [{ terms: { _name: context.name(:membership, :id), id: project_ids } }]
end end
end end
Array(features).map do |feature| Array(features).map do |feature|
condition = condition =
if project_ids == :any if project_ids == :any
{ term: { visibility_level: { _name: QueryFactory.query_name(:any), value: Project::PRIVATE } } } { term: { visibility_level: { _name: context.name(:any), value: Project::PRIVATE } } }
else else
{ terms: { _name: QueryFactory.query_name(:membership, :id), id: filter_ids_by_feature(project_ids, user, feature) } } { terms: { _name: context.name(:membership, :id), id: filter_ids_by_feature(project_ids, user, feature) } }
end end
limit = { limit = {
terms: { terms: {
_name: QueryFactory.query_name(feature, :enabled_or_private), _name: context.name(feature, :enabled_or_private),
"#{feature}_access_level" => [::ProjectFeature::ENABLED, ::ProjectFeature::PRIVATE] "#{feature}_access_level" => [::ProjectFeature::ENABLED, ::ProjectFeature::PRIVATE]
} }
} }
...@@ -224,7 +225,7 @@ module Elastic ...@@ -224,7 +225,7 @@ module Elastic
# If a project feature is specified, access is only granted if the feature # If a project feature is specified, access is only granted if the feature
# is enabled or, for admins & auditors, private. # is enabled or, for admins & auditors, private.
def pick_projects_by_visibility(visibility, user, features) def pick_projects_by_visibility(visibility, user, features)
QueryFactory.query_context(visibility) do |context| context.name(visibility) do
condition = { term: { visibility_level: { _name: context.name, value: visibility } } } condition = { term: { visibility_level: { _name: context.name, value: visibility } } }
limit_by_feature(condition, features, include_members_only: user&.can_read_all_resources?) limit_by_feature(condition, features, include_members_only: user&.can_read_all_resources?)
...@@ -246,7 +247,7 @@ module Elastic ...@@ -246,7 +247,7 @@ module Elastic
features = Array(features) features = Array(features)
features.map do |feature| features.map do |feature|
QueryFactory.query_context(feature, :access_level) do |context| context.name(feature, :access_level) do
limit = limit =
if include_members_only if include_members_only
{ {
......
...@@ -47,7 +47,7 @@ module Elastic ...@@ -47,7 +47,7 @@ module Elastic
if repository_ids.any? if repository_ids.any?
filters << { filters << {
terms: { terms: {
_name: QueryFactory.query_name(type, :related, :repositories), _name: context.name(type, :related, :repositories),
"#{type}.rid" => repository_ids "#{type}.rid" => repository_ids
} }
} }
...@@ -56,7 +56,7 @@ module Elastic ...@@ -56,7 +56,7 @@ module Elastic
if languages.any? if languages.any?
filters << { filters << {
terms: { terms: {
_name: QueryFactory.query_name(type, :match, :languages), _name: context.name(type, :match, :languages),
"#{type}.language" => languages "#{type}.language" => languages
} }
} }
...@@ -85,7 +85,7 @@ module Elastic ...@@ -85,7 +85,7 @@ module Elastic
# we need to do a project visibility check. # we need to do a project visibility check.
# #
# Note that `:current_user` might be `nil` for a anonymous user # Note that `:current_user` might be `nil` for a anonymous user
query_hash = QueryFactory.query_context(:commit, :authorized) { project_ids_filter(query_hash, options) } if options.key?(:current_user) query_hash = context.name(:commit, :authorized) { project_ids_filter(query_hash, options) } if options.key?(:current_user)
if query.blank? if query.blank?
bool_expr[:must] = { match_all: {} } bool_expr[:must] = { match_all: {} }
...@@ -93,7 +93,7 @@ module Elastic ...@@ -93,7 +93,7 @@ module Elastic
else else
bool_expr[:must] = { bool_expr[:must] = {
simple_query_string: { simple_query_string: {
_name: QueryFactory.query_name(:commit, :match, :search_terms), _name: context.name(:commit, :match, :search_terms),
fields: fields, fields: fields,
query: query_with_prefix, query: query_with_prefix,
default_operator: :and default_operator: :and
...@@ -105,7 +105,7 @@ module Elastic ...@@ -105,7 +105,7 @@ module Elastic
bool_expr[:filter] << { bool_expr[:filter] << {
term: { term: {
type: { type: {
_name: QueryFactory.query_name(:doc, :is_a, :commit), _name: context.name(:doc, :is_a, :commit),
value: 'commit' value: 'commit'
} }
} }
...@@ -157,7 +157,7 @@ module Elastic ...@@ -157,7 +157,7 @@ module Elastic
# add the term matching # add the term matching
bool_expr[:must] = { bool_expr[:must] = {
simple_query_string: { simple_query_string: {
_name: QueryFactory.query_name(:blob, :match, :search_terms), _name: context.name(:blob, :match, :search_terms),
query: query.term, query: query.term,
default_operator: :and, default_operator: :and,
fields: %w[blob.content blob.file_name] fields: %w[blob.content blob.file_name]
...@@ -168,13 +168,13 @@ module Elastic ...@@ -168,13 +168,13 @@ module Elastic
# we need to do a project visibility check. # we need to do a project visibility check.
# #
# Note that `:current_user` might be `nil` for a anonymous user # Note that `:current_user` might be `nil` for a anonymous user
query_hash = QueryFactory.query_context(:blob, :authorized) { project_ids_filter(query_hash, options) } if options.key?(:current_user) query_hash = context.name(:blob, :authorized) { project_ids_filter(query_hash, options) } if options.key?(:current_user)
# add the document type filter # add the document type filter
bool_expr[:filter] << { bool_expr[:filter] << {
term: { term: {
type: { type: {
_name: QueryFactory.query_name(:doc, :is_a, type), _name: context.name(:doc, :is_a, type),
value: type value: type
} }
} }
......
...@@ -21,10 +21,10 @@ module Elastic ...@@ -21,10 +21,10 @@ module Elastic
end end
options[:features] = 'issues' options[:features] = 'issues'
QueryFactory.query_context(:issue) do context.name(:issue) do
query_hash = QueryFactory.query_context(:authorized) { project_ids_filter(query_hash, options) } query_hash = context.name(:authorized) { project_ids_filter(query_hash, options) }
query_hash = QueryFactory.query_context(:confidentiality) { confidentiality_filter(query_hash, options) } query_hash = context.name(:confidentiality) { confidentiality_filter(query_hash, options) }
query_hash = QueryFactory.query_context(:match) { state_filter(query_hash, options) } query_hash = context.name(:match) { state_filter(query_hash, options) }
end end
query_hash = apply_sort(query_hash, options) query_hash = apply_sort(query_hash, options)
...@@ -53,7 +53,6 @@ module Elastic ...@@ -53,7 +53,6 @@ module Elastic
return query_hash if authorized_project_ids.to_set == scoped_project_ids.to_set return query_hash if authorized_project_ids.to_set == scoped_project_ids.to_set
end end
context = QueryFactory.current_query_context
filter = { term: { confidential: { _name: context.name(:non_confidential), value: false } } } filter = { term: { confidential: { _name: context.name(:non_confidential), value: false } } }
if current_user if current_user
......
...@@ -21,9 +21,9 @@ module Elastic ...@@ -21,9 +21,9 @@ module Elastic
end end
options[:features] = 'merge_requests' options[:features] = 'merge_requests'
QueryFactory.query_context(:merge_request) do context.name(:merge_request) do
query_hash = QueryFactory.query_context(:authorized) { project_ids_filter(query_hash, options) } query_hash = context.name(:authorized) { project_ids_filter(query_hash, options) }
query_hash = QueryFactory.query_context(:match) { state_filter(query_hash, options) } query_hash = context.name(:match) { state_filter(query_hash, options) }
end end
query_hash = apply_sort(query_hash, options) query_hash = apply_sort(query_hash, options)
......
...@@ -6,8 +6,8 @@ module Elastic ...@@ -6,8 +6,8 @@ module Elastic
def elastic_search(query, options: {}) def elastic_search(query, options: {})
options[:in] = %w(title^2 description) options[:in] = %w(title^2 description)
query_hash = QueryFactory.query_context(:milestone, :match) { basic_query_hash(options[:in], query) } query_hash = basic_query_hash(options[:in], query)
query_hash = QueryFactory.query_context(:milestone, :related) { project_ids_filter(query_hash, options) } query_hash = context.name(:milestone, :related) { project_ids_filter(query_hash, options) }
search(query_hash, options) search(query_hash, options)
end end
......
...@@ -13,9 +13,9 @@ module Elastic ...@@ -13,9 +13,9 @@ module Elastic
options[:in] = ['note'] options[:in] = ['note']
query_hash = basic_query_hash(%w[note], query) query_hash = basic_query_hash(%w[note], query)
QueryFactory.query_context(:note) do context.name(:note) do
query_hash = QueryFactory.query_context(:authorized) { project_ids_filter(query_hash, options) } query_hash = context.name(:authorized) { project_ids_filter(query_hash, options) }
query_hash = QueryFactory.query_context(:confidentiality) { confidentiality_filter(query_hash, options) } query_hash = context.name(:confidentiality) { confidentiality_filter(query_hash, options) }
end end
query_hash[:highlight] = highlight_options(options[:in]) query_hash[:highlight] = highlight_options(options[:in])
...@@ -26,7 +26,6 @@ module Elastic ...@@ -26,7 +26,6 @@ module Elastic
private private
def confidentiality_filter(query_hash, options) def confidentiality_filter(query_hash, options)
context = QueryFactory.current_query_context
current_user = options[:current_user] current_user = options[:current_user]
return query_hash if current_user&.can_read_all_resources? return query_hash if current_user&.can_read_all_resources?
...@@ -103,7 +102,7 @@ module Elastic ...@@ -103,7 +102,7 @@ module Elastic
def project_ids_filter(query_hash, options) def project_ids_filter(query_hash, options)
query_hash[:query][:bool][:filter] ||= [] query_hash[:query][:bool][:filter] ||= []
project_query = QueryFactory.query_context(:project) do project_query = context.name(:project) do
project_ids_query( project_ids_query(
options[:current_user], options[:current_user],
options[:project_ids], options[:project_ids],
...@@ -114,7 +113,7 @@ module Elastic ...@@ -114,7 +113,7 @@ module Elastic
filters = { filters = {
bool: { bool: {
_name: QueryFactory.query_name, _name: context.name,
should: [] should: []
} }
} }
...@@ -138,7 +137,7 @@ module Elastic ...@@ -138,7 +137,7 @@ module Elastic
} }
} }
}, },
{ term: { noteable_type: { _name: QueryFactory.query_name(:noteable, :is_a, noteable_type), value: noteable_type } } } { term: { noteable_type: { _name: context.name(:noteable, :is_a, noteable_type), value: noteable_type } } }
] ]
} }
} }
...@@ -154,7 +153,7 @@ module Elastic ...@@ -154,7 +153,7 @@ module Elastic
override :pick_projects_by_membership override :pick_projects_by_membership
def pick_projects_by_membership(project_ids, user, _ = nil) def pick_projects_by_membership(project_ids, user, _ = nil)
noteable_type_to_feature.map do |noteable_type, feature| noteable_type_to_feature.map do |noteable_type, feature|
QueryFactory.query_context(feature) do |context| context.name(feature) do
condition = condition =
if project_ids == :any if project_ids == :any
{ term: { visibility_level: { _name: context.name(:any), value: Project::PRIVATE } } } { term: { visibility_level: { _name: context.name(:any), value: Project::PRIVATE } } }
...@@ -176,7 +175,7 @@ module Elastic ...@@ -176,7 +175,7 @@ module Elastic
override :limit_by_feature override :limit_by_feature
def limit_by_feature(condition, _ = nil, include_members_only:) def limit_by_feature(condition, _ = nil, include_members_only:)
noteable_type_to_feature.map do |noteable_type, feature| noteable_type_to_feature.map do |noteable_type, feature|
QueryFactory.query_context(feature) do |context| context.name(feature) do
limit = limit =
if include_members_only if include_members_only
{ terms: { _name: context.name(:enabled_or_private), "#{feature}_access_level" => [::ProjectFeature::ENABLED, ::ProjectFeature::PRIVATE] } } { terms: { _name: context.name(:enabled_or_private), "#{feature}_access_level" => [::ProjectFeature::ENABLED, ::ProjectFeature::PRIVATE] } }
......
...@@ -8,9 +8,9 @@ module Elastic ...@@ -8,9 +8,9 @@ module Elastic
query_hash = basic_query_hash(options[:in], query) query_hash = basic_query_hash(options[:in], query)
filters = [{ terms: { _name: QueryFactory.query_name(:doc, :is_a, es_type), type: [es_type] } }] filters = [{ terms: { _name: context.name(:doc, :is_a, es_type), type: [es_type] } }]
QueryFactory.query_context(:project) do |context| context.name(:project) do
if options[:namespace_id] if options[:namespace_id]
filters << { filters << {
terms: { terms: {
......
# frozen_string_literal: true
module Elastic
module Latest
class QueryContext
module Aware
def context
@context ||= QueryContext.new
end
end
def build_name(*args)
::Gitlab::Elastic::ExprName
.new(self)
.build(*current, *args)
end
def name(*args, &block)
name = build_name(*args)
return name.to_s unless block_given?
begin
contexts.push(name)
yield name.to_s
ensure
contexts.pop
end
end
def current
return if contexts.empty?
contexts.last
end
private
def contexts
@contexts ||= []
end
end
end
end
# frozen_string_literal: true
module Elastic
module Latest
class QueryFactory
@stack = []
def self.query_context(*args, &block)
context = if current_query_context
::Gitlab::Elastic::ExprName.build(*current_query_context, *args)
else
::Gitlab::Elastic::ExprName.build(*args)
end
return context unless block_given?
begin
@stack.push(context)
yield context
ensure
@stack.pop
end
end
def self.query_name(*args)
return current_query_context.name(*args) if current_query_context
Gitlab::Elastic::ExprName.build(*args).name
end
def self.current_query_context
return if @stack.empty?
@stack.last
end
end
end
end
...@@ -5,7 +5,7 @@ module Elastic ...@@ -5,7 +5,7 @@ module Elastic
class SnippetClassProxy < ApplicationClassProxy class SnippetClassProxy < ApplicationClassProxy
def elastic_search(query, options: {}) def elastic_search(query, options: {})
query_hash = basic_query_hash(%w(title description), query) query_hash = basic_query_hash(%w(title description), query)
query_hash = QueryFactory.query_context(:snippet, :authorized) { filter(query_hash, options) } query_hash = context.name(:snippet, :authorized) { filter(query_hash, options) }
search(query_hash, options) search(query_hash, options)
end end
...@@ -27,7 +27,7 @@ module Elastic ...@@ -27,7 +27,7 @@ module Elastic
# Match any of the filter conditions, in addition to the existing conditions # Match any of the filter conditions, in addition to the existing conditions
query_hash[:query][:bool][:filter] << { query_hash[:query][:bool][:filter] << {
bool: { bool: {
_name: QueryFactory.query_name, _name: context.name,
should: filter_conditions should: filter_conditions
} }
} }
...@@ -41,7 +41,7 @@ module Elastic ...@@ -41,7 +41,7 @@ module Elastic
# Include accessible personal snippets # Include accessible personal snippets
filter_conditions << { filter_conditions << {
bool: { bool: {
_name: QueryFactory.query_name(:personal), _name: context.name(:personal),
filter: [ filter: [
{ terms: { visibility_level: Gitlab::VisibilityLevel.levels_for_user(user) } } { terms: { visibility_level: Gitlab::VisibilityLevel.levels_for_user(user) } }
], ],
...@@ -53,9 +53,9 @@ module Elastic ...@@ -53,9 +53,9 @@ module Elastic
if user if user
filter_conditions << { filter_conditions << {
bool: { bool: {
_name: QueryFactory.query_name(:authored), _name: context.name(:authored),
filter: [ filter: [
{ term: { author_id: { _name: QueryFactory.query_name(:as_author), value: user.id } } } { term: { author_id: { _name: context.name(:as_author), value: user.id } } }
], ],
must_not: { exists: { field: 'project_id' } } must_not: { exists: { field: 'project_id' } }
} }
...@@ -79,7 +79,7 @@ module Elastic ...@@ -79,7 +79,7 @@ module Elastic
filter_conditions << { filter_conditions << {
bool: { bool: {
_name: QueryFactory.query_name(:membership, :id), _name: context.name(:membership, :id),
must: [ must: [
{ terms: { project_id: project_ids } } { terms: { project_id: project_ids } }
] ]
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
module Elastic module Elastic
module Latest module Latest
module StateFilter module StateFilter
include QueryContext::Aware
private private
def state_filter(query_hash, options) def state_filter(query_hash, options)
...@@ -11,7 +13,7 @@ module Elastic ...@@ -11,7 +13,7 @@ module Elastic
return query_hash if state.blank? || state == 'all' return query_hash if state.blank? || state == 'all'
return query_hash unless API::Helpers::SearchHelpers.search_states.include?(state) return query_hash unless API::Helpers::SearchHelpers.search_states.include?(state)
filter = { match: { state: { _name: QueryFactory.query_name(:state), query: state } } } filter = { match: { state: { _name: context.name(:state), query: state } } }
query_hash[:query][:bool][:filter] << filter query_hash[:query][:bool][:filter] << filter
query_hash query_hash
......
...@@ -2,19 +2,29 @@ ...@@ -2,19 +2,29 @@
module Gitlab module Gitlab
module Elastic module Elastic
class ExprName < Array class ExprName
def self.build(*context) def initialize(context)
new(context) @context = context
@values = []
end end
def name(*context) def build(*context)
return to_s if context.empty? @values.concat(context)
self
end
ExprName.build(*self, *context).to_s def name(*context, &block)
@context.name(*context, &block)
end end
def to_s def to_s
map(&:to_s).join(":") @values.map(&:to_s).join(":")
end
def to_a
@values
end end
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