Commit fa1a735c authored by Mario de la Ossa's avatar Mario de la Ossa Committed by Nick Thomas

Enable elasticsearch per project or group

In order to allow us to incrementally enable ES on gitlab.com we add two
 new classes:

- ElasticsearchIndexedNamespaces
- ElasticsearchIndexedProjects

These classes are used by ApplicationSetting to enable/disable projects
and namespaces (groups) that should be indexed by elasticsearch.

We also have the application setting, `elasticsearch_limit_indexing`,
that enables/disables the new functionality

In order to be able to selectively choose projects/namespaces to use
with elasticsearch, `elasticsearch_limit_indexing` MUST be enabled under
the admin integrations options.
parent 2eb74fb7
...@@ -14,8 +14,6 @@ class SearchController < ApplicationController ...@@ -14,8 +14,6 @@ class SearchController < ApplicationController
layout 'search' layout 'search'
def show def show
search_service = SearchService.new(current_user, params)
@project = search_service.project @project = search_service.project
@group = search_service.group @group = search_service.group
......
...@@ -50,6 +50,10 @@ module SearchHelper ...@@ -50,6 +50,10 @@ module SearchHelper
filename filename
end end
def search_service
@search_service ||= ::SearchService.new(current_user, params)
end
private private
# Autocomplete results for various settings pages # Autocomplete results for various settings pages
......
...@@ -69,3 +69,5 @@ class SearchService ...@@ -69,3 +69,5 @@ class SearchService
attr_reader :current_user, :params attr_reader :current_user, :params
end end
SearchService.prepend(EE::SearchService)
...@@ -87,7 +87,7 @@ ...@@ -87,7 +87,7 @@
= _("Milestones") = _("Milestones")
%span.badge.badge-pill %span.badge.badge-pill
= limited_count(@search_results.limited_milestones_count) = limited_count(@search_results.limited_milestones_count)
- if Gitlab::CurrentSettings.elasticsearch_search? - if search_service.use_elasticsearch?
%li{ class: active_when(@scope == 'blobs') } %li{ class: active_when(@scope == 'blobs') }
= link_to search_filter_path(scope: 'blobs') do = link_to search_filter_path(scope: 'blobs') do
= _("Code") = _("Code")
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
- unless params[:snippets].eql? 'true' - unless params[:snippets].eql? 'true'
= render 'filter' = render 'filter'
= button_tag _("Search"), class: "btn btn-success btn-search" = button_tag _("Search"), class: "btn btn-success btn-search"
- if Gitlab::CurrentSettings.elasticsearch_search? - if search_service.use_elasticsearch?
.form-text.text-muted .form-text.text-muted
= link_to 'Advanced search functionality', help_page_path('user/search/advanced_search_syntax.md'), target: '_blank' = link_to _('Advanced search functionality'), help_page_path('user/search/advanced_search_syntax.md'), target: '_blank'
is enabled. is enabled.
...@@ -103,6 +103,7 @@ ...@@ -103,6 +103,7 @@
- [elastic_batch_project_indexer, 1] - [elastic_batch_project_indexer, 1]
- [elastic_indexer, 1] - [elastic_indexer, 1]
- [elastic_commit_indexer, 1] - [elastic_commit_indexer, 1]
- [elastic_namespace_indexer, 1]
- [export_csv, 1] - [export_csv, 1]
- [incident_management, 2] - [incident_management, 2]
......
...@@ -217,6 +217,7 @@ ActiveRecord::Schema.define(version: 20190322132835) do ...@@ -217,6 +217,7 @@ ActiveRecord::Schema.define(version: 20190322132835) do
t.string "runners_registration_token_encrypted" t.string "runners_registration_token_encrypted"
t.integer "local_markdown_version", default: 0, null: false t.integer "local_markdown_version", default: 0, null: false
t.integer "first_day_of_week", default: 0, null: false t.integer "first_day_of_week", default: 0, null: false
t.boolean "elasticsearch_limit_indexing", default: false, null: false
t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id", using: :btree t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id", using: :btree
t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id", using: :btree t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id", using: :btree
t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id", using: :btree t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id", using: :btree
...@@ -1065,6 +1066,20 @@ ActiveRecord::Schema.define(version: 20190322132835) do ...@@ -1065,6 +1066,20 @@ ActiveRecord::Schema.define(version: 20190322132835) do
t.index ["merge_request_id"], name: "index_draft_notes_on_merge_request_id", using: :btree t.index ["merge_request_id"], name: "index_draft_notes_on_merge_request_id", using: :btree
end end
create_table "elasticsearch_indexed_namespaces", id: false, force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.integer "namespace_id"
t.index ["namespace_id"], name: "index_elasticsearch_indexed_namespaces_on_namespace_id", unique: true, using: :btree
end
create_table "elasticsearch_indexed_projects", id: false, force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.integer "project_id"
t.index ["project_id"], name: "index_elasticsearch_indexed_projects_on_project_id", unique: true, using: :btree
end
create_table "emails", force: :cascade do |t| create_table "emails", force: :cascade do |t|
t.integer "user_id", null: false t.integer "user_id", null: false
t.string "email", null: false t.string "email", null: false
...@@ -3456,6 +3471,8 @@ ActiveRecord::Schema.define(version: 20190322132835) do ...@@ -3456,6 +3471,8 @@ ActiveRecord::Schema.define(version: 20190322132835) do
add_foreign_key "design_management_versions", "design_management_designs", on_delete: :cascade add_foreign_key "design_management_versions", "design_management_designs", on_delete: :cascade
add_foreign_key "draft_notes", "merge_requests", on_delete: :cascade add_foreign_key "draft_notes", "merge_requests", on_delete: :cascade
add_foreign_key "draft_notes", "users", column: "author_id", on_delete: :cascade add_foreign_key "draft_notes", "users", column: "author_id", on_delete: :cascade
add_foreign_key "elasticsearch_indexed_namespaces", "namespaces", on_delete: :cascade
add_foreign_key "elasticsearch_indexed_projects", "projects", on_delete: :cascade
add_foreign_key "environments", "projects", name: "fk_d1c8c1da6a", on_delete: :cascade add_foreign_key "environments", "projects", name: "fk_d1c8c1da6a", on_delete: :cascade
add_foreign_key "epic_issues", "epics", on_delete: :cascade add_foreign_key "epic_issues", "epics", on_delete: :cascade
add_foreign_key "epic_issues", "issues", on_delete: :cascade add_foreign_key "epic_issues", "issues", on_delete: :cascade
......
...@@ -118,11 +118,32 @@ The following Elasticsearch settings are available: ...@@ -118,11 +118,32 @@ The following Elasticsearch settings are available:
| `Use the new repository indexer (beta)` | Perform repository indexing using [GitLab Elasticsearch Indexer](https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer). | | `Use the new repository indexer (beta)` | Perform repository indexing using [GitLab Elasticsearch Indexer](https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer). |
| `Search with Elasticsearch enabled` | Enables/disables using Elasticsearch in search. | | `Search with Elasticsearch enabled` | Enables/disables using Elasticsearch in search. |
| `URL` | The URL to use for connecting to Elasticsearch. Use a comma-separated list to support clustering (e.g., "http://host1, https://host2:9200"). If your Elasticsearch instance is password protected, pass the `username:password` in the URL (e.g., `http://<username>:<password>@<elastic_host>:9200/`). | | `URL` | The URL to use for connecting to Elasticsearch. Use a comma-separated list to support clustering (e.g., "http://host1, https://host2:9200"). If your Elasticsearch instance is password protected, pass the `username:password` in the URL (e.g., `http://<username>:<password>@<elastic_host>:9200/`). |
| `Limit namespaces and projects that can be indexed` | Enabling this will allow you to select namespaces and projects to index. All other namespaces and projects will use database search instead. Please note that if you enable this option but do not select any namespaces or projects, none will be indexed. [Read more below](#limiting-namespaces-and-projects).
| `Using AWS hosted Elasticsearch with IAM credentials` | Sign your Elasticsearch requests using [AWS IAM authorization](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html) or [AWS EC2 Instance Profile Credentials](http://docs.aws.amazon.com/codedeploy/latest/userguide/getting-started-create-iam-instance-profile.html#getting-started-create-iam-instance-profile-cli). The policies must be configured to allow `es:*` actions. | | `Using AWS hosted Elasticsearch with IAM credentials` | Sign your Elasticsearch requests using [AWS IAM authorization](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html) or [AWS EC2 Instance Profile Credentials](http://docs.aws.amazon.com/codedeploy/latest/userguide/getting-started-create-iam-instance-profile.html#getting-started-create-iam-instance-profile-cli). The policies must be configured to allow `es:*` actions. |
| `AWS Region` | The AWS region your Elasticsearch service is located in. | | `AWS Region` | The AWS region your Elasticsearch service is located in. |
| `AWS Access Key` | The AWS access key. | | `AWS Access Key` | The AWS access key. |
| `AWS Secret Access Key` | The AWS secret access key. | | `AWS Secret Access Key` | The AWS secret access key. |
### Limiting namespaces and projects
If you select `Limit namespaces and projects that can be indexed`, more options will become available
![limit namespaces and projects options](img/limit_namespaces_projects_options.png)
You can select namespaces and projects to index exclusively. Please note that if the namespace is a group it will include
any sub-groups and projects belonging to those sub-groups to be indexed as well.
You can filter the selection dropdown by writing part of the namespace or project name you're interested in.
![limit namespace filter](img/limit_namespace_filter.png)
NOTE: **Note**:
If no namespaces or projects are selected, no Elasticsearch indexing will take place.
CAUTION: **Warning**:
If you have already indexed your instance, you will have to regenerate the index in order to delete all existing data
for filtering to work correctly. To do this run the rake tasks `gitlab:elastic:create_empty_index` and
`gitlab:elastic:clear_index_status` Afterwards, removing a namespace or a projeect from the list will delete the data
from the Elasticsearch index as expected.
## Disabling Elasticsearch ## Disabling Elasticsearch
To disable the Elasticsearch integration: To disable the Elasticsearch integration:
......
import 'select2/select2';
import $ from 'jquery';
import { s__ } from '~/locale';
import Api from '~/api';
const onLimitCheckboxChange = (checked, $limitByNamespaces, $limitByProjects) => {
$limitByNamespaces.find('.select2').select2('data', null);
$limitByNamespaces.find('.select2').select2('data', null);
$limitByNamespaces.toggleClass('hidden', !checked);
$limitByProjects.toggleClass('hidden', !checked);
};
const getDropdownConfig = (placeholder, apiPath, textProp) => ({
placeholder,
multiple: true,
initSelection($el, callback) {
callback($el.data('selected'));
},
ajax: {
url: Api.buildUrl(apiPath),
dataType: 'JSON',
quietMillis: 250,
data(search) {
return {
search,
};
},
results(data) {
return {
results: data.map(entity => ({
id: entity.id,
text: entity[textProp],
})),
};
},
},
});
document.addEventListener('DOMContentLoaded', () => {
const $container = $('#js-elasticsearch-settings');
$container
.find('.js-limit-checkbox')
.on('change', e =>
onLimitCheckboxChange(
e.currentTarget.checked,
$container.find('.js-limit-namespaces'),
$container.find('.js-limit-projects'),
),
);
$container
.find('.js-elasticsearch-namespaces')
.select2(
getDropdownConfig(
s__('Elastic|None. Select namespaces to index.'),
Api.namespacesPath,
'full_path',
),
);
$container
.find('.js-elasticsearch-projects')
.select2(
getDropdownConfig(
s__('Elastic|None. Select projects to index.'),
Api.projectsPath,
'name_with_namespace',
),
);
});
...@@ -62,6 +62,9 @@ module EE ...@@ -62,6 +62,9 @@ module EE
:elasticsearch_indexing, :elasticsearch_indexing,
:elasticsearch_search, :elasticsearch_search,
:elasticsearch_url, :elasticsearch_url,
:elasticsearch_limit_indexing,
:elasticsearch_namespace_ids,
:elasticsearch_project_ids,
:geo_status_timeout, :geo_status_timeout,
:help_text, :help_text,
:pseudonymizer_enabled, :pseudonymizer_enabled,
...@@ -78,6 +81,18 @@ module EE ...@@ -78,6 +81,18 @@ module EE
] ]
end end
def elasticsearch_objects_options(objects)
objects.map { |g| { id: g.id, text: g.full_name } }
end
def elasticsearch_namespace_ids
ElasticsearchIndexedNamespace.namespace_ids.join(',')
end
def elasticsearch_project_ids
ElasticsearchIndexedProject.project_ids.join(',')
end
def self.repository_mirror_attributes def self.repository_mirror_attributes
[ [
:mirror_max_capacity, :mirror_max_capacity,
......
...@@ -228,7 +228,11 @@ module Elastic ...@@ -228,7 +228,11 @@ module Elastic
# Should be overridden in the models where some records should be skipped # Should be overridden in the models where some records should be skipped
def searchable? def searchable?
true self.use_elasticsearch?
end
def use_elasticsearch?
self.project&.use_elasticsearch?
end end
def generic_attributes def generic_attributes
......
...@@ -15,6 +15,10 @@ module Elastic ...@@ -15,6 +15,10 @@ module Elastic
included do included do
include ApplicationSearch include ApplicationSearch
def use_elasticsearch?
::Gitlab::CurrentSettings.elasticsearch_indexes_project?(self)
end
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
......
...@@ -25,7 +25,7 @@ module Elastic ...@@ -25,7 +25,7 @@ module Elastic
def self.import def self.import
Project.find_each do |project| Project.find_each do |project|
if project.repository.exists? && !project.repository.empty? if project.repository.exists? && !project.repository.empty? && project.use_elasticsearch?
project.repository.index_commits project.repository.index_commits
project.repository.index_blobs project.repository.index_blobs
end end
......
...@@ -32,6 +32,10 @@ module Elastic ...@@ -32,6 +32,10 @@ module Elastic
data data
end end
def use_elasticsearch?
::Gitlab::CurrentSettings.elasticsearch_indexing?
end
def self.elastic_search(query, options: {}) def self.elastic_search(query, options: {})
query_hash = basic_query_hash(%w(title file_name), query) query_hash = basic_query_hash(%w(title file_name), query)
......
...@@ -25,7 +25,7 @@ module Elastic ...@@ -25,7 +25,7 @@ module Elastic
def self.import def self.import
Project.with_wiki_enabled.find_each do |project| Project.with_wiki_enabled.find_each do |project|
unless project.wiki.empty? if project.use_elasticsearch? && !project.wiki.empty?
project.wiki.index_blobs project.wiki.index_blobs
end end
end end
......
...@@ -14,6 +14,11 @@ module EE ...@@ -14,6 +14,11 @@ module EE
EMAIL_ADDITIONAL_TEXT_CHARACTER_LIMIT = 10_000 EMAIL_ADDITIONAL_TEXT_CHARACTER_LIMIT = 10_000
INSTANCE_REVIEW_MIN_USERS = 100 INSTANCE_REVIEW_MIN_USERS = 100
attr_accessor :elasticsearch_namespace_ids, :elasticsearch_project_ids
after_save -> { update_elasticsearch_containers(ElasticsearchIndexedNamespace, :namespace_id, elasticsearch_namespace_ids) }, on: [:create, :update]
after_save -> { update_elasticsearch_containers(ElasticsearchIndexedProject, :project_id, elasticsearch_project_ids) }, on: [:create, :update]
belongs_to :file_template_project, class_name: "Project" belongs_to :file_template_project, class_name: "Project"
ignore_column :minimum_mirror_sync_time ignore_column :minimum_mirror_sync_time
...@@ -120,6 +125,59 @@ module EE ...@@ -120,6 +125,59 @@ module EE
end end
end end
def update_elasticsearch_containers(klass, attribute, container_ids)
return unless elasticsearch_limit_indexing?
container_ids = container_ids&.split(",")
return unless container_ids.present?
# Destroy any containers that have been removed. This runs callbacks, etc
# #rubocop:disable Cop/DestroyAll
klass.where.not(attribute => container_ids).each_batch do |batch, _index|
batch.destroy_all
end
# #rubocop:enable Cop/DestroyAll
# Disregard any duplicates that are already present
container_ids -= klass.pluck(attribute)
# Add new containers
container_ids.each { |id| klass.create(attribute => id) }
end
def elasticsearch_indexes_project?(project)
return false unless elasticsearch_indexing?
return true unless elasticsearch_limit_indexing?
elasticsearch_limited_projects.exists?(project.id)
end
def elasticsearch_indexes_namespace?(namespace)
return false unless elasticsearch_indexing?
return true unless elasticsearch_limit_indexing?
elasticsearch_limited_namespaces.exists?(namespace.id)
end
def elasticsearch_limited_projects(ignore_namespaces = false)
return ::Project.where(id: ElasticsearchIndexedProject.select(:project_id)) if ignore_namespaces
union = ::Gitlab::SQL::Union.new([
::Project.where(namespace_id: elasticsearch_limited_namespaces.select(:id)),
::Project.where(id: ElasticsearchIndexedProject.select(:project_id))
]).to_sql
::Project.from("(#{union}) projects")
end
def elasticsearch_limited_namespaces(ignore_descendants = false)
namespaces = ::Namespace.where(id: ElasticsearchIndexedNamespace.select(:namespace_id))
return namespaces if ignore_descendants
::Gitlab::ObjectHierarchy.new(namespaces).base_and_descendants
end
def pseudonymizer_available? def pseudonymizer_available?
License.feature_available?(:pseudonymizer) License.feature_available?(:pseudonymizer)
end end
......
...@@ -8,7 +8,7 @@ module EE ...@@ -8,7 +8,7 @@ module EE
end end
def update_elasticsearch_index def update_elasticsearch_index
if ::Gitlab::CurrentSettings.current_application_settings.elasticsearch_indexing? if issue.project&.use_elasticsearch?
::ElasticIndexerWorker.perform_async( ::ElasticIndexerWorker.perform_async(
:update, :update,
'Issue', 'Issue',
......
...@@ -271,6 +271,10 @@ module EE ...@@ -271,6 +271,10 @@ module EE
actual_plan_name == GOLD_PLAN actual_plan_name == GOLD_PLAN
end end
def use_elasticsearch?
::Gitlab::CurrentSettings.elasticsearch_indexes_namespace?(self)
end
private private
def validate_plan_name def validate_plan_name
......
...@@ -14,8 +14,9 @@ module EE ...@@ -14,8 +14,9 @@ module EE
scope :searchable, -> { where(system: false) } scope :searchable, -> { where(system: false) }
end end
# Original method in Elastic::ApplicationSearch
def searchable? def searchable?
!system !system && super
end end
def for_epic? def for_epic?
......
...@@ -6,7 +6,7 @@ module EE ...@@ -6,7 +6,7 @@ module EE
prepended do prepended do
after_commit on: :update do after_commit on: :update do
if ::Gitlab::CurrentSettings.current_application_settings.elasticsearch_indexing? if project.use_elasticsearch?
ElasticIndexerWorker.perform_async(:update, 'Project', project_id, project.es_id) ElasticIndexerWorker.perform_async(:update, 'Project', project_id, project.es_id)
end end
end end
......
...@@ -16,7 +16,7 @@ module EE ...@@ -16,7 +16,7 @@ module EE
end end
def update_elastic_index def update_elastic_index
index_blobs if ::Gitlab::CurrentSettings.elasticsearch_indexing? index_blobs if project.use_elasticsearch?
end end
def path_to_repo def path_to_repo
......
# frozen_string_literal: true
class ElasticsearchIndexedNamespace < ActiveRecord::Base
include EachBatch
self.primary_key = 'namespace_id'
after_commit :index, on: :create
after_commit :delete_from_index, on: :destroy
belongs_to :namespace
validates :namespace_id, presence: true, uniqueness: true
def self.namespace_ids
self.pluck(:namespace_id)
end
private
def index
ElasticNamespaceIndexerWorker.perform_async(namespace_id, :index)
end
def delete_from_index
ElasticNamespaceIndexerWorker.perform_async(namespace_id, :delete)
end
end
# frozen_string_literal: true
class ElasticsearchIndexedProject < ActiveRecord::Base
include EachBatch
self.primary_key = 'project_id'
after_commit :index, on: :create
after_commit :delete_from_index, on: :destroy
belongs_to :project
validates :project_id, presence: true, uniqueness: true
def self.project_ids
self.pluck(:project_id)
end
private
def index
if Gitlab::CurrentSettings.elasticsearch_indexing? && project.searchable?
ElasticIndexerWorker.perform_async(:index, project.class.to_s, project.id, project.es_id)
end
end
def delete_from_index
if Gitlab::CurrentSettings.elasticsearch_indexing? && project.searchable?
ElasticIndexerWorker.perform_async(
:delete,
project.class.to_s,
project.id,
project.es_id,
es_parent: project.es_parent
)
end
end
end
...@@ -4,4 +4,6 @@ class IndexStatus < ApplicationRecord ...@@ -4,4 +4,6 @@ class IndexStatus < ApplicationRecord
belongs_to :project belongs_to :project
validates :project_id, uniqueness: true, presence: true validates :project_id, uniqueness: true, presence: true
scope :for_project, ->(project_id) { where(project_id: project_id) }
end end
...@@ -9,7 +9,7 @@ module EE ...@@ -9,7 +9,7 @@ module EE
override :execute_related_hooks override :execute_related_hooks
def execute_related_hooks def execute_related_hooks
if ::Gitlab::CurrentSettings.elasticsearch_indexing? && default_branch? && should_index_commits? if should_index_commits?
::ElasticCommitIndexerWorker.perform_async(project.id, params[:oldrev], params[:newrev]) ::ElasticCommitIndexerWorker.perform_async(project.id, params[:oldrev], params[:newrev])
end end
...@@ -19,6 +19,8 @@ module EE ...@@ -19,6 +19,8 @@ module EE
private private
def should_index_commits? def should_index_commits?
default_branch? &&
project.use_elasticsearch? &&
::Gitlab::Redis::SharedState.with { |redis| !redis.sismember(:elastic_projects_indexing, project.id) } ::Gitlab::Redis::SharedState.with { |redis| !redis.sismember(:elastic_projects_indexing, project.id) }
end end
......
# frozen_string_literal: true
module EE
module GitPushService
extend ::Gitlab::Utils::Override
protected
override :execute_related_hooks
def execute_related_hooks
if should_index_commits?
::ElasticCommitIndexerWorker.perform_async(project.id, params[:oldrev], params[:newrev])
end
super
end
private
def should_index_commits?
default_branch? &&
project.use_elasticsearch? &&
::Gitlab::Redis::SharedState.with { |redis| !redis.sismember(:elastic_projects_indexing, project.id) }
end
override :pipeline_options
def pipeline_options
{ mirror_update: project.mirror? && project.repository.up_to_date_with_upstream?(branch_name) }
end
end
end
...@@ -8,7 +8,7 @@ module EE ...@@ -8,7 +8,7 @@ module EE
override :execute override :execute
def execute def execute
if ::Gitlab::CurrentSettings.elasticsearch_search? if use_elasticsearch?
::Gitlab::Elastic::SearchResults.new(current_user, params[:search], ::Gitlab::Elastic::SearchResults.new(current_user, params[:search],
elastic_projects, projects, elastic_projects, projects,
elastic_global) elastic_global)
...@@ -17,6 +17,11 @@ module EE ...@@ -17,6 +17,11 @@ module EE
end end
end end
def use_elasticsearch?
::Gitlab::CurrentSettings.elasticsearch_search? &&
!::Gitlab::CurrentSettings.elasticsearch_limit_indexing?
end
def elastic_projects def elastic_projects
strong_memoize(:elastic_projects) do strong_memoize(:elastic_projects) do
if current_user&.full_private_access? if current_user&.full_private_access?
......
...@@ -5,10 +5,17 @@ module EE ...@@ -5,10 +5,17 @@ module EE
module GroupService module GroupService
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
override :use_elasticsearch?
def use_elasticsearch?
group&.use_elasticsearch?
end
override :elastic_projects
def elastic_projects def elastic_projects
@elastic_projects ||= projects.pluck(:id) # rubocop:disable CodeReuse/ActiveRecord @elastic_projects ||= projects.pluck(:id) # rubocop:disable CodeReuse/ActiveRecord
end end
override :elastic_global
def elastic_global def elastic_global
false false
end end
......
...@@ -7,7 +7,7 @@ module EE ...@@ -7,7 +7,7 @@ module EE
override :execute override :execute
def execute def execute
if ::Gitlab::CurrentSettings.elasticsearch_search? if use_elasticsearch?
::Gitlab::Elastic::ProjectSearchResults.new(current_user, ::Gitlab::Elastic::ProjectSearchResults.new(current_user,
params[:search], params[:search],
project.id, project.id,
...@@ -16,6 +16,11 @@ module EE ...@@ -16,6 +16,11 @@ module EE
super super
end end
end end
# This method is used in the top-level SearchService, so cannot be in-lined into #execute
def use_elasticsearch?
project.use_elasticsearch?
end
end end
end end
end end
...@@ -8,12 +8,17 @@ module EE ...@@ -8,12 +8,17 @@ module EE
override :execute override :execute
def execute def execute
if ::Gitlab::CurrentSettings.elasticsearch_search? if use_elasticsearch?
::Gitlab::Elastic::SnippetSearchResults.new(current_user, params[:search]) ::Gitlab::Elastic::SnippetSearchResults.new(current_user, params[:search])
else else
super super
end end
end end
# This method is used in the top-level SearchService, so cannot be in-lined into #execute
def use_elasticsearch?
::Gitlab::CurrentSettings.elasticsearch_search?
end
end end
end end
end end
# frozen_string_literal: true
module EE
module SearchService
# This is a proper method instead of a `delegate` in order to
# avoid adding unnecessary methods to Search::SnippetService
def use_elasticsearch?
search_service.use_elasticsearch?
end
end
end
...@@ -42,6 +42,20 @@ ...@@ -42,6 +42,20 @@
.form-text.text-muted .form-text.text-muted
The url to use for connecting to Elasticsearch. Use a comma-separated list to support clustering (e.g., "http://localhost:9200, http://localhost:9201"). The url to use for connecting to Elasticsearch. Use a comma-separated list to support clustering (e.g., "http://localhost:9200, http://localhost:9201").
.form-group
.form-check
= f.check_box :elasticsearch_limit_indexing, class: 'form-check-input js-limit-checkbox'
= f.label :elasticsearch_limit_indexing, class: 'form-check-label' do
= _('Limit namespaces and projects that can be indexed')
.form-group.js-limit-namespaces{ class: ('hidden' unless @application_setting.elasticsearch_limit_indexing) }
= f.label :elasticsearch_namespace_ids, _('Namespaces to index'), class: 'label-bold'
= f.text_field :elasticsearch_namespace_ids, class: 'js-elasticsearch-namespaces', value: elasticsearch_namespace_ids, data: { selected: elasticsearch_objects_options(@application_setting.elasticsearch_limited_namespaces(true)).to_json }
.form-group.js-limit-projects{ class: ('hidden' unless @application_setting.elasticsearch_limit_indexing) }
= f.label :elasticsearch_project_ids, _('Projects to index'), class: 'label-bold'
= f.text_field :elasticsearch_project_ids, class: 'js-elasticsearch-projects', value: elasticsearch_project_ids, data: { selected: elasticsearch_objects_options(@application_setting.elasticsearch_limited_projects(true)).to_json }
.sub-section .sub-section
%h4 Elasticsearch AWS IAM credentials %h4 Elasticsearch AWS IAM credentials
.form-group .form-group
......
...@@ -50,6 +50,7 @@ ...@@ -50,6 +50,7 @@
- admin_emails - admin_emails
- create_github_webhook - create_github_webhook
- elastic_batch_project_indexer - elastic_batch_project_indexer
- elastic_namespace_indexer
- elastic_commit_indexer - elastic_commit_indexer
- elastic_indexer - elastic_indexer
- export_csv - export_csv
......
...@@ -29,8 +29,7 @@ module EE ...@@ -29,8 +29,7 @@ module EE
end end
def update_wiki_es_indexes(post_received) def update_wiki_es_indexes(post_received)
return unless ::Gitlab::CurrentSettings.current_application_settings return unless post_received.project.use_elasticsearch?
.elasticsearch_indexing?
post_received.project.wiki.index_blobs post_received.project.wiki.index_blobs
end end
......
...@@ -19,6 +19,8 @@ class ElasticBatchProjectIndexerWorker ...@@ -19,6 +19,8 @@ class ElasticBatchProjectIndexerWorker
private private
def run_indexer(project, update_index) def run_indexer(project, update_index)
return unless project.use_elasticsearch?
# Ensure we remove the hold on the project, no matter what, so ElasticCommitIndexerWorker can do its thing # Ensure we remove the hold on the project, no matter what, so ElasticCommitIndexerWorker can do its thing
# We do this before the indexer starts to avoid the possibility of pushes coming in during this time not # We do this before the indexer starts to avoid the possibility of pushes coming in during this time not
# being indexed. # being indexed.
......
...@@ -10,6 +10,8 @@ class ElasticCommitIndexerWorker ...@@ -10,6 +10,8 @@ class ElasticCommitIndexerWorker
project = Project.find(project_id) project = Project.find(project_id)
return true unless project.use_elasticsearch?
Gitlab::Elastic::Indexer.new(project).run(oldrev, newrev) Gitlab::Elastic::Indexer.new(project).run(oldrev, newrev)
end end
end end
...@@ -17,11 +17,9 @@ class ElasticIndexerWorker ...@@ -17,11 +17,9 @@ class ElasticIndexerWorker
record = klass.find(record_id) record = klass.find(record_id)
record.__elasticsearch__.client = client record.__elasticsearch__.client = client
if klass.nested? import(operation, record, klass)
record.__elasticsearch__.__send__ "#{operation}_document", routing: record.es_parent # rubocop:disable GitlabSecurity/PublicSend
else initial_index_project(record) if klass == Project && operation.to_s.match?(/index/)
record.__elasticsearch__.__send__ "#{operation}_document" # rubocop:disable GitlabSecurity/PublicSend
end
update_issue_notes(record, options["changed_fields"]) if klass == Issue update_issue_notes(record, options["changed_fields"]) if klass == Issue
when /delete/ when /delete/
...@@ -57,6 +55,30 @@ class ElasticIndexerWorker ...@@ -57,6 +55,30 @@ class ElasticIndexerWorker
def clear_project_data(record_id, es_id) def clear_project_data(record_id, es_id)
remove_children_documents('project', record_id, es_id) remove_children_documents('project', record_id, es_id)
IndexStatus.for_project(record_id).delete_all
end
def initial_index_project(project)
{
Issue => project.issues,
MergeRequest => project.merge_requests,
Snippet => project.snippets,
Note => project.notes.searchable,
Milestone => project.milestones
}.each do |klass, objects|
objects.find_each { |object| import(:index, object, klass) }
end
# Finally, index blobs/commits/wikis
ElasticCommitIndexerWorker.perform_async(project.id)
end
def import(operation, record, klass)
if klass.nested?
record.__elasticsearch__.__send__ "#{operation}_document", routing: record.es_parent # rubocop:disable GitlabSecurity/PublicSend
else
record.__elasticsearch__.__send__ "#{operation}_document" # rubocop:disable GitlabSecurity/PublicSend
end
end end
def remove_documents_by_project_id(record_id) def remove_documents_by_project_id(record_id)
......
# frozen_string_literal: true
class ElasticNamespaceIndexerWorker
include ApplicationWorker
sidekiq_options retry: 2
def perform(namespace_id, operation)
return true unless Gitlab::CurrentSettings.elasticsearch_indexing?
return true unless Gitlab::CurrentSettings.elasticsearch_limit_indexing?
namespace = Namespace.find(namespace_id)
case operation.to_s
when /index/
index_projects(namespace)
when /delete/
delete_from_index(namespace)
end
end
private
def index_projects(namespace)
# The default of 1000 is good for us since Sidekiq documentation doesn't recommend more than 1000 per batch call
# https://www.rubydoc.info/github/mperham/sidekiq/Sidekiq%2FClient:push_bulk
namespace.all_projects.find_in_batches do |batch|
args = batch.map { |project| [:index, project.class.to_s, project.id, project.es_id] }
ElasticIndexerWorker.bulk_perform_async(args)
end
end
def delete_from_index(namespace)
namespace.all_projects.find_in_batches do |batch|
args = batch.map { |project| [:delete, project.class.to_s, project.id, project.es_id] }
ElasticIndexerWorker.bulk_perform_async(args)
end
end
end
---
title: Allow per-project and per-group enabling of Elasticsearch indexing
merge_request: 9861
author:
type: added
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddElasticsearchLimitIndexingToApplicationSetting < ActiveRecord::Migration[5.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default :application_settings, :elasticsearch_limit_indexing, :boolean, default: false, allow_null: false
end
def down
remove_column :application_settings, :elasticsearch_limit_indexing
end
end
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddElasticNamespaceLinkAndElasticProjectLink < ActiveRecord::Migration[5.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
create_table :elasticsearch_indexed_namespaces, id: false do |t|
t.timestamps_with_timezone null: false
t.references :namespace, nil: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
end
create_table :elasticsearch_indexed_projects, id: false do |t|
t.timestamps_with_timezone null: false
t.references :project, nil: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
end
end
end
# frozen_string_literal: true
module EE
module Gitlab
module FakeApplicationSettings
def elasticsearch_indexes_project?(_project)
false
end
def elasticsearch_indexes_namespace?(_namespace)
false
end
end
end
end
...@@ -57,7 +57,7 @@ namespace :gitlab do ...@@ -57,7 +57,7 @@ namespace :gitlab do
projects = apply_project_filters(Project.with_wiki_enabled) projects = apply_project_filters(Project.with_wiki_enabled)
projects.find_each do |project| projects.find_each do |project|
unless project.wiki.empty? if project.use_elasticsearch? && !project.wiki.empty?
puts "Indexing wiki of #{project.full_name}..." puts "Indexing wiki of #{project.full_name}..."
begin begin
......
# frozen_string_literal: true
FactoryBot.define do
factory :elasticsearch_indexed_namespace do
namespace
end
factory :elasticsearch_indexed_project do
project
end
end
...@@ -45,8 +45,12 @@ describe 'Admin updates EE-only settings' do ...@@ -45,8 +45,12 @@ describe 'Admin updates EE-only settings' do
expect(page).to have_content "Application settings saved successfully" expect(page).to have_content "Application settings saved successfully"
end end
it 'Enable elastic search indexing' do context 'Elasticsearch settings' do
before do
visit integrations_admin_application_settings_path visit integrations_admin_application_settings_path
end
it 'Enable elastic search indexing' do
page.within('.as-elasticsearch') do page.within('.as-elasticsearch') do
check 'Elasticsearch indexing' check 'Elasticsearch indexing'
click_button 'Save changes' click_button 'Save changes'
...@@ -56,6 +60,51 @@ describe 'Admin updates EE-only settings' do ...@@ -56,6 +60,51 @@ describe 'Admin updates EE-only settings' do
expect(page).to have_content "Application settings saved successfully" expect(page).to have_content "Application settings saved successfully"
end end
it 'Allows limiting projects and namespaces to index', :js do
project = create(:project)
namespace = create(:namespace)
page.within('.as-elasticsearch') do
expect(page).not_to have_content('Namespaces to index')
expect(page).not_to have_content('Projects to index')
check 'Limit namespaces and projects that can be indexed'
expect(page).to have_content('Namespaces to index')
expect(page).to have_content('Projects to index')
fill_in 'Namespaces to index', with: namespace.name
wait_for_requests
end
page.within('#select2-drop') do
expect(page).to have_content(namespace.full_path)
end
page.within('.as-elasticsearch') do
find('.js-limit-namespaces .select2-choices input[type=text]').native.send_keys(:enter)
fill_in 'Projects to index', with: project.name
wait_for_requests
end
page.within('#select2-drop') do
expect(page).to have_content(project.full_name)
end
page.within('.as-elasticsearch') do
find('.js-limit-projects .select2-choices input[type=text]').native.send_keys(:enter)
click_button 'Save changes'
end
expect(Gitlab::CurrentSettings.elasticsearch_limit_indexing).to be_truthy
expect(ElasticsearchIndexedNamespace.exists?(namespace_id: namespace.id)).to be_truthy
expect(ElasticsearchIndexedProject.exists?(project_id: project.id)).to be_truthy
expect(page).to have_content "Application settings saved successfully"
end
end
it 'Enable Slack application' do it 'Enable Slack application' do
visit integrations_admin_application_settings_path visit integrations_admin_application_settings_path
allow(Gitlab).to receive(:com?).and_return(true) allow(Gitlab).to receive(:com?).and_return(true)
......
...@@ -209,6 +209,79 @@ describe ApplicationSetting do ...@@ -209,6 +209,79 @@ describe ApplicationSetting do
aws_secret_access_key: 'test-secret-access-key' aws_secret_access_key: 'test-secret-access-key'
) )
end end
context 'limiting namespaces and projects' do
before do
setting.update!(elasticsearch_indexing: true)
setting.update!(elasticsearch_limit_indexing: true)
end
context 'namespaces' do
let(:namespaces) { create_list(:namespace, 3) }
it 'creates ElasticsearchIndexedNamespace objects when given elasticsearch_namespace_ids' do
expect do
setting.update!(elasticsearch_namespace_ids: namespaces.map(&:id).join(','))
end.to change { ElasticsearchIndexedNamespace.count }.by(3)
end
it 'deletes ElasticsearchIndexedNamespace objects not in elasticsearch_namespace_ids' do
create :elasticsearch_indexed_namespace, namespace: namespaces.last
expect do
setting.update!(elasticsearch_namespace_ids: namespaces.first(2).map(&:id).join(','))
end.to change { ElasticsearchIndexedNamespace.count }.from(1).to(2)
expect(ElasticsearchIndexedNamespace.where(namespace_id: namespaces.last.id)).not_to exist
end
it 'tells you if a namespace is allowed to be indexed' do
create :elasticsearch_indexed_namespace, namespace: namespaces.last
expect(setting.elasticsearch_indexes_namespace?(namespaces.last)).to be_truthy
expect(setting.elasticsearch_indexes_namespace?(namespaces.first)).to be_falsey
end
end
context 'projects' do
let(:projects) { create_list(:project, 3) }
it 'creates ElasticsearchIndexedProject objects when given elasticsearch_project_ids' do
expect do
setting.update!(elasticsearch_project_ids: projects.map(&:id).join(','))
end.to change { ElasticsearchIndexedProject.count }.by(3)
end
it 'deletes ElasticsearchIndexedProject objects not in elasticsearch_project_ids' do
create :elasticsearch_indexed_project, project: projects.last
expect do
setting.update!(elasticsearch_project_ids: projects.first(2).map(&:id).join(','))
end.to change { ElasticsearchIndexedProject.count }.from(1).to(2)
expect(ElasticsearchIndexedProject.where(project_id: projects.last.id)).not_to exist
end
it 'tells you if a project is allowed to be indexed' do
create :elasticsearch_indexed_project, project: projects.last
expect(setting.elasticsearch_indexes_project?(projects.last)).to be_truthy
expect(setting.elasticsearch_indexes_project?(projects.first)).to be_falsey
end
end
it 'returns projects that are allowed to be indexed' do
project1 = create(:project)
projects = create_list(:project, 3)
setting.update!(
elasticsearch_project_ids: projects.map(&:id).join(','),
elasticsearch_namespace_ids: project1.namespace.id.to_s
)
expect(setting.elasticsearch_limited_projects).to match_array(projects << project1)
end
end
end end
describe 'custom project templates' do describe 'custom project templates' do
......
...@@ -7,6 +7,52 @@ describe Issue, :elastic do ...@@ -7,6 +7,52 @@ describe Issue, :elastic do
let(:project) { create :project } let(:project) { create :project }
context 'when limited indexing is on' do
set(:project) { create :project, name: 'test1' }
set(:issue) { create :issue, project: project}
before do
stub_ee_application_setting(elasticsearch_limit_indexing: true)
end
context 'when the project is not enabled specifically' do
context '#searchable?' do
it 'returns false' do
expect(issue.searchable?).to be_falsey
end
end
end
context 'when a project is enabled specifically' do
before do
create :elasticsearch_indexed_project, project: project
end
context '#searchable?' do
it 'returns true' do
expect(issue.searchable?).to be_truthy
end
end
end
context 'when a group is enabled' do
set(:group) { create(:group) }
before do
create :elasticsearch_indexed_namespace, namespace: group
end
context '#searchable?' do
it 'returns true' do
project = create :project, name: 'test1', group: group
issue = create :issue, project: project
expect(issue.searchable?).to be_truthy
end
end
end
end
it "searches issues" do it "searches issues" do
Sidekiq::Testing.inline! do Sidekiq::Testing.inline! do
create :issue, title: 'bla-bla term1', project: project create :issue, title: 'bla-bla term1', project: project
......
...@@ -5,6 +5,15 @@ describe MergeRequest, :elastic do ...@@ -5,6 +5,15 @@ describe MergeRequest, :elastic do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true) stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
end end
it_behaves_like 'limited indexing is enabled' do
set(:object) { create :merge_request, source_project: project }
set(:group) { create(:group) }
let(:group_object) do
project = create :project, name: 'test1', group: group
create :merge_request, source_project: project
end
end
it "searches merge requests" do it "searches merge requests" do
project = create :project, :repository project = create :project, :repository
......
...@@ -5,6 +5,15 @@ describe Milestone, :elastic do ...@@ -5,6 +5,15 @@ describe Milestone, :elastic do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true) stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
end end
it_behaves_like 'limited indexing is enabled' do
set(:object) { create :milestone, project: project }
set(:group) { create(:group) }
let(:group_object) do
project = create :project, name: 'test1', group: group
create :milestone, project: project
end
end
it "searches milestones" do it "searches milestones" do
project = create :project project = create :project
......
...@@ -5,6 +5,35 @@ describe Note, :elastic do ...@@ -5,6 +5,35 @@ describe Note, :elastic do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true) stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
end end
it_behaves_like 'limited indexing is enabled' do
set(:object) { create :note, project: project }
set(:group) { create(:group) }
let(:group_object) do
project = create :project, name: 'test1', group: group
create :note, project: project
end
context '#searchable?' do
before do
create :elasticsearch_indexed_project, project: project
end
it 'also works on diff notes' do
notes = []
notes << create(:diff_note_on_merge_request, note: "term")
notes << create(:diff_note_on_commit, note: "term")
notes << create(:legacy_diff_note_on_merge_request, note: "term")
notes << create(:legacy_diff_note_on_commit, note: "term")
notes.each do |note|
create :elasticsearch_indexed_project, project: note.noteable.project
expect(note.searchable?).to be_truthy
end
end
end
end
it "searches notes" do it "searches notes" do
issue = create :issue issue = create :issue
......
...@@ -5,6 +5,93 @@ describe Project, :elastic do ...@@ -5,6 +5,93 @@ describe Project, :elastic do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true) stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
end end
context 'when limited indexing is on' do
set(:project) { create :project, name: 'test1' }
before do
stub_ee_application_setting(elasticsearch_limit_indexing: true)
end
context 'when the project is not enabled specifically' do
context '#searchable?' do
it 'returns false' do
expect(project.searchable?).to be_falsey
end
end
context '#use_elasticsearch?' do
it 'returns false' do
expect(project.use_elasticsearch?).to be_falsey
end
end
end
context 'when a project is enabled specifically' do
before do
create :elasticsearch_indexed_project, project: project
end
context '#searchable?' do
it 'returns true' do
expect(project.searchable?).to be_truthy
end
end
context '#use_elasticsearch?' do
it 'returns true' do
expect(project.use_elasticsearch?).to be_truthy
end
end
it 'only indexes enabled projects' do
Sidekiq::Testing.inline! do
# We have to trigger indexing of the previously-created project because we don't have a way to
# enable ES for it before it's created, at which point it won't be indexed anymore
ElasticIndexerWorker.perform_async(:index, project.class.to_s, project.id, project.es_id)
create :project, path: 'test2', description: 'awesome project'
create :project
Gitlab::Elastic::Helper.refresh_index
end
expect(described_class.elastic_search('test*', options: { project_ids: :any }).total_count).to eq(1)
expect(described_class.elastic_search('test2', options: { project_ids: :any }).total_count).to eq(0)
end
end
context 'when a group is enabled' do
set(:group) { create(:group) }
before do
create :elasticsearch_indexed_namespace, namespace: group
end
context '#searchable?' do
it 'returns true' do
project = create :project, name: 'test1', group: group
expect(project.searchable?).to be_truthy
end
end
it 'indexes only projects under the group', :nested_groups do
Sidekiq::Testing.inline! do
create :project, name: 'test1', group: create(:group, parent: group)
create :project, name: 'test2', description: 'awesome project'
create :project, name: 'test3', group: group
create :project, path: 'someone_elses_project', name: 'test4'
Gitlab::Elastic::Helper.refresh_index
end
expect(described_class.elastic_search('test*', options: { project_ids: :any }).total_count).to eq(2)
expect(described_class.elastic_search('test3', options: { project_ids: :any }).total_count).to eq(1)
expect(described_class.elastic_search('test2', options: { project_ids: :any }).total_count).to eq(0)
expect(described_class.elastic_search('test4', options: { project_ids: :any }).total_count).to eq(0)
end
end
end
it "finds projects" do it "finds projects" do
project_ids = [] project_ids = []
......
...@@ -5,6 +5,16 @@ describe Snippet, :elastic do ...@@ -5,6 +5,16 @@ describe Snippet, :elastic do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true) stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
end end
it 'always returns global result for Elasticsearch indexing for #use_elasticsearch?' do
snippet = create :snippet
expect(snippet.use_elasticsearch?).to eq(true)
stub_ee_application_setting(elasticsearch_indexing: false)
expect(snippet.use_elasticsearch?).to eq(false)
end
context 'searching snippets by code' do context 'searching snippets by code' do
let!(:author) { create(:user) } let!(:author) { create(:user) }
let!(:project) { create(:project) } let!(:project) { create(:project) }
......
...@@ -30,4 +30,30 @@ describe Namespace do ...@@ -30,4 +30,30 @@ describe Namespace do
it_behaves_like 'plan helper', namespace_plan it_behaves_like 'plan helper', namespace_plan
end end
end end
describe '#use_elasticsearch?' do
let(:namespace) { create :namespace }
it 'returns false if elasticsearch indexing is disabled' do
stub_ee_application_setting(elasticsearch_indexing: false)
expect(namespace.use_elasticsearch?).to eq(false)
end
it 'returns true if elasticsearch indexing enabled but limited indexing disabled' do
stub_ee_application_setting(elasticsearch_indexing: true, elasticsearch_limit_indexing: false)
expect(namespace.use_elasticsearch?).to eq(true)
end
it 'returns true if it is enabled specifically' do
stub_ee_application_setting(elasticsearch_indexing: true, elasticsearch_limit_indexing: true)
expect(namespace.use_elasticsearch?).to eq(false)
::Gitlab::CurrentSettings.update!(elasticsearch_namespace_ids: namespace.id.to_s)
expect(namespace.use_elasticsearch?).to eq(true)
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
describe ElasticsearchIndexedNamespace do
before do
stub_ee_application_setting(elasticsearch_indexing: true)
end
it_behaves_like 'an elasticsearch indexed container' do
let(:container) { :elasticsearch_indexed_namespace }
let(:attribute) { :namespace_id }
let(:index_action) do
expect(ElasticNamespaceIndexerWorker).to receive(:perform_async).with(subject.namespace_id, :index)
end
let(:delete_action) do
expect(ElasticNamespaceIndexerWorker).to receive(:perform_async).with(subject.namespace_id, :delete)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe ElasticsearchIndexedProject do
before do
stub_ee_application_setting(elasticsearch_indexing: true)
end
it_behaves_like 'an elasticsearch indexed container' do
let(:container) { :elasticsearch_indexed_project }
let(:attribute) { :project_id }
let(:index_action) do
expect(ElasticIndexerWorker).to receive(:perform_async).with(:index, 'Project', subject.project_id, any_args)
end
let(:delete_action) do
expect(ElasticIndexerWorker).to receive(:perform_async).with(:delete, 'Project', subject.project_id, any_args)
end
end
end
...@@ -52,16 +52,18 @@ describe ProjectImportState, type: :model do ...@@ -52,16 +52,18 @@ describe ProjectImportState, type: :model do
context 'no index status' do context 'no index status' do
it 'schedules a full index of the repository' do it 'schedules a full index of the repository' do
expect(ElasticCommitIndexerWorker).to receive(:perform_async).with(import_state.project_id, nil) expect(ElasticCommitIndexerWorker).to receive(:perform_async).with(import_state.project_id, Gitlab::Git::BLANK_SHA)
import_state.finish import_state.finish
end end
end end
context 'with index status' do context 'with index status' do
let!(:index_status) { import_state.project.create_index_status!(indexed_at: Time.now, last_commit: 'foo') } let!(:index_status) { import_state.project.index_status }
it 'schedules a progressive index of the repository' do it 'schedules a progressive index of the repository' do
index_status.update!(indexed_at: Time.now, last_commit: 'foo')
expect(ElasticCommitIndexerWorker).to receive(:perform_async).with(import_state.project_id, index_status.last_commit) expect(ElasticCommitIndexerWorker).to receive(:perform_async).with(import_state.project_id, index_status.last_commit)
import_state.finish import_state.finish
......
...@@ -65,6 +65,47 @@ describe Git::BranchPushService do ...@@ -65,6 +65,47 @@ describe Git::BranchPushService do
execute_service(project, user, oldrev, newrev, ref) execute_service(project, user, oldrev, newrev, ref)
end end
context 'when limited indexing is on' do
before do
stub_ee_application_setting(elasticsearch_limit_indexing: true)
end
context 'when the project is not enabled specifically' do
it 'does not run ElasticCommitIndexerWorker' do
expect(ElasticCommitIndexerWorker).not_to receive(:perform_async)
subject.execute
end
end
context 'when a project is enabled specifically' do
before do
create :elasticsearch_indexed_project, project: project
end
it 'runs ElasticCommitIndexerWorker' do
expect(ElasticCommitIndexerWorker).to receive(:perform_async).with(project.id, oldrev, newrev)
subject.execute
end
end
context 'when a group is enabled' do
let(:group) { create(:group) }
let(:project) { create(:project, :repository, :mirror, group: group) }
before do
create :elasticsearch_indexed_namespace, namespace: group
end
it 'runs ElasticCommitIndexerWorker' do
expect(ElasticCommitIndexerWorker).to receive(:perform_async).with(project.id, oldrev, newrev)
subject.execute
end
end
end
end end
end end
......
# frozen_string_literal: true
shared_examples 'limited indexing is enabled' do
set(:project) { create :project, :repository, name: 'test1' }
before do
stub_ee_application_setting(elasticsearch_limit_indexing: true)
end
context 'when the project is not enabled specifically' do
context '#searchable?' do
it 'returns false' do
expect(object.searchable?).to be_falsey
end
end
end
context 'when a project is enabled specifically' do
before do
create :elasticsearch_indexed_project, project: project
end
context '#searchable?' do
it 'returns true' do
expect(object.searchable?).to be_truthy
end
end
end
context 'when a group is enabled' do
before do
create :elasticsearch_indexed_namespace, namespace: group
end
context '#searchable?' do
it 'returns true' do
expect(group_object.searchable?).to be_truthy
end
end
end
end
# frozen_string_literal: true
shared_examples 'an elasticsearch indexed container' do
describe 'validations' do
subject { create container }
it 'validates uniqueness of main attribute' do
is_expected.to validate_uniqueness_of(attribute)
end
end
describe 'callbacks' do
subject { build container }
describe 'on save' do
it 'triggers index_project' do
is_expected.to receive(:index)
subject.save!
end
it 'performs the expected action' do
index_action
subject.save!
end
end
describe 'on destroy' do
subject { create container }
it 'triggers delete_from_index' do
is_expected.to receive(:delete_from_index)
subject.destroy!
end
it 'performs the expected action' do
delete_action
subject.destroy!
end
end
end
end
...@@ -5,6 +5,26 @@ describe ElasticBatchProjectIndexerWorker do ...@@ -5,6 +5,26 @@ describe ElasticBatchProjectIndexerWorker do
let(:projects) { create_list(:project, 2) } let(:projects) { create_list(:project, 2) }
describe '#perform' do describe '#perform' do
before do
stub_ee_application_setting(elasticsearch_indexing: true)
end
context 'with elasticsearch only enabled for a particular project' do
before do
stub_ee_application_setting(elasticsearch_limit_indexing: true)
create :elasticsearch_indexed_project, project: projects.first
end
it 'only indexes the enabled project' do
projects.each { |project| expect_index(project, false).and_call_original }
expect(Gitlab::Elastic::Indexer).to receive(:new).with(projects.first).and_return(double(run: true))
expect(Gitlab::Elastic::Indexer).not_to receive(:new).with(projects.last)
worker.perform(projects.first.id, projects.last.id)
end
end
it 'runs the indexer for projects in the batch range' do it 'runs the indexer for projects in the batch range' do
projects.each { |project| expect_index(project, false) } projects.each { |project| expect_index(project, false) }
...@@ -32,7 +52,7 @@ describe ElasticBatchProjectIndexerWorker do ...@@ -32,7 +52,7 @@ describe ElasticBatchProjectIndexerWorker do
context 'update_index = false' do context 'update_index = false' do
it 'indexes all projects it receives even if already indexed' do it 'indexes all projects it receives even if already indexed' do
projects.first.build_index_status.update!(last_commit: 'foo') projects.first.index_status.update!(last_commit: 'foo')
expect_index(projects.first, false).and_call_original expect_index(projects.first, false).and_call_original
expect_next_instance_of(Gitlab::Elastic::Indexer) do |indexer| expect_next_instance_of(Gitlab::Elastic::Indexer) do |indexer|
...@@ -45,8 +65,6 @@ describe ElasticBatchProjectIndexerWorker do ...@@ -45,8 +65,6 @@ describe ElasticBatchProjectIndexerWorker do
context 'with update_index' do context 'with update_index' do
it 'reindexes projects that were already indexed' do it 'reindexes projects that were already indexed' do
projects.first.create_index_status!
expect_index(projects.first, true) expect_index(projects.first, true)
expect_index(projects.last, true) expect_index(projects.last, true)
...@@ -54,7 +72,7 @@ describe ElasticBatchProjectIndexerWorker do ...@@ -54,7 +72,7 @@ describe ElasticBatchProjectIndexerWorker do
end end
it 'starts indexing at the last indexed commit' do it 'starts indexing at the last indexed commit' do
projects.first.create_index_status!(last_commit: 'foo') projects.first.index_status.update!(last_commit: 'foo')
expect_index(projects.first, true).and_call_original expect_index(projects.first, true).and_call_original
expect_any_instance_of(Gitlab::Elastic::Indexer).to receive(:run).with('foo') expect_any_instance_of(Gitlab::Elastic::Indexer).to receive(:run).with('foo')
......
...@@ -106,4 +106,31 @@ describe ElasticIndexerWorker, :elastic do ...@@ -106,4 +106,31 @@ describe ElasticIndexerWorker, :elastic do
expect(Elasticsearch::Model.search('*').total_count).to be(0) expect(Elasticsearch::Model.search('*').total_count).to be(0)
end end
it 'indexes all nested objects for a Project' do
# To be able to access it outside the following block
project = nil
Sidekiq::Testing.disable! do
project = create :project, :repository
create :issue, project: project
create :milestone, project: project
create :note, project: project
create :merge_request, target_project: project, source_project: project
create :project_snippet, project: project
end
expect(ElasticCommitIndexerWorker).to receive(:perform_async).with(project.id).and_call_original
# Nothing should be in the index at this point
expect(Elasticsearch::Model.search('*').total_count).to be(0)
Sidekiq::Testing.inline! do
subject.perform("index", "Project", project.id, project.es_id)
end
Gitlab::Elastic::Helper.refresh_index
## All database objects + data from repository. The absolute value does not matter
expect(Elasticsearch::Model.search('*').total_count).to be > 40
end
end end
# frozen_string_literal: true
require 'spec_helper'
describe ElasticNamespaceIndexerWorker, :elastic do
subject { described_class.new }
before do
stub_ee_application_setting(elasticsearch_indexing: true)
stub_ee_application_setting(elasticsearch_limit_indexing: true)
end
it 'returns true if ES disabled' do
stub_ee_application_setting(elasticsearch_indexing: false)
expect(ElasticIndexerWorker).not_to receive(:perform_async)
expect(subject.perform(1, "index")).to be_truthy
end
it 'returns true if limited indexing is not enabled' do
stub_ee_application_setting(elasticsearch_limit_indexing: false)
expect(ElasticIndexerWorker).not_to receive(:perform_async)
expect(subject.perform(1, "index")).to be_truthy
end
describe 'indexing and deleting' do
set(:namespace) { create :namespace }
let(:projects) { create_list :project, 3, namespace: namespace }
it 'indexes all projects belonging to the namespace' do
args = projects.map { |project| [:index, project.class.to_s, project.id, project.es_id] }
expect(ElasticIndexerWorker).to receive(:bulk_perform_async).with(args)
subject.perform(namespace.id, :index)
end
it 'deletes all projects belonging to the namespace' do
args = projects.map { |project| [:delete, project.class.to_s, project.id, project.es_id] }
expect(ElasticIndexerWorker).to receive(:bulk_perform_async).with(args)
subject.perform(namespace.id, :delete)
end
end
end
...@@ -70,5 +70,55 @@ describe PostReceive do ...@@ -70,5 +70,55 @@ describe PostReceive do
described_class.new.perform(gl_repository, key_id, base64_changes) described_class.new.perform(gl_repository, key_id, base64_changes)
end end
context 'when limited indexing is on' do
before do
stub_ee_application_setting(
elasticsearch_search: true,
elasticsearch_indexing: true,
elasticsearch_limit_indexing: true
)
end
context 'when the project is not enabled specifically' do
it 'does not trigger wiki index update' do
expect(ProjectWiki).not_to receive(:new)
described_class.new.perform(gl_repository, key_id, base64_changes)
end
end
context 'when a project is enabled specifically' do
before do
create :elasticsearch_indexed_project, project: project
end
it 'triggers wiki index update' do
expect_next_instance_of(ProjectWiki) do |project_wiki|
expect(project_wiki).to receive(:index_blobs)
end
described_class.new.perform(gl_repository, key_id, base64_changes)
end
end
context 'when a group is enabled' do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
let(:key) { create(:key, user: group.owner) }
before do
create :elasticsearch_indexed_namespace, namespace: group
end
it 'triggers wiki index update' do
expect_next_instance_of(ProjectWiki) do |project_wiki|
expect(project_wiki).to receive(:index_blobs)
end
described_class.new.perform(gl_repository, key_id, base64_changes)
end
end
end
end end
end end
...@@ -32,3 +32,5 @@ module Gitlab ...@@ -32,3 +32,5 @@ module Gitlab
alias_method :has_attribute?, :[] alias_method :has_attribute?, :[]
end end
end end
Gitlab::FakeApplicationSettings.prepend(EE::Gitlab::FakeApplicationSettings)
...@@ -749,6 +749,9 @@ msgstr "" ...@@ -749,6 +749,9 @@ msgstr ""
msgid "Advanced permissions, Large File Storage and Two-Factor authentication settings." msgid "Advanced permissions, Large File Storage and Two-Factor authentication settings."
msgstr "" msgstr ""
msgid "Advanced search functionality"
msgstr ""
msgid "Advanced settings" msgid "Advanced settings"
msgstr "" msgstr ""
...@@ -3742,6 +3745,12 @@ msgstr "" ...@@ -3742,6 +3745,12 @@ msgstr ""
msgid "Elasticsearch integration. Elasticsearch AWS IAM." msgid "Elasticsearch integration. Elasticsearch AWS IAM."
msgstr "" msgstr ""
msgid "Elastic|None. Select namespaces to index."
msgstr ""
msgid "Elastic|None. Select projects to index."
msgstr ""
msgid "Email" msgid "Email"
msgstr "" msgstr ""
...@@ -6327,6 +6336,9 @@ msgstr "" ...@@ -6327,6 +6336,9 @@ msgstr ""
msgid "Licenses" msgid "Licenses"
msgstr "" msgstr ""
msgid "Limit namespaces and projects that can be indexed"
msgstr ""
msgid "Limited to showing %d event at most" msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most" msgid_plural "Limited to showing %d events at most"
msgstr[0] "" msgstr[0] ""
...@@ -6908,6 +6920,9 @@ msgstr "" ...@@ -6908,6 +6920,9 @@ msgstr ""
msgid "Name:" msgid "Name:"
msgstr "" msgstr ""
msgid "Namespaces to index"
msgstr ""
msgid "Naming, tags, avatar" msgid "Naming, tags, avatar"
msgstr "" msgstr ""
...@@ -8307,6 +8322,9 @@ msgstr "" ...@@ -8307,6 +8322,9 @@ msgstr ""
msgid "Projects that belong to a group are prefixed with the group namespace. Existing projects may be moved into a group." msgid "Projects that belong to a group are prefixed with the group namespace. Existing projects may be moved into a group."
msgstr "" msgstr ""
msgid "Projects to index"
msgstr ""
msgid "Projects with write access" msgid "Projects with write access"
msgstr "" msgstr ""
......
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