InfoGrab DocsInfoGrab Docs

고급 검색 마이그레이션 스타일 가이드

요약

이 기능은 GitLab 13.0 이상에서 생성된 인덱스에서만 지원됩니다. scripts/elastic-migration을 실행하고 프롬프트에 따라 다음을 생성합니다: 마이그레이션을 정의하는 마이그레이션 파일: ee/elastic/migrate/YYYYMMDDHHMMSS_migration_name.rb

새 고급 검색 마이그레이션 만들기#

이 기능은 GitLab 13.0 이상에서 생성된 인덱스에서만 지원됩니다.

스크립트로 만들기#

히스토리

scripts/elastic-migration을 실행하고 프롬프트에 따라 다음을 생성합니다:

  • 마이그레이션을 정의하는 마이그레이션 파일: ee/elastic/migrate/YYYYMMDDHHMMSS_migration_name.rb

  • 마이그레이션을 테스트하는 스펙 파일: ee/spec/elastic/migrate/YYYYMMDDHHMMSS_migration_name_spec.rb

  • 마이그레이션을 식별하는 딕셔너리 파일: ee/elastic/docs/YYYYMMDDHHMMSS_migration_name.yml

수동으로 만들기#

히스토리

ee/elastic/migrate/ 폴더에 파일명 형식 YYYYMMDDHHMMSS_migration_name.rb으로 새 파일을 만드세요. 이 형식은 Rails 데이터베이스 마이그레이션과 동일합니다.

# frozen_string_literal: true

class MigrationName < Elastic::Migration
  # Important: Any updates to the Elastic index mappings must be replicated in the respective
  # configuration files:
  #   - `Elastic::Latest::Config`, for the main index.
  #   - `Elastic::Latest::Config`, for standalone indices.

  def migrate
  end

  # Check if the migration has completed
  # Return true if completed, otherwise return false
  def completed?
  end
end

적용된 마이그레이션은 gitlab-#{RAILS_ENV}-migrations 인덱스에 저장됩니다. 실행되지 않은 모든 마이그레이션은 Elastic::MigrationWorker cron worker에 의해 순차적으로 적용됩니다.

Elastic 인덱스 매핑을 업데이트하려면 각 파일에 설정을 적용합니다:

마이그레이션은 재시도 횟수 제한을 설정할 수 있으며 실패 처리 및 중단 표시가 가능합니다. 마이그레이션 재시도를 지원하기 위해 필요한 데이터 또는 인덱스 정리는 마이그레이션 내에서 처리해야 합니다.

건너뛴 마이그레이션#

true 또는 false로 평가되는 skip_if proc를 추가하여 마이그레이션을 건너뛸 수 있습니다:

class MigrationName < Elastic::Migration
  skip_if ->() { true|false }

마이그레이션은 조건이 false인 경우에만 실행됩니다. 건너뛴 마이그레이션은 대기 중인 마이그레이션 목록에 표시되지 않습니다.

건너뛴 마이그레이션은 사용 중단(obsolete)으로 표시할 수 있지만, 이러한 마이그레이션이 항상 건너뛰어지도록 skip_if 조건을 유지해야 합니다. 건너뛴 마이그레이션이 사용 중단되면 변경 사항을 적용하는 유일한 방법은 인덱스를 처음부터 재생성하는 것입니다.

건너뛴 마이그레이션의 문서 파일을 다음 속성으로 업데이트하세요:

skippable: true
skip_condition: '<description>'

인덱스 설정 및 매핑 변경을 위한 마이그레이션#

인덱스 설정 및 매핑 변경은 기존 인덱스에 즉시 적용되지 않고 새로 생성된 인덱스에만 적용됩니다.

설정 변경(예: 분석기 추가)을 적용하려면 다음 중 하나를 선택합니다:

매핑 변경을 적용하려면 다음 중 하나를 선택합니다:

무중단 재인덱스 마이그레이션#

대상 인덱스에 대한 새 인덱스를 생성하고 기존 문서를 복사합니다.

class MigrationName < Elastic::Migration
  def migrate
    Elastic::ReindexingTask.create!(targets: %w[Issue], options: { skip_pending_migrations_check: true })
  end

  def completed?
    true
  end
end

스펙 지원 헬퍼#

다음 헬퍼 메서드는 ee/spec/support/helpers/elasticsearch_helpers.rbElasticsearchHelpers에서 사용할 수 있습니다. ElasticsearchHelpersElasticsearch 스펙 메타데이터를 사용할 때 자동으로 포함됩니다.

assert_names_in_query#

Elasticsearch 쿼리에 이름 있는 쿼리(named queries)가 존재(with)하거나 존재하지 않는(without)지 검증합니다.

assert_fields_in_query#

Elasticsearch 쿼리에 지정된 필드가 포함되어 있는지 검증합니다.

assert_named_queries#

이 메서드는 Elasticsearch에 검색 요청을 전송해야 합니다. 직접 생성된 쿼리를 테스트하려면 assert_names_in_query를 사용하세요.

이름 있는 쿼리(named queries)로 Elasticsearch에 요청이 이루어졌는지 검증합니다. without을 사용하여 이름 있는 쿼리가 요청에 없음을 검증합니다.

assert_routing_field#

특정 라우팅으로 Elasticsearch에 요청이 이루어졌는지 검증합니다.

ensure_elasticsearch_index!#

모든 Elastic::ProcessBookkeepingService 클래스에 대해 execute를 실행하고 refresh_index!를 호출합니다. 이 메서드는 track! 메서드를 사용하여 인덱싱 대기열에 추가된 레코드를 인덱싱합니다.

refresh_index!#

모든 인덱스(마이그레이션 인덱스 포함)에서 Elasticsearch 인덱스 새로 고침을 수행합니다. 이를 통해 인덱스에서 최근 수행된 작업을 검색에 사용할 수 있습니다.

set_elasticsearch_migration_to#

마이그레이션 인덱스의 현재 마이그레이션을 특정 마이그레이션(이름 또는 버전 기준)으로 설정합니다. 마이그레이션은 기본적으로 완료된 것으로 표시되며 including: false를 전달하여 대기 중 상태로 설정할 수 있습니다.

es_helper#

Gitlab::Elastic::Helper.default의 인스턴스를 제공합니다.

es_client#

Gitlab::Search::Client.new의 인스턴스를 제공합니다.

warm_elasticsearch_migrations_cache!#

각 마이그레이션에 대해 migration_has_finished?를 호출하여 ::Elastic::DataMigrationService 마이그레이션 캐시를 준비합니다.

elastic_wiki_indexer_worker_random_delay_range#

0과 ElasticWikiIndexerWorker::MAX_JOBS_PER_HOUR 사이의 임의 지연값을 반환합니다.

elastic_delete_group_wiki_worker_random_delay_range#

0과 Search::Wiki::ElasticDeleteGroupWikiWorker::MAX_JOBS_PER_HOUR 사이의 임의 지연값을 반환합니다.

elastic_group_association_deletion_worker_random_delay_range#

0과 Search::ElasticGroupAssociationDeletionWorker::MAX_JOBS_PER_HOUR 사이의 임의 지연값을 반환합니다.

items_in_index#

제공된 인덱스 이름에 존재하는 id의 배열을 반환합니다.

백필 헬퍼 선택#

필드를 백필할 때 사용할 헬퍼를 결정하려면 다음 표를 참조하세요:

MigrationBackfillHelper MigrationProjectScopedUpdateByQueryHelper
메커니즘 필드가 없는 문서를 검색하고 참조를 빌드한 후 재인덱싱을 위한 북키핑 파이프라인을 통해 추적합니다. 필드가 없는 문서를 가진 프로젝트를 검색하고 Painless 스크립트로 비동기 update_by_query 작업을 실행합니다. 이중 모드 전략을 사용합니다: 대규모 프로젝트는 안전 모드(하나씩 처리), 소규모 프로젝트는 속도 모드(여러 개를 함께 배치 처리 가능).
적합한 용도 재인덱싱 시 인덱서가 필드를 채우는 ActiveRecord 모델(DOCUMENT_TYPE)에서 지원하는 인덱스. 필드 값이 프로젝트/네임스페이스에서 파생되고 인덱스가 Go 기반 인덱서로 채워지는 프로젝트 범위 인덱스(커밋, blob, 위키).
배치 제한 검색당 10,000개 문서(Elasticsearch 기본값). 작업당 설정 가능한 batch_size(안전 모드)와 배치 업데이트용 speed_mode_batch_size(속도 모드). 두 값 모두 마이그레이션 상태를 통해 런타임에 조정 가능.
동시성 순차 처리. 동시 처리 — 최대 max_concurrent_tasks(기본값: 50)개 프로젝트를 동시에 처리. 마이그레이션 상태를 통해 런타임에 조정 가능.
최적화 없음 - 항상 순차 처리. 프로젝트 크기에 따라 자동 최적화: 대규모 프로젝트(문서 10,000개 이상)는 신중하게 처리, 소규모 프로젝트는 배치 처리.

마이그레이션 헬퍼#

다음 마이그레이션 헬퍼는 ee/app/workers/concerns/elastic/에서 사용할 수 있습니다:

Search::Elastic::MigrationBackfillHelper#

인덱스의 특정 필드를 백필합니다.

요구 사항:

  • 필드의 매핑이 이미 추가되어 있어야 합니다.

  • 필드는 항상 값을 가져야 합니다. 필드가 null이 될 수 있는 경우 Search::Elastic::MigrationReindexBasedOnSchemaVersion을 사용합니다.

  • 단일 필드의 경우 field_name 메서드와 DOCUMENT_TYPE 상수를 정의합니다.

  • 여러 필드의 경우 field_names 메서드와 DOCUMENT_TYPE 상수를 정의합니다.

이 헬퍼는 쿼리당 10,000개 항목의 배치 크기 제한이 있으며, 이는 Elasticsearch의 기본 검색 결과 제한입니다. 이 제약은 대규모 데이터셋을 백필할 때 성능에 상당한 영향을 미칩니다. 마이그레이션이 더 작은 배치로 데이터를 천천히 반복해야 하기 때문입니다.

단일 필드 예시:

class MigrationName < Elastic::Migration
  include ::Search::Elastic::MigrationBackfillHelper

  batched!
  batch_size 9_000
  throttle_delay 1.minute

  DOCUMENT_TYPE = Issue

  private

  def field_name
    :schema_version
  end
end

여러 필드 예시:

class MigrationName < Elastic::Migration
  include ::Search::Elastic::MigrationBackfillHelper

  batched!
  batch_size 9_000
  throttle_delay 1.minute

  DOCUMENT_TYPE = Issue

  private

  def field_names
    %w[schema_version visibility_level]
  end
end

이 마이그레이션은 'migration backfills fields' 공유 예시로 테스트할 수 있습니다.

describe MigrationName, :elastic_delete_by_query, :sidekiq_inline, feature_category: :global_search do
  include_examples 'migration backfills fields' do
    let_it_be(:project) { create(:project) }
    let(:version) { 20251204143000 }
    let(:expected_throttle_delay) { 1.minute }
    let(:expected_batch_size) { 9000 }
    let(:objects) { create_list(:milestone, 3, project: project) }
    let(:expected_fields) { { traversal_ids: project.elastic_namespace_ancestry } }
  end
end

Search::Elastic::MigrationUpdateMappingsHelper#

지정된 매핑으로 put_mapping을 호출하여 인덱스의 매핑을 업데이트합니다.

new_mappings 메서드와 DOCUMENT_TYPE 상수가 필요합니다.

class MigrationName < Elastic::Migration
  include ::Search::Elastic::MigrationUpdateMappingsHelper

  DOCUMENT_TYPE = Issue

  private

  def new_mappings
    {
      schema_version: {
        type: 'short'
      }
    }
  end
end

이 마이그레이션은 'migration adds mapping' 공유 예시로 테스트할 수 있습니다.

describe 'migration', :elastic, :sidekiq_inline do
  include_examples 'migration adds mapping'
end

Search::Elastic::MigrationRemoveFieldsHelper#

인덱스에서 지정된 필드를 제거합니다.

DOCUMENT_TYPE과 일치하는 문서 중 지정된 필드를 가진 것이 있는지 배치로 확인합니다. 문서가 존재하면 Painless 스크립트를 사용하여 update_by_query를 수행합니다.

  • 단일 필드의 경우 field_to_remove 메서드와 DOCUMENT_TYPE 상수를 정의합니다.

  • 여러 필드의 경우 fields_to_remove 메서드와 DOCUMENT_TYPE 상수를 정의합니다.

class MigrationName < Elastic::Migration
  include ::Search::Elastic::MigrationRemoveFieldsHelper

  batched!
  throttle_delay 1.minute

  DOCUMENT_TYPE = User

  private

  def fields_to_remove
    %w[two_factor_enabled has_projects]
  end
end

기본 배치 크기는 10_000입니다. BATCH_SIZE를 지정하여 이 값을 재정의할 수 있습니다:

class MigrationName < Elastic::Migration
  include ::Search::Elastic::MigrationRemoveFieldsHelper

  batched!
  BATCH_SIZE = 100

  ...
end

이 마이그레이션은 'migration removes field' 공유 예시로 테스트할 수 있습니다.

include_examples 'migration removes field' do
  let(:expected_throttle_delay) { 1.minute }
  let(:objects) { create_list(:work_item, 6) }
  let(:index_name) { ::Search::Elastic::Types::WorkItem.index_name }
  let(:field) 
  let(:type) { 'long' }
end

매핑에 type 외의 항목이 포함된 경우 type 변수를 생략하고 대신 mapping을 정의합니다:

include_examples 'migration removes field' do
  let(:expected_throttle_delay) { 1.minute }
  let(:objects) { create_list(:work_item, 6) }
  let(:index_name) { ::Search::Elastic::Types::WorkItem.index_name }
  let(:field) 
  let(:mapping) { { type: 'dense_vector', dims: 768, index: true, similarity: 'cosine' } }
end

expecting token of type [VALUE_NUMBER] but found [FIELD_NAME] 오류가 발생하면 value 변수를 정의합니다:

include_examples 'migration removes field' do
  let(:expected_throttle_delay) { 1.minute }
  let(:objects) { create_list(:work_item, 6) }
  let(:index_name) { ::Search::Elastic::Types::WorkItem.index_name }
  let(:field) 
  let(:mapping) { { type: 'dense_vector', dims: 768, index: true, similarity: 'cosine' } }
  let(:value) { Array.new(768, 1) }
end

Search::Elastic::MigrationObsolete#

마이그레이션이 더 이상 필요하지 않을 때 사용 중단으로 표시합니다.

class MigrationName < Elastic::Migration
  include ::Search::Elastic::MigrationObsolete
end

건너뛸 수 있는 마이그레이션을 사용 중단으로 표시할 때는 skip_if 조건을 유지해야 합니다.

이 마이그레이션은 'a deprecated advanced search migration' 공유 예시로 테스트할 수 있습니다. 마이그레이션을 사용 중단으로 표시하는 프로세스를 따르세요.

Search::Elastic::MigrationCreateIndexHelper#

새 인덱스를 생성합니다.

요구 사항:

  • target_classdocument_type 메서드

  • 클래스에 대한 매핑 및 인덱스 설정

같은 마일스톤에서 인덱스를 채우기 위한 후속 마이그레이션을 수행해야 합니다.

class MigrationName < Elastic::Migration
  include ::Search::Elastic::MigrationCreateIndexHelper

  retry_on_failure

  def document_type
    :epic
  end

  def target_class
    Epic
  end
end

이 마이그레이션은 'migration creates a new index' 공유 예시로 테스트할 수 있습니다.

it_behaves_like 'migration creates a new index', 20240501134252, WorkItem

Search::Elastic::MigrationReindexTaskHelper#

새 인덱스를 생성하고 기존 데이터를 새 인덱스로 복사하는 재인덱스 작업을 만듭니다.

요구 사항:

  • targets 메서드.

targets 항목은 유효한 Ruby 상수 문자열(constantize를 통해 확인 가능)이어야 합니다. 네임스페이스가 있는 클래스의 경우 전체 경로를 사용합니다. 예를 들어 MigrationIndexConfig 대신 Elastic::MigrationIndexConfig를 사용합니다.

class MigrationName < Elastic::Migration
  include ::Search::Elastic::MigrationReindexTaskHelper

  def targets
    %w[MergeRequest]
  end
end

다음 스펙으로 이 마이그레이션을 테스트할 수 있습니다.

let(:migration) { described_class.new(version) }
let(:task) { Search::Elastic::ReindexingTask.last }
let(:targets) { %w[MergeRequest] }

it 'does not have migration options set', :aggregate_failures do
  expect(migration).not_to be_batched
  expect(migration).not_to be_retry_on_failure
end

describe '#migrate', :aggregate_failures do
  it 'creates reindexing task with correct target and options' do
    expect { migration.migrate }.to change { Search::Elastic::ReindexingTask.count }.by(1)
    expect(task.targets).to eq(targets)
    expect(task.options).to eq('skip_pending_migrations_check' => true)
  end
end

describe '#completed?' do
  it 'always returns true' do
    expect(migration.completed?).to be(true)
  end
end

Search::Elastic::MigrationReindexBasedOnSchemaVersion#

지정된 문서 타입을 저장하는 인덱스의 모든 문서를 재인덱싱하고 schema_version을 업데이트합니다.

DOCUMENT_TYPENEW_SCHEMA_VERSION 상수가 필요합니다. 인덱스 매핑에는 YYVV(연도/버전) 형식의 schema_version 정수 필드가 있어야 합니다.

이 마이그레이션 헬퍼는 스크롤 API를 사용하여 더 큰 배치(잠재적으로 10,000개 레코드 초과)를 더 효율적으로 처리합니다. 큐 임계값은 북키핑 큐가 용량을 초과할 때 마이그레이션을 일시 중지하여 다운스트림 처리가 과부하되지 않도록 합니다.

이전 인덱스 매핑 schema_versionYYMM 또는 YYWW 형식을 사용했습니다. 새 버전은 YYVV 형식을 사용해야 합니다.

class MigrationName < Elastic::Migration
  include Search::Elastic::MigrationReindexBasedOnSchemaVersion

  batched!
  batch_size 9_000
  throttle_delay 1.minute

  DOCUMENT_TYPE = WorkItem
  NEW_SCHEMA_VERSION = 24_46
  UPDATE_BATCH_SIZE = 100
end

이 마이그레이션은 'migration reindex based on schema_version' 공유 예시로 테스트할 수 있습니다.

include_examples 'migration reindex based on schema_version' do
  let(:expected_throttle_delay) { 1.minute }
  let(:expected_batch_size) { 9_000 }
  let(:objects) { create_list(:project, 3) }
end

Search::Elastic::MigrationDeleteBasedOnSchemaVersion#

지정된 문서 타입을 저장하는 인덱스에서 schema_version이 주어진 값보다 낮은 모든 문서를 삭제합니다.

DOCUMENT_TYPE 상수와 schema_version 메서드가 필요합니다. 인덱스 매핑에는 YYVV(연도/버전) 형식의 schema_version 정수 필드가 있어야 합니다.

이전 인덱스 매핑 schema_versionYYMM 또는 YYWW 형식을 사용했습니다. 새 버전은 YYVV 형식을 사용해야 합니다.

class MigrationName < Elastic::Migration
  include ::Search::Elastic::MigrationDeleteBasedOnSchemaVersion

  DOCUMENT_TYPE = Issue

  batch_size 10_000
  batched!
  throttle_delay 1.minute
  retry_on_failure

  def schema_version
    23_12
  end
end

이 마이그레이션은 'migration deletes documents based on schema version' 공유 예시로 테스트할 수 있습니다.

include_examples 'migration deletes documents based on schema version' do
  let(:objects) { create_list(:issue, 3) }
  let(:expected_throttle_delay) { 1.minute }
  let(:expected_batch_size) { 20000 }
end

Search::Elastic::MigrationDatabaseBackfillHelper#

limited_indexing 설정을 존중하면서 데이터베이스의 모든 문서를 Elasticsearch 인덱스로 재인덱싱합니다.

DOCUMENT_TYPE 상수와 respect_limited_indexing? 메서드가 필요합니다.

class MigrationName < Elastic::Migration
  include ::Search::Elastic::MigrationDatabaseBackfillHelper

  batch_size 10_000
  batched!
  throttle_delay 1.minute
  retry_on_failure

  DOCUMENT_TYPE = Issue

  def respect_limited_indexing?
    true
  end
end

이 마이그레이션은 'migration reindexes all data' 공유 예시로 테스트할 수 있습니다. 팩토리 이름이 DOCUMENT_TYPE과 다른 경우 올바른 팩토리 이름으로 factory_to_create_objects를 재정의합니다.

include_examples 'migration reindexes all data' do
  let(:objects) { create_list(:issue, 3) }
  let(:factory_to_create_objects) 
  let(:expected_throttle_delay) { 30.seconds }
  let(:expected_batch_size) { 30_000 }
end

Search::Elastic::MigrationProjectScopedUpdateByQueryHelper#

비동기 update_by_query 작업을 프로젝트별로 그룹화하여 프로젝트 범위 인덱스(커밋, blob, 위키)의 필드를 백필합니다. 북키핑 파이프라인을 통해 문서를 재인덱싱하는 MigrationBackfillHelper와 달리, 이 헬퍼는 Elasticsearch에서 직접 Painless 스크립트로 문서를 제자리 업데이트합니다.

이 헬퍼는 Rails 북키핑 파이프라인(Elastic::ProcessBookkeepingService)이 아닌 Go 기반 인덱서(gitlab-elasticsearch-indexer)로 채워지는 인덱스를 위한 것입니다. ActiveRecord 모델로 지원되는 인덱스에는 MigrationBackfillHelper 또는 MigrationReindexBasedOnSchemaVersion을 대신 사용하세요.

이중 모드 마이그레이션 전략#

이 헬퍼는 프로젝트 크기에 따라 전략을 자동으로 조정합니다:

  • 안전 모드: 대규모 프로젝트(기본적으로 10,000개 이상의 문서)의 경우 - 신중한 동시성 제어로 한 번에 하나의 프로젝트를 처리합니다.

  • 속도 모드: 소규모 프로젝트의 경우 - 더 빠른 처리를 위해 단일 업데이트에서 여러 프로젝트를 일괄 처리할 수 있습니다.

헬퍼는 남은 프로젝트 중 large_project_threshold를 초과하는 문서 수를 가진 것이 있는지 확인하여 사용할 모드를 결정합니다. 대규모 프로젝트가 남아 있을 때는 안전 모드로 먼저 실행되고, 모든 대규모 프로젝트가 완료된 후 속도 모드로 자동 전환됩니다.

필수 메서드#

마이그레이션은 네 가지 메서드를 구현해야 합니다:

# The document type to filter by (e.g., 'commit', 'blob', 'wiki_blob')
def document_type_value
  'commit'
end

# The field being backfilled
def field_name
  'traversal_ids'
end

# Painless script for updating a single project's documents
# Used in safe mode (large projects) and as fallback in speed mode
def update_script(project)
  {
    source: "ctx._source.traversal_ids = params.traversal_ids",
    params: { traversal_ids: project.namespace.traversal_ids }
  }
end

# Painless script for batch updating multiple projects at once
# Used in speed mode for small projects
# Must be implemented by all migrations using this helper
def batch_update_script(projects)
  # Convert IDs to strings to match ES keyword fields
  project_values = projects.index_by { |p| p.id.to_s }.transform_values(&:namespace_ancestry)
  {
    source: "ctx._source.traversal_ids = params.project_values[ctx._source.rid.toString()]",
    params: { project_values: project_values }
  }
end
필수: 배치 처리 구현#

마이그레이션은 속도 모드에서 배치 처리를 활성화하기 위해 batch_update_script(projects) 메서드를 구현해야 합니다:

# Returns a Painless script that works across multiple projects
# Receives an array of Project objects in this batch
# @param projects [Array] Projects being batched together
# @return [Hash] Script with :source and :params keys
def batch_update_script(projects)
  # Build a lookup map from project data
  # IMPORTANT: Convert project IDs to strings to match keyword fields in Elasticsearch
  project_values = projects.index_by { |p| p.id.to_s }.transform_values(&:namespace_ancestry)
  {
    source: "ctx._source.traversal_ids = params.project_values[ctx._source.rid.toString()]",
    params: { project_values: project_values }
  }
end
선택적 재정의#
# Override to change the Elasticsearch field storing project ID (default: 'rid')
def project_id_field
  'rid'
end

# Override to customize eager loading (default: Project.with_namespace)
def project_relation
  Project.includes(:namespace, :route)
end
마이그레이션 상태를 통한 설정#

모든 마이그레이션 파라미터는 코드 변경 없이 런타임에 조정할 수 있습니다:

파라미터 기본값 설명
batch_size 상속됨 update_by_query 작업당 최대 문서 수
max_concurrent_tasks 50 허용되는 최대 동시 업데이트 작업 수
speed_mode_batch_size 10,000 속도 모드에서 프로젝트를 일괄 처리할 때 최대 문서 수
large_project_threshold 10,000 "대규모" 프로젝트의 문서 수 임계값

Rails 콘솔 사용:

migration = Elastic::DataMigrationService[20260216153009].send(:migration)
migration.set_migration_state(
  migration.migration_state.merge(
    batch_size: 5_000,
    max_concurrent_tasks: 100,
    speed_mode_batch_size: 20_000,
    large_project_threshold: 15_000
  )
)

또는 Elasticsearch에서 직접 마이그레이션 상태를 업데이트합니다. 마이그레이션 상태는 #{target_name}-migrations 인덱스(예: gitlab-production-migrations)에 저장됩니다. 각 마이그레이션 문서는 버전 번호로 인덱싱되며 상태는 state 필드에 있습니다. 마이그레이션 버전을 문서 ID로 사용하여 Update API를 사용하세요:

# Update migration parameters for migration 20260216153009
curl --request POST "localhost:9200/gitlab-production-migrations/_update/20260216153009" \
  --header 'Content-Type: application/json' --data'
{
  "script": {
    "source": "ctx._source.state.max_concurrent_tasks = params.max_tasks; ctx._source.state.batch_size = params.batch_size; ctx._source.state.speed_mode_batch_size = params.speed_batch_size; ctx._source.state.large_project_threshold = params.threshold",
    "params": {
      "max_tasks": 100,
      "batch_size": 5000,
      "speed_batch_size": 20000,
      "threshold": 15000
    }
  }
}'
마이그레이션 예시#
class BackfillTraversalIdsOnCommits < Elastic::Migration
  include ::Search::Elastic::MigrationProjectScopedUpdateByQueryHelper

  batched!
  batch_size 10_000
  throttle_delay 5.seconds
  retry_on_failure

  def index_name
    ::Elastic::Latest::CommitConfig.index_name
  end

  private

  def document_type_value
    'commit'
  end

  def field_name
    'traversal_ids'
  end

  def update_script(project)
    {
      source: "ctx._source.traversal_ids = params.traversal_ids",
      params: { traversal_ids: project.elastic_namespace_ancestry }
    }
  end

  # Enable batching for faster processing in speed mode
  def batch_update_script(projects)
    # Convert IDs to strings to match keyword fields in ES
    project_values = projects.index_by { |p| p.id.to_s }.transform_values(&:namespace_ancestry)
    {
      source: "ctx._source.traversal_ids = params.project_values[ctx._source.rid.toString()]",
      params: { project_values: project_values }
    }
  end
end
마이그레이션 진행 상황 및 로깅#

헬퍼는 현재 모드를 기록하고 더 나은 관찰성을 위해 문서 수를 포함합니다:

Running migration in safe mode (processing large projects)
In Progress: update_by_query task, project_id: 123, document_count: 15000
Completed: update_by_query task, project_id: 123, document_count: 15000

Running migration in speed mode (batching small projects)
In Progress: update_by_query task, project_id: 456, document_count: 2000
Completed: update_by_query task, project_id: 456, document_count: 2000

'migration backfills a field using project-scoped update_by_query' 공유 예시로 이 마이그레이션을 테스트할 수 있습니다. 사용하는 스펙은 it_behaves_like 블록 내에 두 가지 메서드를 정의해야 합니다:

  • index_documents_for_projects(projects) — 주어진 프로젝트에 대한 문서를 인덱싱합니다.

  • remove_field_from_indexed_documents(project_ids) — 인덱싱된 문서에서 타깃 필드를 제거합니다(rid가 키워드 필드이므로 프로젝트 ID는 문자열입니다).

RSpec.describe BackfillTraversalIdsOnCommits, :elastic, :sidekiq_inline, feature_category: :global_search do
  let(:version) { 20260216153009 }
  let(:version_mapping_migration) { 20260216152309 }
  let(:expected_batch_size) { 10_000 }
  let(:expected_throttle_delay) { 5.seconds }

  let_it_be_with_reload(:projects) { create_list(:project, 3, :repository) }

  it_behaves_like 'migration backfills a field using project-scoped update_by_query' do
    def index_documents_for_projects(projects)
      projects.each { |p| p.repository.index_commits_and_blobs }
    end

    def remove_field_from_indexed_documents(project_ids)
      client = Gitlab::Search::Client.new

      client.update_by_query({
        index: index_name,
        wait_for_completion: true,
        refresh: true,
        body: {
          script: {
            source: "ctx._source.remove('traversal_ids');",
            lang: "painless"
          },
          query: {
            bool: {
              must: [{ exists: { field: 'traversal_ids' } }],
              filter: [{ terms: { rid: project_ids } }]
            }
          }
        }
      })
    end
  end
end

Search::Elastic::MigrationHelper#

이전 예시에 맞지 않는 마이그레이션에서 사용할 수 있는 메서드를 포함합니다.

class MigrationName < Elastic::Migration
  include ::Search::Elastic::MigrationHelper

  def migrate
  ...
  end

  def completed?
  ...
  end
end

Elastic::MigrationWorker에서 지원하는 마이그레이션 옵션#

Elastic::MigrationWorker는 다음 마이그레이션 옵션을 지원합니다:

  • batched! - 마이그레이션을 배치로 실행할 수 있게 허용합니다. 설정하면 Elastic::MigrationWorker가 아래에 설명된 throttle_delay 옵션으로 설정된 지연을 두고 자신을 다시 큐에 추가합니다. 배치 처리는 migrate 메서드에서 처리해야 합니다. 이 설정은 재큐잉만 제어합니다.

  • batch_size - batched! 마이그레이션 실행 중 수정되는 문서 수를 설정합니다. 이 크기는 업데이트가 완료될 수 있도록 충분한 시간을 허용하는 값으로 설정해야 합니다. 아래에 설명된 throttle_delay 옵션과 함께 조정할 수 있습니다. 배치 처리는 커스텀 migrate 메서드 또는 이 설정을 사용하는 Search::Elastic::MigrationBackfillHelper migrate 메서드에서 처리해야 합니다. 기본값은 1000개 문서입니다.

  • throttle_delay - 배치 실행 사이의 대기 시간을 설정합니다. 이 시간은 각 마이그레이션 배치가 완료될 수 있도록 충분히 높게 설정해야 합니다. 또한, Elastic::MigrationWorker cron worker가 실행되는 빈도인 5분 미만으로 설정해야 합니다. 기본값은 3분입니다.

  • pause_indexing! - 마이그레이션이 실행되는 동안 인덱싱을 일시 중지합니다. 이 설정은 마이그레이션이 실행되기 전의 인덱싱 설정을 기록하고 마이그레이션이 완료되면 해당 값으로 다시 설정합니다.

  • space_requirements! - 마이그레이션이 실행될 때 클러스터에서 충분한 여유 공간이 있는지 확인합니다. 이 설정은 마이그레이션이 실행될 때 필요한 스토리지를 사용할 수 없는 경우 마이그레이션을 중단합니다. 마이그레이션은 space_required_bytes 메서드를 정의하여 필요한 공간을 바이트 단위로 제공해야 합니다.

  • retry_on_failure - 실패 시 재시도 기능을 활성화합니다. 기본적으로 마이그레이션을 30번 재시도합니다. 재시도 횟수가 소진되면 마이그레이션은 중단됨으로 표시됩니다. 재시도 횟수를 사용자 지정하려면 max_attempts 인수를 전달합니다: retry_on_failure max_attempts: 10

# frozen_string_literal: true

class BatchedMigrationName < Elastic::Migration
  # Declares a migration should be run in batches
  batched!
  throttle_delay 10.minutes
  pause_indexing!
  space_requirements!
  retry_on_failure

  # ...
end

마이그레이션에서 다운타임 방지#

마이그레이션 되돌리기#

마이그레이션이 GitLab.com에서 실패하거나 중단되면, 마이그레이션을 도입한 변경 사항을 되돌리는 것을 선호합니다. 이렇게 하면 Self-managed 고객이 손상된 마이그레이션을 받지 않고 백포트의 필요성이 줄어듭니다.

다중 버전 호환성#

고급 검색 마이그레이션은 다른 GitLab 변경 사항과 마찬가지로 여러 버전의 애플리케이션이 동시에 실행되는 경우를 지원해야 합니다.

배포 순서에 따라 마이그레이션이 시작되거나 완료된 상태에서 마이그레이션 이전의 애플리케이션 코드로 실행 중인 서버가 여전히 있을 수 있습니다. 모든 고급 검색 마이그레이션이 배포 완료 후 시작되도록 보장할 수 있을 때까지 이를 고려해야 합니다.

위험도 높은 마이그레이션#

Elasticsearch는 트랜잭션을 지원하지 않으므로, 마이그레이션이 시작된 후 또는 완료된 후 애플리케이션 코드가 되돌려지는 상황을 수용할 수 있도록 항상 마이그레이션을 설계해야 합니다.

이러한 이유로 일반적으로 파괴적인 작업(예: 일부 데이터를 이동한 후 삭제)은 마이그레이션이 성공적으로 완료된 후 이후 머지 리퀘스트로 연기합니다. 중요한 데이터 손실 위험이 있는 경우 Self-managed 고객을 위해 다른 릴리즈로도 연기해야 합니다.

마이그레이션 실행 시간 계산#

마이그레이션이 GitLab.com에서 실행되는 데 얼마나 걸릴지 이해하는 것이 중요합니다. 마이그레이션에서 처리될 문서 수를 도출하세요. 이 수치는 데이터베이스 또는 기존 Elasticsearch 인덱스를 쿼리하여 얻을 수 있습니다. 다음 공식을 사용하여 실행 시간을 계산합니다:

> batch_size = 9_000
=> 9000
> throttle_delay = 1.minute
=> 1 minute
> number_of_documents = 15_536_906
=> 15536906
> (number_of_documents / batch_size) * throttle_delay
=> 1726 minutes
> (number_of_documents / batch_size) * throttle_delay / 1.hour
=> 28

고급 검색 마이그레이션 모범 사례#

최선의 결과를 위해 다음 모범 사례를 따르세요:

  • 각 문서 타입에 대한 모든 마이그레이션을 정렬하여 Search::Elastic::MigrationUpdateMappingsHelper를 사용하는 마이그레이션이 Search::Elastic::MigrationBackfillHelper를 사용하는 마이그레이션보다 먼저 실행되도록 합니다. 이렇게 하면 모든 마이그레이션이 적용되지 않은 경우 동일한 문서를 여러 번 재인덱싱하지 않고 백필 시간이 줄어듭니다.

  • 배치로 작업할 때 배치 크기를 9,000개 문서 미만으로 유지합니다. 대량 인덱서는 매분 실행되며 10,000개 문서의 배치를 처리하도록 설정되어 있습니다. 이렇게 하면 대량 인덱서가 다른 마이그레이션 배치를 시도하기 전에 레코드를 처리할 시간이 있습니다.

  • 문서 수가 최신 상태인지 확인하려면 마이그레이션이 완료되었는지 확인하기 전에 인덱스를 새로 고쳐야 합니다.

  • 마이그레이션이 시작될 때, 완료 여부 확인이 발생할 때, 마이그레이션이 완료될 때 각 마이그레이션에 로깅 문을 추가합니다. 이러한 로그는 마이그레이션 문제를 디버깅할 때 유용합니다.

  • Elasticsearch Reindex API 작업을 사용하는 경우 인덱싱을 일시 중지합니다.

  • 마이그레이션이 실패할 가능성이 있는 경우 재시도 제한을 추가하는 것을 고려합니다. 이렇게 하면 문제가 발생할 경우 마이그레이션이 중단될 수 있습니다.

  • 마이그레이션 파일 내에 인덱스 매핑과 설정을 인라인으로 포함합니다. 이렇게 하면 공유 설정 클래스의 향후 코드 변경으로부터 마이그레이션을 보호합니다. 외부 클래스나 모듈을 참조하는 대신 private 메서드에서 매핑과 설정을 정의합니다. 이는 시간이 지나도 마이그레이션이 안정적으로 유지되도록 데이터베이스 마이그레이션과 동일한 패턴을 따릅니다.

매핑 및 설정 인라인화#

인덱스를 생성하거나 업데이트할 때 private 메서드를 사용하여 마이그레이션 파일에 직접 매핑과 설정을 정의합니다:

class MigrationName < Elastic::Migration
  include ::Search::Elastic::MigrationHelper

  def migrate
    helper.create_index(
      index_name: 'my-index',
      settings: index_settings,
      mappings: index_mappings
    )
  end

  private

  def index_settings
    {
      number_of_shards: 1,
      number_of_replicas: 1
    }
  end

  def index_mappings
    {
      properties: {
        id: { type: 'keyword' },
        name: { type: 'text' },
        created_at: { type: 'date' }
      }
    }
  end
end

이 접근 방식은 향후 GitLab 버전에서 공유 설정 클래스가 변경되더라도 마이그레이션이 올바르게 계속 작동하도록 합니다.

고급 검색 마이그레이션 정리#

고급 검색 마이그레이션은 일반적으로 오랜 기간 동안 여러 코드 경로를 지원해야 하므로, 안전하게 할 수 있을 때 이를 정리하는 것이 중요합니다.

GitLab 필수 중단점을 완전히 마이그레이션되지 않은 인덱스에 대한 하위 호환성을 안전하게 제거하는 시기로 선택합니다. 업그레이드 문서에 이를 기록합니다.

GitLab Housekeeper는 정리 프로세스를 자동화하는 데 사용됩니다. 이 프로세스에는 기존 마이그레이션을 사용 중단으로 표시하고 사용 중단된 마이그레이션을 삭제하는 작업이 포함됩니다. 마이그레이션이 사용 중단으로 표시되면 마이그레이션 코드는 사용 중단 마이그레이션 코드로 교체되고 테스트는 사용 중단 마이그레이션 공유 예시로 교체됩니다:

  • 고급 검색 마이그레이션에서 호출되는 코드를 유지 관리할 필요가 없습니다.

  • 더 이상 지원하지 않는 마이그레이션에 대한 테스트를 실행하는 데 CI 시간을 낭비하지 않습니다.

  • 이 마이그레이션을 실행하지 않고 타깃 버전으로 직접 업그레이드하는 운영자에게 처음부터 재인덱싱하라는 메시지가 표시됩니다.

마지막 필수 중단점 직전의 마지막 마이너 버전에서 생성된 마이그레이션은 정리하지 않는 것이 더욱 안전합니다. 예를 들어 마지막 필수 중단점이 %14.0이었다면 %13.12에서만 추가된 마이그레이션은 정리하지 않아야 합니다. 이 추가적인 안전망은 GitLab.com에서 완료되는 데 여러 주가 걸릴 수 있는 마이그레이션을 허용합니다. GitLab.com 배포가 자동화되어 있고 이 정리를 방지하는 자동화된 검사가 없기 때문에 추가적인 주의가 필요합니다. 또한 자동화된 검사가 있더라도 고급 검색 마이그레이션 때문에 GitLab.com 배포를 차단하고 싶지 않을 것입니다. 마이그레이션이 아직 완료되는 데 일주일 정도 남아 있을 수 있으며 배포를 너무 오래 차단하게 됩니다.

마이그레이션을 사용 중단으로 표시하는 프로세스#

Keeps::MarkOldAdvancedSearchMigrationsAsObsolete Keep을 수동으로 실행하여 마이그레이션을 사용 중단으로 표시합니다.

마지막 필수 중단점 이전 두 버전에서 생성된 모든 마이그레이션에 대해 Keep은 다음을 수행합니다:

  • 마이그레이션의 내용을 유지하고 하단에 prepend를 추가합니다:
 ClassName.prepend ::Search::Elastic::MigrationObsolete
  • 스펙 파일 내용을 'a deprecated advanced search migration' 공유 예시로 교체합니다.

  • Global Search 백엔드 엔지니어를 담당자로 무작위 선택합니다.

  • 딕셔너리 파일을 업데이트하여 마이그레이션을 사용 중단으로 표시합니다.

MR 담당자는 다음을 수행해야 합니다:

  • 딕셔너리 파일에 올바른 marked_obsolete_by_urlmarked_obsolete_in_milestone이 있는지 확인합니다.

  • .rubocop_todo/ 디렉터리에 마이그레이션 또는 스펙 파일에 대한 참조가 없는지 확인합니다.

  • Elastic::DataMigrationService.migration_has_finished?(:migration_name_in_lowercase)를 찾아 이 마이그레이션에 대한 하위 호환성 처리 로직을 제거합니다.

  • 필요한 변경 사항을 머지 리퀘스트에 푸시합니다.

사용 중단된 마이그레이션 제거 프로세스#

Keeps::DeleteObsoleteAdvancedSearchMigrations Keep을 수동으로 실행하여 사용 중단된 마이그레이션과 스펙을 제거합니다. Keep은 가장 최근의 사용 중단된 마이그레이션을 제외한 모든 것을 제거합니다.

  • 마지막 필수 중단점 이전에 사용 중단으로 표시된 사용 중단된 마이그레이션을 선택합니다.

  • 첫 번째 단계에 모든 사용 중단된 마이그레이션이 포함된 경우 적용되지 않은 마이그레이션이 있는 고객을 위한 안전 장치로 사용 중단된 마이그레이션 하나를 유지합니다.

  • 해당 마이그레이션의 마이그레이션 파일과 스펙 파일을 삭제합니다.

  • 머지 리퀘스트를 생성하고 Global Search 팀원에게 할당합니다.

MR 담당자는 다음을 수행해야 합니다:

  • 기본 브랜치의 마이그레이션을 마이그레이션 묘지에 백업합니다.

  • .rubocop_todo/ 디렉터리에 마이그레이션 또는 스펙 파일에 대한 참조가 없는지 확인합니다.

  • 필요한 변경 사항을 머지 리퀘스트에 푸시합니다.

마이그레이션 모니터링을 위한 ChatOps 명령#

Slack(또는 ChatOps가 활성화된 채널)에서 언제든지 마이그레이션 상태를 확인할 수 있습니다:

/chatops gitlab run search_migrations --help
/chatops gitlab run search_migrations list
/chatops gitlab run search_migrations get MigrationName
/chatops gitlab run search_migrations get VersionNumber

위 명령은 search_migrations ChatOps 플러그인을 사용하여 현재 마이그레이션 상태를 가져옵니다.

고급 검색 마이그레이션 스타일 가이드

GitLab v19.1
원문 보기
요약

이 기능은 GitLab 13.0 이상에서 생성된 인덱스에서만 지원됩니다. scripts/elastic-migration을 실행하고 프롬프트에 따라 다음을 생성합니다: 마이그레이션을 정의하는 마이그레이션 파일: ee/elastic/migrate/YYYYMMDDHHMMSS_migration_name.rb

새 고급 검색 마이그레이션 만들기#

이 기능은 GitLab 13.0 이상에서 생성된 인덱스에서만 지원됩니다.

스크립트로 만들기#

히스토리

scripts/elastic-migration을 실행하고 프롬프트에 따라 다음을 생성합니다:

  • 마이그레이션을 정의하는 마이그레이션 파일: ee/elastic/migrate/YYYYMMDDHHMMSS_migration_name.rb

  • 마이그레이션을 테스트하는 스펙 파일: ee/spec/elastic/migrate/YYYYMMDDHHMMSS_migration_name_spec.rb

  • 마이그레이션을 식별하는 딕셔너리 파일: ee/elastic/docs/YYYYMMDDHHMMSS_migration_name.yml

수동으로 만들기#

히스토리

ee/elastic/migrate/ 폴더에 파일명 형식 YYYYMMDDHHMMSS_migration_name.rb으로 새 파일을 만드세요. 이 형식은 Rails 데이터베이스 마이그레이션과 동일합니다.

# frozen_string_literal: true

class MigrationName < Elastic::Migration
  # Important: Any updates to the Elastic index mappings must be replicated in the respective
  # configuration files:
  #   - `Elastic::Latest::Config`, for the main index.
  #   - `Elastic::Latest::Config`, for standalone indices.

  def migrate
  end

  # Check if the migration has completed
  # Return true if completed, otherwise return false
  def completed?
  end
end

적용된 마이그레이션은 gitlab-#{RAILS_ENV}-migrations 인덱스에 저장됩니다. 실행되지 않은 모든 마이그레이션은 Elastic::MigrationWorker cron worker에 의해 순차적으로 적용됩니다.

Elastic 인덱스 매핑을 업데이트하려면 각 파일에 설정을 적용합니다:

마이그레이션은 재시도 횟수 제한을 설정할 수 있으며 실패 처리 및 중단 표시가 가능합니다. 마이그레이션 재시도를 지원하기 위해 필요한 데이터 또는 인덱스 정리는 마이그레이션 내에서 처리해야 합니다.

건너뛴 마이그레이션#

true 또는 false로 평가되는 skip_if proc를 추가하여 마이그레이션을 건너뛸 수 있습니다:

class MigrationName < Elastic::Migration
  skip_if ->() { true|false }

마이그레이션은 조건이 false인 경우에만 실행됩니다. 건너뛴 마이그레이션은 대기 중인 마이그레이션 목록에 표시되지 않습니다.

건너뛴 마이그레이션은 사용 중단(obsolete)으로 표시할 수 있지만, 이러한 마이그레이션이 항상 건너뛰어지도록 skip_if 조건을 유지해야 합니다. 건너뛴 마이그레이션이 사용 중단되면 변경 사항을 적용하는 유일한 방법은 인덱스를 처음부터 재생성하는 것입니다.

건너뛴 마이그레이션의 문서 파일을 다음 속성으로 업데이트하세요:

skippable: true
skip_condition: '<description>'

인덱스 설정 및 매핑 변경을 위한 마이그레이션#

인덱스 설정 및 매핑 변경은 기존 인덱스에 즉시 적용되지 않고 새로 생성된 인덱스에만 적용됩니다.

설정 변경(예: 분석기 추가)을 적용하려면 다음 중 하나를 선택합니다:

매핑 변경을 적용하려면 다음 중 하나를 선택합니다:

무중단 재인덱스 마이그레이션#

대상 인덱스에 대한 새 인덱스를 생성하고 기존 문서를 복사합니다.

class MigrationName < Elastic::Migration
  def migrate
    Elastic::ReindexingTask.create!(targets: %w[Issue], options: { skip_pending_migrations_check: true })
  end

  def completed?
    true
  end
end

스펙 지원 헬퍼#

다음 헬퍼 메서드는 ee/spec/support/helpers/elasticsearch_helpers.rbElasticsearchHelpers에서 사용할 수 있습니다. ElasticsearchHelpersElasticsearch 스펙 메타데이터를 사용할 때 자동으로 포함됩니다.

assert_names_in_query#

Elasticsearch 쿼리에 이름 있는 쿼리(named queries)가 존재(with)하거나 존재하지 않는(without)지 검증합니다.

assert_fields_in_query#

Elasticsearch 쿼리에 지정된 필드가 포함되어 있는지 검증합니다.

assert_named_queries#

이 메서드는 Elasticsearch에 검색 요청을 전송해야 합니다. 직접 생성된 쿼리를 테스트하려면 assert_names_in_query를 사용하세요.

이름 있는 쿼리(named queries)로 Elasticsearch에 요청이 이루어졌는지 검증합니다. without을 사용하여 이름 있는 쿼리가 요청에 없음을 검증합니다.

assert_routing_field#

특정 라우팅으로 Elasticsearch에 요청이 이루어졌는지 검증합니다.

ensure_elasticsearch_index!#

모든 Elastic::ProcessBookkeepingService 클래스에 대해 execute를 실행하고 refresh_index!를 호출합니다. 이 메서드는 track! 메서드를 사용하여 인덱싱 대기열에 추가된 레코드를 인덱싱합니다.

refresh_index!#

모든 인덱스(마이그레이션 인덱스 포함)에서 Elasticsearch 인덱스 새로 고침을 수행합니다. 이를 통해 인덱스에서 최근 수행된 작업을 검색에 사용할 수 있습니다.

set_elasticsearch_migration_to#

마이그레이션 인덱스의 현재 마이그레이션을 특정 마이그레이션(이름 또는 버전 기준)으로 설정합니다. 마이그레이션은 기본적으로 완료된 것으로 표시되며 including: false를 전달하여 대기 중 상태로 설정할 수 있습니다.

es_helper#

Gitlab::Elastic::Helper.default의 인스턴스를 제공합니다.

es_client#

Gitlab::Search::Client.new의 인스턴스를 제공합니다.

warm_elasticsearch_migrations_cache!#

각 마이그레이션에 대해 migration_has_finished?를 호출하여 ::Elastic::DataMigrationService 마이그레이션 캐시를 준비합니다.

elastic_wiki_indexer_worker_random_delay_range#

0과 ElasticWikiIndexerWorker::MAX_JOBS_PER_HOUR 사이의 임의 지연값을 반환합니다.

elastic_delete_group_wiki_worker_random_delay_range#

0과 Search::Wiki::ElasticDeleteGroupWikiWorker::MAX_JOBS_PER_HOUR 사이의 임의 지연값을 반환합니다.

elastic_group_association_deletion_worker_random_delay_range#

0과 Search::ElasticGroupAssociationDeletionWorker::MAX_JOBS_PER_HOUR 사이의 임의 지연값을 반환합니다.

items_in_index#

제공된 인덱스 이름에 존재하는 id의 배열을 반환합니다.

백필 헬퍼 선택#

필드를 백필할 때 사용할 헬퍼를 결정하려면 다음 표를 참조하세요:

MigrationBackfillHelper MigrationProjectScopedUpdateByQueryHelper
메커니즘 필드가 없는 문서를 검색하고 참조를 빌드한 후 재인덱싱을 위한 북키핑 파이프라인을 통해 추적합니다. 필드가 없는 문서를 가진 프로젝트를 검색하고 Painless 스크립트로 비동기 update_by_query 작업을 실행합니다. 이중 모드 전략을 사용합니다: 대규모 프로젝트는 안전 모드(하나씩 처리), 소규모 프로젝트는 속도 모드(여러 개를 함께 배치 처리 가능).
적합한 용도 재인덱싱 시 인덱서가 필드를 채우는 ActiveRecord 모델(DOCUMENT_TYPE)에서 지원하는 인덱스. 필드 값이 프로젝트/네임스페이스에서 파생되고 인덱스가 Go 기반 인덱서로 채워지는 프로젝트 범위 인덱스(커밋, blob, 위키).
배치 제한 검색당 10,000개 문서(Elasticsearch 기본값). 작업당 설정 가능한 batch_size(안전 모드)와 배치 업데이트용 speed_mode_batch_size(속도 모드). 두 값 모두 마이그레이션 상태를 통해 런타임에 조정 가능.
동시성 순차 처리. 동시 처리 — 최대 max_concurrent_tasks(기본값: 50)개 프로젝트를 동시에 처리. 마이그레이션 상태를 통해 런타임에 조정 가능.
최적화 없음 - 항상 순차 처리. 프로젝트 크기에 따라 자동 최적화: 대규모 프로젝트(문서 10,000개 이상)는 신중하게 처리, 소규모 프로젝트는 배치 처리.

마이그레이션 헬퍼#

다음 마이그레이션 헬퍼는 ee/app/workers/concerns/elastic/에서 사용할 수 있습니다:

Search::Elastic::MigrationBackfillHelper#

인덱스의 특정 필드를 백필합니다.

요구 사항:

  • 필드의 매핑이 이미 추가되어 있어야 합니다.

  • 필드는 항상 값을 가져야 합니다. 필드가 null이 될 수 있는 경우 Search::Elastic::MigrationReindexBasedOnSchemaVersion을 사용합니다.

  • 단일 필드의 경우 field_name 메서드와 DOCUMENT_TYPE 상수를 정의합니다.

  • 여러 필드의 경우 field_names 메서드와 DOCUMENT_TYPE 상수를 정의합니다.

이 헬퍼는 쿼리당 10,000개 항목의 배치 크기 제한이 있으며, 이는 Elasticsearch의 기본 검색 결과 제한입니다. 이 제약은 대규모 데이터셋을 백필할 때 성능에 상당한 영향을 미칩니다. 마이그레이션이 더 작은 배치로 데이터를 천천히 반복해야 하기 때문입니다.

단일 필드 예시:

class MigrationName < Elastic::Migration
  include ::Search::Elastic::MigrationBackfillHelper

  batched!
  batch_size 9_000
  throttle_delay 1.minute

  DOCUMENT_TYPE = Issue

  private

  def field_name
    :schema_version
  end
end

여러 필드 예시:

class MigrationName < Elastic::Migration
  include ::Search::Elastic::MigrationBackfillHelper

  batched!
  batch_size 9_000
  throttle_delay 1.minute

  DOCUMENT_TYPE = Issue

  private

  def field_names
    %w[schema_version visibility_level]
  end
end

이 마이그레이션은 'migration backfills fields' 공유 예시로 테스트할 수 있습니다.

describe MigrationName, :elastic_delete_by_query, :sidekiq_inline, feature_category: :global_search do
  include_examples 'migration backfills fields' do
    let_it_be(:project) { create(:project) }
    let(:version) { 20251204143000 }
    let(:expected_throttle_delay) { 1.minute }
    let(:expected_batch_size) { 9000 }
    let(:objects) { create_list(:milestone, 3, project: project) }
    let(:expected_fields) { { traversal_ids: project.elastic_namespace_ancestry } }
  end
end

Search::Elastic::MigrationUpdateMappingsHelper#

지정된 매핑으로 put_mapping을 호출하여 인덱스의 매핑을 업데이트합니다.

new_mappings 메서드와 DOCUMENT_TYPE 상수가 필요합니다.

class MigrationName < Elastic::Migration
  include ::Search::Elastic::MigrationUpdateMappingsHelper

  DOCUMENT_TYPE = Issue

  private

  def new_mappings
    {
      schema_version: {
        type: 'short'
      }
    }
  end
end

이 마이그레이션은 'migration adds mapping' 공유 예시로 테스트할 수 있습니다.

describe 'migration', :elastic, :sidekiq_inline do
  include_examples 'migration adds mapping'
end

Search::Elastic::MigrationRemoveFieldsHelper#

인덱스에서 지정된 필드를 제거합니다.

DOCUMENT_TYPE과 일치하는 문서 중 지정된 필드를 가진 것이 있는지 배치로 확인합니다. 문서가 존재하면 Painless 스크립트를 사용하여 update_by_query를 수행합니다.

  • 단일 필드의 경우 field_to_remove 메서드와 DOCUMENT_TYPE 상수를 정의합니다.

  • 여러 필드의 경우 fields_to_remove 메서드와 DOCUMENT_TYPE 상수를 정의합니다.

class MigrationName < Elastic::Migration
  include ::Search::Elastic::MigrationRemoveFieldsHelper

  batched!
  throttle_delay 1.minute

  DOCUMENT_TYPE = User

  private

  def fields_to_remove
    %w[two_factor_enabled has_projects]
  end
end

기본 배치 크기는 10_000입니다. BATCH_SIZE를 지정하여 이 값을 재정의할 수 있습니다:

class MigrationName < Elastic::Migration
  include ::Search::Elastic::MigrationRemoveFieldsHelper

  batched!
  BATCH_SIZE = 100

  ...
end

이 마이그레이션은 'migration removes field' 공유 예시로 테스트할 수 있습니다.

include_examples 'migration removes field' do
  let(:expected_throttle_delay) { 1.minute }
  let(:objects) { create_list(:work_item, 6) }
  let(:index_name) { ::Search::Elastic::Types::WorkItem.index_name }
  let(:field) 
  let(:type) { 'long' }
end

매핑에 type 외의 항목이 포함된 경우 type 변수를 생략하고 대신 mapping을 정의합니다:

include_examples 'migration removes field' do
  let(:expected_throttle_delay) { 1.minute }
  let(:objects) { create_list(:work_item, 6) }
  let(:index_name) { ::Search::Elastic::Types::WorkItem.index_name }
  let(:field) 
  let(:mapping) { { type: 'dense_vector', dims: 768, index: true, similarity: 'cosine' } }
end

expecting token of type [VALUE_NUMBER] but found [FIELD_NAME] 오류가 발생하면 value 변수를 정의합니다:

include_examples 'migration removes field' do
  let(:expected_throttle_delay) { 1.minute }
  let(:objects) { create_list(:work_item, 6) }
  let(:index_name) { ::Search::Elastic::Types::WorkItem.index_name }
  let(:field) 
  let(:mapping) { { type: 'dense_vector', dims: 768, index: true, similarity: 'cosine' } }
  let(:value) { Array.new(768, 1) }
end

Search::Elastic::MigrationObsolete#

마이그레이션이 더 이상 필요하지 않을 때 사용 중단으로 표시합니다.

class MigrationName < Elastic::Migration
  include ::Search::Elastic::MigrationObsolete
end

건너뛸 수 있는 마이그레이션을 사용 중단으로 표시할 때는 skip_if 조건을 유지해야 합니다.

이 마이그레이션은 'a deprecated advanced search migration' 공유 예시로 테스트할 수 있습니다. 마이그레이션을 사용 중단으로 표시하는 프로세스를 따르세요.

Search::Elastic::MigrationCreateIndexHelper#

새 인덱스를 생성합니다.

요구 사항:

  • target_classdocument_type 메서드

  • 클래스에 대한 매핑 및 인덱스 설정

같은 마일스톤에서 인덱스를 채우기 위한 후속 마이그레이션을 수행해야 합니다.

class MigrationName < Elastic::Migration
  include ::Search::Elastic::MigrationCreateIndexHelper

  retry_on_failure

  def document_type
    :epic
  end

  def target_class
    Epic
  end
end

이 마이그레이션은 'migration creates a new index' 공유 예시로 테스트할 수 있습니다.

it_behaves_like 'migration creates a new index', 20240501134252, WorkItem

Search::Elastic::MigrationReindexTaskHelper#

새 인덱스를 생성하고 기존 데이터를 새 인덱스로 복사하는 재인덱스 작업을 만듭니다.

요구 사항:

  • targets 메서드.

targets 항목은 유효한 Ruby 상수 문자열(constantize를 통해 확인 가능)이어야 합니다. 네임스페이스가 있는 클래스의 경우 전체 경로를 사용합니다. 예를 들어 MigrationIndexConfig 대신 Elastic::MigrationIndexConfig를 사용합니다.

class MigrationName < Elastic::Migration
  include ::Search::Elastic::MigrationReindexTaskHelper

  def targets
    %w[MergeRequest]
  end
end

다음 스펙으로 이 마이그레이션을 테스트할 수 있습니다.

let(:migration) { described_class.new(version) }
let(:task) { Search::Elastic::ReindexingTask.last }
let(:targets) { %w[MergeRequest] }

it 'does not have migration options set', :aggregate_failures do
  expect(migration).not_to be_batched
  expect(migration).not_to be_retry_on_failure
end

describe '#migrate', :aggregate_failures do
  it 'creates reindexing task with correct target and options' do
    expect { migration.migrate }.to change { Search::Elastic::ReindexingTask.count }.by(1)
    expect(task.targets).to eq(targets)
    expect(task.options).to eq('skip_pending_migrations_check' => true)
  end
end

describe '#completed?' do
  it 'always returns true' do
    expect(migration.completed?).to be(true)
  end
end

Search::Elastic::MigrationReindexBasedOnSchemaVersion#

지정된 문서 타입을 저장하는 인덱스의 모든 문서를 재인덱싱하고 schema_version을 업데이트합니다.

DOCUMENT_TYPENEW_SCHEMA_VERSION 상수가 필요합니다. 인덱스 매핑에는 YYVV(연도/버전) 형식의 schema_version 정수 필드가 있어야 합니다.

이 마이그레이션 헬퍼는 스크롤 API를 사용하여 더 큰 배치(잠재적으로 10,000개 레코드 초과)를 더 효율적으로 처리합니다. 큐 임계값은 북키핑 큐가 용량을 초과할 때 마이그레이션을 일시 중지하여 다운스트림 처리가 과부하되지 않도록 합니다.

이전 인덱스 매핑 schema_versionYYMM 또는 YYWW 형식을 사용했습니다. 새 버전은 YYVV 형식을 사용해야 합니다.

class MigrationName < Elastic::Migration
  include Search::Elastic::MigrationReindexBasedOnSchemaVersion

  batched!
  batch_size 9_000
  throttle_delay 1.minute

  DOCUMENT_TYPE = WorkItem
  NEW_SCHEMA_VERSION = 24_46
  UPDATE_BATCH_SIZE = 100
end

이 마이그레이션은 'migration reindex based on schema_version' 공유 예시로 테스트할 수 있습니다.

include_examples 'migration reindex based on schema_version' do
  let(:expected_throttle_delay) { 1.minute }
  let(:expected_batch_size) { 9_000 }
  let(:objects) { create_list(:project, 3) }
end

Search::Elastic::MigrationDeleteBasedOnSchemaVersion#

지정된 문서 타입을 저장하는 인덱스에서 schema_version이 주어진 값보다 낮은 모든 문서를 삭제합니다.

DOCUMENT_TYPE 상수와 schema_version 메서드가 필요합니다. 인덱스 매핑에는 YYVV(연도/버전) 형식의 schema_version 정수 필드가 있어야 합니다.

이전 인덱스 매핑 schema_versionYYMM 또는 YYWW 형식을 사용했습니다. 새 버전은 YYVV 형식을 사용해야 합니다.

class MigrationName < Elastic::Migration
  include ::Search::Elastic::MigrationDeleteBasedOnSchemaVersion

  DOCUMENT_TYPE = Issue

  batch_size 10_000
  batched!
  throttle_delay 1.minute
  retry_on_failure

  def schema_version
    23_12
  end
end

이 마이그레이션은 'migration deletes documents based on schema version' 공유 예시로 테스트할 수 있습니다.

include_examples 'migration deletes documents based on schema version' do
  let(:objects) { create_list(:issue, 3) }
  let(:expected_throttle_delay) { 1.minute }
  let(:expected_batch_size) { 20000 }
end

Search::Elastic::MigrationDatabaseBackfillHelper#

limited_indexing 설정을 존중하면서 데이터베이스의 모든 문서를 Elasticsearch 인덱스로 재인덱싱합니다.

DOCUMENT_TYPE 상수와 respect_limited_indexing? 메서드가 필요합니다.

class MigrationName < Elastic::Migration
  include ::Search::Elastic::MigrationDatabaseBackfillHelper

  batch_size 10_000
  batched!
  throttle_delay 1.minute
  retry_on_failure

  DOCUMENT_TYPE = Issue

  def respect_limited_indexing?
    true
  end
end

이 마이그레이션은 'migration reindexes all data' 공유 예시로 테스트할 수 있습니다. 팩토리 이름이 DOCUMENT_TYPE과 다른 경우 올바른 팩토리 이름으로 factory_to_create_objects를 재정의합니다.

include_examples 'migration reindexes all data' do
  let(:objects) { create_list(:issue, 3) }
  let(:factory_to_create_objects) 
  let(:expected_throttle_delay) { 30.seconds }
  let(:expected_batch_size) { 30_000 }
end

Search::Elastic::MigrationProjectScopedUpdateByQueryHelper#

비동기 update_by_query 작업을 프로젝트별로 그룹화하여 프로젝트 범위 인덱스(커밋, blob, 위키)의 필드를 백필합니다. 북키핑 파이프라인을 통해 문서를 재인덱싱하는 MigrationBackfillHelper와 달리, 이 헬퍼는 Elasticsearch에서 직접 Painless 스크립트로 문서를 제자리 업데이트합니다.

이 헬퍼는 Rails 북키핑 파이프라인(Elastic::ProcessBookkeepingService)이 아닌 Go 기반 인덱서(gitlab-elasticsearch-indexer)로 채워지는 인덱스를 위한 것입니다. ActiveRecord 모델로 지원되는 인덱스에는 MigrationBackfillHelper 또는 MigrationReindexBasedOnSchemaVersion을 대신 사용하세요.

이중 모드 마이그레이션 전략#

이 헬퍼는 프로젝트 크기에 따라 전략을 자동으로 조정합니다:

  • 안전 모드: 대규모 프로젝트(기본적으로 10,000개 이상의 문서)의 경우 - 신중한 동시성 제어로 한 번에 하나의 프로젝트를 처리합니다.

  • 속도 모드: 소규모 프로젝트의 경우 - 더 빠른 처리를 위해 단일 업데이트에서 여러 프로젝트를 일괄 처리할 수 있습니다.

헬퍼는 남은 프로젝트 중 large_project_threshold를 초과하는 문서 수를 가진 것이 있는지 확인하여 사용할 모드를 결정합니다. 대규모 프로젝트가 남아 있을 때는 안전 모드로 먼저 실행되고, 모든 대규모 프로젝트가 완료된 후 속도 모드로 자동 전환됩니다.

필수 메서드#

마이그레이션은 네 가지 메서드를 구현해야 합니다:

# The document type to filter by (e.g., 'commit', 'blob', 'wiki_blob')
def document_type_value
  'commit'
end

# The field being backfilled
def field_name
  'traversal_ids'
end

# Painless script for updating a single project's documents
# Used in safe mode (large projects) and as fallback in speed mode
def update_script(project)
  {
    source: "ctx._source.traversal_ids = params.traversal_ids",
    params: { traversal_ids: project.namespace.traversal_ids }
  }
end

# Painless script for batch updating multiple projects at once
# Used in speed mode for small projects
# Must be implemented by all migrations using this helper
def batch_update_script(projects)
  # Convert IDs to strings to match ES keyword fields
  project_values = projects.index_by { |p| p.id.to_s }.transform_values(&:namespace_ancestry)
  {
    source: "ctx._source.traversal_ids = params.project_values[ctx._source.rid.toString()]",
    params: { project_values: project_values }
  }
end
필수: 배치 처리 구현#

마이그레이션은 속도 모드에서 배치 처리를 활성화하기 위해 batch_update_script(projects) 메서드를 구현해야 합니다:

# Returns a Painless script that works across multiple projects
# Receives an array of Project objects in this batch
# @param projects [Array] Projects being batched together
# @return [Hash] Script with :source and :params keys
def batch_update_script(projects)
  # Build a lookup map from project data
  # IMPORTANT: Convert project IDs to strings to match keyword fields in Elasticsearch
  project_values = projects.index_by { |p| p.id.to_s }.transform_values(&:namespace_ancestry)
  {
    source: "ctx._source.traversal_ids = params.project_values[ctx._source.rid.toString()]",
    params: { project_values: project_values }
  }
end
선택적 재정의#
# Override to change the Elasticsearch field storing project ID (default: 'rid')
def project_id_field
  'rid'
end

# Override to customize eager loading (default: Project.with_namespace)
def project_relation
  Project.includes(:namespace, :route)
end
마이그레이션 상태를 통한 설정#

모든 마이그레이션 파라미터는 코드 변경 없이 런타임에 조정할 수 있습니다:

파라미터 기본값 설명
batch_size 상속됨 update_by_query 작업당 최대 문서 수
max_concurrent_tasks 50 허용되는 최대 동시 업데이트 작업 수
speed_mode_batch_size 10,000 속도 모드에서 프로젝트를 일괄 처리할 때 최대 문서 수
large_project_threshold 10,000 "대규모" 프로젝트의 문서 수 임계값

Rails 콘솔 사용:

migration = Elastic::DataMigrationService[20260216153009].send(:migration)
migration.set_migration_state(
  migration.migration_state.merge(
    batch_size: 5_000,
    max_concurrent_tasks: 100,
    speed_mode_batch_size: 20_000,
    large_project_threshold: 15_000
  )
)

또는 Elasticsearch에서 직접 마이그레이션 상태를 업데이트합니다. 마이그레이션 상태는 #{target_name}-migrations 인덱스(예: gitlab-production-migrations)에 저장됩니다. 각 마이그레이션 문서는 버전 번호로 인덱싱되며 상태는 state 필드에 있습니다. 마이그레이션 버전을 문서 ID로 사용하여 Update API를 사용하세요:

# Update migration parameters for migration 20260216153009
curl --request POST "localhost:9200/gitlab-production-migrations/_update/20260216153009" \
  --header 'Content-Type: application/json' --data'
{
  "script": {
    "source": "ctx._source.state.max_concurrent_tasks = params.max_tasks; ctx._source.state.batch_size = params.batch_size; ctx._source.state.speed_mode_batch_size = params.speed_batch_size; ctx._source.state.large_project_threshold = params.threshold",
    "params": {
      "max_tasks": 100,
      "batch_size": 5000,
      "speed_batch_size": 20000,
      "threshold": 15000
    }
  }
}'
마이그레이션 예시#
class BackfillTraversalIdsOnCommits < Elastic::Migration
  include ::Search::Elastic::MigrationProjectScopedUpdateByQueryHelper

  batched!
  batch_size 10_000
  throttle_delay 5.seconds
  retry_on_failure

  def index_name
    ::Elastic::Latest::CommitConfig.index_name
  end

  private

  def document_type_value
    'commit'
  end

  def field_name
    'traversal_ids'
  end

  def update_script(project)
    {
      source: "ctx._source.traversal_ids = params.traversal_ids",
      params: { traversal_ids: project.elastic_namespace_ancestry }
    }
  end

  # Enable batching for faster processing in speed mode
  def batch_update_script(projects)
    # Convert IDs to strings to match keyword fields in ES
    project_values = projects.index_by { |p| p.id.to_s }.transform_values(&:namespace_ancestry)
    {
      source: "ctx._source.traversal_ids = params.project_values[ctx._source.rid.toString()]",
      params: { project_values: project_values }
    }
  end
end
마이그레이션 진행 상황 및 로깅#

헬퍼는 현재 모드를 기록하고 더 나은 관찰성을 위해 문서 수를 포함합니다:

Running migration in safe mode (processing large projects)
In Progress: update_by_query task, project_id: 123, document_count: 15000
Completed: update_by_query task, project_id: 123, document_count: 15000

Running migration in speed mode (batching small projects)
In Progress: update_by_query task, project_id: 456, document_count: 2000
Completed: update_by_query task, project_id: 456, document_count: 2000

'migration backfills a field using project-scoped update_by_query' 공유 예시로 이 마이그레이션을 테스트할 수 있습니다. 사용하는 스펙은 it_behaves_like 블록 내에 두 가지 메서드를 정의해야 합니다:

  • index_documents_for_projects(projects) — 주어진 프로젝트에 대한 문서를 인덱싱합니다.

  • remove_field_from_indexed_documents(project_ids) — 인덱싱된 문서에서 타깃 필드를 제거합니다(rid가 키워드 필드이므로 프로젝트 ID는 문자열입니다).

RSpec.describe BackfillTraversalIdsOnCommits, :elastic, :sidekiq_inline, feature_category: :global_search do
  let(:version) { 20260216153009 }
  let(:version_mapping_migration) { 20260216152309 }
  let(:expected_batch_size) { 10_000 }
  let(:expected_throttle_delay) { 5.seconds }

  let_it_be_with_reload(:projects) { create_list(:project, 3, :repository) }

  it_behaves_like 'migration backfills a field using project-scoped update_by_query' do
    def index_documents_for_projects(projects)
      projects.each { |p| p.repository.index_commits_and_blobs }
    end

    def remove_field_from_indexed_documents(project_ids)
      client = Gitlab::Search::Client.new

      client.update_by_query({
        index: index_name,
        wait_for_completion: true,
        refresh: true,
        body: {
          script: {
            source: "ctx._source.remove('traversal_ids');",
            lang: "painless"
          },
          query: {
            bool: {
              must: [{ exists: { field: 'traversal_ids' } }],
              filter: [{ terms: { rid: project_ids } }]
            }
          }
        }
      })
    end
  end
end

Search::Elastic::MigrationHelper#

이전 예시에 맞지 않는 마이그레이션에서 사용할 수 있는 메서드를 포함합니다.

class MigrationName < Elastic::Migration
  include ::Search::Elastic::MigrationHelper

  def migrate
  ...
  end

  def completed?
  ...
  end
end

Elastic::MigrationWorker에서 지원하는 마이그레이션 옵션#

Elastic::MigrationWorker는 다음 마이그레이션 옵션을 지원합니다:

  • batched! - 마이그레이션을 배치로 실행할 수 있게 허용합니다. 설정하면 Elastic::MigrationWorker가 아래에 설명된 throttle_delay 옵션으로 설정된 지연을 두고 자신을 다시 큐에 추가합니다. 배치 처리는 migrate 메서드에서 처리해야 합니다. 이 설정은 재큐잉만 제어합니다.

  • batch_size - batched! 마이그레이션 실행 중 수정되는 문서 수를 설정합니다. 이 크기는 업데이트가 완료될 수 있도록 충분한 시간을 허용하는 값으로 설정해야 합니다. 아래에 설명된 throttle_delay 옵션과 함께 조정할 수 있습니다. 배치 처리는 커스텀 migrate 메서드 또는 이 설정을 사용하는 Search::Elastic::MigrationBackfillHelper migrate 메서드에서 처리해야 합니다. 기본값은 1000개 문서입니다.

  • throttle_delay - 배치 실행 사이의 대기 시간을 설정합니다. 이 시간은 각 마이그레이션 배치가 완료될 수 있도록 충분히 높게 설정해야 합니다. 또한, Elastic::MigrationWorker cron worker가 실행되는 빈도인 5분 미만으로 설정해야 합니다. 기본값은 3분입니다.

  • pause_indexing! - 마이그레이션이 실행되는 동안 인덱싱을 일시 중지합니다. 이 설정은 마이그레이션이 실행되기 전의 인덱싱 설정을 기록하고 마이그레이션이 완료되면 해당 값으로 다시 설정합니다.

  • space_requirements! - 마이그레이션이 실행될 때 클러스터에서 충분한 여유 공간이 있는지 확인합니다. 이 설정은 마이그레이션이 실행될 때 필요한 스토리지를 사용할 수 없는 경우 마이그레이션을 중단합니다. 마이그레이션은 space_required_bytes 메서드를 정의하여 필요한 공간을 바이트 단위로 제공해야 합니다.

  • retry_on_failure - 실패 시 재시도 기능을 활성화합니다. 기본적으로 마이그레이션을 30번 재시도합니다. 재시도 횟수가 소진되면 마이그레이션은 중단됨으로 표시됩니다. 재시도 횟수를 사용자 지정하려면 max_attempts 인수를 전달합니다: retry_on_failure max_attempts: 10

# frozen_string_literal: true

class BatchedMigrationName < Elastic::Migration
  # Declares a migration should be run in batches
  batched!
  throttle_delay 10.minutes
  pause_indexing!
  space_requirements!
  retry_on_failure

  # ...
end

마이그레이션에서 다운타임 방지#

마이그레이션 되돌리기#

마이그레이션이 GitLab.com에서 실패하거나 중단되면, 마이그레이션을 도입한 변경 사항을 되돌리는 것을 선호합니다. 이렇게 하면 Self-managed 고객이 손상된 마이그레이션을 받지 않고 백포트의 필요성이 줄어듭니다.

다중 버전 호환성#

고급 검색 마이그레이션은 다른 GitLab 변경 사항과 마찬가지로 여러 버전의 애플리케이션이 동시에 실행되는 경우를 지원해야 합니다.

배포 순서에 따라 마이그레이션이 시작되거나 완료된 상태에서 마이그레이션 이전의 애플리케이션 코드로 실행 중인 서버가 여전히 있을 수 있습니다. 모든 고급 검색 마이그레이션이 배포 완료 후 시작되도록 보장할 수 있을 때까지 이를 고려해야 합니다.

위험도 높은 마이그레이션#

Elasticsearch는 트랜잭션을 지원하지 않으므로, 마이그레이션이 시작된 후 또는 완료된 후 애플리케이션 코드가 되돌려지는 상황을 수용할 수 있도록 항상 마이그레이션을 설계해야 합니다.

이러한 이유로 일반적으로 파괴적인 작업(예: 일부 데이터를 이동한 후 삭제)은 마이그레이션이 성공적으로 완료된 후 이후 머지 리퀘스트로 연기합니다. 중요한 데이터 손실 위험이 있는 경우 Self-managed 고객을 위해 다른 릴리즈로도 연기해야 합니다.

마이그레이션 실행 시간 계산#

마이그레이션이 GitLab.com에서 실행되는 데 얼마나 걸릴지 이해하는 것이 중요합니다. 마이그레이션에서 처리될 문서 수를 도출하세요. 이 수치는 데이터베이스 또는 기존 Elasticsearch 인덱스를 쿼리하여 얻을 수 있습니다. 다음 공식을 사용하여 실행 시간을 계산합니다:

> batch_size = 9_000
=> 9000
> throttle_delay = 1.minute
=> 1 minute
> number_of_documents = 15_536_906
=> 15536906
> (number_of_documents / batch_size) * throttle_delay
=> 1726 minutes
> (number_of_documents / batch_size) * throttle_delay / 1.hour
=> 28

고급 검색 마이그레이션 모범 사례#

최선의 결과를 위해 다음 모범 사례를 따르세요:

  • 각 문서 타입에 대한 모든 마이그레이션을 정렬하여 Search::Elastic::MigrationUpdateMappingsHelper를 사용하는 마이그레이션이 Search::Elastic::MigrationBackfillHelper를 사용하는 마이그레이션보다 먼저 실행되도록 합니다. 이렇게 하면 모든 마이그레이션이 적용되지 않은 경우 동일한 문서를 여러 번 재인덱싱하지 않고 백필 시간이 줄어듭니다.

  • 배치로 작업할 때 배치 크기를 9,000개 문서 미만으로 유지합니다. 대량 인덱서는 매분 실행되며 10,000개 문서의 배치를 처리하도록 설정되어 있습니다. 이렇게 하면 대량 인덱서가 다른 마이그레이션 배치를 시도하기 전에 레코드를 처리할 시간이 있습니다.

  • 문서 수가 최신 상태인지 확인하려면 마이그레이션이 완료되었는지 확인하기 전에 인덱스를 새로 고쳐야 합니다.

  • 마이그레이션이 시작될 때, 완료 여부 확인이 발생할 때, 마이그레이션이 완료될 때 각 마이그레이션에 로깅 문을 추가합니다. 이러한 로그는 마이그레이션 문제를 디버깅할 때 유용합니다.

  • Elasticsearch Reindex API 작업을 사용하는 경우 인덱싱을 일시 중지합니다.

  • 마이그레이션이 실패할 가능성이 있는 경우 재시도 제한을 추가하는 것을 고려합니다. 이렇게 하면 문제가 발생할 경우 마이그레이션이 중단될 수 있습니다.

  • 마이그레이션 파일 내에 인덱스 매핑과 설정을 인라인으로 포함합니다. 이렇게 하면 공유 설정 클래스의 향후 코드 변경으로부터 마이그레이션을 보호합니다. 외부 클래스나 모듈을 참조하는 대신 private 메서드에서 매핑과 설정을 정의합니다. 이는 시간이 지나도 마이그레이션이 안정적으로 유지되도록 데이터베이스 마이그레이션과 동일한 패턴을 따릅니다.

매핑 및 설정 인라인화#

인덱스를 생성하거나 업데이트할 때 private 메서드를 사용하여 마이그레이션 파일에 직접 매핑과 설정을 정의합니다:

class MigrationName < Elastic::Migration
  include ::Search::Elastic::MigrationHelper

  def migrate
    helper.create_index(
      index_name: 'my-index',
      settings: index_settings,
      mappings: index_mappings
    )
  end

  private

  def index_settings
    {
      number_of_shards: 1,
      number_of_replicas: 1
    }
  end

  def index_mappings
    {
      properties: {
        id: { type: 'keyword' },
        name: { type: 'text' },
        created_at: { type: 'date' }
      }
    }
  end
end

이 접근 방식은 향후 GitLab 버전에서 공유 설정 클래스가 변경되더라도 마이그레이션이 올바르게 계속 작동하도록 합니다.

고급 검색 마이그레이션 정리#

고급 검색 마이그레이션은 일반적으로 오랜 기간 동안 여러 코드 경로를 지원해야 하므로, 안전하게 할 수 있을 때 이를 정리하는 것이 중요합니다.

GitLab 필수 중단점을 완전히 마이그레이션되지 않은 인덱스에 대한 하위 호환성을 안전하게 제거하는 시기로 선택합니다. 업그레이드 문서에 이를 기록합니다.

GitLab Housekeeper는 정리 프로세스를 자동화하는 데 사용됩니다. 이 프로세스에는 기존 마이그레이션을 사용 중단으로 표시하고 사용 중단된 마이그레이션을 삭제하는 작업이 포함됩니다. 마이그레이션이 사용 중단으로 표시되면 마이그레이션 코드는 사용 중단 마이그레이션 코드로 교체되고 테스트는 사용 중단 마이그레이션 공유 예시로 교체됩니다:

  • 고급 검색 마이그레이션에서 호출되는 코드를 유지 관리할 필요가 없습니다.

  • 더 이상 지원하지 않는 마이그레이션에 대한 테스트를 실행하는 데 CI 시간을 낭비하지 않습니다.

  • 이 마이그레이션을 실행하지 않고 타깃 버전으로 직접 업그레이드하는 운영자에게 처음부터 재인덱싱하라는 메시지가 표시됩니다.

마지막 필수 중단점 직전의 마지막 마이너 버전에서 생성된 마이그레이션은 정리하지 않는 것이 더욱 안전합니다. 예를 들어 마지막 필수 중단점이 %14.0이었다면 %13.12에서만 추가된 마이그레이션은 정리하지 않아야 합니다. 이 추가적인 안전망은 GitLab.com에서 완료되는 데 여러 주가 걸릴 수 있는 마이그레이션을 허용합니다. GitLab.com 배포가 자동화되어 있고 이 정리를 방지하는 자동화된 검사가 없기 때문에 추가적인 주의가 필요합니다. 또한 자동화된 검사가 있더라도 고급 검색 마이그레이션 때문에 GitLab.com 배포를 차단하고 싶지 않을 것입니다. 마이그레이션이 아직 완료되는 데 일주일 정도 남아 있을 수 있으며 배포를 너무 오래 차단하게 됩니다.

마이그레이션을 사용 중단으로 표시하는 프로세스#

Keeps::MarkOldAdvancedSearchMigrationsAsObsolete Keep을 수동으로 실행하여 마이그레이션을 사용 중단으로 표시합니다.

마지막 필수 중단점 이전 두 버전에서 생성된 모든 마이그레이션에 대해 Keep은 다음을 수행합니다:

  • 마이그레이션의 내용을 유지하고 하단에 prepend를 추가합니다:
 ClassName.prepend ::Search::Elastic::MigrationObsolete
  • 스펙 파일 내용을 'a deprecated advanced search migration' 공유 예시로 교체합니다.

  • Global Search 백엔드 엔지니어를 담당자로 무작위 선택합니다.

  • 딕셔너리 파일을 업데이트하여 마이그레이션을 사용 중단으로 표시합니다.

MR 담당자는 다음을 수행해야 합니다:

  • 딕셔너리 파일에 올바른 marked_obsolete_by_urlmarked_obsolete_in_milestone이 있는지 확인합니다.

  • .rubocop_todo/ 디렉터리에 마이그레이션 또는 스펙 파일에 대한 참조가 없는지 확인합니다.

  • Elastic::DataMigrationService.migration_has_finished?(:migration_name_in_lowercase)를 찾아 이 마이그레이션에 대한 하위 호환성 처리 로직을 제거합니다.

  • 필요한 변경 사항을 머지 리퀘스트에 푸시합니다.

사용 중단된 마이그레이션 제거 프로세스#

Keeps::DeleteObsoleteAdvancedSearchMigrations Keep을 수동으로 실행하여 사용 중단된 마이그레이션과 스펙을 제거합니다. Keep은 가장 최근의 사용 중단된 마이그레이션을 제외한 모든 것을 제거합니다.

  • 마지막 필수 중단점 이전에 사용 중단으로 표시된 사용 중단된 마이그레이션을 선택합니다.

  • 첫 번째 단계에 모든 사용 중단된 마이그레이션이 포함된 경우 적용되지 않은 마이그레이션이 있는 고객을 위한 안전 장치로 사용 중단된 마이그레이션 하나를 유지합니다.

  • 해당 마이그레이션의 마이그레이션 파일과 스펙 파일을 삭제합니다.

  • 머지 리퀘스트를 생성하고 Global Search 팀원에게 할당합니다.

MR 담당자는 다음을 수행해야 합니다:

  • 기본 브랜치의 마이그레이션을 마이그레이션 묘지에 백업합니다.

  • .rubocop_todo/ 디렉터리에 마이그레이션 또는 스펙 파일에 대한 참조가 없는지 확인합니다.

  • 필요한 변경 사항을 머지 리퀘스트에 푸시합니다.

마이그레이션 모니터링을 위한 ChatOps 명령#

Slack(또는 ChatOps가 활성화된 채널)에서 언제든지 마이그레이션 상태를 확인할 수 있습니다:

/chatops gitlab run search_migrations --help
/chatops gitlab run search_migrations list
/chatops gitlab run search_migrations get MigrationName
/chatops gitlab run search_migrations get VersionNumber

위 명령은 search_migrations ChatOps 플러그인을 사용하여 현재 마이그레이션 상태를 가져옵니다.