고급 검색 개발 팁
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 파라미터는 선택 사항이며, 지정한 고급 검색 마이그레이션(예: 매핑에 필요한
필드를 추가한 마이그레이션)이 완료된 뒤에만 업데이트가 수행되도록 보장합니다.
테스트#
Elasticsearch 테스트는 모든 머지 리퀘스트에서 실행되지는 않습니다. Elasticsearch와 PostgreSQL 운영 버전으로 테스트를 실행하려면 머지
리퀘스트에 ~pipeline:run-search-tests 또는 ~group::global search 레이블을 추가합니다.
고급 검색 마이그레이션#
인덱스 매핑을 변경하는 마이그레이션 테스트#
- 인덱스에 변경 사항이 아직 적용되지 않았는지 확인합니다. 마이그레이션 크론 워커가 백그라운드에서 실행되므로 마이그레이션이 이미 적용되었을 수 있습니다.
-
선택 사항. 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
- Kibana에서
-
- 로그에 기록되는 메시지를 확인하려면 로그를 실시간으로 확인합니다:
tail -f log/elasticsearch.log. - 다음 방법 중 하나로 마이그레이션을 실행합니다.
Elastic::MigrationWorker.new.perform마이그레이션 워커를 실행합니다. GitLab 18.0 이상에서는elastic_migration_worker_enabled애플리케이션 설정이 활성화되어 있어야 합니다.- 대기 중인 마이그레이션을 사용합니다:
::Elastic::DataMigrationService.pending_migrations.first.migrate. - 버전을 사용합니다:
Elastic::DataMigrationService[20250220214819].migrate. 버전은 해당 마이그레이션 버전으로 바꿉니다.
- 마이그레이션 상태를 확인합니다.
- Kibana에서 마이그레이션 레코드를 확인합니다:
GET gitlab-development-migrations/_doc/20250220214819(버전은 바꿉니다). 시작 시각과 상태 등의 정보가 담겨 있습니다. - Kibana에서 매핑이 변경되었는지 확인합니다:
GET gitlab-development-some-index/_mapping.
- Kibana에서 마이그레이션 레코드를 확인합니다:
쿼리 변경 분석#
개발자는 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를 상속하지만 형제가 아니라 주 파인더이므로
위 목록에는 포함하지 않았습니다.