InfoGrab DocsInfoGrab Docs

고급 검색 개발 팁

요약

Elasticsearch 클러스터를 다루려면 Kibana를 사용합니다. 클러스터의 상태와 정보를 확인할 수 있습니다. Search::Elastic::TriggerIndexingWorker가 비동기로 실행됩니다. [0, 0] 이 표시될 때까지 실행합니다.

Kibana#

Elasticsearch 클러스터를 다루려면 Kibana를 사용합니다.

다운로드 안내를 참고합니다.

인덱스 상태 확인#

다음을 실행하면

bundle exec rake gitlab:elastic:info

클러스터의 상태와 정보를 확인할 수 있습니다.

처음부터 모든 인덱스를 생성하고 로컬 데이터로 채우기#

옵션 1: Rake task#

다음을 실행하면

bundle exec rake gitlab:elastic:index

Search::Elastic::TriggerIndexingWorker가 비동기로 실행됩니다.

다음을

Elastic::ProcessInitialBookkeepingService.new.execute

[0, 0] 이 표시될 때까지 실행합니다. [0, 0]은 큐에 남은 ref가 없다는 뜻입니다.

옵션 2: 수동#

Search::Elastic::TriggerIndexingWorker의 단계를 수동으로 실행합니다.

Sidekiq 이 job을 제대로 가져오지 못할 때가 있으므로 Sidekiq을 재시작해야 할 수 있습니다. Rails 콘솔에서 단계를 직접 실행하려면 다음을 사용합니다.

task_executor_service = Search::RakeTaskExecutorService.new(logger: ::Gitlab::Elasticsearch::Logger.build)
task_executor_service.execute(:recreate_index)
task_executor_service.execute(:clear_index_status)
task_executor_service.execute(:clear_reindex_status)
task_executor_service.execute(:resume_indexing)
task_executor_service.execute(:index_namespaces)
task_executor_service.execute(:index_projects)
task_executor_service.execute(:index_snippets)
task_executor_service.execute(:index_users)

다음을

Elastic::ProcessInitialBookkeepingService.new.execute

[0, 0] 이 표시될 때까지 실행합니다. [0, 0]은 큐에 남은 ref가 없다는 뜻입니다.

옵션 3: 재인덱싱 task#

먼저 기존 인덱스를 삭제한 다음, 대상으로 삼을 인덱스에 대한 ReindexingTask를 생성합니다. 이렇게 하면 현재 구성을 기준으로 새 인덱스가 만들어지고 데이터가 복사됩니다.

Search::Elastic::ReindexingTask.create!(targets: %w[MergeRequest])

다음을 실행합니다.

ElasticClusterReindexingCronWorker.new.perform

다음 값이

Search::Elastic::ReindexingTask.last.state

success가 될 때까지 반복합니다.

인덱스 데이터#

데이터베이스 레코드를 추가하고 인덱싱하려면 track! 메서드를 호출한 뒤 북키퍼를 실행합니다.

Elastic::ProcessBookkeepingService.track!(MergeRequest.first)
Elastic::ProcessBookkeepingService.track!(*MergeRequest.all)

Elastic::ProcessBookkeepingService.new.execute

종속 연관 인덱스 업데이트#

elastic_index_dependant_association을 사용하면 특정 필드가 변경될 때 인덱스의 연관 레코드를 자동으로 업데이트할 수 있습니다. 예를 들어 프로젝트의 visibility_level 이 변경될 때 모든 작업 항목을 재인덱싱하려면 다음과 같이 작성합니다.

  elastic_index_dependant_association :work_items, on_change: :visibility_level, depends_on_finished_migration: :add_mapping_migration

depends_on_finished_migration 파라미터는 선택 사항이며, 지정한 고급 검색 마이그레이션(예: 매핑에 필요한 필드를 추가한 마이그레이션)이 완료된 뒤에만 업데이트가 수행되도록 보장합니다.

테스트#

Warning

Elasticsearch 테스트는 모든 머지 리퀘스트에서 실행되지는 않습니다. Elasticsearch와 PostgreSQL 운영 버전으로 테스트를 실행하려면 머지 리퀘스트에 ~pipeline:run-search-tests 또는 ~group::global search 레이블을 추가합니다.

고급 검색 마이그레이션#

인덱스 매핑을 변경하는 마이그레이션 테스트#

  1. 인덱스에 변경 사항이 아직 적용되지 않았는지 확인합니다. 마이그레이션 크론 워커가 백그라운드에서 실행되므로 마이그레이션이 이미 적용되었을 수 있습니다.
    • 선택 사항. GitLab 18.0 이상에서 마이그레이션 워커를 비활성화하려면 다음 명령을 실행합니다.

        settings = ApplicationSetting.last # Ensure this setting does not return `nil`
        settings.elastic_migration_worker_enabled = false
        settings.save!
      
    • 마이그레이션이 대기 중인지 확인합니다: ::Elastic::DataMigrationService.pending_migrations.

    • 마이그레이션이 완료되지 않았는지 확인합니다: Elastic::DataMigrationService.pending_migrations.first.completed?.

    • 매핑이 아직 적용되지 않았는지 확인합니다.

      • Kibana에서 GET gitlab-development-some-index/_mapping으로 확인하거나
      • curl 요청으로 확인합니다: curl "http://localhost:9200/gitlab-development-some-index/_mappings" | jq
  2. 로그에 기록되는 메시지를 확인하려면 로그를 실시간으로 확인합니다: tail -f log/elasticsearch.log.
  3. 다음 방법 중 하나로 마이그레이션을 실행합니다.
    • Elastic::MigrationWorker.new.perform 마이그레이션 워커를 실행합니다. GitLab 18.0 이상에서는 elastic_migration_worker_enabled 애플리케이션 설정이 활성화되어 있어야 합니다.
    • 대기 중인 마이그레이션을 사용합니다: ::Elastic::DataMigrationService.pending_migrations.first.migrate.
    • 버전을 사용합니다: Elastic::DataMigrationService[20250220214819].migrate. 버전은 해당 마이그레이션 버전으로 바꿉니다.
  4. 마이그레이션 상태를 확인합니다.
    • Kibana에서 마이그레이션 레코드를 확인합니다: GET gitlab-development-migrations/_doc/20250220214819(버전은 바꿉니다). 시작 시각과 상태 등의 정보가 담겨 있습니다.
    • Kibana에서 매핑이 변경되었는지 확인합니다: GET gitlab-development-some-index/_mapping.

쿼리 변경 분석#

개발자는 GitLab 스테이징 Rails 콘솔을 사용해 코드 리뷰에서 변경 전후 쿼리를 비교할 수 있습니다.

Rails 콘솔에서는 Gitlab::Search::Client로 쿼리를 구성할 수 있습니다.

이 헬퍼를 사용한 쿼리 예시는 다음과 같습니다.

  Gitlab::Search::Client.new.search(
    index: 'gitlab-production-vulnerabilities',
    routing: 'group_110', # data is distributed across shards and the query builder passes routing information.
    body: {
      query: {
        term: { vulnerability_id: 4356 }
      }
    }
  )

취약점 고급 검색 파인더#

ee/lib/search/advanced_finders/security/vulnerability/의 취약점 고급 검색 파인더는 일반 글로벌 검색 프레임워크와 다른 패턴을 따릅니다.

검색 수준#

일반 Search::Level 클래스(lib/search/level.rb)는 project, group, global의 세 가지 수준을 인식합니다. Search::AdvancedFinders::Security::Vulnerability::BaseFinder는 네 번째 수준인 organization을 도입하며, 이 수준은 vulnerable 객체가 Organizations::Organization 인스턴스일 때 설정됩니다. organization 수준은 Search::Level 이 인식하지 않으며 일반 글로벌 검색 프레임워크 어디에서도 사용되지 않습니다.

권한 부여 및 스코핑#

일반 프레임워크는 Filters.by_search_level_and_membership, by_user_accessible_namespaces, search_level_filter를 사용해 가시성과 멤버십 규칙을 적용합니다. VulnerabilityQueryBuilder는 이 메서드들을 호출하지 않습니다. Search::Elastic::Filters에서 사용하는 메서드는 by_traversal_ids 하나뿐이며, 이 메서드는 traversal_ids 필드에 접두사 필터를 적용할 뿐 search_level을 읽지 않습니다.

이 메서드들을 사용하지 않는 것은 의도된 선택입니다. Search::Elastic::Filters::ALLOWED_SEARCH_LEVELS 는 %i[global group project] 이고, fetch_search_level!는 이 목록에 없는 값에 대해 ArgumentError, 'search_level invalid'를 발생시킵니다. fetch_search_level!를 호출하는 Filters 메서드를 VulnerabilityQueryBuilder에 연결하면 조직 수준 쿼리에서 런타임 오류가 발생합니다.

스코핑은 파인더와 VulnerabilityFilters가 처리합니다.

  • project 및 group 수준에서는 traversal_ids가 vulnerable 객체의 네임스페이스 상위 계층 접두사로 설정됩니다.
  • organization 수준에서는 traversal_ids가 nil 입니다(접두사 필터 없음). 스코핑은 VulnerabilityFilters의 by_organization_id가 제공하며, 이 메서드는 backfill_organization_id_in_vulnerabilities 마이그레이션에 의해 제어됩니다. 해당 마이그레이션이 끝나기 전까지 파인더는 project_id를 센티널 값([0])으로 설정해, 스코프 없이 실행되는 대신 쿼리가 결과를 반환하지 않도록 합니다.

VulnerabilityFilters.by_archived_projects는 :project 수준에서 아카이브 필터를 건너뛰기 위해 search_level을 읽으므로, 스코핑이 파인더만으로 전부 처리되는 것은 아닙니다.

샤드 라우팅#

es_search_options는 Elasticsearch 샤드 라우팅을 위해 root_ancestor_ids를 전달합니다.

  • project 및 group 수준에서는 vulnerable 객체의 단일 루트 네임스페이스 ID 입니다.
  • organization 수준에서는 해당 조직에 속한 최상위 네임스페이스 ID 목록입니다 (ES_ROUTING_MAX_COUNT + 1로 제한됩니다. 이 한도를 넘으면 라우팅이 적용되지 않고 모든 샤드를 대상으로 쿼리합니다).

형제 파인더#

형제 파인더(CountBySeverityFinder, CountByAgeFinder, CountOverTimeFinder, IdentifierNamesFinder, RiskScoresFinder, TopCwesFinder)는 모두 BaseFinder를 상속하며 search_params와 es_search_options를 그대로 재사용합니다. VulnerabilitySorts와 VulnerabilityAggregations도 search_level을 참조하지 않으므로 organization 수준은 정렬이나 집계 로직에 영향을 주지 않습니다.

SearchFinder도 BaseFinder를 상속하지만 형제가 아니라 주 파인더이므로 위 목록에는 포함하지 않았습니다.

고급 검색 개발 팁

GitLab v19.4
원문 보기

요약

Elasticsearch 클러스터를 다루려면 Kibana를 사용합니다. 클러스터의 상태와 정보를 확인할 수 있습니다. Search::Elastic::TriggerIndexingWorker가 비동기로 실행됩니다. [0, 0] 이 표시될 때까지 실행합니다.

Kibana#

Elasticsearch 클러스터를 다루려면 Kibana를 사용합니다.

다운로드 안내를 참고합니다.

인덱스 상태 확인#

다음을 실행하면

bundle exec rake gitlab:elastic:info

클러스터의 상태와 정보를 확인할 수 있습니다.

처음부터 모든 인덱스를 생성하고 로컬 데이터로 채우기#

옵션 1: Rake task#

다음을 실행하면

bundle exec rake gitlab:elastic:index

Search::Elastic::TriggerIndexingWorker가 비동기로 실행됩니다.

다음을

Elastic::ProcessInitialBookkeepingService.new.execute

[0, 0] 이 표시될 때까지 실행합니다. [0, 0]은 큐에 남은 ref가 없다는 뜻입니다.

옵션 2: 수동#

Search::Elastic::TriggerIndexingWorker의 단계를 수동으로 실행합니다.

Sidekiq 이 job을 제대로 가져오지 못할 때가 있으므로 Sidekiq을 재시작해야 할 수 있습니다. Rails 콘솔에서 단계를 직접 실행하려면 다음을 사용합니다.

task_executor_service = Search::RakeTaskExecutorService.new(logger: ::Gitlab::Elasticsearch::Logger.build)
task_executor_service.execute(:recreate_index)
task_executor_service.execute(:clear_index_status)
task_executor_service.execute(:clear_reindex_status)
task_executor_service.execute(:resume_indexing)
task_executor_service.execute(:index_namespaces)
task_executor_service.execute(:index_projects)
task_executor_service.execute(:index_snippets)
task_executor_service.execute(:index_users)

다음을

Elastic::ProcessInitialBookkeepingService.new.execute

[0, 0] 이 표시될 때까지 실행합니다. [0, 0]은 큐에 남은 ref가 없다는 뜻입니다.

옵션 3: 재인덱싱 task#

먼저 기존 인덱스를 삭제한 다음, 대상으로 삼을 인덱스에 대한 ReindexingTask를 생성합니다. 이렇게 하면 현재 구성을 기준으로 새 인덱스가 만들어지고 데이터가 복사됩니다.

Search::Elastic::ReindexingTask.create!(targets: %w[MergeRequest])

다음을 실행합니다.

ElasticClusterReindexingCronWorker.new.perform

다음 값이

Search::Elastic::ReindexingTask.last.state

success가 될 때까지 반복합니다.

인덱스 데이터#

데이터베이스 레코드를 추가하고 인덱싱하려면 track! 메서드를 호출한 뒤 북키퍼를 실행합니다.

Elastic::ProcessBookkeepingService.track!(MergeRequest.first)
Elastic::ProcessBookkeepingService.track!(*MergeRequest.all)

Elastic::ProcessBookkeepingService.new.execute

종속 연관 인덱스 업데이트#

elastic_index_dependant_association을 사용하면 특정 필드가 변경될 때 인덱스의 연관 레코드를 자동으로 업데이트할 수 있습니다. 예를 들어 프로젝트의 visibility_level 이 변경될 때 모든 작업 항목을 재인덱싱하려면 다음과 같이 작성합니다.

  elastic_index_dependant_association :work_items, on_change: :visibility_level, depends_on_finished_migration: :add_mapping_migration

depends_on_finished_migration 파라미터는 선택 사항이며, 지정한 고급 검색 마이그레이션(예: 매핑에 필요한 필드를 추가한 마이그레이션)이 완료된 뒤에만 업데이트가 수행되도록 보장합니다.

테스트#

Warning

Elasticsearch 테스트는 모든 머지 리퀘스트에서 실행되지는 않습니다. Elasticsearch와 PostgreSQL 운영 버전으로 테스트를 실행하려면 머지 리퀘스트에 ~pipeline:run-search-tests 또는 ~group::global search 레이블을 추가합니다.

고급 검색 마이그레이션#

인덱스 매핑을 변경하는 마이그레이션 테스트#

  1. 인덱스에 변경 사항이 아직 적용되지 않았는지 확인합니다. 마이그레이션 크론 워커가 백그라운드에서 실행되므로 마이그레이션이 이미 적용되었을 수 있습니다.
    • 선택 사항. GitLab 18.0 이상에서 마이그레이션 워커를 비활성화하려면 다음 명령을 실행합니다.

        settings = ApplicationSetting.last # Ensure this setting does not return `nil`
        settings.elastic_migration_worker_enabled = false
        settings.save!
      
    • 마이그레이션이 대기 중인지 확인합니다: ::Elastic::DataMigrationService.pending_migrations.

    • 마이그레이션이 완료되지 않았는지 확인합니다: Elastic::DataMigrationService.pending_migrations.first.completed?.

    • 매핑이 아직 적용되지 않았는지 확인합니다.

      • Kibana에서 GET gitlab-development-some-index/_mapping으로 확인하거나
      • curl 요청으로 확인합니다: curl "http://localhost:9200/gitlab-development-some-index/_mappings" | jq
  2. 로그에 기록되는 메시지를 확인하려면 로그를 실시간으로 확인합니다: tail -f log/elasticsearch.log.
  3. 다음 방법 중 하나로 마이그레이션을 실행합니다.
    • Elastic::MigrationWorker.new.perform 마이그레이션 워커를 실행합니다. GitLab 18.0 이상에서는 elastic_migration_worker_enabled 애플리케이션 설정이 활성화되어 있어야 합니다.
    • 대기 중인 마이그레이션을 사용합니다: ::Elastic::DataMigrationService.pending_migrations.first.migrate.
    • 버전을 사용합니다: Elastic::DataMigrationService[20250220214819].migrate. 버전은 해당 마이그레이션 버전으로 바꿉니다.
  4. 마이그레이션 상태를 확인합니다.
    • Kibana에서 마이그레이션 레코드를 확인합니다: GET gitlab-development-migrations/_doc/20250220214819(버전은 바꿉니다). 시작 시각과 상태 등의 정보가 담겨 있습니다.
    • Kibana에서 매핑이 변경되었는지 확인합니다: GET gitlab-development-some-index/_mapping.

쿼리 변경 분석#

개발자는 GitLab 스테이징 Rails 콘솔을 사용해 코드 리뷰에서 변경 전후 쿼리를 비교할 수 있습니다.

Rails 콘솔에서는 Gitlab::Search::Client로 쿼리를 구성할 수 있습니다.

이 헬퍼를 사용한 쿼리 예시는 다음과 같습니다.

  Gitlab::Search::Client.new.search(
    index: 'gitlab-production-vulnerabilities',
    routing: 'group_110', # data is distributed across shards and the query builder passes routing information.
    body: {
      query: {
        term: { vulnerability_id: 4356 }
      }
    }
  )

취약점 고급 검색 파인더#

ee/lib/search/advanced_finders/security/vulnerability/의 취약점 고급 검색 파인더는 일반 글로벌 검색 프레임워크와 다른 패턴을 따릅니다.

검색 수준#

일반 Search::Level 클래스(lib/search/level.rb)는 project, group, global의 세 가지 수준을 인식합니다. Search::AdvancedFinders::Security::Vulnerability::BaseFinder는 네 번째 수준인 organization을 도입하며, 이 수준은 vulnerable 객체가 Organizations::Organization 인스턴스일 때 설정됩니다. organization 수준은 Search::Level 이 인식하지 않으며 일반 글로벌 검색 프레임워크 어디에서도 사용되지 않습니다.

권한 부여 및 스코핑#

일반 프레임워크는 Filters.by_search_level_and_membership, by_user_accessible_namespaces, search_level_filter를 사용해 가시성과 멤버십 규칙을 적용합니다. VulnerabilityQueryBuilder는 이 메서드들을 호출하지 않습니다. Search::Elastic::Filters에서 사용하는 메서드는 by_traversal_ids 하나뿐이며, 이 메서드는 traversal_ids 필드에 접두사 필터를 적용할 뿐 search_level을 읽지 않습니다.

이 메서드들을 사용하지 않는 것은 의도된 선택입니다. Search::Elastic::Filters::ALLOWED_SEARCH_LEVELS 는 %i[global group project] 이고, fetch_search_level!는 이 목록에 없는 값에 대해 ArgumentError, 'search_level invalid'를 발생시킵니다. fetch_search_level!를 호출하는 Filters 메서드를 VulnerabilityQueryBuilder에 연결하면 조직 수준 쿼리에서 런타임 오류가 발생합니다.

스코핑은 파인더와 VulnerabilityFilters가 처리합니다.

  • project 및 group 수준에서는 traversal_ids가 vulnerable 객체의 네임스페이스 상위 계층 접두사로 설정됩니다.
  • organization 수준에서는 traversal_ids가 nil 입니다(접두사 필터 없음). 스코핑은 VulnerabilityFilters의 by_organization_id가 제공하며, 이 메서드는 backfill_organization_id_in_vulnerabilities 마이그레이션에 의해 제어됩니다. 해당 마이그레이션이 끝나기 전까지 파인더는 project_id를 센티널 값([0])으로 설정해, 스코프 없이 실행되는 대신 쿼리가 결과를 반환하지 않도록 합니다.

VulnerabilityFilters.by_archived_projects는 :project 수준에서 아카이브 필터를 건너뛰기 위해 search_level을 읽으므로, 스코핑이 파인더만으로 전부 처리되는 것은 아닙니다.

샤드 라우팅#

es_search_options는 Elasticsearch 샤드 라우팅을 위해 root_ancestor_ids를 전달합니다.

  • project 및 group 수준에서는 vulnerable 객체의 단일 루트 네임스페이스 ID 입니다.
  • organization 수준에서는 해당 조직에 속한 최상위 네임스페이스 ID 목록입니다 (ES_ROUTING_MAX_COUNT + 1로 제한됩니다. 이 한도를 넘으면 라우팅이 적용되지 않고 모든 샤드를 대상으로 쿼리합니다).

형제 파인더#

형제 파인더(CountBySeverityFinder, CountByAgeFinder, CountOverTimeFinder, IdentifierNamesFinder, RiskScoresFinder, TopCwesFinder)는 모두 BaseFinder를 상속하며 search_params와 es_search_options를 그대로 재사용합니다. VulnerabilitySorts와 VulnerabilityAggregations도 search_level을 참조하지 않으므로 organization 수준은 정렬이나 집계 로직에 영향을 주지 않습니다.

SearchFinder도 BaseFinder를 상속하지만 형제가 아니라 주 파인더이므로 위 목록에는 포함하지 않았습니다.