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