InfoGrab DocsInfoGrab Docs

Advanced search 개발 가이드라인

요약

Advanced search를 활성화하고 초기 인덱싱을 수행하는 방법은 Elasticsearch 연동 문서를 참고하세요. 다음 녹화 영상 및 발표 자료는 Advanced search 구현에 대한 심층적인 지식을 제공합니다:


Advanced search 개발 가이드라인#

  이 페이지에는 Elasticsearch를 기반으로 하는 Advanced search를 개발하고 사용하는 방법에 대한 정보가 포함되어 있습니다.

Advanced search를 활성화하고 초기 인덱싱을 수행하는 방법은 Elasticsearch 연동 문서를 참고하세요.

심층 학습 자료#

다음 녹화 영상 및 발표 자료는 Advanced search 구현에 대한 심층적인 지식을 제공합니다:

날짜 주제 발표자 자료 GitLab 버전
2024년 7월 Advanced search 기초, 연동, 인덱싱 및 검색 Terri Chu YouTube 녹화 영상 (GitLab 팀원 전용)Google 슬라이드 (GitLab 팀원 전용) GitLab 17.0
2021년 6월 Advanced search를 위한 GitLab의 데이터 마이그레이션 프로세스 Dmitry Gruzd 블로그 게시물 GitLab 13.12
2020년 8월 멀티 인덱스 지원을 위한 GitLab 전용 아키텍처 Mark Chao YouTube 녹화 영상Google 슬라이드 GitLab 13.3
2019년 6월 GitLab Elasticsearch 연동 Mario de la Ossa YouTube 녹화 영상Google 슬라이드PDF GitLab 12.0

Elasticsearch 구성#

지원 버전#

버전 호환성을 참고하세요.

Elasticsearch 쿼리를 크게 변경하는 개발자는 지원되는 모든 버전에 대해 기능을 테스트해야 합니다.

개발 환경 설정#

Elasticsearch GDK 설정 지침을 참고하세요.

Elasticsearch가 실행 중인지 확인하세요:

curl "http://localhost:9200"
  • Kibana를 실행하여 로컬 Elasticsearch 클러스터와 상호 작용하세요. 또는 Cerebro 또는 유사한 도구를 사용할 수 있습니다.

Elasticsearch 로그를 실시간으로 확인하려면 다음 명령어를 실행하세요:

tail -f log/elasticsearch.log

SaaS 모드로 실행하는 경우, GitLab.com에서 Advanced search가 구성되는 방식을 모방하기 위해 인덱싱할 네임스페이스 및 프로젝트 데이터의 양을 제한해야 합니다.

네임스페이스 제한이 활성화되지 않은 경우, Advanced search는 기본적으로 모든 네임스페이스(무료 네임스페이스 포함)에 대해 활성화됩니다.

유용한 Rake 태스크#

  • gitlab:elastic:test:index_size: 현재 인덱스가 사용하는 공간과 인덱스에 있는 문서 수를 알려줍니다.

  • gitlab:elastic:test:index_size_change: 인덱스 크기를 출력하고, 재인덱싱한 후 다시 인덱스 크기를 출력합니다. 인덱싱 크기 개선 사항을 테스트할 때 유용합니다.

또한, 테스트를 위해 대용량 리포지터리나 여러 포크가 필요한 경우 다음 지침을 따르는 것을 고려하세요.

개발 워크플로#

개발 팁#

디버깅 및 문제 해결#

Elasticsearch 쿼리 디버깅#

ELASTIC_CLIENT_DEBUG 환경 변수는 개발 또는 테스트 환경에서 Elasticsearch 클라이언트의 debug 옵션을 활성화합니다. 코드 또는 테스트에서 생성된 Elasticsearch HTTP 쿼리를 디버깅해야 하는 경우, 스펙 실행 전이나 Rails 콘솔을 시작하기 전에 활성화할 수 있습니다:

ELASTIC_CLIENT_DEBUG=1 bundle exec rspec ee/spec/workers/search/elastic/trigger_indexing_worker_spec.rb

export ELASTIC_CLIENT_DEBUG=1
rails console

flood stage disk watermark [95%] exceeded 오류 발생 시#

다음과 같은 오류가 발생할 수 있습니다:

[2018-10-31T15:54:19,762][WARN ][o.e.c.r.a.DiskThresholdMonitor] [pval5Ct]
   flood stage disk watermark [95%] exceeded on
   [pval5Ct7SieH90t5MykM5w][pval5Ct][/usr/local/var/lib/elasticsearch/nodes/0] free: 56.2gb[3%],
   all indices on this node will be marked read-only

이는 디스크 공간 임계값을 초과했기 때문입니다. 기본 95% 임계값을 기준으로 남은 디스크 공간이 충분하지 않다고 판단한 것입니다.

또한 read_only_allow_delete 설정이 true로 설정되어 인덱싱, forcemerge 등이 차단됩니다:

curl "http://localhost:9200/gitlab-development/_settings?pretty"

elasticsearch.yml 파일에 다음을 추가하세요:

# turn off the disk allocator
cluster.routing.allocation.disk.threshold_enabled: false

또는

# set your own limits
cluster.routing.allocation.disk.threshold_enabled: true
cluster.routing.allocation.disk.watermark.flood_stage: 5gb   # ES 6.x only
cluster.routing.allocation.disk.watermark.low: 15gb
cluster.routing.allocation.disk.watermark.high: 10gb

Elasticsearch를 재시작하면 read_only_allow_delete가 자동으로 해제됩니다.

출처: "Disk-based Shard Allocation | Elasticsearch Reference" 5.66.x

성능 모니터링#

Prometheus#

GitLab은 모든 웹/API 요청과 Sidekiq job의 요청 수 및 처리 시간에 관련된 Prometheus 메트릭을 내보냅니다. 이를 통해 성능 추세를 진단하고, 전체 성능에서 Elasticsearch 처리 시간이 다른 작업에 비해 어느 정도 영향을 미치는지 비교할 수 있습니다.

인덱싱 대기열#

GitLab은 인덱싱 대기열에 대한 Prometheus 메트릭도 내보냅니다. 이를 통해 성능 병목 현상을 진단하고, GitLab 인스턴스 또는 Elasticsearch 서버가 업데이트 볼륨을 따라잡을 수 있는지 확인할 수 있습니다.

로그#

모든 인덱싱은 Sidekiq에서 처리되므로, Elasticsearch 연동과 관련된 로그 대부분은 sidekiq.log에서 확인할 수 있습니다. 특히, Elasticsearch에 요청을 보내는 모든 Sidekiq 워커는 요청 수와 Elasticsearch 쿼리/쓰기에 소요된 시간을 로깅합니다. 이를 통해 클러스터가 인덱싱 속도를 따라가고 있는지 파악할 수 있습니다.

Elasticsearch 검색은 요청을 처리하는 일반 웹 워커를 통해 수행됩니다. 페이지 로드 또는 API 요청 중 Elasticsearch에 요청을 보내는 경우, 요청 수와 소요 시간이 production_json.log에 기록됩니다. 이 로그에는 데이터베이스 및 Gitaly 요청에 소요된 시간도 포함되어 있어, 검색의 어느 부분에서 성능 문제가 발생하는지 진단하는 데 도움이 될 수 있습니다.

Elasticsearch 전용 추가 로그는 elasticsearch.log로 전송되며, 성능 문제 진단에 유용한 정보가 포함될 수 있습니다.

Performance Bar#

Elasticsearch 요청은 Performance Bar에 표시됩니다. 이를 로컬 개발 환경과 배포된 GitLab 인스턴스 모두에서 활용하여 검색 성능 저하를 진단할 수 있습니다. 실행된 정확한 쿼리를 확인할 수 있어 검색이 느린 원인을 파악하는 데 유용합니다.

상관 관계 ID 및 X-Opaque-Id#

상관 관계 ID는 Rails에서 Elasticsearch로 보내는 모든 요청에 X-Opaque-Id 헤더로 전달됩니다. 이를 통해 클러스터의 모든 태스크를 GitLab의 요청과 연결하여 추적할 수 있습니다.

아키텍처#

Elasticsearch와 통신하는 데 사용되는 프레임워크는 이 에픽에서 추적 중인 리팩토링 작업이 진행 중입니다.

인덱싱 개요#

Advanced search는 데이터를 선택적으로 인덱싱합니다. 각 데이터 유형은 특정 인덱싱 파이프라인을 따릅니다:

데이터 유형 대기열에 넣는 방법 대기열 위치 인덱싱 발생 위치
데이터베이스 레코드 ActiveRecord 콜백 및 Gitlab::EventStore를 통한 레코드 변경 추적 Redis ZSET ElasticIndexInitialBulkCronWorker, ElasticIndexBulkCronWorker
Git 리포지터리 데이터 브랜치 푸시 서비스 및 기본 브랜치 변경 워커 Sidekiq Search::Elastic::CommitIndexerWorker, ElasticWikiIndexerWorker

인덱싱 구성 요소#

외부 인덱서#

리포지터리 콘텐츠의 경우, GitLab은 파일을 효율적으로 처리하기 위해 Go로 작성된 전용 인덱서를 사용합니다.

Rails 인덱싱 라이프사이클#

  • 초기 인덱싱: 관리자가 Admin UI 또는 Rake 태스크를 통해 첫 번째 전체 인덱스를 트리거합니다

  • 지속적 업데이트: 초기 설정 이후 GitLab은 다음을 통해 인덱스 최신 상태를 유지합니다:

/ee/app/models/concerns/elastic/application_versioned_search.rb에 정의된 모델 콜백(after_create, after_update, after_destroy)

  • 보류 중인 모든 변경 사항을 추적하는 Redis ZSET

  • Elasticsearch의 Bulk Request API를 사용하여 이 대기열을 배치로 처리하는 예약된 Sidekiq 워커

검색 및 보안#

쿼리 빌더 프레임워크는 검색 쿼리를 생성하고 접근 제어 로직을 처리합니다. 코드베이스의 이 부분은 개발 및 코드 리뷰 중에 특별한 주의가 필요하며, 역사적으로 보안 취약점의 원인이 되어왔습니다.

검색 결과를 반환하는 마지막 단계는 쿼리 문제나 경쟁 조건을 감지하기 위해 현재 사용자에 대한 미인가 결과를 삭제하는 것입니다.

마이그레이션 프레임워크#

GitLab Advanced search에는 인덱스 유지 관리 및 업데이트를 간소화하는 견고한 마이그레이션 프레임워크가 포함되어 있습니다. 이 시스템은 다음과 같은 중요한 이점을 제공합니다:

  • 선택적 재인덱싱: 필요한 경우에만 특정 문서 유형을 업데이트하여 전체 재인덱싱을 방지합니다

  • 자동화된 유지 관리: 사람의 개입 없이 업데이트가 진행됩니다

  • 일관된 경험: GitLab.com과 GitLab Self-Managed 인스턴스 모두에 동일한 마이그레이션 경로를 제공합니다

프레임워크 구성 요소#

마이그레이션 시스템은 다음으로 구성됩니다:

  • 마이그레이션 러너: 5분마다 실행하여 보류 중인 마이그레이션을 확인하고 처리하는 cron 워커

  • 마이그레이션 파일: 데이터베이스 마이그레이션과 유사하게, 이 Ruby 파일들은 YAML 문서와 함께 마이그레이션 단계를 정의합니다

  • 마이그레이션 상태 추적: 모든 마이그레이션 상태는 전용 Elasticsearch 인덱스에 저장됩니다

  • 마이그레이션 라이프사이클 상태: 각 마이그레이션은 다음 단계를 거칩니다: 대기 중 → 진행 중 → 완료 (또는 문제가 발생한 경우 중단됨)

구성 옵션#

마이그레이션은 다양한 파라미터로 세부 조정할 수 있습니다:

  • 배치 처리: 최적의 성능을 위한 문서 배치 크기 제어

  • 스로틀링: 마이그레이션 속도와 시스템 부하 간의 균형을 맞추기 위한 인덱싱 속도 조정

  • 공간 요구 사항: 중단을 방지하기 위해 마이그레이션 시작 전 충분한 디스크 공간 확인

  • 건너뛰기 조건: 마이그레이션을 건너뛰기 위한 조건 정의

이 프레임워크는 모든 GitLab 설치에 대해 인덱스 스키마 변경, 필드 업데이트, 데이터 마이그레이션을 안정적이고 최소한의 영향으로 처리합니다.

Search DSL#

이 섹션에서는 GitLab이 지원하는 Search DSL(Domain Specific Language)을 다루며, Elasticsearch와 OpenSearch 구현 모두와 호환됩니다.

커스텀 라우팅#

커스텀 라우팅은 Elasticsearch에서 문서 유형에 사용됩니다. 라우팅 형식은 일반적으로 프로젝트 연관 데이터의 경우 project_<project_id>, 그룹 연관 데이터의 경우 group_<root_namespace_id>입니다. 라우팅은 인덱싱 및 검색 작업 중에 설정되며 Elasticsearch에 데이터를 저장할 샤드를 알려줍니다. 커스텀 라우팅을 사용하는 이점과 트레이드오프는 다음과 같습니다:

  • 모든 샤드를 확인할 필요가 없으므로 프로젝트 및 그룹 범위 검색이 훨씬 빠릅니다.

  • 전역 및 그룹 범위 검색에서 너무 많은 샤드가 영향을 받는 경우 라우팅이 사용되지 않습니다.

  • 샤드 크기 불균형이 발생할 수 있습니다.

기존 분석기 및 토크나이저#

다음 분석기 및 토크나이저는 ee/lib/elastic/latest/config.rb에 정의되어 있습니다.

분석기#

path_analyzer 블롭의 경로를 인덱싱할 때 사용됩니다. path_tokenizerlowercaseasciifolding 필터를 사용합니다.

예시는 아래의 path_tokenizer 설명을 참조하세요.

sha_analyzer 블롭과 커밋에 사용됩니다. sha_tokenizerlowercaseasciifolding 필터를 사용합니다.

예시는 아래의 sha_tokenizer 설명을 참조하세요.

code_analyzer 블롭의 파일명과 콘텐츠를 인덱싱할 때 사용됩니다. whitespace 토크나이저를 사용합니다

word_delimiter_graph, lowercase, asciifolding 필터를 사용합니다.

whitespace 토크나이저는 토큰 분리 방식을 보다 세밀하게 제어하기 위해 선택되었습니다. 예를 들어 Foo::bar(4) 문자열은 제대로 검색되려면 Foo, bar(4) 같은 토큰을 생성해야 합니다.

토큰 분리 방식에 대한 설명은 code 필터를 참조하세요.

토크나이저#

sha_tokenizer 이는 SHA를 임의의 부분 문자열(최소 5자)로 검색할 수 있도록 edgeNGram 토크나이저를 사용하는 커스텀 토크나이저입니다.

예시:

240c29dc7e는 다음과 같이 변환됩니다:

  • 240c2

  • 240c29

  • 240c29d

  • 240c29dc

  • 240c29dc7

  • 240c29dc7e

path_tokenizer 이는 입력으로 제공된 경로의 길이에 관계없이 경로를 검색할 수 있도록 reverse: true 옵션을 사용하는 path_hierarchy 토크나이저를 활용하는 커스텀 토크나이저입니다.

예시:

'/some/path/application.js'는 다음과 같이 변환됩니다:

  • '/some/path/application.js'

  • 'some/path/application.js'

  • 'path/application.js'

  • 'application.js'

주의사항#

  • 검색은 자체 분석기를 가질 수 있습니다. 분석기를 수정할 때 반드시 확인하세요.

  • Character 필터(토큰 필터와 달리)는 항상 원래 문자를 대체합니다. 이러한 필터는 정확한 검색을 방해할 수 있습니다.

구현 가이드#

Elasticsearch에 새 문서 유형 추가#

Elasticsearch의 기존 인덱스 중 하나에 데이터를 추가할 수 없는 경우, 다음 지침에 따라 새 인덱스를 설정하고 데이터를 채웁니다.

새 문서 유형 추가를 위한 권장 절차#

모든 머지 리퀘스트는 Global Search 팀 멤버에게 리뷰를 받아야 합니다:

인덱싱이 완료되면 인덱스를 검색에 사용할 수 있습니다.

인덱스 생성#

모든 새 인덱스에는 다음이 포함되어야 합니다:

  • project_idnamespace_id 필드(사용 가능한 경우). 두 필드 중 하나는 커스텀 라우팅에 사용되어야 합니다.

  • 효율적인 전역 및 그룹 검색을 위한 traversal_ids 필드. object.namespace.elastic_namespace_ancestry로 필드를 채웁니다.

  • 인가(authorization)를 위한 필드:

프로젝트 데이터의 경우 - visibility_level

  • 그룹 데이터의 경우 - namespace_visibility_level

  • 필수 액세스 레벨 필드. 이는 issues_access_level 또는 repository_access_level과 같은 프로젝트 기능 액세스 레벨에 해당합니다.

  • YYVV(연도/버전) 형식의 schema_version 정수 필드. YY는 두 자리 연도이고, VV는 해당 연도 내 롤링 카운터(01-99)입니다. 스키마 버전은 레퍼런스 인덱스 클래스(Search::Elastic::References:: 또는 Elastic::Latest::InstanceProxy)의 상수(SCHEMA_VERSION)에 정의되어야 합니다. 이 필드는 어떤 버전의 문서 구조가 인덱싱되었는지 추적하고 데이터 마이그레이션을 가능하게 합니다. 인덱스 매핑이 변경될 때 반드시 증가시켜야 하며, 필드 콘텐츠가 변경될 때도 증가시킬 수 있습니다.

ee/lib/search/elastic/types/Search::Elastic::Types:: 클래스를 생성합니다.

다음 클래스 메서드를 정의합니다:

index_name: gitlab-<env>-<type> 형식(예: gitlab-production-work_items).

  • mappings: 필드, 데이터 타입, 분석기 등 인덱스 스키마를 포함하는 해시.

  • settings: 복제본 및 토크나이저 등 인덱스 설정을 포함하는 해시. 대부분의 경우 기본값으로 충분합니다.

scripts/elastic-migration을 실행하고 안내에 따라 인덱스를 생성하는 새 고급 검색 마이그레이션을 추가합니다. 마이그레이션 이름은 CreateIndex 형식이어야 합니다.

Search::Elastic::MigrationCreateIndexHelper 헬퍼와 생성된 스펙 파일에 'migration creates a new index' 공유 예시를 사용합니다.

타깃 클래스를 Gitlab::Elastic::Helper::ES_SEPARATE_CLASSES에 추가합니다.

인덱스 생성을 테스트하려면 콘솔에서 Elastic::MigrationWorker.new.perform을 실행하고, 올바른 매핑과 설정으로 인덱스가 생성되었는지 확인합니다:

curl "http://localhost:9200/gitlab-development-<type>/_mappings" | jq .`
curl "http://localhost:9200/gitlab-development-<type>/_settings" | jq .`
PostgreSQL에서 Elasticsearch로의 매핑#

기본 키 및 외래 키의 데이터 타입은 데이터베이스의 칼럼 타입과 일치해야 합니다. 예를 들어, 데이터베이스 칼럼 타입 integer는 매핑에서 integer로, bigintlong으로 매핑됩니다.

[Nested fields](https://www.elastic.co/guide/en/elasticsearch/reference/current/nested.html#_limits_on_nested_mappings_and_objects)는 상당한 오버헤드를 유발합니다. 대신 평탄화된 다중 값 방식을 권장합니다.
PostgreSQL 타입 Elasticsearch 매핑
bigint long
smallint short
integer integer
boolean boolean
array keyword
timestamp date
character varying, text 쿼리 요구 사항에 따라 다름. 전문 검색에는 text, 용어 쿼리·정렬·집계에는 keyword 사용
예상 쿼리 유효성 검사#

새 인덱스를 생성하기 전에, 계획된 매핑이 예상 쿼리를 지원하는지 유효성을 검사하는 것이 중요합니다. 매핑 호환성을 사전에 검증하면 나중에 인덱스를 재구축해야 하는 문제를 방지할 수 있습니다.

새 Elastic Reference 생성#

ee/lib/search/elastic/references/Search::Elastic::References:: 클래스를 생성합니다.

이 Reference는 Elasticsearch에서 벌크 작업을 수행하는 데 사용됩니다. 파일은 Search::Elastic::Reference를 상속해야 하며 다음 상수와 메서드를 정의해야 합니다:

include Search::Elastic::Concerns::DatabaseReference # if there is a corresponding database record for every document

SCHEMA_VERSION = 24_46 # integer in YYVV format

override :serialize
def self.serialize(record)
   # a string representation of the reference
end

override :instantiate
def self.instantiate(string)
   # deserialize the string and call initialize
end

override :preload_indexing_data
def self.preload_indexing_data(refs)
   # remove this method if `Search::Elastic::Concerns::DatabaseReference` is included
   # otherwise return refs
end

def initialize
   # initialize with instance variables
end

override :identifier
def identifier
   # a way to identify the reference
end

override :routing
def routing
   # Optional: an identifier to route the document in Elasticsearch
end

override :operation
def operation
   # one of `:index`, `:upsert` or `:delete`
end

override :serialize
def serialize
   # a string representation of the reference
end

override :as_indexed_json
def as_indexed_json
   # a hash containing the document representation for this reference
end

override :index_name
def index_name
   # index name
end

def model_klass
   # set to the model class if `Search::Elastic::Concerns::DatabaseReference` is included
end

인덱스에 데이터를 추가하려면 Elastic::ProcessBookkeepingService.track!()에서 새 Reference 클래스의 인스턴스를 호출하여 인덱싱할 Reference 큐에 데이터를 추가합니다. 크론 워커가 큐에 있는 Reference를 가져와 Elasticsearch에 항목을 벌크 인덱싱합니다.

인덱싱 작업이 정상적으로 동작하는지 테스트하려면 Reference 클래스의 인스턴스를 사용하여 Elastic::ProcessBookkeepingService.track!()을 호출하고 Elastic::ProcessBookkeepingService.new.execute를 실행합니다. 로그에서 업데이트 내용을 확인할 수 있습니다. 인덱스의 문서를 확인하려면 다음 명령을 실행합니다:

curl "http://localhost:9200/gitlab-development-<type>/_search"
일반적인 주의사항#
  • 인덱스 작업은 실제로 upsert를 수행합니다. 문서가 이미 존재하면 전송된 필드를 기존 문서 필드와 병합하여 부분 업데이트를 수행합니다. 필드를 명시적으로 제거하거나 비워두려면 as_indexed_json에서 nil 또는 빈 배열을 전송해야 합니다.

데이터 일관성#

이제 인덱스와 새 문서 유형을 Elasticsearch에 대량 인덱싱하는 방법이 마련되었으므로, 인덱스에 데이터를 추가해야 합니다. 이 과정은 초기 데이터 백필(backfill)과 인덱스 데이터를 최신 상태로 유지하기 위한 지속적인 업데이트로 구성됩니다.

백필은 인덱싱해야 하는 모든 문서에 대해 Search::Elastic::Reference 인스턴스를 인자로 Elastic::ProcessInitialBookkeepingService.track!()을 호출하여 수행합니다.

지속적인 업데이트는 생성/업데이트/삭제해야 하는 모든 문서에 대해 Search::Elastic::Reference 인스턴스를 인자로 Elastic::ProcessBookkeepingService.track!()을 호출하여 수행합니다.

데이터 백필#

scripts/elastic-migration을 실행하고 안내에 따라 새 고급 검색 마이그레이션을 추가하여 데이터를 백필합니다.

MigrationDatabaseBackfillHelper를 사용하세요. BackfillWorkItems 마이그레이션을 예시로 참고할 수 있습니다.

백필을 테스트하려면 콘솔에서 Elastic::MigrationWorker.new.perform을 몇 번 실행하여 인덱스가 채워졌는지 확인하세요.

마이그레이션 진행 상황을 확인하려면 로그를 실시간으로 모니터링하세요:

tail -f log/elasticsearch.log
지속적인 업데이트#

ActiveRecord 객체의 경우, 모델에 ApplicationVersionedSearch 컨설턴트를 포함하여 콜백 기반으로 데이터를 인덱싱할 수 있습니다. 이 방식이 적합하지 않은 경우, 문서를 인덱싱해야 할 때마다 Search::Elastic::Reference 인스턴스를 인자로 Elastic::ProcessBookkeepingService.track!()을 호출하세요.

일부 GitLab Self-Managed 인스턴스는 Elasticsearch가 활성화되어 있지 않고 네임스페이스 제한이 활성화될 수 있으므로, 항상 Gitlab::CurrentSettings.elasticsearch_indexing?use_elasticsearch?를 확인하세요.

또한 인덱스가 인덱스 요청을 처리할 수 있는지 확인하세요. 예를 들어, 현재 메이저 릴리즈에서 추가된 인덱스인 경우 인덱스를 추가하는 마이그레이션이 완료되었는지 확인하세요: Elastic::DataMigrationService.migration_has_finished?

전송 및 삭제#

프로젝트 및 그룹의 전송과 삭제는 고아 데이터가 발생하지 않도록 인덱스를 업데이트해야 합니다. 고아 데이터는 전송으로 인해 커스텀 라우팅이 변경될 때 발생할 수 있습니다. 이전 샤드의 데이터를 정리해야 합니다. 전송에 대한 Elasticsearch 업데이트는 Projects::TransferServiceGroups::TransferService에서 처리됩니다.

project_id 필드를 포함하는 인덱스는 Search::Elastic::DeleteWorker를 사용해야 합니다. namespace_id 필드를 포함하고 project_id 필드가 없는 인덱스는 Search::ElasticGroupAssociationDeletionWorker를 사용해야 합니다.

  • 인덱싱된 클래스를 ElasticDeleteProjectWorkerexcluded_classes에 추가하세요.

  • 인덱스에서 문서를 삭제하는 새 서비스를 ::Search::Elastic::Delete 네임스페이스에 생성하세요.

  • 새 서비스를 사용하도록 워커를 업데이트하세요.

새 문서 유형에 대한 검색 구현#

검색 데이터는 SearchControllerSearch API에서 사용할 수 있습니다. 두 곳 모두 SearchService를 사용하여 결과를 반환합니다. SearchServiceSearchControllerSearch API 외부에서도 결과를 반환하는 데 사용할 수 있습니다.

새 문서 유형에 대한 검색 구현 권장 프로세스#

다음 머지 리퀘스트를 생성하고 Global Search 팀 구성원에게 리뷰를 요청하세요:

검색 스코프#

SearchService전역, 그룹, 프로젝트 레벨에서의 검색을 제공합니다.

새 스코프는 다음 상수에 추가해야 합니다:

  • 각 EE SearchService 파일의 ALLOWED_SCOPES (또는 allowed_scopes 메서드 오버라이드)

  • Gitlab::Search::AbuseDetectionALLOWED_SCOPES

  • Search::Navigationsearch_tab_ability_map 메서드. 필요한 경우 EE 버전에서 오버라이드

    스코프에 대한 전역 검색을 비활성화할 수 있습니다. 전역 검색 비활성화를 위해 다음 변경을 수행할 수 있습니다:

  • app/models/application_setting.rbsearch jsonb 접근자 아래에 기본값이 trueglobal_search_SCOPE_enabled 애플리케이션 설정을 추가하세요.

  • JSON 스키마 유효성 검사기 파일 application_setting_search.json에 항목을 추가하세요.

  • ApplicationSettingsHelperglobal_search_settings_checkboxes 메서드에 항목을 생성하여 Admin UI에 설정 체크박스를 추가하세요.

  • SearchServiceglobal_search_enabled_for_scope? 메서드에 추가하세요.

  • EE 전용 설정은 파일의 EE 버전에 추가해야 합니다.

결과 클래스#

사용 가능한 검색 결과 클래스는 다음과 같습니다:

검색 유형 검색 레벨 클래스
Basic search global Gitlab::SearchResults
Basic search group Gitlab::GroupSearchResults
Basic search project Gitlab::ProjectSearchResults
Advanced search global Gitlab::Elastic::SearchResults
Advanced search group Gitlab::Elastic::GroupSearchResults

| Advanced search | project | Gitlab::Elastic::ProjectSearchResults | | Exact code search | global | Search::Zoekt::SearchResults | | Exact code search | group | Search::Zoekt::SearchResults | | Exact code search | project | Search::Zoekt::SearchResults | | All search types | All levels | Search::EmptySearchResults |

결과 클래스는 다음 데이터를 반환합니다:

  • objects - Elasticsearch에서 변환된 데이터베이스 레코드 또는 PORO의 페이지네이션 결과

  • formatted_count - Elasticsearch에서 반환된 문서 수

  • highlight_map - Elasticsearch에서 반환된 하이라이트된 필드의 맵

  • failed? - 오류 발생 여부

  • error - Elasticsearch에서 반환된 오류 메시지

  • aggregations - (선택 사항) Elasticsearch에서 반환된 집계

새 scope는 Gitlab::Elastic::SearchResults 클래스 내 다음 메서드에 대한 지원을 추가해야 합니다:

  • objects

  • formatted_count

  • highlight_map

  • failed?

  • error

기존 scope 업데이트#

업데이트에는 문서 필드 추가 및 제거, 또는 인가(권한 부여) 변경이 포함될 수 있습니다. 기존 scope를 업데이트하려면 쿼리 및 인덱싱용 JSON 생성에 사용되는 코드를 찾으세요.

  • 쿼리는 QueryBuilder 클래스에서 생성됩니다

  • 인덱싱된 문서는 Reference 클래스에서 빌드됩니다

레거시 Proxy 프레임워크도 지원합니다:

  • 쿼리는 ClassProxy 클래스에서 생성됩니다

  • 인덱싱된 문서는 InstanceProxy 클래스에서 빌드됩니다

레거시 프레임워크에서 사용되는 경우에도 새 검색 필터는 항상 QueryBuilder 프레임워크에 생성하는 것을 목표로 하세요.

필드 추가#

인덱스에 필드 추가#
  • 인덱스 매핑에 필드를 추가하여 새로 생성된 인덱스에 반영하고, 동일한 MR 내에서 기존 인덱스에 필드를 추가하는 마이그레이션을 생성하여 매핑 스키마 드리프트를 방지하세요. MigrationUpdateMappingsHelper를 사용하세요.

  • 문서 JSON에서 새 필드를 채웁니다. 코드는 ::Elastic::DataMigrationService.migration_has_finished?를 사용하여 마이그레이션이 완료되었는지 확인해야 합니다.

  • 문서 JSON의 SCHEMA_VERSION을 올립니다. 형식은 연도와 버전 번호입니다: YYVV

  • 인덱스에서 필드를 백필(backfill)하는 마이그레이션을 생성합니다. null이 허용되지 않는 필드의 경우 MigrationBackfillHelper를 사용하고, null이 허용되는 필드의 경우 MigrationReindexBasedOnSchemaVersion을 사용하세요.

새 필드가 연관 레코드인 경우#
검색 서비스에 필드 노출#
  • Search::Filter concern에 필터를 추가하세요. 이 concern은 Search::GlobalService, Search::GroupService, Search::ProjectService에서 사용됩니다.

  • scope_options 메서드를 업데이트하여 scope에 해당 필드를 전달하세요. 이 메서드는 Gitlab::Elastic::SearchResults에 정의되어 있으며 Gitlab::Elastic::GroupSearchResultsGitlab::Elastic::ProjectSearchResults에서 재정의됩니다.

  • 기존 필터를 추가하거나 새 필터를 생성하여 쿼리 빌더에서 해당 필드를 사용하세요.

  • SearchController에서 검색의 필터 사용을 추적하세요.

기존 필드의 매핑 변경#

  • 인덱스 매핑에서 필드 타입을 업데이트하여 새로 생성된 인덱스에 변경 사항을 반영하세요.

  • 문서 JSON의 SCHEMA_VERSION을 올립니다. 형식은 연도와 버전 번호입니다: YYVV

  • Zero downtime 리인덱싱을 사용하여 모든 문서를 리인덱싱하는 마이그레이션을 생성합니다. Search::Elastic::MigrationReindexTaskHelper를 사용하세요.

필드 콘텐츠 변경#

  • 문서 JSON에서 필드 콘텐츠를 업데이트하세요.

  • 문서 JSON의 SCHEMA_VERSION을 올립니다. 형식은 연도와 버전 번호입니다: YYVV

  • 문서를 업데이트하는 마이그레이션을 생성합니다. MigrationReindexBasedOnSchemaVersion을 사용하세요.

인덱스에서 문서 정리#

하나의 인덱스가 별도의 인덱스로 분리되는 경우 또는 버그로 인해 인덱스에 남아 있는 데이터를 제거하는 경우에 사용할 수 있습니다.

  • 문서 JSON의 SCHEMA_VERSION을 올립니다. 형식은 연도와 버전 번호입니다: YYVV

  • 모든 레코드를 인덱싱하는 마이그레이션을 생성합니다. MigrationDatabaseBackfillHelper를 사용하세요.

  • 이전 SCHEMA_VERSION을 가진 모든 문서를 제거하는 마이그레이션을 생성합니다. MigrationDeleteBasedOnSchemaVersion을 사용하세요.

필드 제거#

멀티 버전 호환성을 지원하기 위해 제거는 여러 마일스톤에 걸쳐 분할되어야 합니다. 동적 매핑 오류를 방지하려면 Zero downtime 리인덱싱을 수행하기 전에 모든 문서에서 해당 필드를 제거해야 합니다.

마일스톤 M:

  • 새로 생성되는 인덱스에서 해당 필드를 제거하기 위해 인덱스 매핑에서 필드를 삭제합니다.

  • 문서 JSON에서 해당 필드를 더 이상 채우지 않도록 중단합니다.

  • 문서 JSON의 SCHEMA_VERSION을 올립니다. 형식은 연도와 버전 번호인 YYVV입니다.

  • 쿼리 빌더에서 해당 필드를 사용하는 필터를 제거합니다.

  • scope_options 메서드를 업데이트하여 업데이트 중인 스코프에서 해당 필드를 제거합니다. 이 메서드는 Gitlab::Elastic::SearchResults에 정의되어 있으며, Gitlab::Elastic::GroupSearchResultsGitlab::Elastic::ProjectSearchResults에서 재정의됩니다.

해당 필드를 다른 스코프에서 사용하지 않는 경우:

  • Search::Filter concern에서 해당 필드를 제거합니다. 이 concern은 Search::GlobalService, Search::GroupService, Search::ProjectService에서 사용됩니다.

  • SearchController의 검색에서 필터 추적을 제거합니다.

마일스톤 M+1:

인가(권한 부여) 업데이트#

QueryBuilder 프레임워크에서 인가(권한 부여)는 프로젝트 수준에서 by_search_level_and_membership 필터로 처리되며, 그룹 수준에서는 by_search_level_and_group_membership 필터로 처리됩니다.

레거시 Proxy 프레임워크에서는 인가(권한 부여)가 클래스 내부에서 처리됩니다.

두 프레임워크 모두 Search::GroupsFinderSearch::ProjectsFinder를 사용하여 사용자가 직접 검색 접근 권한을 가진 그룹과 프로젝트를 쿼리합니다. 검색은 각 스코프에 대한 그룹 및 프로젝트 가시성 수준과 기능 접근 수준 설정에 의존합니다. 자세한 내용은 역할 및 권한 문서를 참조하세요.

쿼리 빌더 프레임워크#

쿼리 빌더 프레임워크는 Elasticsearch 쿼리를 구성하는 데 사용됩니다. Elastic::Latest::ApplicationClassProxy 클래스와 이를 상속하는 클래스에 구현된 레거시 쿼리 프레임워크도 지원합니다.

새로운 문서 타입은 반드시 쿼리 빌더 프레임워크를 사용해야 합니다.

쿼리 생성#

쿼리는 다음을 사용하여 구성됩니다:

  • Search::Elastic::Queries의 쿼리

  • ::Search::Elastic::Filters의 하나 이상의 필터

  • (선택 사항) ::Search::Elastic::Aggregations의 집계

  • ::Search::Elastic::Formats의 하나 이상의 형식

새로운 스코프는 Search::Elastic::QueryBuilder를 상속하는 새로운 쿼리 빌더 클래스를 생성해야 합니다.

쿼리 빌더 프레임워크는 일반적인 검색 시나리오를 처리하기 위해 미리 구성된 필터 모음을 제공합니다. 이 필터들은 원시 Elasticsearch 쿼리 DSL을 직접 작성하지 않고도 복잡한 쿼리 조건을 구성하는 과정을 단순화합니다.

QUERY_COMPONENTS로 쿼리 빌더 구성#

쿼리 빌더는 파이프라인을 QUERY_COMPONENTS 해시로 선언합니다. 기본 클래스가 해시를 순회하며 각 메서드를 실행 중인 query_hash에 적용하므로, 서브클래스는 build를 재정의하지 않아도 됩니다.

해시 키는 모듈(Filters, Formats, Sorts, Aggregations)입니다. 값은 해당 모듈의 메서드 배열입니다. 각 메서드는 실행 중인 query_hash:와 빌더의 options:를 받아 다음 query_hash를 반환합니다.

skip_if_size_zero는 size가 0으로 설정된 경우 쿼리 컴포넌트를 건너뜁니다. 예를 들어, 개수만 조회하는 쿼리를 실행할 때는 정렬을 적용할 필요가 없습니다.

Search::Elastic::UserQueryBuilder의 최소 예시:

QUERY_COMPONENTS = {
  ::Search::Elastic::Filters => %i[by_forbidden_states by_user_accessible_namespaces],
  ::Search::Elastic::Formats => %i[size source_fields],
  ::Search::Elastic::Sorts::Base => %i[sort_by]
}.freeze

Search::Elastic::WorkItemQueryBuilder의 더 복잡한 예시:

QUERY_COMPONENTS = {
  ::Search::Elastic::Filters => %i[
    by_combined_search_level_and_membership
    by_combined_confidentiality
    by_state
    by_not_hidden
    by_label_ids
    by_archived
    by_work_item_type_ids
    by_author
    by_assignees
    by_milestone
    by_milestone_state
    by_label_names
    by_weight
    by_health_status
    by_closed_at
    by_created_at
    by_updated_at
    by_due_date
    by_iids
  ],
  ::Search::Elastic::Aggregations => %i[by_label_ids by_work_item_type_ids],
  ::Search::Elastic::Formats => [
    { method: :source_fields, skip_if_size_zero: true },
    { method: :page, skip_if_size_zero: true },
    { method: :size, skip_if_size_zero: true }
  ],
  ::Search::Elastic::Sorts::WorkItem => [
    { method: :sort_by, skip_if_size_zero: true }
  ]
}.freeze

서브클래스 훅#

파이프라인에 연결하려면 서브클래스에서 이 메서드들을 오버라이드하세요.

메서드 실행 시점 목적
extra_options 초기화 시점. options에 병합할 기본값을 반환합니다.
prepare_options build_initial_query_hash 이전. options를 변경합니다(기본값 설정, 입력값 변환).
build_initial_query_hash prepare_options 이후. 시작 query_hash(전문 검색 쿼리, IID 쿼리, 또는 빈 bool)를 반환합니다.

메서드별 플래그#

조건부 동작을 표현하려면 단순 심볼을 메서드 이름과 플래그를 지정하는 해시로 대체하세요.

플래그 메서드 적용 조건
migration: 지정한 마이그레이션이 완료된 경우.
unless_migration: 지정한 마이그레이션이 아직 완료되지 않은 경우.
skip_if_size_zero: 파이프라인 끝에서 query_hash[:size]가 0보다 큰 경우. 집계 쿼리도 실행하는 빌더의 Formats 및 Sorts 메서드에 사용하세요. 집계는 size: 0을 설정하기 때문입니다.

점진적 롤아웃 중에 하나의 필터를 다른 필터로 교체하려면 migration:unless_migration:을 쌍으로 사용하세요. 마이그레이션이 완료된 후에는 레거시 항목과 migration: 플래그를 제거하세요.

QUERY_COMPONENTS = {
  ::Search::Elastic::Filters => [
    :by_type,
    { method: :by_search_level_and_membership, migration: :migration_name },
    { method: :by_project_authorization, unless_migration: :migration_name },
    :by_archived
  ],
  ::Search::Elastic::Formats => [
    { method: :source_fields, skip_if_size_zero: true },
    { method: :size, skip_if_size_zero: true }
  ]
}.freeze

필터 및 집계 계약#

QUERY_COMPONENTS에 명시된 모든 메서드는 query_hash:options:를 인수로 받아 업데이트된 query_hash를 반환해야 합니다. 특정 옵션이 설정된 경우에만 동작하는 메서드 (예: options[:aggregation]true일 때의 Aggregations.by_label_ids)는 그렇지 않은 경우에 query_hash를 변경 없이 반환해야 합니다. 그래야 파이프라인에서 조건 없이 배치할 수 있습니다.

필터 생성#

필터는 효과적인 Elasticsearch 쿼리를 구성하는 핵심 구성 요소입니다. 필터는 관련성 점수에 영향을 주지 않으면서 검색 결과 범위를 좁혀 줍니다.

모든 필터는 문서화되어야 합니다.

필터는 Search::Elastic::Filters의 클래스 레벨 메서드로 생성됩니다.

메서드 이름은 by_로 시작해야 합니다.

메서드는 query_hashoptions 매개변수만 받아야 합니다.

query_hash는 다음 형식의 해시를 포함해야 합니다.

 { "query":
   { "bool":
     {
       "must": [],
       "must_not": [],
       "should": [],
       "filters": [],
       "minimum_should_match": null
     }
   }
 }

add_filter를 사용하여 쿼리 해시에 필터를 추가하세요. 필터는 점수 계산을 피하기 위해 filters에 추가해야 합니다. 점수 계산은 쿼리 자체가 담당합니다.

필터에 이름을 추가하려면 필터 주위에 context.name(:filters)를 사용하세요. 이를 통해 쿼리와 필터 중 어느 부분이 검색 결과를 반환했는지 식별하는 데 도움이 됩니다.

  def by_new_filter_type(query_hash:, options:)
      filter_selected_value = options[:field_value]

      context.name(:filters) do
        add_filter(query_hash, :query, :bool, :filter) do
          { term: { field_name: { _name: context.name(:field_name), value: filter_selected_value } } }
        end
      end
  end

쿼리와 필터의 차이 이해#

Elasticsearch의 쿼리는 문서 필터링과 관련성 점수 계산이라는 두 가지 핵심 목적을 수행합니다. 검색 기능을 구축할 때:

  • 쿼리는 검색 기준과 얼마나 일치하는지에 따라 결과를 순위화하는 관련성 점수가 필요할 때 필수적입니다. Boolean 쿼리의 must, should, must_not 절을 사용하며, 이 절들은 모두 문서의 최종 관련성 점수에 영향을 줍니다.

  • 필터(쿼리 컨텍스트 내)는 문서의 점수에 영향을 주지 않으면서 검색 결과에 포함할지 여부를 결정합니다. 관련성에 따른 순위 없이 결과를 포함하거나 제외하기만 하면 되는 검색 작업에서는 필터만 사용하는 것이 더 효율적이며 대규모에서 성능이 더 좋습니다.

검색 요구 사항에 맞는 적절한 방식을 선택하세요. 순위가 매겨진 결과가 필요할 때는 스코어링 절이 포함된 쿼리를 사용하고, 단순 포함/제외 로직에는 필터를 활용하세요.

필터 요구 사항 및 사용법#

필터를 사용하려면:

  • 인덱스 매핑에 각 필터 문서에 명시된 모든 필수 필드가 포함되어 있어야 합니다.

  • 필터를 호출할 때 options 해시를 통해 적절한 파라미터를 전달합니다.

  • 각 필터는 적절한 JSON 구조를 생성하여 query_hash에 추가합니다.

필터는 서로 조합하여 정교한 검색 쿼리를 만들 수 있으며, 동시에 읽기 쉽고 유지 관리하기 좋은 코드를 유지할 수 있습니다.

Elasticsearch에 쿼리 전송#

쿼리는 Gitlab::Elastic::SearchResults에서 ::Gitlab::Search::Client로 전송됩니다. 결과는 Search::Elastic::ResponseMapper를 통해 파싱되어 Elasticsearch의 응답을 변환합니다.

모델 요구 사항#

모델은 to_ability_name 메서드에 응답할 수 있어야 하며, 이를 통해 리댁션(redaction) 로직이 Ability.allowed?(current_user, :"read_#{object.to_ability_name}", object)?를 확인할 수 있습니다. 해당 메서드가 없으면 추가해야 합니다.

모델은 N+1 문제를 방지하기 위해 preload_search_data 스코프를 정의해야 합니다.

사용 가능한 쿼리#

모든 쿼리 빌더는 Elasticsearch의 Boolean 쿼리 구문을 준수하는 표준화된 query_hash 구조를 반환해야 합니다. Search::Elastic::BoolExpr 클래스는 Boolean 쿼리를 구성하기 위한 인터페이스를 제공합니다.

필수 쿼리 해시 구조는 다음과 같습니다:

{
  "query": {
    "bool": {
      "must": [],
      "must_not": [],
      "should": [],
      "filters": [],
      "minimum_should_match": null
    }
  }
}

by_iid#

iid 필드와 문서 유형으로 쿼리합니다. typeiid 필드가 필요합니다.

{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "iid": {
              "_name": "milestone:related:iid",
              "value": 1
            }
          }
        },
        {
          "term": {
            "type": {
              "_name": "doc:is_a:milestone",
              "value": "milestone"
            }
          }
        }
      ]
    }
  }
}

by_full_text#

전문(full text) 검색을 수행합니다. 쿼리 문자열에 고급 검색 구문이 사용된 경우 이 쿼리는 by_multi_match_query 또는 by_simple_query_string을 사용합니다.

by_multi_match_query#

multi_match Elasticsearch API를 사용합니다. 다음 옵션으로 커스터마이즈할 수 있습니다:

  • count_only - Boolean 쿼리 절 filter를 사용합니다. 스코어링 및 하이라이팅은 수행되지 않습니다.

  • query - 쿼리가 전달되지 않으면 match_all Elasticsearch API를 사용합니다.

  • keyword_match_clause - :should가 전달되면 Boolean 쿼리 절 should를 사용합니다. 기본값: must

{
  "query": {
    "bool": {
      "must": [
        {
          "bool": {
            "must": [],
            "must_not": [],
            "should": [
              {
                "multi_match": {
                  "_name": "project:multi_match:and:search_terms",
                  "fields": [
                    "name^10",
                    "name_with_namespace^2",
                    "path_with_namespace",
                    "path^9",
                    "description"
                  ],
                  "query": "search",
                  "operator": "and",
                  "lenient": true
                }
              },
              {
                "multi_match": {
                  "_name": "project:multi_match_phrase:search_terms",
                  "type": "phrase",
                  "fields": [
                    "name^10",
                    "name_with_namespace^2",
                    "path_with_namespace",
                    "path^9",
                    "description"
                  ],
                  "query": "search",
                  "lenient": true
                }
              }
            ],
            "filter": [],
            "minimum_should_match": 1
          }
        }
      ],
      "must_not": [],
      "should": [],
      "filter": [],
      "minimum_should_match": null
    }
  }
}

by_simple_query_string#

simple_query_string Elasticsearch API를 사용합니다. 다음 옵션으로 커스터마이즈할 수 있습니다:

  • count_only - Boolean 쿼리 절 filter를 사용합니다. 스코어링 및 하이라이팅은 수행되지 않습니다.

  • query - 쿼리가 전달되지 않으면 match_all Elasticsearch API를 사용합니다.

  • keyword_match_clause - :should가 전달되면 Boolean 쿼리 절 should를 사용합니다. 기본값: must

{
  "query": {
    "bool": {
      "must": [
        {
          "simple_query_string": {
            "_name": "project:match:search_terms",
            "fields": [
              "name^10",
              "name_with_namespace^2",
              "path_with_namespace",
              "path^9",
              "description"
            ],
            "query": "search",
            "lenient": true,
            "default_operator": "and"
          }
        }
      ],
      "must_not": [],
      "should": [],
      "filter": [],
      "minimum_should_match": null
    }
  }
}

Available Filters#

다음 섹션에서는 사용 가능한 각 필터, 필수 필드, 지원되는 옵션, 출력 예시를 자세히 설명합니다.

by_type#

type 필드가 필요합니다. 옵션에서 doc_type으로 쿼리합니다.

{
  "term": {
    "type": {
      "_name": "filters:doc:is_a:milestone",
      "value": "milestone"
    }
  }
}

by_group_level_confidentiality#

current_usergroup_ids 필드가 필요합니다. 기밀 그룹 엔티티를 읽을 수 있는 사용자 권한을 기반으로 쿼리합니다.

{
  "bool": {
    "must": [
      {
        "term": {
          "confidential": {
            "value": true,
            "_name": "confidential:true"
          }
        }
      },
      {
        "terms": {
          "namespace_id": [
            1
          ],
          "_name": "groups:can:read_confidential_work_items"
        }
      }
    ]
  },
  "should": {
    "term": {
      "confidential": {
        "value": false,
        "_name": "confidential:false"
      }
    }
  }
}

by_project_confidentiality#

confidential, author_id, assignee_id, project_id 필드가 필요합니다. 옵션에서 confidential로 쿼리합니다.

{
  "bool": {
    "should": [
      {
        "term": {
          "confidential": {
            "_name": "filters:confidentiality:projects:non_confidential",
            "value": false
          }
        }
      },
      {
        "bool": {
          "must": [
            {
              "term": {
                "confidential": {
                  "_name": "filters:confidentiality:projects:confidential",
                  "value": true
                }
              }
            },
            {
              "bool": {
                "should": [
                  {
                    "term": {
                      "author_id": {
                        "_name": "filters:confidentiality:projects:confidential:as_author",
                        "value": 1
                      }
                    }
                  },
                  {
                    "term": {
                      "assignee_id": {
                        "_name": "filters:confidentiality:projects:confidential:as_assignee",
                        "value": 1
                      }
                    }
                  },
                  {
                    "terms": {
                      "_name": "filters:confidentiality:projects:confidential:project:membership:id",
                      "project_id": [
                        12345
                      ]
                    }
                  }
                ]
              }
            }
          ]
        }
      }
    ]
  }
}

by_combined_confidentiality#

search_level 필드와 use_group_authorization 또는 use_project_authorization 중 하나 이상이 필요합니다. 옵션에 confidential이 포함된 쿼리입니다. 이 필터는 use_group_authorizationuse_project_authorization가 모두 제공된 경우 by_project_confidentialityby_group_level_confidentiality를 하나의 쿼리로 결합합니다. 필수 필드는 해당 메서드를 참고하세요.

[
  {
    "bool": {
      "should": [
        {
          "bool": {
            "filter": [
              {
                "bool": {
                  "should": [
                    {
                      "term": {
                        "confidential": {
                          "_name": "filters:confidentiality:projects:non_confidential",
                          "value": false
                        }
                      }
                    },
                    {
                      "bool": {
                        "must": [
                          {
                            "term": {
                              "confidential": {
                                "_name": "filters:confidentiality:projects:confidential",
                                "value": true
                              }
                            }
                          },
                          {
                            "bool": {
                              "should": [
                                {
                                  "term": {
                                    "author_id": {
                                      "_name": "filters:confidentiality:projects:confidential:as_author",
                                      "value": 278964
                                    }
                                  }
                                },
                                {
                                  "term": {
                                    "assignee_id": {
                                      "_name": "filters:confidentiality:projects:confidential:as_assignee",
                                      "value": 278964
                                    }
                                  }
                                },
                                {
                                  "terms": {
                                    "_name": "filters:confidentiality:projects:confidential:project:membership:id",
                                    "project_id": []
                                  }
                                }
                              ]
                            }
                          }
                        ]
                      }
                    }
                  ]
                }
              }
            ]
          }
        },
        {
          "bool": {
            "filter": [
              {
                "bool": {
                  "should": [
                    {
                      "bool": {
                        "_name": "filters:confidentiality:groups:non_confidential:public",
                        "must": [
                          {
                            "term": {
                              "confidential": {
                                "value": false
                              }
                            }
                          },
                          {
                            "term": {
                              "namespace_visibility_level": {
                                "value": 20
                              }
                            }
                          }
                        ]
                      }
                    },
                    {
                      "bool": {
                        "_name": "filters:confidentiality:groups:non_confidential:internal",
                        "must": [
                          {
                            "term": {
                              "confidential": {
                                "value": false
                              }
                            }
                          },
                          {
                            "term": {
                              "namespace_visibility_level": {
                                "value": 10
                              }
                            }
                          }
                        ]
                      }
                    },
                    {
                      "bool": {
                        "_name": "filters:confidentiality:groups:non_confidential:private",
                        "must": [
                          {
                            "term": {
                              "confidential": {
                                "value": false
                              }
                            }
                          }
                        ],
                        "should": [
                          {
                            "prefix": {
                              "traversal_ids": {
                                "_name": "filters:confidentiality:groups:non_confidential:private:ancestry_filter:descendants",
                                "value": "9970-"
                              }
                            }
                          }
                        ],
                        "minimum_should_match": 1
                      }
                    },
                    {
                      "bool": {
                        "_name": "filters:confidentiality:groups:non_confidential:private",
                        "must": [
                          {
                            "term": {
                              "confidential": {
                                "value": false
                              }
                            }
                          },
                          {
                            "terms": {
                              "_name": "filters:confidentiality:groups:non_confidential:private:project:membership",
                              "namespace_id": [
                                9971
                              ]
                            }
                          }
                        ]
                      }
                    },
                    {
                      "bool": {
                        "_name": "filters:confidentiality:groups:confidential:private",
                        "must": [
                          {
                            "term": {
                              "confidential": {
                                "value": true
                              }
                            }
                          }
                        ],
                        "should": [
                          {
                            "prefix": {
                              "traversal_ids": {
                                "_name": "filters:confidentiality:groups:confidential:private:ancestry_filter:descendants",
                                "value": "9970-"
                              }
                            }
                          }
                        ],
                        "minimum_should_match": 1
                      }
                    }
                  ],
                  "minimum_should_match": 1
                }
              }
            ]
          }
        }
      ],
      "minimum_should_match": 1
    }
  }
]

by_note_confidentiality#

노트의 기밀성 필터를 적용합니다. 노트에는 두 가지 수준의 기밀성이 있습니다:

  • 노트 자체의 기밀성 (confidential 필드)

  • 이슈의 기밀성 (issue.confidential, issue.author_id, issue.assignee_id)

confidential, issue.confidential, issue.author_id, issue.assignee_id, project_id, traversal_ids 필드가 필요합니다.

다음 조건 중 하나라도 충족되면 노트가 표시됩니다:

  • 비기밀 이슈에 달린 노트이며 노트 자체도 기밀이 아닌 경우

  • 기밀 이슈에 달린 노트이지만 사용자가 작성자/담당자이거나 project_id 또는 traversal_ids를 통해 프로젝트 접근 권한이 있는 경우

  • 노트가 기밀이지만 사용자가 project_id 또는 traversal_ids를 통해 프로젝트 접근 권한이 있는 경우

이 필터는 효율적인 그룹 수준 검색을 위해 project_id 조건과 traversal_ids 기반 인가를 모두 사용합니다.

{
  "bool": {
    "minimum_should_match": 1,
    "should": [
      {
        "bool": {
          "filter": [
            {
              "bool": {
                "_name": "filters:confidentiality:notes:not_on_issue_or_not_confidential",
                "should": [
                  {
                    "bool": {
                      "_name": "filters:confidentiality:notes:not_on_issue",
                      "must_not": [{ "exists": { "field": "issue" } }]
                    }
                  },
                  {
                    "term": {
                      "issue.confidential": {
                        "_name": "filters:confidentiality:notes:non_confidential_issue",
                        "value": false
                      }
                    }
                  }
                ]
              }
            },
            {
              "bool": {
                "_name": "filters:confidentiality:notes:not_confidential",
                "should": [
                  { "bool": { "must_not": [{ "exists": { "field": "confidential" } }] } },
                  { "term": { "confidential": false } }
                ]
              }
            }
          ]
        }
      },
      {
        "bool": {
          "filter": [
            {
              "term": {
                "issue.confidential": {
                  "_name": "filters:confidentiality:notes:issue:confidential",
                  "value": true
                }
              }
            },
            {
              "bool": {
                "_name": "filters:confidentiality:notes:not_confidential",
                "should": [
                  { "bool": { "must_not": [{ "exists": { "field": "confidential" } }] } },
                  { "term": { "confidential": false } }
                ]
              }
            },
            {
              "bool": {
                "minimum_should_match": 1,
                "should": [
                  {
                    "term": {
                      "issue.author_id": {
                        "_name": "filters:confidentiality:notes:confidential:as_author",
                        "value": 1
                      }
                    }
                  },
                  {
                    "term": {
                      "issue.assignee_id": {
                        "_name": "filters:confidentiality:notes:confidential:as_assignee",
                        "value": 1
                      }
                    }
                  },
                  {
                    "terms": {
                      "_name": "filters:confidentiality:notes:private:project:member",
                      "project_id": [1]
                    }
                  },
                  {
                    "bool": {
                      "minimum_should_match": 1,
                      "should": [
                        {
                          "prefix": {
                            "traversal_ids": {
                              "_name": "filters:confidentiality:notes:private:ancestry_filter:descendants",
                              "value": "123-"
                            }
                          }
                        }
                      ]
                    }
                  }
                ]
              }
            }
          ]
        }
      },
      {
        "bool": {
          "filter": [
            {
              "term": {
                "confidential": {
                  "_name": "filters:confidentiality:notes:confidential",
                  "value": true
                }
              }
            }
          ],
          "minimum_should_match": 1,
          "should": [
            {
              "terms": {
                "_name": "filters:confidentiality:notes:private:project:member",
                "project_id": [1]
              }
            },
            {
              "bool": {
                "minimum_should_match": 1,
                "should": [
                  {
                    "prefix": {
                      "traversal_ids": {
                        "_name": "filters:confidentiality:notes:private:ancestry_filter:descendants",
                        "value": "123-"
                      }
                    }
                  }
                ]
              }
            }
          ]
        }
      }
    ]
  }
}

by_label_ids#

label_ids 필드가 필요합니다. 옵션에서 label_names를 사용하여 쿼리합니다.

{
  "bool": {
    "must": [
      {
        "terms": {
          "_name": "filters:label_ids",
          "label_ids": [
            1
          ]
        }
      }
    ]
  }
}

by_archived#

archived 필드가 필요합니다. 옵션에서 search_levelinclude_archived를 사용하여 쿼리합니다.

{
  "bool": {
    "_name": "filters:non_archived",
    "should": [
      {
        "bool": {
          "filter": {
            "term": {
              "archived": {
                "value": false
              }
            }
          }
        }
      },
      {
        "bool": {
          "must_not": {
            "exists": {
              "field": "archived"
            }
          }
        }
      }
    ]
  }
}

by_state#

state 필드가 필요합니다. all, opened, closed, merged 값을 지원합니다. 옵션에서 state를 사용하여 쿼리합니다.

{
  "match": {
    "state": {
      "_name": "filters:state",
      "query": "opened"
    }
  }
}

by_not_hidden#

hidden 필드가 필요합니다. 관리자에게는 적용되지 않습니다.

{
  "term": {
    "hidden": {
      "_name": "filters:not_hidden",
      "value": false
    }
  }
}

by_work_item_type_ids#

work_item_type_id 필드가 필요합니다. 옵션에서 work_item_type_ids 또는 not_work_item_type_ids를 사용하여 쿼리합니다.

{
  "bool": {
    "must_not": {
      "terms": {
        "_name": "filters:not_work_item_type_ids",
        "work_item_type_id": [
          8
        ]
      }
    }
  }
}

by_author#

author_id 필드가 필요합니다. 옵션에서 author_username 또는 not_author_username을 사용하여 쿼리합니다.

{
  "bool": {
    "should": [
      {
        "term": {
          "author_id": {
            "_name": "filters:author",
            "value": 1
          }
        }
      }
    ],
    "minimum_should_match": 1
  }
}

by_target_branch#

target_branch 필드가 필요합니다. 옵션에서 target_branch 또는 not_target_branch를 사용하여 쿼리합니다.

{
  "bool": {
    "should": [
      {
        "term": {
          "target_branch": {
            "_name": "filters:target_branch",
            "value": "master"
          }
        }
      }
    ],
    "minimum_should_match": 1
  }
}

by_source_branch#

source_branch 필드가 필요합니다. 옵션에서 source_branch 또는 not_source_branch를 사용하여 쿼리합니다.

{
  "bool": {
    "should": [
      {
        "term": {
          "source_branch": {
            "_name": "filters:source_branch",
            "value": "master"
          }
        }
      }
    ],
    "minimum_should_match": 1
  }
}

by_search_level_and_group_membership#

current_user, group_ids, traversal_id, search_level 필드가 필요합니다. search_level을 사용하여 쿼리하고, 각 그룹에 대해 사용자가 보유한 권한에 따라 namespace_visibility_level을 기준으로 필터링합니다.

이 필터는 검색 대상 데이터에 project_id 필드가 없는 경우 by_search_level_and_membership 대신 사용할 수 있습니다.

예시는 인증된 사용자를 대상으로 합니다. 인가(authorization)를 가진 사용자, 관리자, 외부 사용자, 또는 익명 사용자의 경우 JSON이 다를 수 있습니다.
global#
{
  "bool": {
    "should": [
      {
        "bool": {
          "filter": [
            {
              "term": {
                "namespace_visibility_level": {
                  "value": 20,
                  "_name": "filters:namespace_visibility_level:public"
                }
              }
            }
          ]
        }
      },
      {
        "bool": {
          "filter": [
            {
              "term": {
                "namespace_visibility_level": {
                  "value": 10,
                  "_name": "filters:namespace_visibility_level:internal"
                }
              }
            }
          ]
        }
      },
      {
        "bool": {
          "filter": [
            {
              "term": {
                "namespace_visibility_level": {
                  "value": 0,
                  "_name": "filters:namespace_visibility_level:private"
                }
              }
            },
            {
              "terms": {
                "namespace_id": [
                  33,
                  22
                ]
              }
            }
          ]
        }
      }
    ],
    "minimum_should_match": 1
  }
}
group#
[
  {
    "bool": {
      "_name": "filters:level:group",
      "minimum_should_match": 1,
      "should": [
        {
          "prefix": {
            "traversal_ids": {
              "_name": "filters:level:group:ancestry_filter:descendants",
              "value": "22-"
            }
          }
        }
      ]
    }
  },
  {
    "bool": {
      "should": [
        {
          "bool": {
            "filter": [
              {
                "term": {
                  "namespace_visibility_level": {
                    "value": 20,
                    "_name": "filters:namespace_visibility_level:public"
                  }
                }
              }
            ]
          }
        },
        {
          "bool": {
            "filter": [
              {
                "term": {
                  "namespace_visibility_level": {
                    "value": 10,
                    "_name": "filters:namespace_visibility_level:internal"
                  }
                }
              }
            ]
          }
        },
        {
          "bool": {
            "filter": [
              {
                "term": {
                  "namespace_visibility_level": {
                    "value": 0,
                    "_name": "filters:namespace_visibility_level:private"
                  }
                }
              },
              {
                "terms": {
                  "namespace_id": [
                    22
                  ]
                }
              }
            ]
          }
        }
      ],
      "minimum_should_match": 1
    }
  },
  {
    "bool": {
      "_name": "filters:level:group",
      "minimum_should_match": 1,
      "should": [
        {
          "prefix": {
            "traversal_ids": {
              "_name": "filters:level:group:ancestry_filter:descendants",
              "value": "22-"
            }
          }
        }
      ]
    }
  }
]

by_search_level_and_membership#

project_id, traversal_id 및 프로젝트 가시성(기본값은 visibility_level이며 project_visibility_level_field 옵션으로 설정 가능) 필드가 필요합니다. *_access_level 기능 필드를 지원합니다. search_level과 옵션으로 project_ids, group_ids, features, current_user를 옵션에 포함하여 쿼리합니다.

다음에 대한 필터링이 적용됩니다:

  • 전역, 그룹, 또는 프로젝트에 대한 검색 레벨

  • 그룹 및 프로젝트에 대한 직접 멤버십 또는 그룹에 대한 직접 접근을 통한 공유 멤버십

  • features를 통해 전달된 모든 기능 접근 레벨

    예시는 로그인한 사용자를 기준으로 표시됩니다. JSON은 인가가 있는 사용자, 관리자, 외부 사용자, 또는 익명 사용자에 따라 다를 수 있습니다.

global#
{
  "bool": {
    "_name": "filters:permissions:global",
    "should": [
      {
        "bool": {
          "must": [
            {
              "terms": {
                "_name": "filters:permissions:global:visibility_level:public_and_internal",
                "visibility_level": [
                  20,
                  10
                ]
              }
            }
          ],
          "should": [
            {
              "terms": {
                "_name": "filters:permissions:global:repository_access_level:enabled",
                "repository_access_level": [
                  20
                ]
              }
            }
          ],
          "minimum_should_match": 1
        }
      },
      {
        "bool": {
          "must": [
            {
              "bool": {
                "should": [
                  {
                    "terms": {
                      "_name": "filters:permissions:global:repository_access_level:enabled_or_private",
                      "repository_access_level": [
                        20,
                        10
                      ]
                    }
                  }
                ],
                "minimum_should_match": 1
              }
            }
          ],
          "should": [
            {
              "prefix": {
                "traversal_ids": {
                  "_name": "filters:permissions:global:ancestry_filter:descendants",
                  "value": "123-"
                }
              }
            },
            {
              "terms": {
                "_name": "filters:permissions:global:project:member",
                "project_id": [
                  456
                ]
              }
            }
          ],
          "minimum_should_match": 1
        }
      }
    ],
    "minimum_should_match": 1
  }
}
그룹#
[
  {
    "bool": {
      "_name": "filters:level:group",
      "minimum_should_match": 1,
      "should": [
        {
          "prefix": {
            "traversal_ids": {
              "_name": "filters:level:group:ancestry_filter:descendants",
              "value": "123-"
            }
          }
        }
      ]
    }
  },
  {
    "bool": {
      "_name": "filters:permissions:group",
      "should": [
        {
          "bool": {
            "must": [
              {
                "terms": {
                  "_name": "filters:permissions:group:visibility_level:public_and_internal",
                  "visibility_level": [
                    20,
                    10
                  ]
                }
              }
            ],
            "should": [
              {
                "terms": {
                  "_name": "filters:permissions:group:repository_access_level:enabled",
                  "repository_access_level": [
                    20
                  ]
                }
              }
            ],
            "minimum_should_match": 1
          }
        },
        {
          "bool": {
            "must": [
              {
                "bool": {
                  "should": [
                    {
                      "terms": {
                        "_name": "filters:permissions:group:repository_access_level:enabled_or_private",
                        "repository_access_level": [
                          20,
                          10
                        ]
                      }
                    }
                  ],
                  "minimum_should_match": 1
                }
              }
            ],
            "should": [
              {
                "prefix": {
                  "traversal_ids": {
                    "_name": "filters:permissions:group:ancestry_filter:descendants",
                    "value": "123-"
                  }
                }
              }
            ],
            "minimum_should_match": 1
          }
        }
      ],
      "minimum_should_match": 1
    }
  }
]
프로젝트#
[
  {
    "bool": {
      "_name": "filters:level:project",
      "must": {
        "terms": {
          "project_id": [
            456
          ]
        }
      }
    }
  },
  {
    "bool": {
      "_name": "filters:permissions:project",
      "should": [
        {
          "bool": {
            "must": [
              {
                "terms": {
                  "_name": "filters:permissions:project:visibility_level:public_and_internal",
                  "visibility_level": [
                    20,
                    10
                  ]
                }
              }
            ],
            "should": [
              {
                "terms": {
                  "_name": "filters:permissions:project:repository_access_level:enabled",
                  "repository_access_level": [
                    20
                  ]
                }
              }
            ],
            "minimum_should_match": 1
          }
        },
        {
          "bool": {
            "must": [
              {
                "bool": {
                  "should": [
                    {
                      "terms": {
                        "_name": "filters:permissions:project:repository_access_level:enabled_or_private",
                        "repository_access_level": [
                          20,
                          10
                        ]
                      }
                    }
                  ],
                  "minimum_should_match": 1
                }
              }
            ],
            "should": [
              {
                "prefix": {
                  "traversal_ids": {
                    "_name": "filters:permissions:project:ancestry_filter:descendants",
                    "value": "123-"
                  }
                }
              }
            ],
            "minimum_should_match": 1
          }
        }
      ],
      "minimum_should_match": 1
    }
  }
]

by_combined_search_level_and_membership#

search_level 필드와 use_group_authorization 또는 use_project_authorization 중 하나 이상이 필요합니다. 이 필터는 use_group_authorizationuse_project_authorization이 모두 제공된 경우 by_search_level_and_membershipby_search_level_and_group_membership을 하나의 쿼리로 결합합니다. 필수 필드에 대해서는 해당 메서드를 참고하세요.

[
  {
    "bool": {
      "should": [
        {
          "bool": {
            "filter": [
              {
                "bool": {
                  "should": [
                    {
                      "bool": {
                        "should": [
                          {
                            "prefix": {
                              "traversal_ids": {
                                "_name": "filters:permissions:global:private_access:ancestry_filter:descendants",
                                "value": "9970-"
                              }
                            }
                          }
                        ],
                        "filter": [
                          {
                            "terms": {
                              "_name": "filters:permissions:global:private_access:issues_access_level:enabled_or_private",
                              "issues_access_level": [
                                20,
                                10
                              ]
                            }
                          }
                        ],
                        "minimum_should_match": 1
                      }
                    },
                    {
                      "bool": {
                        "filter": [
                          {
                            "terms": {
                              "_name": "filters:permissions:global:private_access:issues_access_level:enabled_or_private",
                              "issues_access_level": [
                                20,
                                10
                              ]
                            }
                          },
                          {
                            "terms": {
                              "_name": "filters:permissions:global:private_access:project:member",
                              "project_id": [
                                278964
                              ]
                            }
                          }
                        ]
                      }
                    },
                    {
                      "bool": {
                        "should": [
                          {
                            "terms": {
                              "_name": "filters:permissions:global:issues_access_level:enabled",
                              "issues_access_level": [
                                20
                              ]
                            }
                          }
                        ],
                        "filter": [
                          {
                            "terms": {
                              "_name": "filters:permissions:global:project_visibility_level:public_and_internal",
                              "project_visibility_level": [
                                20,
                                10
                              ]
                            }
                          }
                        ],
                        "minimum_should_match": 1
                      }
                    }
                  ],
                  "minimum_should_match": 1
                }
              }
            ]
          }
        },
        {
          "bool": {
            "filter": [
              {
                "bool": {
                  "_name": "filters:permissions:global",
                  "should": [
                    {
                      "bool": {
                        "filter": [
                          {
                            "terms": {
                              "_name": "filters:permissions:global:namespace_visibility_level:public_and_internal",
                              "namespace_visibility_level": [
                                20,
                                10
                              ]
                            }
                          }
                        ]
                      }
                    },
                    {
                      "bool": {
                        "must": [
                          {
                            "terms": {
                              "_name": "filters:permissions:global:namespace_visibility_level:private",
                              "namespace_visibility_level": [
                                0
                              ]
                            }
                          }
                        ],
                        "should": [
                          {
                            "prefix": {
                              "traversal_ids": {
                                "_name": "filters:permissions:global:ancestry_filter:descendants",
                                "value": "9970-"
                              }
                            }
                          }
                        ],
                        "minimum_should_match": 1
                      }
                    },
                    {
                      "bool": {
                        "must": [
                          {
                            "terms": {
                              "_name": "filters:permissions:global:namespace_visibility_level:private",
                              "namespace_visibility_level": [
                                0
                              ]
                            }
                          },
                          {
                            "terms": {
                              "_name": "filters:permissions:global:project:membership",
                              "namespace_id": [
                                9971
                              ]
                            }
                          }
                        ]
                      }
                    }
                  ],
                  "minimum_should_match": 1
                }
              }
            ]
          }
        }
      ],
      "minimum_should_match": 1
    }
  }
]

by_user_accessible_namespaces#

사용자의 네임스페이스(그룹 및 프로젝트) 접근 권한을 기반으로 문서를 필터링합니다. 이 필터는 사용자 검색 쿼리에 특화되어 있으며, 글로벌, 그룹, 프로젝트 검색 레벨에서 네임스페이스 레벨의 인가(권한 부여)를 처리합니다.

필수 필드:

  • namespace_ancestry_ids - Namespace#elastic_namespace_ancestry / Project#elastic_namespace_ancestry(프로젝트 세그먼트인 p<id> 포함)에서 채워지는 키워드 필드로, 네임스페이스 계층 필터링에서 prefixterms 쿼리에 사용됨

  • current_user - 검색을 수행하는 사용자 (글로벌 범위용)

  • search_level - :global, :group, :project 중 하나

선택 필드:

  • group_id - 그룹 레벨 검색용 그룹 ID

  • project_id - 프로젝트 레벨 검색용 프로젝트 ID

  • autocomplete - 자동 완성 검색을 위한 boolean 플래그

검색 레벨별 동작:

  • 글로벌: 모든 사용자를 반환합니다.

자동 완성 사용 시: 인가된 그룹 및 프로젝트를 통해 접근 가능한 사용자를 반환합니다. traversal_id_prefixes를 사용하여 그룹 계층과 직접 프로젝트 멤버십을 매칭합니다.

  • 그룹: 지정된 그룹과 그 하위 그룹의 사용자, 그리고 상위 그룹의 사용자를 반환합니다.

  • 프로젝트: 지정된 프로젝트와 그 상위 그룹의 사용자를 반환합니다.

글로벌 검색 예시:

{
  "bool": {
    "should": [
      {
        "prefix": {
          "namespace_ancestry_ids": {
            "_name": "namespace:ancestry_filter:descendants",
            "value": "285-"
          }
        }
      },
      {
        "prefix": {
          "namespace_ancestry_ids": {
            "_name": "namespace:ancestry_filter:descendants",
            "value": "417-418-419-"
          }
        }
      },
      {
        "terms": {
          "namespace_ancestry_ids": [
            "417-418-419-p91-"
          ],
          "_name": "namespace:ancestry_filter:ancestors"
        }
      }
    ],
    "minimum_should_match": 1
  }
}

하위 그룹에서의 그룹 검색 예시:

{
  "bool": {
    "should": [
      {
        "prefix": {
          "namespace_ancestry_ids": {
            "_name": "namespace:ancestry_filter:descendants",
            "value": "807-806-805"
          }
        }
      },
      {
        "terms": {
          "namespace_ancestry_ids": [
            "807-","807-806-"
          ],
          "_name": "namespace:ancestry_filter:ancestors"
        }
      }
    ],
    "minimum_should_match": 1
  }
}

프로젝트 검색 예시:

{
  "bool": {
    "should": [
      {
        "terms": {
          "namespace_ancestry_ids": [
            "807-","807-806-", "807-806-805", "807-806-805-p123"
          ],
          "_name": "namespace:ancestry_filter:ancestors"
        }
      }
    ],
    "minimum_should_match": 1
  }
}

by_noteable_type#

noteable_type 필드가 필요합니다. 옵션에서 noteable_type으로 쿼리합니다. noteable_id 필드만 반환하도록 _source를 설정합니다.

{
  "term": {
    "noteable_type": {
      "_name": "filters:related:issue",
      "value": "Issue"
    }
  }
}

by_iids#

여러 IID 값으로 문서를 필터링합니다.

필수 필드:

  • iids - 일치시킬 IID 값의 배열
{
  "bool": {
    "_name": "filters:iids",
    "filter": {
      "terms": {
        "iid": [1, 2, 3]
      }
    }
  }
}

by_closed_at#

종료 날짜 범위로 필터링합니다. 선택 필드 중 최소 하나는 제공해야 합니다.

선택 필드:

  • closed_after - 최소 종료 날짜의 ISO 날짜 문자열

  • closed_before - 최대 종료 날짜의 ISO 날짜 문자열

{
  "bool": {
    "_name": "filters:closed_after",
    "must": {
      "range": {
        "closed_at": {
          "gte": "2025-01-01T00:00:00Z"
        }
      }
    }
  }
}

by_created_at#

생성 날짜 범위로 필터링합니다. 선택 필드 중 최소 하나는 제공해야 합니다.

선택 필드:

  • created_after - 최소 생성 날짜의 ISO 날짜 문자열

  • created_before - 최대 생성 날짜의 ISO 날짜 문자열

{
  "bool": {
    "_name": "filters:created_after",
    "must": {
      "range": {
        "created_at": {
          "gte": "2025-01-01T00:00:00Z"
        }
      }
    }
  }
}

by_updated_at#

업데이트 날짜 범위로 필터링합니다. 선택 필드 중 최소 하나는 제공해야 합니다.

선택 필드:

  • updated_after - 최소 업데이트 날짜의 ISO 날짜 문자열

  • updated_before - 최대 업데이트 날짜의 ISO 날짜 문자열

{
  "bool": {
    "_name": "filters:updated_after",
    "must": {
      "range": {
        "updated_at": {
          "gte": "2025-01-01T00:00:00Z"
        }
      }
    }
  }
}

by_due_date#

마감 날짜 범위로 필터링합니다. 선택 필드 중 최소 하나는 제공해야 합니다.

선택적 필드:

  • due_after - 최소 마감일에 대한 ISO 날짜 문자열

  • due_before - 최대 마감일에 대한 ISO 날짜 문자열

{
  "bool": {
    "_name": "filters:due_after",
    "must": {
      "range": {
        "due_date": {
          "gte": "2025-01-01T00:00:00Z"
        }
      }
    }
  }
}

by_milestone#

마일스톤 제목 또는 마일스톤 존재 여부로 필터링합니다. 선택적 필드 중 하나 이상을 제공해야 합니다. 마일스톤 제목 필터(milestone_title, not_milestone_title)와 마일스톤 존재 여부 필터(any_milestones, none_milestones)는 상호 배타적입니다.

선택적 필드:

  • milestone_title - 포함할 마일스톤 제목의 배열

  • not_milestone_title - 제외할 마일스톤 제목의 배열

  • any_milestones - 불리언, 마일스톤이 있는 문서를 필터링

  • none_milestones - 불리언, 마일스톤이 없는 문서를 필터링

milestone_title 사용 예시:

{
  "bool": {
    "must": {
      "terms": {
        "_name": "filters:milestone_title",
        "milestone_title": ["18.1", "18.2"]
      }
    }
  }
}

none_milestones 사용 예시:

{
  "bool": {
    "_name": "filters:none_milestones",
    "must_not": {
      "exists": {
        "field": "milestone_title"
      }
    }
  }
}

by_milestone_state#

시간적 조건을 사용하여 마일스톤 상태로 필터링합니다.

필수 필드:

  • milestone_state_filters - 다음 중 하나 이상을 포함하는 배열: :upcoming, :started, :not_upcoming, :not_started

:upcoming 필터 예시:

{
  "bool": {
    "_name": "filters:milestone_state_upcoming",
    "must": [
      {
        "term": {
          "milestone_state": "active"
        }
      },
      {
        "range": {
          "milestone_start_date": {
            "gt": "now/d"
          }
        }
      }
    ]
  }
}

:started 필터 예시:

{
  "bool": {
    "_name": "filters:milestone_state_started",
    "must": [
      {
        "term": {
          "milestone_state": "active"
        }
      },
      {
        "bool": {
          "should": [
            {
              "range": {
                "milestone_start_date": {
                  "lte": "now/d"
                }
              }
            },
            {
              "bool": {
                "must_not": {
                  "exists": {
                    "field": "milestone_start_date"
                  }
                }
              }
            }
          ]
        }
      },
      {
        "bool": {
          "should": [
            {
              "range": {
                "milestone_due_date": {
                  "gte": "now/d"
                }
              }
            },
            {
              "bool": {
                "must_not": {
                  "exists": {
                    "field": "milestone_due_date"
                  }
                }
              }
            }
          ]
        }
      }
    ],
    "must_not": {
      "bool": {
        "must": [
          {
            "bool": {
              "must_not": {
                "exists": {
                  "field": "milestone_start_date"
                }
              }
            }
          },
          {
            "bool": {
              "must_not": {
                "exists": {
                  "field": "milestone_due_date"
                }
              }
            }
          }
        ]
      }
    }
  }
}

by_assignees#

담당자 ID로 필터링하며, 다양한 매칭 모드를 지원합니다. 선택 필드 중 하나 이상을 반드시 제공해야 합니다.

선택 필드:

  • assignee_ids - 모두 존재해야 하는 담당자 ID 배열

  • not_assignee_ids - 제외할 담당자 ID 배열

  • or_assignee_ids - 하나라도 매칭되면 되는 담당자 ID 배열

  • none_assignees - boolean, 담당자가 없는 문서를 필터링

  • any_assignees - boolean, 담당자가 있는 문서를 필터링

assignee_ids 사용 예시 (모두 매칭해야 함):

{
  "bool": {
    "_name": "filters:assignee_ids",
    "must": [
      {
        "term": {
          "assignee_id": 123
        }
      },
      {
        "term": {
          "assignee_id": 456
        }
      }
    ]
  }
}

or_assignee_ids 사용 예시 (하나라도 매칭 가능):

{
  "bool": {
    "must": {
      "terms": {
        "_name": "filters:or_assignee_ids",
        "assignee_id": [123, 456, 789]
      }
    }
  }
}

none_assignees 사용 예시:

{
  "bool": {
    "_name": "filters:none_assignees",
    "must_not": {
      "exists": {
        "field": "assignee_id"
      }
    }
  }
}

by_weight#

이슈 가중치(정수 값)로 필터링합니다. 선택 필드 중 하나 이상을 반드시 제공해야 합니다.

선택 필드:

  • weight - 매칭할 정확한 가중치 값 (정수)

  • not_weight - 제외할 가중치 값 (정수)

  • none_weight - boolean, 가중치가 없는 문서를 필터링

  • any_weight - boolean, 가중치가 있는 문서를 필터링

{
  "term": {
    "weight": {
      "_name": "filters:weight",
      "value": 3
    }
  }
}

by_health_status#

health status 필드로 필터링합니다. 선택 필드 중 하나 이상을 반드시 제공해야 합니다.

선택 필드:

  • health_status - 포함할 health status ID 배열

  • not_health_status - 제외할 health status ID 배열

  • none_health_status - boolean, health status가 없는 문서를 필터링

  • any_health_status - boolean, health status가 있는 문서를 필터링

{
  "bool": {
    "must": {
      "terms": {
        "_name": "filters:health_status",
        "health_status": [1, 2]
      }
    }
  }
}

by_label_names#

라벨 이름으로 필터링하며, 다양한 매칭 모드와 범위가 지정된 라벨 와일드카드를 지원합니다. 선택 필드 중 적어도 하나는 반드시 제공해야 합니다.

선택 필드:

  • label_names - 모두 존재해야 하는 라벨 이름 배열

  • not_label_names - 제외할 라벨 이름 배열

  • or_label_names - 하나라도 일치하면 되는 라벨 이름 배열

  • none_label_names - 불리언, 라벨이 없는 문서를 필터링함

  • any_label_names - 불리언, 라벨이 하나 이상 있는 문서를 필터링함

"workflow::*"와 같은 범위 지정 라벨 와일드카드를 지원하여 "workflow::"로 시작하는 모든 라벨에 매칭합니다. 와일드카드는 Elasticsearch에서 접두사 쿼리로 변환됩니다.

정확한 일치를 사용하는 예시:

{
  "bool": {
    "_name": "filters:label_names",
    "must": [
      {
        "term": {
          "label_names": "advanced search"
        }
      },
      {
        "term": {
          "label_names": "GLQL"
        }
      }
    ]
  }
}

범위 지정 라벨 와일드카드를 사용하는 예시:

{
  "bool": {
    "_name": "filters:or_label_names",
    "should": [
      {
        "prefix": {
          "label_names": "workflow::"
        }
      },
      {
        "term": {
          "label_names": "backend"
        }
      }
    ],
    "minimum_should_match": 1
  }
}

Testing scopes#

Rails 콘솔에서 모든 scope를 테스트합니다.

search_service = ::SearchService.new(User.first, { search: 'foo', scope: 'SCOPE_NAME' })
search_service.search_objects

Permissions tests#

검색 코드는 SearchService#redact_unauthorized_results에서 최종 보안 검사를 수행합니다. 이를 통해 권한이 없는 결과가 조회 권한이 없는 사용자에게 반환되는 것을 방지합니다. 이 검사는 버그나 인덱싱 지연으로 인한 Elasticsearch 권한 데이터의 불일치를 처리하기 위해 Ruby에서 수행됩니다.

새로운 scope는 적절한 접근 제어를 보장하기 위해 가시성 스펙을 추가해야 합니다. 권한이 올바르게 적용되는지 테스트하려면, EE 스펙에서 'search respects visibility' 공유 예시를 사용하여 테스트를 추가하세요:

  • ee/spec/services/ee/search/global_service_spec.rb

  • ee/spec/services/ee/search/group_service_spec.rb

  • ee/spec/services/ee/search/project_service_spec.rb

Zero-downtime reindexing with multiple indices#

다중 인덱스 기능이 아직 완전히 구현되지 않았으므로 현재는 해당되지 않습니다.

현재 GitLab은 단일 버전의 설정만 처리할 수 있습니다. 설정 또는 스키마 변경 사항이 있으면 모든 것을 처음부터 다시 인덱싱해야 합니다. 재인덱싱에는 오랜 시간이 걸릴 수 있으므로 검색 기능 다운타임이 발생할 수 있습니다.

다운타임을 방지하기 위해 GitLab은 동시에 작동할 수 있는 다중 인덱스 지원을 개발 중입니다. 스키마가 변경될 때마다 관리자는 새 인덱스를 생성하고 해당 인덱스로 재인덱싱할 수 있으며, 검색은 계속해서 안정적인 기존 인덱스로 수행됩니다. 모든 데이터 업데이트는 두 인덱스 모두에 전달됩니다. 새 인덱스가 준비되면 관리자가 해당 인덱스를 활성으로 표시하여 모든 검색을 새 인덱스로 유도하고, 기존 인덱스를 제거할 수 있습니다.

이는 예를 들어 AWS로 또는 AWS에서 이동하는 등 새 서버로 마이그레이션할 때도 유용합니다.

현재 이 새로운 설계로의 마이그레이션 과정을 진행 중입니다. 지금은 모든 것이 단일 버전으로만 동작하도록 고정되어 있습니다.

Advanced search 개발 가이드라인

GitLab v19.1
원문 보기
요약

Advanced search를 활성화하고 초기 인덱싱을 수행하는 방법은 Elasticsearch 연동 문서를 참고하세요. 다음 녹화 영상 및 발표 자료는 Advanced search 구현에 대한 심층적인 지식을 제공합니다:


Advanced search 개발 가이드라인#

  이 페이지에는 Elasticsearch를 기반으로 하는 Advanced search를 개발하고 사용하는 방법에 대한 정보가 포함되어 있습니다.

Advanced search를 활성화하고 초기 인덱싱을 수행하는 방법은 Elasticsearch 연동 문서를 참고하세요.

심층 학습 자료#

다음 녹화 영상 및 발표 자료는 Advanced search 구현에 대한 심층적인 지식을 제공합니다:

날짜 주제 발표자 자료 GitLab 버전
2024년 7월 Advanced search 기초, 연동, 인덱싱 및 검색 Terri Chu YouTube 녹화 영상 (GitLab 팀원 전용)Google 슬라이드 (GitLab 팀원 전용) GitLab 17.0
2021년 6월 Advanced search를 위한 GitLab의 데이터 마이그레이션 프로세스 Dmitry Gruzd 블로그 게시물 GitLab 13.12
2020년 8월 멀티 인덱스 지원을 위한 GitLab 전용 아키텍처 Mark Chao YouTube 녹화 영상Google 슬라이드 GitLab 13.3
2019년 6월 GitLab Elasticsearch 연동 Mario de la Ossa YouTube 녹화 영상Google 슬라이드PDF GitLab 12.0

Elasticsearch 구성#

지원 버전#

버전 호환성을 참고하세요.

Elasticsearch 쿼리를 크게 변경하는 개발자는 지원되는 모든 버전에 대해 기능을 테스트해야 합니다.

개발 환경 설정#

Elasticsearch GDK 설정 지침을 참고하세요.

Elasticsearch가 실행 중인지 확인하세요:

curl "http://localhost:9200"
  • Kibana를 실행하여 로컬 Elasticsearch 클러스터와 상호 작용하세요. 또는 Cerebro 또는 유사한 도구를 사용할 수 있습니다.

Elasticsearch 로그를 실시간으로 확인하려면 다음 명령어를 실행하세요:

tail -f log/elasticsearch.log

SaaS 모드로 실행하는 경우, GitLab.com에서 Advanced search가 구성되는 방식을 모방하기 위해 인덱싱할 네임스페이스 및 프로젝트 데이터의 양을 제한해야 합니다.

네임스페이스 제한이 활성화되지 않은 경우, Advanced search는 기본적으로 모든 네임스페이스(무료 네임스페이스 포함)에 대해 활성화됩니다.

유용한 Rake 태스크#

  • gitlab:elastic:test:index_size: 현재 인덱스가 사용하는 공간과 인덱스에 있는 문서 수를 알려줍니다.

  • gitlab:elastic:test:index_size_change: 인덱스 크기를 출력하고, 재인덱싱한 후 다시 인덱스 크기를 출력합니다. 인덱싱 크기 개선 사항을 테스트할 때 유용합니다.

또한, 테스트를 위해 대용량 리포지터리나 여러 포크가 필요한 경우 다음 지침을 따르는 것을 고려하세요.

개발 워크플로#

개발 팁#

디버깅 및 문제 해결#

Elasticsearch 쿼리 디버깅#

ELASTIC_CLIENT_DEBUG 환경 변수는 개발 또는 테스트 환경에서 Elasticsearch 클라이언트의 debug 옵션을 활성화합니다. 코드 또는 테스트에서 생성된 Elasticsearch HTTP 쿼리를 디버깅해야 하는 경우, 스펙 실행 전이나 Rails 콘솔을 시작하기 전에 활성화할 수 있습니다:

ELASTIC_CLIENT_DEBUG=1 bundle exec rspec ee/spec/workers/search/elastic/trigger_indexing_worker_spec.rb

export ELASTIC_CLIENT_DEBUG=1
rails console

flood stage disk watermark [95%] exceeded 오류 발생 시#

다음과 같은 오류가 발생할 수 있습니다:

[2018-10-31T15:54:19,762][WARN ][o.e.c.r.a.DiskThresholdMonitor] [pval5Ct]
   flood stage disk watermark [95%] exceeded on
   [pval5Ct7SieH90t5MykM5w][pval5Ct][/usr/local/var/lib/elasticsearch/nodes/0] free: 56.2gb[3%],
   all indices on this node will be marked read-only

이는 디스크 공간 임계값을 초과했기 때문입니다. 기본 95% 임계값을 기준으로 남은 디스크 공간이 충분하지 않다고 판단한 것입니다.

또한 read_only_allow_delete 설정이 true로 설정되어 인덱싱, forcemerge 등이 차단됩니다:

curl "http://localhost:9200/gitlab-development/_settings?pretty"

elasticsearch.yml 파일에 다음을 추가하세요:

# turn off the disk allocator
cluster.routing.allocation.disk.threshold_enabled: false

또는

# set your own limits
cluster.routing.allocation.disk.threshold_enabled: true
cluster.routing.allocation.disk.watermark.flood_stage: 5gb   # ES 6.x only
cluster.routing.allocation.disk.watermark.low: 15gb
cluster.routing.allocation.disk.watermark.high: 10gb

Elasticsearch를 재시작하면 read_only_allow_delete가 자동으로 해제됩니다.

출처: "Disk-based Shard Allocation | Elasticsearch Reference" 5.66.x

성능 모니터링#

Prometheus#

GitLab은 모든 웹/API 요청과 Sidekiq job의 요청 수 및 처리 시간에 관련된 Prometheus 메트릭을 내보냅니다. 이를 통해 성능 추세를 진단하고, 전체 성능에서 Elasticsearch 처리 시간이 다른 작업에 비해 어느 정도 영향을 미치는지 비교할 수 있습니다.

인덱싱 대기열#

GitLab은 인덱싱 대기열에 대한 Prometheus 메트릭도 내보냅니다. 이를 통해 성능 병목 현상을 진단하고, GitLab 인스턴스 또는 Elasticsearch 서버가 업데이트 볼륨을 따라잡을 수 있는지 확인할 수 있습니다.

로그#

모든 인덱싱은 Sidekiq에서 처리되므로, Elasticsearch 연동과 관련된 로그 대부분은 sidekiq.log에서 확인할 수 있습니다. 특히, Elasticsearch에 요청을 보내는 모든 Sidekiq 워커는 요청 수와 Elasticsearch 쿼리/쓰기에 소요된 시간을 로깅합니다. 이를 통해 클러스터가 인덱싱 속도를 따라가고 있는지 파악할 수 있습니다.

Elasticsearch 검색은 요청을 처리하는 일반 웹 워커를 통해 수행됩니다. 페이지 로드 또는 API 요청 중 Elasticsearch에 요청을 보내는 경우, 요청 수와 소요 시간이 production_json.log에 기록됩니다. 이 로그에는 데이터베이스 및 Gitaly 요청에 소요된 시간도 포함되어 있어, 검색의 어느 부분에서 성능 문제가 발생하는지 진단하는 데 도움이 될 수 있습니다.

Elasticsearch 전용 추가 로그는 elasticsearch.log로 전송되며, 성능 문제 진단에 유용한 정보가 포함될 수 있습니다.

Performance Bar#

Elasticsearch 요청은 Performance Bar에 표시됩니다. 이를 로컬 개발 환경과 배포된 GitLab 인스턴스 모두에서 활용하여 검색 성능 저하를 진단할 수 있습니다. 실행된 정확한 쿼리를 확인할 수 있어 검색이 느린 원인을 파악하는 데 유용합니다.

상관 관계 ID 및 X-Opaque-Id#

상관 관계 ID는 Rails에서 Elasticsearch로 보내는 모든 요청에 X-Opaque-Id 헤더로 전달됩니다. 이를 통해 클러스터의 모든 태스크를 GitLab의 요청과 연결하여 추적할 수 있습니다.

아키텍처#

Elasticsearch와 통신하는 데 사용되는 프레임워크는 이 에픽에서 추적 중인 리팩토링 작업이 진행 중입니다.

인덱싱 개요#

Advanced search는 데이터를 선택적으로 인덱싱합니다. 각 데이터 유형은 특정 인덱싱 파이프라인을 따릅니다:

데이터 유형 대기열에 넣는 방법 대기열 위치 인덱싱 발생 위치
데이터베이스 레코드 ActiveRecord 콜백 및 Gitlab::EventStore를 통한 레코드 변경 추적 Redis ZSET ElasticIndexInitialBulkCronWorker, ElasticIndexBulkCronWorker
Git 리포지터리 데이터 브랜치 푸시 서비스 및 기본 브랜치 변경 워커 Sidekiq Search::Elastic::CommitIndexerWorker, ElasticWikiIndexerWorker

인덱싱 구성 요소#

외부 인덱서#

리포지터리 콘텐츠의 경우, GitLab은 파일을 효율적으로 처리하기 위해 Go로 작성된 전용 인덱서를 사용합니다.

Rails 인덱싱 라이프사이클#

  • 초기 인덱싱: 관리자가 Admin UI 또는 Rake 태스크를 통해 첫 번째 전체 인덱스를 트리거합니다

  • 지속적 업데이트: 초기 설정 이후 GitLab은 다음을 통해 인덱스 최신 상태를 유지합니다:

/ee/app/models/concerns/elastic/application_versioned_search.rb에 정의된 모델 콜백(after_create, after_update, after_destroy)

  • 보류 중인 모든 변경 사항을 추적하는 Redis ZSET

  • Elasticsearch의 Bulk Request API를 사용하여 이 대기열을 배치로 처리하는 예약된 Sidekiq 워커

검색 및 보안#

쿼리 빌더 프레임워크는 검색 쿼리를 생성하고 접근 제어 로직을 처리합니다. 코드베이스의 이 부분은 개발 및 코드 리뷰 중에 특별한 주의가 필요하며, 역사적으로 보안 취약점의 원인이 되어왔습니다.

검색 결과를 반환하는 마지막 단계는 쿼리 문제나 경쟁 조건을 감지하기 위해 현재 사용자에 대한 미인가 결과를 삭제하는 것입니다.

마이그레이션 프레임워크#

GitLab Advanced search에는 인덱스 유지 관리 및 업데이트를 간소화하는 견고한 마이그레이션 프레임워크가 포함되어 있습니다. 이 시스템은 다음과 같은 중요한 이점을 제공합니다:

  • 선택적 재인덱싱: 필요한 경우에만 특정 문서 유형을 업데이트하여 전체 재인덱싱을 방지합니다

  • 자동화된 유지 관리: 사람의 개입 없이 업데이트가 진행됩니다

  • 일관된 경험: GitLab.com과 GitLab Self-Managed 인스턴스 모두에 동일한 마이그레이션 경로를 제공합니다

프레임워크 구성 요소#

마이그레이션 시스템은 다음으로 구성됩니다:

  • 마이그레이션 러너: 5분마다 실행하여 보류 중인 마이그레이션을 확인하고 처리하는 cron 워커

  • 마이그레이션 파일: 데이터베이스 마이그레이션과 유사하게, 이 Ruby 파일들은 YAML 문서와 함께 마이그레이션 단계를 정의합니다

  • 마이그레이션 상태 추적: 모든 마이그레이션 상태는 전용 Elasticsearch 인덱스에 저장됩니다

  • 마이그레이션 라이프사이클 상태: 각 마이그레이션은 다음 단계를 거칩니다: 대기 중 → 진행 중 → 완료 (또는 문제가 발생한 경우 중단됨)

구성 옵션#

마이그레이션은 다양한 파라미터로 세부 조정할 수 있습니다:

  • 배치 처리: 최적의 성능을 위한 문서 배치 크기 제어

  • 스로틀링: 마이그레이션 속도와 시스템 부하 간의 균형을 맞추기 위한 인덱싱 속도 조정

  • 공간 요구 사항: 중단을 방지하기 위해 마이그레이션 시작 전 충분한 디스크 공간 확인

  • 건너뛰기 조건: 마이그레이션을 건너뛰기 위한 조건 정의

이 프레임워크는 모든 GitLab 설치에 대해 인덱스 스키마 변경, 필드 업데이트, 데이터 마이그레이션을 안정적이고 최소한의 영향으로 처리합니다.

Search DSL#

이 섹션에서는 GitLab이 지원하는 Search DSL(Domain Specific Language)을 다루며, Elasticsearch와 OpenSearch 구현 모두와 호환됩니다.

커스텀 라우팅#

커스텀 라우팅은 Elasticsearch에서 문서 유형에 사용됩니다. 라우팅 형식은 일반적으로 프로젝트 연관 데이터의 경우 project_<project_id>, 그룹 연관 데이터의 경우 group_<root_namespace_id>입니다. 라우팅은 인덱싱 및 검색 작업 중에 설정되며 Elasticsearch에 데이터를 저장할 샤드를 알려줍니다. 커스텀 라우팅을 사용하는 이점과 트레이드오프는 다음과 같습니다:

  • 모든 샤드를 확인할 필요가 없으므로 프로젝트 및 그룹 범위 검색이 훨씬 빠릅니다.

  • 전역 및 그룹 범위 검색에서 너무 많은 샤드가 영향을 받는 경우 라우팅이 사용되지 않습니다.

  • 샤드 크기 불균형이 발생할 수 있습니다.

기존 분석기 및 토크나이저#

다음 분석기 및 토크나이저는 ee/lib/elastic/latest/config.rb에 정의되어 있습니다.

분석기#

path_analyzer 블롭의 경로를 인덱싱할 때 사용됩니다. path_tokenizerlowercaseasciifolding 필터를 사용합니다.

예시는 아래의 path_tokenizer 설명을 참조하세요.

sha_analyzer 블롭과 커밋에 사용됩니다. sha_tokenizerlowercaseasciifolding 필터를 사용합니다.

예시는 아래의 sha_tokenizer 설명을 참조하세요.

code_analyzer 블롭의 파일명과 콘텐츠를 인덱싱할 때 사용됩니다. whitespace 토크나이저를 사용합니다

word_delimiter_graph, lowercase, asciifolding 필터를 사용합니다.

whitespace 토크나이저는 토큰 분리 방식을 보다 세밀하게 제어하기 위해 선택되었습니다. 예를 들어 Foo::bar(4) 문자열은 제대로 검색되려면 Foo, bar(4) 같은 토큰을 생성해야 합니다.

토큰 분리 방식에 대한 설명은 code 필터를 참조하세요.

토크나이저#

sha_tokenizer 이는 SHA를 임의의 부분 문자열(최소 5자)로 검색할 수 있도록 edgeNGram 토크나이저를 사용하는 커스텀 토크나이저입니다.

예시:

240c29dc7e는 다음과 같이 변환됩니다:

  • 240c2

  • 240c29

  • 240c29d

  • 240c29dc

  • 240c29dc7

  • 240c29dc7e

path_tokenizer 이는 입력으로 제공된 경로의 길이에 관계없이 경로를 검색할 수 있도록 reverse: true 옵션을 사용하는 path_hierarchy 토크나이저를 활용하는 커스텀 토크나이저입니다.

예시:

'/some/path/application.js'는 다음과 같이 변환됩니다:

  • '/some/path/application.js'

  • 'some/path/application.js'

  • 'path/application.js'

  • 'application.js'

주의사항#

  • 검색은 자체 분석기를 가질 수 있습니다. 분석기를 수정할 때 반드시 확인하세요.

  • Character 필터(토큰 필터와 달리)는 항상 원래 문자를 대체합니다. 이러한 필터는 정확한 검색을 방해할 수 있습니다.

구현 가이드#

Elasticsearch에 새 문서 유형 추가#

Elasticsearch의 기존 인덱스 중 하나에 데이터를 추가할 수 없는 경우, 다음 지침에 따라 새 인덱스를 설정하고 데이터를 채웁니다.

새 문서 유형 추가를 위한 권장 절차#

모든 머지 리퀘스트는 Global Search 팀 멤버에게 리뷰를 받아야 합니다:

인덱싱이 완료되면 인덱스를 검색에 사용할 수 있습니다.

인덱스 생성#

모든 새 인덱스에는 다음이 포함되어야 합니다:

  • project_idnamespace_id 필드(사용 가능한 경우). 두 필드 중 하나는 커스텀 라우팅에 사용되어야 합니다.

  • 효율적인 전역 및 그룹 검색을 위한 traversal_ids 필드. object.namespace.elastic_namespace_ancestry로 필드를 채웁니다.

  • 인가(authorization)를 위한 필드:

프로젝트 데이터의 경우 - visibility_level

  • 그룹 데이터의 경우 - namespace_visibility_level

  • 필수 액세스 레벨 필드. 이는 issues_access_level 또는 repository_access_level과 같은 프로젝트 기능 액세스 레벨에 해당합니다.

  • YYVV(연도/버전) 형식의 schema_version 정수 필드. YY는 두 자리 연도이고, VV는 해당 연도 내 롤링 카운터(01-99)입니다. 스키마 버전은 레퍼런스 인덱스 클래스(Search::Elastic::References:: 또는 Elastic::Latest::InstanceProxy)의 상수(SCHEMA_VERSION)에 정의되어야 합니다. 이 필드는 어떤 버전의 문서 구조가 인덱싱되었는지 추적하고 데이터 마이그레이션을 가능하게 합니다. 인덱스 매핑이 변경될 때 반드시 증가시켜야 하며, 필드 콘텐츠가 변경될 때도 증가시킬 수 있습니다.

ee/lib/search/elastic/types/Search::Elastic::Types:: 클래스를 생성합니다.

다음 클래스 메서드를 정의합니다:

index_name: gitlab-<env>-<type> 형식(예: gitlab-production-work_items).

  • mappings: 필드, 데이터 타입, 분석기 등 인덱스 스키마를 포함하는 해시.

  • settings: 복제본 및 토크나이저 등 인덱스 설정을 포함하는 해시. 대부분의 경우 기본값으로 충분합니다.

scripts/elastic-migration을 실행하고 안내에 따라 인덱스를 생성하는 새 고급 검색 마이그레이션을 추가합니다. 마이그레이션 이름은 CreateIndex 형식이어야 합니다.

Search::Elastic::MigrationCreateIndexHelper 헬퍼와 생성된 스펙 파일에 'migration creates a new index' 공유 예시를 사용합니다.

타깃 클래스를 Gitlab::Elastic::Helper::ES_SEPARATE_CLASSES에 추가합니다.

인덱스 생성을 테스트하려면 콘솔에서 Elastic::MigrationWorker.new.perform을 실행하고, 올바른 매핑과 설정으로 인덱스가 생성되었는지 확인합니다:

curl "http://localhost:9200/gitlab-development-<type>/_mappings" | jq .`
curl "http://localhost:9200/gitlab-development-<type>/_settings" | jq .`
PostgreSQL에서 Elasticsearch로의 매핑#

기본 키 및 외래 키의 데이터 타입은 데이터베이스의 칼럼 타입과 일치해야 합니다. 예를 들어, 데이터베이스 칼럼 타입 integer는 매핑에서 integer로, bigintlong으로 매핑됩니다.

[Nested fields](https://www.elastic.co/guide/en/elasticsearch/reference/current/nested.html#_limits_on_nested_mappings_and_objects)는 상당한 오버헤드를 유발합니다. 대신 평탄화된 다중 값 방식을 권장합니다.
PostgreSQL 타입 Elasticsearch 매핑
bigint long
smallint short
integer integer
boolean boolean
array keyword
timestamp date
character varying, text 쿼리 요구 사항에 따라 다름. 전문 검색에는 text, 용어 쿼리·정렬·집계에는 keyword 사용
예상 쿼리 유효성 검사#

새 인덱스를 생성하기 전에, 계획된 매핑이 예상 쿼리를 지원하는지 유효성을 검사하는 것이 중요합니다. 매핑 호환성을 사전에 검증하면 나중에 인덱스를 재구축해야 하는 문제를 방지할 수 있습니다.

새 Elastic Reference 생성#

ee/lib/search/elastic/references/Search::Elastic::References:: 클래스를 생성합니다.

이 Reference는 Elasticsearch에서 벌크 작업을 수행하는 데 사용됩니다. 파일은 Search::Elastic::Reference를 상속해야 하며 다음 상수와 메서드를 정의해야 합니다:

include Search::Elastic::Concerns::DatabaseReference # if there is a corresponding database record for every document

SCHEMA_VERSION = 24_46 # integer in YYVV format

override :serialize
def self.serialize(record)
   # a string representation of the reference
end

override :instantiate
def self.instantiate(string)
   # deserialize the string and call initialize
end

override :preload_indexing_data
def self.preload_indexing_data(refs)
   # remove this method if `Search::Elastic::Concerns::DatabaseReference` is included
   # otherwise return refs
end

def initialize
   # initialize with instance variables
end

override :identifier
def identifier
   # a way to identify the reference
end

override :routing
def routing
   # Optional: an identifier to route the document in Elasticsearch
end

override :operation
def operation
   # one of `:index`, `:upsert` or `:delete`
end

override :serialize
def serialize
   # a string representation of the reference
end

override :as_indexed_json
def as_indexed_json
   # a hash containing the document representation for this reference
end

override :index_name
def index_name
   # index name
end

def model_klass
   # set to the model class if `Search::Elastic::Concerns::DatabaseReference` is included
end

인덱스에 데이터를 추가하려면 Elastic::ProcessBookkeepingService.track!()에서 새 Reference 클래스의 인스턴스를 호출하여 인덱싱할 Reference 큐에 데이터를 추가합니다. 크론 워커가 큐에 있는 Reference를 가져와 Elasticsearch에 항목을 벌크 인덱싱합니다.

인덱싱 작업이 정상적으로 동작하는지 테스트하려면 Reference 클래스의 인스턴스를 사용하여 Elastic::ProcessBookkeepingService.track!()을 호출하고 Elastic::ProcessBookkeepingService.new.execute를 실행합니다. 로그에서 업데이트 내용을 확인할 수 있습니다. 인덱스의 문서를 확인하려면 다음 명령을 실행합니다:

curl "http://localhost:9200/gitlab-development-<type>/_search"
일반적인 주의사항#
  • 인덱스 작업은 실제로 upsert를 수행합니다. 문서가 이미 존재하면 전송된 필드를 기존 문서 필드와 병합하여 부분 업데이트를 수행합니다. 필드를 명시적으로 제거하거나 비워두려면 as_indexed_json에서 nil 또는 빈 배열을 전송해야 합니다.

데이터 일관성#

이제 인덱스와 새 문서 유형을 Elasticsearch에 대량 인덱싱하는 방법이 마련되었으므로, 인덱스에 데이터를 추가해야 합니다. 이 과정은 초기 데이터 백필(backfill)과 인덱스 데이터를 최신 상태로 유지하기 위한 지속적인 업데이트로 구성됩니다.

백필은 인덱싱해야 하는 모든 문서에 대해 Search::Elastic::Reference 인스턴스를 인자로 Elastic::ProcessInitialBookkeepingService.track!()을 호출하여 수행합니다.

지속적인 업데이트는 생성/업데이트/삭제해야 하는 모든 문서에 대해 Search::Elastic::Reference 인스턴스를 인자로 Elastic::ProcessBookkeepingService.track!()을 호출하여 수행합니다.

데이터 백필#

scripts/elastic-migration을 실행하고 안내에 따라 새 고급 검색 마이그레이션을 추가하여 데이터를 백필합니다.

MigrationDatabaseBackfillHelper를 사용하세요. BackfillWorkItems 마이그레이션을 예시로 참고할 수 있습니다.

백필을 테스트하려면 콘솔에서 Elastic::MigrationWorker.new.perform을 몇 번 실행하여 인덱스가 채워졌는지 확인하세요.

마이그레이션 진행 상황을 확인하려면 로그를 실시간으로 모니터링하세요:

tail -f log/elasticsearch.log
지속적인 업데이트#

ActiveRecord 객체의 경우, 모델에 ApplicationVersionedSearch 컨설턴트를 포함하여 콜백 기반으로 데이터를 인덱싱할 수 있습니다. 이 방식이 적합하지 않은 경우, 문서를 인덱싱해야 할 때마다 Search::Elastic::Reference 인스턴스를 인자로 Elastic::ProcessBookkeepingService.track!()을 호출하세요.

일부 GitLab Self-Managed 인스턴스는 Elasticsearch가 활성화되어 있지 않고 네임스페이스 제한이 활성화될 수 있으므로, 항상 Gitlab::CurrentSettings.elasticsearch_indexing?use_elasticsearch?를 확인하세요.

또한 인덱스가 인덱스 요청을 처리할 수 있는지 확인하세요. 예를 들어, 현재 메이저 릴리즈에서 추가된 인덱스인 경우 인덱스를 추가하는 마이그레이션이 완료되었는지 확인하세요: Elastic::DataMigrationService.migration_has_finished?

전송 및 삭제#

프로젝트 및 그룹의 전송과 삭제는 고아 데이터가 발생하지 않도록 인덱스를 업데이트해야 합니다. 고아 데이터는 전송으로 인해 커스텀 라우팅이 변경될 때 발생할 수 있습니다. 이전 샤드의 데이터를 정리해야 합니다. 전송에 대한 Elasticsearch 업데이트는 Projects::TransferServiceGroups::TransferService에서 처리됩니다.

project_id 필드를 포함하는 인덱스는 Search::Elastic::DeleteWorker를 사용해야 합니다. namespace_id 필드를 포함하고 project_id 필드가 없는 인덱스는 Search::ElasticGroupAssociationDeletionWorker를 사용해야 합니다.

  • 인덱싱된 클래스를 ElasticDeleteProjectWorkerexcluded_classes에 추가하세요.

  • 인덱스에서 문서를 삭제하는 새 서비스를 ::Search::Elastic::Delete 네임스페이스에 생성하세요.

  • 새 서비스를 사용하도록 워커를 업데이트하세요.

새 문서 유형에 대한 검색 구현#

검색 데이터는 SearchControllerSearch API에서 사용할 수 있습니다. 두 곳 모두 SearchService를 사용하여 결과를 반환합니다. SearchServiceSearchControllerSearch API 외부에서도 결과를 반환하는 데 사용할 수 있습니다.

새 문서 유형에 대한 검색 구현 권장 프로세스#

다음 머지 리퀘스트를 생성하고 Global Search 팀 구성원에게 리뷰를 요청하세요:

검색 스코프#

SearchService전역, 그룹, 프로젝트 레벨에서의 검색을 제공합니다.

새 스코프는 다음 상수에 추가해야 합니다:

  • 각 EE SearchService 파일의 ALLOWED_SCOPES (또는 allowed_scopes 메서드 오버라이드)

  • Gitlab::Search::AbuseDetectionALLOWED_SCOPES

  • Search::Navigationsearch_tab_ability_map 메서드. 필요한 경우 EE 버전에서 오버라이드

    스코프에 대한 전역 검색을 비활성화할 수 있습니다. 전역 검색 비활성화를 위해 다음 변경을 수행할 수 있습니다:

  • app/models/application_setting.rbsearch jsonb 접근자 아래에 기본값이 trueglobal_search_SCOPE_enabled 애플리케이션 설정을 추가하세요.

  • JSON 스키마 유효성 검사기 파일 application_setting_search.json에 항목을 추가하세요.

  • ApplicationSettingsHelperglobal_search_settings_checkboxes 메서드에 항목을 생성하여 Admin UI에 설정 체크박스를 추가하세요.

  • SearchServiceglobal_search_enabled_for_scope? 메서드에 추가하세요.

  • EE 전용 설정은 파일의 EE 버전에 추가해야 합니다.

결과 클래스#

사용 가능한 검색 결과 클래스는 다음과 같습니다:

검색 유형 검색 레벨 클래스
Basic search global Gitlab::SearchResults
Basic search group Gitlab::GroupSearchResults
Basic search project Gitlab::ProjectSearchResults
Advanced search global Gitlab::Elastic::SearchResults
Advanced search group Gitlab::Elastic::GroupSearchResults

| Advanced search | project | Gitlab::Elastic::ProjectSearchResults | | Exact code search | global | Search::Zoekt::SearchResults | | Exact code search | group | Search::Zoekt::SearchResults | | Exact code search | project | Search::Zoekt::SearchResults | | All search types | All levels | Search::EmptySearchResults |

결과 클래스는 다음 데이터를 반환합니다:

  • objects - Elasticsearch에서 변환된 데이터베이스 레코드 또는 PORO의 페이지네이션 결과

  • formatted_count - Elasticsearch에서 반환된 문서 수

  • highlight_map - Elasticsearch에서 반환된 하이라이트된 필드의 맵

  • failed? - 오류 발생 여부

  • error - Elasticsearch에서 반환된 오류 메시지

  • aggregations - (선택 사항) Elasticsearch에서 반환된 집계

새 scope는 Gitlab::Elastic::SearchResults 클래스 내 다음 메서드에 대한 지원을 추가해야 합니다:

  • objects

  • formatted_count

  • highlight_map

  • failed?

  • error

기존 scope 업데이트#

업데이트에는 문서 필드 추가 및 제거, 또는 인가(권한 부여) 변경이 포함될 수 있습니다. 기존 scope를 업데이트하려면 쿼리 및 인덱싱용 JSON 생성에 사용되는 코드를 찾으세요.

  • 쿼리는 QueryBuilder 클래스에서 생성됩니다

  • 인덱싱된 문서는 Reference 클래스에서 빌드됩니다

레거시 Proxy 프레임워크도 지원합니다:

  • 쿼리는 ClassProxy 클래스에서 생성됩니다

  • 인덱싱된 문서는 InstanceProxy 클래스에서 빌드됩니다

레거시 프레임워크에서 사용되는 경우에도 새 검색 필터는 항상 QueryBuilder 프레임워크에 생성하는 것을 목표로 하세요.

필드 추가#

인덱스에 필드 추가#
  • 인덱스 매핑에 필드를 추가하여 새로 생성된 인덱스에 반영하고, 동일한 MR 내에서 기존 인덱스에 필드를 추가하는 마이그레이션을 생성하여 매핑 스키마 드리프트를 방지하세요. MigrationUpdateMappingsHelper를 사용하세요.

  • 문서 JSON에서 새 필드를 채웁니다. 코드는 ::Elastic::DataMigrationService.migration_has_finished?를 사용하여 마이그레이션이 완료되었는지 확인해야 합니다.

  • 문서 JSON의 SCHEMA_VERSION을 올립니다. 형식은 연도와 버전 번호입니다: YYVV

  • 인덱스에서 필드를 백필(backfill)하는 마이그레이션을 생성합니다. null이 허용되지 않는 필드의 경우 MigrationBackfillHelper를 사용하고, null이 허용되는 필드의 경우 MigrationReindexBasedOnSchemaVersion을 사용하세요.

새 필드가 연관 레코드인 경우#
검색 서비스에 필드 노출#
  • Search::Filter concern에 필터를 추가하세요. 이 concern은 Search::GlobalService, Search::GroupService, Search::ProjectService에서 사용됩니다.

  • scope_options 메서드를 업데이트하여 scope에 해당 필드를 전달하세요. 이 메서드는 Gitlab::Elastic::SearchResults에 정의되어 있으며 Gitlab::Elastic::GroupSearchResultsGitlab::Elastic::ProjectSearchResults에서 재정의됩니다.

  • 기존 필터를 추가하거나 새 필터를 생성하여 쿼리 빌더에서 해당 필드를 사용하세요.

  • SearchController에서 검색의 필터 사용을 추적하세요.

기존 필드의 매핑 변경#

  • 인덱스 매핑에서 필드 타입을 업데이트하여 새로 생성된 인덱스에 변경 사항을 반영하세요.

  • 문서 JSON의 SCHEMA_VERSION을 올립니다. 형식은 연도와 버전 번호입니다: YYVV

  • Zero downtime 리인덱싱을 사용하여 모든 문서를 리인덱싱하는 마이그레이션을 생성합니다. Search::Elastic::MigrationReindexTaskHelper를 사용하세요.

필드 콘텐츠 변경#

  • 문서 JSON에서 필드 콘텐츠를 업데이트하세요.

  • 문서 JSON의 SCHEMA_VERSION을 올립니다. 형식은 연도와 버전 번호입니다: YYVV

  • 문서를 업데이트하는 마이그레이션을 생성합니다. MigrationReindexBasedOnSchemaVersion을 사용하세요.

인덱스에서 문서 정리#

하나의 인덱스가 별도의 인덱스로 분리되는 경우 또는 버그로 인해 인덱스에 남아 있는 데이터를 제거하는 경우에 사용할 수 있습니다.

  • 문서 JSON의 SCHEMA_VERSION을 올립니다. 형식은 연도와 버전 번호입니다: YYVV

  • 모든 레코드를 인덱싱하는 마이그레이션을 생성합니다. MigrationDatabaseBackfillHelper를 사용하세요.

  • 이전 SCHEMA_VERSION을 가진 모든 문서를 제거하는 마이그레이션을 생성합니다. MigrationDeleteBasedOnSchemaVersion을 사용하세요.

필드 제거#

멀티 버전 호환성을 지원하기 위해 제거는 여러 마일스톤에 걸쳐 분할되어야 합니다. 동적 매핑 오류를 방지하려면 Zero downtime 리인덱싱을 수행하기 전에 모든 문서에서 해당 필드를 제거해야 합니다.

마일스톤 M:

  • 새로 생성되는 인덱스에서 해당 필드를 제거하기 위해 인덱스 매핑에서 필드를 삭제합니다.

  • 문서 JSON에서 해당 필드를 더 이상 채우지 않도록 중단합니다.

  • 문서 JSON의 SCHEMA_VERSION을 올립니다. 형식은 연도와 버전 번호인 YYVV입니다.

  • 쿼리 빌더에서 해당 필드를 사용하는 필터를 제거합니다.

  • scope_options 메서드를 업데이트하여 업데이트 중인 스코프에서 해당 필드를 제거합니다. 이 메서드는 Gitlab::Elastic::SearchResults에 정의되어 있으며, Gitlab::Elastic::GroupSearchResultsGitlab::Elastic::ProjectSearchResults에서 재정의됩니다.

해당 필드를 다른 스코프에서 사용하지 않는 경우:

  • Search::Filter concern에서 해당 필드를 제거합니다. 이 concern은 Search::GlobalService, Search::GroupService, Search::ProjectService에서 사용됩니다.

  • SearchController의 검색에서 필터 추적을 제거합니다.

마일스톤 M+1:

인가(권한 부여) 업데이트#

QueryBuilder 프레임워크에서 인가(권한 부여)는 프로젝트 수준에서 by_search_level_and_membership 필터로 처리되며, 그룹 수준에서는 by_search_level_and_group_membership 필터로 처리됩니다.

레거시 Proxy 프레임워크에서는 인가(권한 부여)가 클래스 내부에서 처리됩니다.

두 프레임워크 모두 Search::GroupsFinderSearch::ProjectsFinder를 사용하여 사용자가 직접 검색 접근 권한을 가진 그룹과 프로젝트를 쿼리합니다. 검색은 각 스코프에 대한 그룹 및 프로젝트 가시성 수준과 기능 접근 수준 설정에 의존합니다. 자세한 내용은 역할 및 권한 문서를 참조하세요.

쿼리 빌더 프레임워크#

쿼리 빌더 프레임워크는 Elasticsearch 쿼리를 구성하는 데 사용됩니다. Elastic::Latest::ApplicationClassProxy 클래스와 이를 상속하는 클래스에 구현된 레거시 쿼리 프레임워크도 지원합니다.

새로운 문서 타입은 반드시 쿼리 빌더 프레임워크를 사용해야 합니다.

쿼리 생성#

쿼리는 다음을 사용하여 구성됩니다:

  • Search::Elastic::Queries의 쿼리

  • ::Search::Elastic::Filters의 하나 이상의 필터

  • (선택 사항) ::Search::Elastic::Aggregations의 집계

  • ::Search::Elastic::Formats의 하나 이상의 형식

새로운 스코프는 Search::Elastic::QueryBuilder를 상속하는 새로운 쿼리 빌더 클래스를 생성해야 합니다.

쿼리 빌더 프레임워크는 일반적인 검색 시나리오를 처리하기 위해 미리 구성된 필터 모음을 제공합니다. 이 필터들은 원시 Elasticsearch 쿼리 DSL을 직접 작성하지 않고도 복잡한 쿼리 조건을 구성하는 과정을 단순화합니다.

QUERY_COMPONENTS로 쿼리 빌더 구성#

쿼리 빌더는 파이프라인을 QUERY_COMPONENTS 해시로 선언합니다. 기본 클래스가 해시를 순회하며 각 메서드를 실행 중인 query_hash에 적용하므로, 서브클래스는 build를 재정의하지 않아도 됩니다.

해시 키는 모듈(Filters, Formats, Sorts, Aggregations)입니다. 값은 해당 모듈의 메서드 배열입니다. 각 메서드는 실행 중인 query_hash:와 빌더의 options:를 받아 다음 query_hash를 반환합니다.

skip_if_size_zero는 size가 0으로 설정된 경우 쿼리 컴포넌트를 건너뜁니다. 예를 들어, 개수만 조회하는 쿼리를 실행할 때는 정렬을 적용할 필요가 없습니다.

Search::Elastic::UserQueryBuilder의 최소 예시:

QUERY_COMPONENTS = {
  ::Search::Elastic::Filters => %i[by_forbidden_states by_user_accessible_namespaces],
  ::Search::Elastic::Formats => %i[size source_fields],
  ::Search::Elastic::Sorts::Base => %i[sort_by]
}.freeze

Search::Elastic::WorkItemQueryBuilder의 더 복잡한 예시:

QUERY_COMPONENTS = {
  ::Search::Elastic::Filters => %i[
    by_combined_search_level_and_membership
    by_combined_confidentiality
    by_state
    by_not_hidden
    by_label_ids
    by_archived
    by_work_item_type_ids
    by_author
    by_assignees
    by_milestone
    by_milestone_state
    by_label_names
    by_weight
    by_health_status
    by_closed_at
    by_created_at
    by_updated_at
    by_due_date
    by_iids
  ],
  ::Search::Elastic::Aggregations => %i[by_label_ids by_work_item_type_ids],
  ::Search::Elastic::Formats => [
    { method: :source_fields, skip_if_size_zero: true },
    { method: :page, skip_if_size_zero: true },
    { method: :size, skip_if_size_zero: true }
  ],
  ::Search::Elastic::Sorts::WorkItem => [
    { method: :sort_by, skip_if_size_zero: true }
  ]
}.freeze

서브클래스 훅#

파이프라인에 연결하려면 서브클래스에서 이 메서드들을 오버라이드하세요.

메서드 실행 시점 목적
extra_options 초기화 시점. options에 병합할 기본값을 반환합니다.
prepare_options build_initial_query_hash 이전. options를 변경합니다(기본값 설정, 입력값 변환).
build_initial_query_hash prepare_options 이후. 시작 query_hash(전문 검색 쿼리, IID 쿼리, 또는 빈 bool)를 반환합니다.

메서드별 플래그#

조건부 동작을 표현하려면 단순 심볼을 메서드 이름과 플래그를 지정하는 해시로 대체하세요.

플래그 메서드 적용 조건
migration: 지정한 마이그레이션이 완료된 경우.
unless_migration: 지정한 마이그레이션이 아직 완료되지 않은 경우.
skip_if_size_zero: 파이프라인 끝에서 query_hash[:size]가 0보다 큰 경우. 집계 쿼리도 실행하는 빌더의 Formats 및 Sorts 메서드에 사용하세요. 집계는 size: 0을 설정하기 때문입니다.

점진적 롤아웃 중에 하나의 필터를 다른 필터로 교체하려면 migration:unless_migration:을 쌍으로 사용하세요. 마이그레이션이 완료된 후에는 레거시 항목과 migration: 플래그를 제거하세요.

QUERY_COMPONENTS = {
  ::Search::Elastic::Filters => [
    :by_type,
    { method: :by_search_level_and_membership, migration: :migration_name },
    { method: :by_project_authorization, unless_migration: :migration_name },
    :by_archived
  ],
  ::Search::Elastic::Formats => [
    { method: :source_fields, skip_if_size_zero: true },
    { method: :size, skip_if_size_zero: true }
  ]
}.freeze

필터 및 집계 계약#

QUERY_COMPONENTS에 명시된 모든 메서드는 query_hash:options:를 인수로 받아 업데이트된 query_hash를 반환해야 합니다. 특정 옵션이 설정된 경우에만 동작하는 메서드 (예: options[:aggregation]true일 때의 Aggregations.by_label_ids)는 그렇지 않은 경우에 query_hash를 변경 없이 반환해야 합니다. 그래야 파이프라인에서 조건 없이 배치할 수 있습니다.

필터 생성#

필터는 효과적인 Elasticsearch 쿼리를 구성하는 핵심 구성 요소입니다. 필터는 관련성 점수에 영향을 주지 않으면서 검색 결과 범위를 좁혀 줍니다.

모든 필터는 문서화되어야 합니다.

필터는 Search::Elastic::Filters의 클래스 레벨 메서드로 생성됩니다.

메서드 이름은 by_로 시작해야 합니다.

메서드는 query_hashoptions 매개변수만 받아야 합니다.

query_hash는 다음 형식의 해시를 포함해야 합니다.

 { "query":
   { "bool":
     {
       "must": [],
       "must_not": [],
       "should": [],
       "filters": [],
       "minimum_should_match": null
     }
   }
 }

add_filter를 사용하여 쿼리 해시에 필터를 추가하세요. 필터는 점수 계산을 피하기 위해 filters에 추가해야 합니다. 점수 계산은 쿼리 자체가 담당합니다.

필터에 이름을 추가하려면 필터 주위에 context.name(:filters)를 사용하세요. 이를 통해 쿼리와 필터 중 어느 부분이 검색 결과를 반환했는지 식별하는 데 도움이 됩니다.

  def by_new_filter_type(query_hash:, options:)
      filter_selected_value = options[:field_value]

      context.name(:filters) do
        add_filter(query_hash, :query, :bool, :filter) do
          { term: { field_name: { _name: context.name(:field_name), value: filter_selected_value } } }
        end
      end
  end

쿼리와 필터의 차이 이해#

Elasticsearch의 쿼리는 문서 필터링과 관련성 점수 계산이라는 두 가지 핵심 목적을 수행합니다. 검색 기능을 구축할 때:

  • 쿼리는 검색 기준과 얼마나 일치하는지에 따라 결과를 순위화하는 관련성 점수가 필요할 때 필수적입니다. Boolean 쿼리의 must, should, must_not 절을 사용하며, 이 절들은 모두 문서의 최종 관련성 점수에 영향을 줍니다.

  • 필터(쿼리 컨텍스트 내)는 문서의 점수에 영향을 주지 않으면서 검색 결과에 포함할지 여부를 결정합니다. 관련성에 따른 순위 없이 결과를 포함하거나 제외하기만 하면 되는 검색 작업에서는 필터만 사용하는 것이 더 효율적이며 대규모에서 성능이 더 좋습니다.

검색 요구 사항에 맞는 적절한 방식을 선택하세요. 순위가 매겨진 결과가 필요할 때는 스코어링 절이 포함된 쿼리를 사용하고, 단순 포함/제외 로직에는 필터를 활용하세요.

필터 요구 사항 및 사용법#

필터를 사용하려면:

  • 인덱스 매핑에 각 필터 문서에 명시된 모든 필수 필드가 포함되어 있어야 합니다.

  • 필터를 호출할 때 options 해시를 통해 적절한 파라미터를 전달합니다.

  • 각 필터는 적절한 JSON 구조를 생성하여 query_hash에 추가합니다.

필터는 서로 조합하여 정교한 검색 쿼리를 만들 수 있으며, 동시에 읽기 쉽고 유지 관리하기 좋은 코드를 유지할 수 있습니다.

Elasticsearch에 쿼리 전송#

쿼리는 Gitlab::Elastic::SearchResults에서 ::Gitlab::Search::Client로 전송됩니다. 결과는 Search::Elastic::ResponseMapper를 통해 파싱되어 Elasticsearch의 응답을 변환합니다.

모델 요구 사항#

모델은 to_ability_name 메서드에 응답할 수 있어야 하며, 이를 통해 리댁션(redaction) 로직이 Ability.allowed?(current_user, :"read_#{object.to_ability_name}", object)?를 확인할 수 있습니다. 해당 메서드가 없으면 추가해야 합니다.

모델은 N+1 문제를 방지하기 위해 preload_search_data 스코프를 정의해야 합니다.

사용 가능한 쿼리#

모든 쿼리 빌더는 Elasticsearch의 Boolean 쿼리 구문을 준수하는 표준화된 query_hash 구조를 반환해야 합니다. Search::Elastic::BoolExpr 클래스는 Boolean 쿼리를 구성하기 위한 인터페이스를 제공합니다.

필수 쿼리 해시 구조는 다음과 같습니다:

{
  "query": {
    "bool": {
      "must": [],
      "must_not": [],
      "should": [],
      "filters": [],
      "minimum_should_match": null
    }
  }
}

by_iid#

iid 필드와 문서 유형으로 쿼리합니다. typeiid 필드가 필요합니다.

{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "iid": {
              "_name": "milestone:related:iid",
              "value": 1
            }
          }
        },
        {
          "term": {
            "type": {
              "_name": "doc:is_a:milestone",
              "value": "milestone"
            }
          }
        }
      ]
    }
  }
}

by_full_text#

전문(full text) 검색을 수행합니다. 쿼리 문자열에 고급 검색 구문이 사용된 경우 이 쿼리는 by_multi_match_query 또는 by_simple_query_string을 사용합니다.

by_multi_match_query#

multi_match Elasticsearch API를 사용합니다. 다음 옵션으로 커스터마이즈할 수 있습니다:

  • count_only - Boolean 쿼리 절 filter를 사용합니다. 스코어링 및 하이라이팅은 수행되지 않습니다.

  • query - 쿼리가 전달되지 않으면 match_all Elasticsearch API를 사용합니다.

  • keyword_match_clause - :should가 전달되면 Boolean 쿼리 절 should를 사용합니다. 기본값: must

{
  "query": {
    "bool": {
      "must": [
        {
          "bool": {
            "must": [],
            "must_not": [],
            "should": [
              {
                "multi_match": {
                  "_name": "project:multi_match:and:search_terms",
                  "fields": [
                    "name^10",
                    "name_with_namespace^2",
                    "path_with_namespace",
                    "path^9",
                    "description"
                  ],
                  "query": "search",
                  "operator": "and",
                  "lenient": true
                }
              },
              {
                "multi_match": {
                  "_name": "project:multi_match_phrase:search_terms",
                  "type": "phrase",
                  "fields": [
                    "name^10",
                    "name_with_namespace^2",
                    "path_with_namespace",
                    "path^9",
                    "description"
                  ],
                  "query": "search",
                  "lenient": true
                }
              }
            ],
            "filter": [],
            "minimum_should_match": 1
          }
        }
      ],
      "must_not": [],
      "should": [],
      "filter": [],
      "minimum_should_match": null
    }
  }
}

by_simple_query_string#

simple_query_string Elasticsearch API를 사용합니다. 다음 옵션으로 커스터마이즈할 수 있습니다:

  • count_only - Boolean 쿼리 절 filter를 사용합니다. 스코어링 및 하이라이팅은 수행되지 않습니다.

  • query - 쿼리가 전달되지 않으면 match_all Elasticsearch API를 사용합니다.

  • keyword_match_clause - :should가 전달되면 Boolean 쿼리 절 should를 사용합니다. 기본값: must

{
  "query": {
    "bool": {
      "must": [
        {
          "simple_query_string": {
            "_name": "project:match:search_terms",
            "fields": [
              "name^10",
              "name_with_namespace^2",
              "path_with_namespace",
              "path^9",
              "description"
            ],
            "query": "search",
            "lenient": true,
            "default_operator": "and"
          }
        }
      ],
      "must_not": [],
      "should": [],
      "filter": [],
      "minimum_should_match": null
    }
  }
}

Available Filters#

다음 섹션에서는 사용 가능한 각 필터, 필수 필드, 지원되는 옵션, 출력 예시를 자세히 설명합니다.

by_type#

type 필드가 필요합니다. 옵션에서 doc_type으로 쿼리합니다.

{
  "term": {
    "type": {
      "_name": "filters:doc:is_a:milestone",
      "value": "milestone"
    }
  }
}

by_group_level_confidentiality#

current_usergroup_ids 필드가 필요합니다. 기밀 그룹 엔티티를 읽을 수 있는 사용자 권한을 기반으로 쿼리합니다.

{
  "bool": {
    "must": [
      {
        "term": {
          "confidential": {
            "value": true,
            "_name": "confidential:true"
          }
        }
      },
      {
        "terms": {
          "namespace_id": [
            1
          ],
          "_name": "groups:can:read_confidential_work_items"
        }
      }
    ]
  },
  "should": {
    "term": {
      "confidential": {
        "value": false,
        "_name": "confidential:false"
      }
    }
  }
}

by_project_confidentiality#

confidential, author_id, assignee_id, project_id 필드가 필요합니다. 옵션에서 confidential로 쿼리합니다.

{
  "bool": {
    "should": [
      {
        "term": {
          "confidential": {
            "_name": "filters:confidentiality:projects:non_confidential",
            "value": false
          }
        }
      },
      {
        "bool": {
          "must": [
            {
              "term": {
                "confidential": {
                  "_name": "filters:confidentiality:projects:confidential",
                  "value": true
                }
              }
            },
            {
              "bool": {
                "should": [
                  {
                    "term": {
                      "author_id": {
                        "_name": "filters:confidentiality:projects:confidential:as_author",
                        "value": 1
                      }
                    }
                  },
                  {
                    "term": {
                      "assignee_id": {
                        "_name": "filters:confidentiality:projects:confidential:as_assignee",
                        "value": 1
                      }
                    }
                  },
                  {
                    "terms": {
                      "_name": "filters:confidentiality:projects:confidential:project:membership:id",
                      "project_id": [
                        12345
                      ]
                    }
                  }
                ]
              }
            }
          ]
        }
      }
    ]
  }
}

by_combined_confidentiality#

search_level 필드와 use_group_authorization 또는 use_project_authorization 중 하나 이상이 필요합니다. 옵션에 confidential이 포함된 쿼리입니다. 이 필터는 use_group_authorizationuse_project_authorization가 모두 제공된 경우 by_project_confidentialityby_group_level_confidentiality를 하나의 쿼리로 결합합니다. 필수 필드는 해당 메서드를 참고하세요.

[
  {
    "bool": {
      "should": [
        {
          "bool": {
            "filter": [
              {
                "bool": {
                  "should": [
                    {
                      "term": {
                        "confidential": {
                          "_name": "filters:confidentiality:projects:non_confidential",
                          "value": false
                        }
                      }
                    },
                    {
                      "bool": {
                        "must": [
                          {
                            "term": {
                              "confidential": {
                                "_name": "filters:confidentiality:projects:confidential",
                                "value": true
                              }
                            }
                          },
                          {
                            "bool": {
                              "should": [
                                {
                                  "term": {
                                    "author_id": {
                                      "_name": "filters:confidentiality:projects:confidential:as_author",
                                      "value": 278964
                                    }
                                  }
                                },
                                {
                                  "term": {
                                    "assignee_id": {
                                      "_name": "filters:confidentiality:projects:confidential:as_assignee",
                                      "value": 278964
                                    }
                                  }
                                },
                                {
                                  "terms": {
                                    "_name": "filters:confidentiality:projects:confidential:project:membership:id",
                                    "project_id": []
                                  }
                                }
                              ]
                            }
                          }
                        ]
                      }
                    }
                  ]
                }
              }
            ]
          }
        },
        {
          "bool": {
            "filter": [
              {
                "bool": {
                  "should": [
                    {
                      "bool": {
                        "_name": "filters:confidentiality:groups:non_confidential:public",
                        "must": [
                          {
                            "term": {
                              "confidential": {
                                "value": false
                              }
                            }
                          },
                          {
                            "term": {
                              "namespace_visibility_level": {
                                "value": 20
                              }
                            }
                          }
                        ]
                      }
                    },
                    {
                      "bool": {
                        "_name": "filters:confidentiality:groups:non_confidential:internal",
                        "must": [
                          {
                            "term": {
                              "confidential": {
                                "value": false
                              }
                            }
                          },
                          {
                            "term": {
                              "namespace_visibility_level": {
                                "value": 10
                              }
                            }
                          }
                        ]
                      }
                    },
                    {
                      "bool": {
                        "_name": "filters:confidentiality:groups:non_confidential:private",
                        "must": [
                          {
                            "term": {
                              "confidential": {
                                "value": false
                              }
                            }
                          }
                        ],
                        "should": [
                          {
                            "prefix": {
                              "traversal_ids": {
                                "_name": "filters:confidentiality:groups:non_confidential:private:ancestry_filter:descendants",
                                "value": "9970-"
                              }
                            }
                          }
                        ],
                        "minimum_should_match": 1
                      }
                    },
                    {
                      "bool": {
                        "_name": "filters:confidentiality:groups:non_confidential:private",
                        "must": [
                          {
                            "term": {
                              "confidential": {
                                "value": false
                              }
                            }
                          },
                          {
                            "terms": {
                              "_name": "filters:confidentiality:groups:non_confidential:private:project:membership",
                              "namespace_id": [
                                9971
                              ]
                            }
                          }
                        ]
                      }
                    },
                    {
                      "bool": {
                        "_name": "filters:confidentiality:groups:confidential:private",
                        "must": [
                          {
                            "term": {
                              "confidential": {
                                "value": true
                              }
                            }
                          }
                        ],
                        "should": [
                          {
                            "prefix": {
                              "traversal_ids": {
                                "_name": "filters:confidentiality:groups:confidential:private:ancestry_filter:descendants",
                                "value": "9970-"
                              }
                            }
                          }
                        ],
                        "minimum_should_match": 1
                      }
                    }
                  ],
                  "minimum_should_match": 1
                }
              }
            ]
          }
        }
      ],
      "minimum_should_match": 1
    }
  }
]

by_note_confidentiality#

노트의 기밀성 필터를 적용합니다. 노트에는 두 가지 수준의 기밀성이 있습니다:

  • 노트 자체의 기밀성 (confidential 필드)

  • 이슈의 기밀성 (issue.confidential, issue.author_id, issue.assignee_id)

confidential, issue.confidential, issue.author_id, issue.assignee_id, project_id, traversal_ids 필드가 필요합니다.

다음 조건 중 하나라도 충족되면 노트가 표시됩니다:

  • 비기밀 이슈에 달린 노트이며 노트 자체도 기밀이 아닌 경우

  • 기밀 이슈에 달린 노트이지만 사용자가 작성자/담당자이거나 project_id 또는 traversal_ids를 통해 프로젝트 접근 권한이 있는 경우

  • 노트가 기밀이지만 사용자가 project_id 또는 traversal_ids를 통해 프로젝트 접근 권한이 있는 경우

이 필터는 효율적인 그룹 수준 검색을 위해 project_id 조건과 traversal_ids 기반 인가를 모두 사용합니다.

{
  "bool": {
    "minimum_should_match": 1,
    "should": [
      {
        "bool": {
          "filter": [
            {
              "bool": {
                "_name": "filters:confidentiality:notes:not_on_issue_or_not_confidential",
                "should": [
                  {
                    "bool": {
                      "_name": "filters:confidentiality:notes:not_on_issue",
                      "must_not": [{ "exists": { "field": "issue" } }]
                    }
                  },
                  {
                    "term": {
                      "issue.confidential": {
                        "_name": "filters:confidentiality:notes:non_confidential_issue",
                        "value": false
                      }
                    }
                  }
                ]
              }
            },
            {
              "bool": {
                "_name": "filters:confidentiality:notes:not_confidential",
                "should": [
                  { "bool": { "must_not": [{ "exists": { "field": "confidential" } }] } },
                  { "term": { "confidential": false } }
                ]
              }
            }
          ]
        }
      },
      {
        "bool": {
          "filter": [
            {
              "term": {
                "issue.confidential": {
                  "_name": "filters:confidentiality:notes:issue:confidential",
                  "value": true
                }
              }
            },
            {
              "bool": {
                "_name": "filters:confidentiality:notes:not_confidential",
                "should": [
                  { "bool": { "must_not": [{ "exists": { "field": "confidential" } }] } },
                  { "term": { "confidential": false } }
                ]
              }
            },
            {
              "bool": {
                "minimum_should_match": 1,
                "should": [
                  {
                    "term": {
                      "issue.author_id": {
                        "_name": "filters:confidentiality:notes:confidential:as_author",
                        "value": 1
                      }
                    }
                  },
                  {
                    "term": {
                      "issue.assignee_id": {
                        "_name": "filters:confidentiality:notes:confidential:as_assignee",
                        "value": 1
                      }
                    }
                  },
                  {
                    "terms": {
                      "_name": "filters:confidentiality:notes:private:project:member",
                      "project_id": [1]
                    }
                  },
                  {
                    "bool": {
                      "minimum_should_match": 1,
                      "should": [
                        {
                          "prefix": {
                            "traversal_ids": {
                              "_name": "filters:confidentiality:notes:private:ancestry_filter:descendants",
                              "value": "123-"
                            }
                          }
                        }
                      ]
                    }
                  }
                ]
              }
            }
          ]
        }
      },
      {
        "bool": {
          "filter": [
            {
              "term": {
                "confidential": {
                  "_name": "filters:confidentiality:notes:confidential",
                  "value": true
                }
              }
            }
          ],
          "minimum_should_match": 1,
          "should": [
            {
              "terms": {
                "_name": "filters:confidentiality:notes:private:project:member",
                "project_id": [1]
              }
            },
            {
              "bool": {
                "minimum_should_match": 1,
                "should": [
                  {
                    "prefix": {
                      "traversal_ids": {
                        "_name": "filters:confidentiality:notes:private:ancestry_filter:descendants",
                        "value": "123-"
                      }
                    }
                  }
                ]
              }
            }
          ]
        }
      }
    ]
  }
}

by_label_ids#

label_ids 필드가 필요합니다. 옵션에서 label_names를 사용하여 쿼리합니다.

{
  "bool": {
    "must": [
      {
        "terms": {
          "_name": "filters:label_ids",
          "label_ids": [
            1
          ]
        }
      }
    ]
  }
}

by_archived#

archived 필드가 필요합니다. 옵션에서 search_levelinclude_archived를 사용하여 쿼리합니다.

{
  "bool": {
    "_name": "filters:non_archived",
    "should": [
      {
        "bool": {
          "filter": {
            "term": {
              "archived": {
                "value": false
              }
            }
          }
        }
      },
      {
        "bool": {
          "must_not": {
            "exists": {
              "field": "archived"
            }
          }
        }
      }
    ]
  }
}

by_state#

state 필드가 필요합니다. all, opened, closed, merged 값을 지원합니다. 옵션에서 state를 사용하여 쿼리합니다.

{
  "match": {
    "state": {
      "_name": "filters:state",
      "query": "opened"
    }
  }
}

by_not_hidden#

hidden 필드가 필요합니다. 관리자에게는 적용되지 않습니다.

{
  "term": {
    "hidden": {
      "_name": "filters:not_hidden",
      "value": false
    }
  }
}

by_work_item_type_ids#

work_item_type_id 필드가 필요합니다. 옵션에서 work_item_type_ids 또는 not_work_item_type_ids를 사용하여 쿼리합니다.

{
  "bool": {
    "must_not": {
      "terms": {
        "_name": "filters:not_work_item_type_ids",
        "work_item_type_id": [
          8
        ]
      }
    }
  }
}

by_author#

author_id 필드가 필요합니다. 옵션에서 author_username 또는 not_author_username을 사용하여 쿼리합니다.

{
  "bool": {
    "should": [
      {
        "term": {
          "author_id": {
            "_name": "filters:author",
            "value": 1
          }
        }
      }
    ],
    "minimum_should_match": 1
  }
}

by_target_branch#

target_branch 필드가 필요합니다. 옵션에서 target_branch 또는 not_target_branch를 사용하여 쿼리합니다.

{
  "bool": {
    "should": [
      {
        "term": {
          "target_branch": {
            "_name": "filters:target_branch",
            "value": "master"
          }
        }
      }
    ],
    "minimum_should_match": 1
  }
}

by_source_branch#

source_branch 필드가 필요합니다. 옵션에서 source_branch 또는 not_source_branch를 사용하여 쿼리합니다.

{
  "bool": {
    "should": [
      {
        "term": {
          "source_branch": {
            "_name": "filters:source_branch",
            "value": "master"
          }
        }
      }
    ],
    "minimum_should_match": 1
  }
}

by_search_level_and_group_membership#

current_user, group_ids, traversal_id, search_level 필드가 필요합니다. search_level을 사용하여 쿼리하고, 각 그룹에 대해 사용자가 보유한 권한에 따라 namespace_visibility_level을 기준으로 필터링합니다.

이 필터는 검색 대상 데이터에 project_id 필드가 없는 경우 by_search_level_and_membership 대신 사용할 수 있습니다.

예시는 인증된 사용자를 대상으로 합니다. 인가(authorization)를 가진 사용자, 관리자, 외부 사용자, 또는 익명 사용자의 경우 JSON이 다를 수 있습니다.
global#
{
  "bool": {
    "should": [
      {
        "bool": {
          "filter": [
            {
              "term": {
                "namespace_visibility_level": {
                  "value": 20,
                  "_name": "filters:namespace_visibility_level:public"
                }
              }
            }
          ]
        }
      },
      {
        "bool": {
          "filter": [
            {
              "term": {
                "namespace_visibility_level": {
                  "value": 10,
                  "_name": "filters:namespace_visibility_level:internal"
                }
              }
            }
          ]
        }
      },
      {
        "bool": {
          "filter": [
            {
              "term": {
                "namespace_visibility_level": {
                  "value": 0,
                  "_name": "filters:namespace_visibility_level:private"
                }
              }
            },
            {
              "terms": {
                "namespace_id": [
                  33,
                  22
                ]
              }
            }
          ]
        }
      }
    ],
    "minimum_should_match": 1
  }
}
group#
[
  {
    "bool": {
      "_name": "filters:level:group",
      "minimum_should_match": 1,
      "should": [
        {
          "prefix": {
            "traversal_ids": {
              "_name": "filters:level:group:ancestry_filter:descendants",
              "value": "22-"
            }
          }
        }
      ]
    }
  },
  {
    "bool": {
      "should": [
        {
          "bool": {
            "filter": [
              {
                "term": {
                  "namespace_visibility_level": {
                    "value": 20,
                    "_name": "filters:namespace_visibility_level:public"
                  }
                }
              }
            ]
          }
        },
        {
          "bool": {
            "filter": [
              {
                "term": {
                  "namespace_visibility_level": {
                    "value": 10,
                    "_name": "filters:namespace_visibility_level:internal"
                  }
                }
              }
            ]
          }
        },
        {
          "bool": {
            "filter": [
              {
                "term": {
                  "namespace_visibility_level": {
                    "value": 0,
                    "_name": "filters:namespace_visibility_level:private"
                  }
                }
              },
              {
                "terms": {
                  "namespace_id": [
                    22
                  ]
                }
              }
            ]
          }
        }
      ],
      "minimum_should_match": 1
    }
  },
  {
    "bool": {
      "_name": "filters:level:group",
      "minimum_should_match": 1,
      "should": [
        {
          "prefix": {
            "traversal_ids": {
              "_name": "filters:level:group:ancestry_filter:descendants",
              "value": "22-"
            }
          }
        }
      ]
    }
  }
]

by_search_level_and_membership#

project_id, traversal_id 및 프로젝트 가시성(기본값은 visibility_level이며 project_visibility_level_field 옵션으로 설정 가능) 필드가 필요합니다. *_access_level 기능 필드를 지원합니다. search_level과 옵션으로 project_ids, group_ids, features, current_user를 옵션에 포함하여 쿼리합니다.

다음에 대한 필터링이 적용됩니다:

  • 전역, 그룹, 또는 프로젝트에 대한 검색 레벨

  • 그룹 및 프로젝트에 대한 직접 멤버십 또는 그룹에 대한 직접 접근을 통한 공유 멤버십

  • features를 통해 전달된 모든 기능 접근 레벨

    예시는 로그인한 사용자를 기준으로 표시됩니다. JSON은 인가가 있는 사용자, 관리자, 외부 사용자, 또는 익명 사용자에 따라 다를 수 있습니다.

global#
{
  "bool": {
    "_name": "filters:permissions:global",
    "should": [
      {
        "bool": {
          "must": [
            {
              "terms": {
                "_name": "filters:permissions:global:visibility_level:public_and_internal",
                "visibility_level": [
                  20,
                  10
                ]
              }
            }
          ],
          "should": [
            {
              "terms": {
                "_name": "filters:permissions:global:repository_access_level:enabled",
                "repository_access_level": [
                  20
                ]
              }
            }
          ],
          "minimum_should_match": 1
        }
      },
      {
        "bool": {
          "must": [
            {
              "bool": {
                "should": [
                  {
                    "terms": {
                      "_name": "filters:permissions:global:repository_access_level:enabled_or_private",
                      "repository_access_level": [
                        20,
                        10
                      ]
                    }
                  }
                ],
                "minimum_should_match": 1
              }
            }
          ],
          "should": [
            {
              "prefix": {
                "traversal_ids": {
                  "_name": "filters:permissions:global:ancestry_filter:descendants",
                  "value": "123-"
                }
              }
            },
            {
              "terms": {
                "_name": "filters:permissions:global:project:member",
                "project_id": [
                  456
                ]
              }
            }
          ],
          "minimum_should_match": 1
        }
      }
    ],
    "minimum_should_match": 1
  }
}
그룹#
[
  {
    "bool": {
      "_name": "filters:level:group",
      "minimum_should_match": 1,
      "should": [
        {
          "prefix": {
            "traversal_ids": {
              "_name": "filters:level:group:ancestry_filter:descendants",
              "value": "123-"
            }
          }
        }
      ]
    }
  },
  {
    "bool": {
      "_name": "filters:permissions:group",
      "should": [
        {
          "bool": {
            "must": [
              {
                "terms": {
                  "_name": "filters:permissions:group:visibility_level:public_and_internal",
                  "visibility_level": [
                    20,
                    10
                  ]
                }
              }
            ],
            "should": [
              {
                "terms": {
                  "_name": "filters:permissions:group:repository_access_level:enabled",
                  "repository_access_level": [
                    20
                  ]
                }
              }
            ],
            "minimum_should_match": 1
          }
        },
        {
          "bool": {
            "must": [
              {
                "bool": {
                  "should": [
                    {
                      "terms": {
                        "_name": "filters:permissions:group:repository_access_level:enabled_or_private",
                        "repository_access_level": [
                          20,
                          10
                        ]
                      }
                    }
                  ],
                  "minimum_should_match": 1
                }
              }
            ],
            "should": [
              {
                "prefix": {
                  "traversal_ids": {
                    "_name": "filters:permissions:group:ancestry_filter:descendants",
                    "value": "123-"
                  }
                }
              }
            ],
            "minimum_should_match": 1
          }
        }
      ],
      "minimum_should_match": 1
    }
  }
]
프로젝트#
[
  {
    "bool": {
      "_name": "filters:level:project",
      "must": {
        "terms": {
          "project_id": [
            456
          ]
        }
      }
    }
  },
  {
    "bool": {
      "_name": "filters:permissions:project",
      "should": [
        {
          "bool": {
            "must": [
              {
                "terms": {
                  "_name": "filters:permissions:project:visibility_level:public_and_internal",
                  "visibility_level": [
                    20,
                    10
                  ]
                }
              }
            ],
            "should": [
              {
                "terms": {
                  "_name": "filters:permissions:project:repository_access_level:enabled",
                  "repository_access_level": [
                    20
                  ]
                }
              }
            ],
            "minimum_should_match": 1
          }
        },
        {
          "bool": {
            "must": [
              {
                "bool": {
                  "should": [
                    {
                      "terms": {
                        "_name": "filters:permissions:project:repository_access_level:enabled_or_private",
                        "repository_access_level": [
                          20,
                          10
                        ]
                      }
                    }
                  ],
                  "minimum_should_match": 1
                }
              }
            ],
            "should": [
              {
                "prefix": {
                  "traversal_ids": {
                    "_name": "filters:permissions:project:ancestry_filter:descendants",
                    "value": "123-"
                  }
                }
              }
            ],
            "minimum_should_match": 1
          }
        }
      ],
      "minimum_should_match": 1
    }
  }
]

by_combined_search_level_and_membership#

search_level 필드와 use_group_authorization 또는 use_project_authorization 중 하나 이상이 필요합니다. 이 필터는 use_group_authorizationuse_project_authorization이 모두 제공된 경우 by_search_level_and_membershipby_search_level_and_group_membership을 하나의 쿼리로 결합합니다. 필수 필드에 대해서는 해당 메서드를 참고하세요.

[
  {
    "bool": {
      "should": [
        {
          "bool": {
            "filter": [
              {
                "bool": {
                  "should": [
                    {
                      "bool": {
                        "should": [
                          {
                            "prefix": {
                              "traversal_ids": {
                                "_name": "filters:permissions:global:private_access:ancestry_filter:descendants",
                                "value": "9970-"
                              }
                            }
                          }
                        ],
                        "filter": [
                          {
                            "terms": {
                              "_name": "filters:permissions:global:private_access:issues_access_level:enabled_or_private",
                              "issues_access_level": [
                                20,
                                10
                              ]
                            }
                          }
                        ],
                        "minimum_should_match": 1
                      }
                    },
                    {
                      "bool": {
                        "filter": [
                          {
                            "terms": {
                              "_name": "filters:permissions:global:private_access:issues_access_level:enabled_or_private",
                              "issues_access_level": [
                                20,
                                10
                              ]
                            }
                          },
                          {
                            "terms": {
                              "_name": "filters:permissions:global:private_access:project:member",
                              "project_id": [
                                278964
                              ]
                            }
                          }
                        ]
                      }
                    },
                    {
                      "bool": {
                        "should": [
                          {
                            "terms": {
                              "_name": "filters:permissions:global:issues_access_level:enabled",
                              "issues_access_level": [
                                20
                              ]
                            }
                          }
                        ],
                        "filter": [
                          {
                            "terms": {
                              "_name": "filters:permissions:global:project_visibility_level:public_and_internal",
                              "project_visibility_level": [
                                20,
                                10
                              ]
                            }
                          }
                        ],
                        "minimum_should_match": 1
                      }
                    }
                  ],
                  "minimum_should_match": 1
                }
              }
            ]
          }
        },
        {
          "bool": {
            "filter": [
              {
                "bool": {
                  "_name": "filters:permissions:global",
                  "should": [
                    {
                      "bool": {
                        "filter": [
                          {
                            "terms": {
                              "_name": "filters:permissions:global:namespace_visibility_level:public_and_internal",
                              "namespace_visibility_level": [
                                20,
                                10
                              ]
                            }
                          }
                        ]
                      }
                    },
                    {
                      "bool": {
                        "must": [
                          {
                            "terms": {
                              "_name": "filters:permissions:global:namespace_visibility_level:private",
                              "namespace_visibility_level": [
                                0
                              ]
                            }
                          }
                        ],
                        "should": [
                          {
                            "prefix": {
                              "traversal_ids": {
                                "_name": "filters:permissions:global:ancestry_filter:descendants",
                                "value": "9970-"
                              }
                            }
                          }
                        ],
                        "minimum_should_match": 1
                      }
                    },
                    {
                      "bool": {
                        "must": [
                          {
                            "terms": {
                              "_name": "filters:permissions:global:namespace_visibility_level:private",
                              "namespace_visibility_level": [
                                0
                              ]
                            }
                          },
                          {
                            "terms": {
                              "_name": "filters:permissions:global:project:membership",
                              "namespace_id": [
                                9971
                              ]
                            }
                          }
                        ]
                      }
                    }
                  ],
                  "minimum_should_match": 1
                }
              }
            ]
          }
        }
      ],
      "minimum_should_match": 1
    }
  }
]

by_user_accessible_namespaces#

사용자의 네임스페이스(그룹 및 프로젝트) 접근 권한을 기반으로 문서를 필터링합니다. 이 필터는 사용자 검색 쿼리에 특화되어 있으며, 글로벌, 그룹, 프로젝트 검색 레벨에서 네임스페이스 레벨의 인가(권한 부여)를 처리합니다.

필수 필드:

  • namespace_ancestry_ids - Namespace#elastic_namespace_ancestry / Project#elastic_namespace_ancestry(프로젝트 세그먼트인 p<id> 포함)에서 채워지는 키워드 필드로, 네임스페이스 계층 필터링에서 prefixterms 쿼리에 사용됨

  • current_user - 검색을 수행하는 사용자 (글로벌 범위용)

  • search_level - :global, :group, :project 중 하나

선택 필드:

  • group_id - 그룹 레벨 검색용 그룹 ID

  • project_id - 프로젝트 레벨 검색용 프로젝트 ID

  • autocomplete - 자동 완성 검색을 위한 boolean 플래그

검색 레벨별 동작:

  • 글로벌: 모든 사용자를 반환합니다.

자동 완성 사용 시: 인가된 그룹 및 프로젝트를 통해 접근 가능한 사용자를 반환합니다. traversal_id_prefixes를 사용하여 그룹 계층과 직접 프로젝트 멤버십을 매칭합니다.

  • 그룹: 지정된 그룹과 그 하위 그룹의 사용자, 그리고 상위 그룹의 사용자를 반환합니다.

  • 프로젝트: 지정된 프로젝트와 그 상위 그룹의 사용자를 반환합니다.

글로벌 검색 예시:

{
  "bool": {
    "should": [
      {
        "prefix": {
          "namespace_ancestry_ids": {
            "_name": "namespace:ancestry_filter:descendants",
            "value": "285-"
          }
        }
      },
      {
        "prefix": {
          "namespace_ancestry_ids": {
            "_name": "namespace:ancestry_filter:descendants",
            "value": "417-418-419-"
          }
        }
      },
      {
        "terms": {
          "namespace_ancestry_ids": [
            "417-418-419-p91-"
          ],
          "_name": "namespace:ancestry_filter:ancestors"
        }
      }
    ],
    "minimum_should_match": 1
  }
}

하위 그룹에서의 그룹 검색 예시:

{
  "bool": {
    "should": [
      {
        "prefix": {
          "namespace_ancestry_ids": {
            "_name": "namespace:ancestry_filter:descendants",
            "value": "807-806-805"
          }
        }
      },
      {
        "terms": {
          "namespace_ancestry_ids": [
            "807-","807-806-"
          ],
          "_name": "namespace:ancestry_filter:ancestors"
        }
      }
    ],
    "minimum_should_match": 1
  }
}

프로젝트 검색 예시:

{
  "bool": {
    "should": [
      {
        "terms": {
          "namespace_ancestry_ids": [
            "807-","807-806-", "807-806-805", "807-806-805-p123"
          ],
          "_name": "namespace:ancestry_filter:ancestors"
        }
      }
    ],
    "minimum_should_match": 1
  }
}

by_noteable_type#

noteable_type 필드가 필요합니다. 옵션에서 noteable_type으로 쿼리합니다. noteable_id 필드만 반환하도록 _source를 설정합니다.

{
  "term": {
    "noteable_type": {
      "_name": "filters:related:issue",
      "value": "Issue"
    }
  }
}

by_iids#

여러 IID 값으로 문서를 필터링합니다.

필수 필드:

  • iids - 일치시킬 IID 값의 배열
{
  "bool": {
    "_name": "filters:iids",
    "filter": {
      "terms": {
        "iid": [1, 2, 3]
      }
    }
  }
}

by_closed_at#

종료 날짜 범위로 필터링합니다. 선택 필드 중 최소 하나는 제공해야 합니다.

선택 필드:

  • closed_after - 최소 종료 날짜의 ISO 날짜 문자열

  • closed_before - 최대 종료 날짜의 ISO 날짜 문자열

{
  "bool": {
    "_name": "filters:closed_after",
    "must": {
      "range": {
        "closed_at": {
          "gte": "2025-01-01T00:00:00Z"
        }
      }
    }
  }
}

by_created_at#

생성 날짜 범위로 필터링합니다. 선택 필드 중 최소 하나는 제공해야 합니다.

선택 필드:

  • created_after - 최소 생성 날짜의 ISO 날짜 문자열

  • created_before - 최대 생성 날짜의 ISO 날짜 문자열

{
  "bool": {
    "_name": "filters:created_after",
    "must": {
      "range": {
        "created_at": {
          "gte": "2025-01-01T00:00:00Z"
        }
      }
    }
  }
}

by_updated_at#

업데이트 날짜 범위로 필터링합니다. 선택 필드 중 최소 하나는 제공해야 합니다.

선택 필드:

  • updated_after - 최소 업데이트 날짜의 ISO 날짜 문자열

  • updated_before - 최대 업데이트 날짜의 ISO 날짜 문자열

{
  "bool": {
    "_name": "filters:updated_after",
    "must": {
      "range": {
        "updated_at": {
          "gte": "2025-01-01T00:00:00Z"
        }
      }
    }
  }
}

by_due_date#

마감 날짜 범위로 필터링합니다. 선택 필드 중 최소 하나는 제공해야 합니다.

선택적 필드:

  • due_after - 최소 마감일에 대한 ISO 날짜 문자열

  • due_before - 최대 마감일에 대한 ISO 날짜 문자열

{
  "bool": {
    "_name": "filters:due_after",
    "must": {
      "range": {
        "due_date": {
          "gte": "2025-01-01T00:00:00Z"
        }
      }
    }
  }
}

by_milestone#

마일스톤 제목 또는 마일스톤 존재 여부로 필터링합니다. 선택적 필드 중 하나 이상을 제공해야 합니다. 마일스톤 제목 필터(milestone_title, not_milestone_title)와 마일스톤 존재 여부 필터(any_milestones, none_milestones)는 상호 배타적입니다.

선택적 필드:

  • milestone_title - 포함할 마일스톤 제목의 배열

  • not_milestone_title - 제외할 마일스톤 제목의 배열

  • any_milestones - 불리언, 마일스톤이 있는 문서를 필터링

  • none_milestones - 불리언, 마일스톤이 없는 문서를 필터링

milestone_title 사용 예시:

{
  "bool": {
    "must": {
      "terms": {
        "_name": "filters:milestone_title",
        "milestone_title": ["18.1", "18.2"]
      }
    }
  }
}

none_milestones 사용 예시:

{
  "bool": {
    "_name": "filters:none_milestones",
    "must_not": {
      "exists": {
        "field": "milestone_title"
      }
    }
  }
}

by_milestone_state#

시간적 조건을 사용하여 마일스톤 상태로 필터링합니다.

필수 필드:

  • milestone_state_filters - 다음 중 하나 이상을 포함하는 배열: :upcoming, :started, :not_upcoming, :not_started

:upcoming 필터 예시:

{
  "bool": {
    "_name": "filters:milestone_state_upcoming",
    "must": [
      {
        "term": {
          "milestone_state": "active"
        }
      },
      {
        "range": {
          "milestone_start_date": {
            "gt": "now/d"
          }
        }
      }
    ]
  }
}

:started 필터 예시:

{
  "bool": {
    "_name": "filters:milestone_state_started",
    "must": [
      {
        "term": {
          "milestone_state": "active"
        }
      },
      {
        "bool": {
          "should": [
            {
              "range": {
                "milestone_start_date": {
                  "lte": "now/d"
                }
              }
            },
            {
              "bool": {
                "must_not": {
                  "exists": {
                    "field": "milestone_start_date"
                  }
                }
              }
            }
          ]
        }
      },
      {
        "bool": {
          "should": [
            {
              "range": {
                "milestone_due_date": {
                  "gte": "now/d"
                }
              }
            },
            {
              "bool": {
                "must_not": {
                  "exists": {
                    "field": "milestone_due_date"
                  }
                }
              }
            }
          ]
        }
      }
    ],
    "must_not": {
      "bool": {
        "must": [
          {
            "bool": {
              "must_not": {
                "exists": {
                  "field": "milestone_start_date"
                }
              }
            }
          },
          {
            "bool": {
              "must_not": {
                "exists": {
                  "field": "milestone_due_date"
                }
              }
            }
          }
        ]
      }
    }
  }
}

by_assignees#

담당자 ID로 필터링하며, 다양한 매칭 모드를 지원합니다. 선택 필드 중 하나 이상을 반드시 제공해야 합니다.

선택 필드:

  • assignee_ids - 모두 존재해야 하는 담당자 ID 배열

  • not_assignee_ids - 제외할 담당자 ID 배열

  • or_assignee_ids - 하나라도 매칭되면 되는 담당자 ID 배열

  • none_assignees - boolean, 담당자가 없는 문서를 필터링

  • any_assignees - boolean, 담당자가 있는 문서를 필터링

assignee_ids 사용 예시 (모두 매칭해야 함):

{
  "bool": {
    "_name": "filters:assignee_ids",
    "must": [
      {
        "term": {
          "assignee_id": 123
        }
      },
      {
        "term": {
          "assignee_id": 456
        }
      }
    ]
  }
}

or_assignee_ids 사용 예시 (하나라도 매칭 가능):

{
  "bool": {
    "must": {
      "terms": {
        "_name": "filters:or_assignee_ids",
        "assignee_id": [123, 456, 789]
      }
    }
  }
}

none_assignees 사용 예시:

{
  "bool": {
    "_name": "filters:none_assignees",
    "must_not": {
      "exists": {
        "field": "assignee_id"
      }
    }
  }
}

by_weight#

이슈 가중치(정수 값)로 필터링합니다. 선택 필드 중 하나 이상을 반드시 제공해야 합니다.

선택 필드:

  • weight - 매칭할 정확한 가중치 값 (정수)

  • not_weight - 제외할 가중치 값 (정수)

  • none_weight - boolean, 가중치가 없는 문서를 필터링

  • any_weight - boolean, 가중치가 있는 문서를 필터링

{
  "term": {
    "weight": {
      "_name": "filters:weight",
      "value": 3
    }
  }
}

by_health_status#

health status 필드로 필터링합니다. 선택 필드 중 하나 이상을 반드시 제공해야 합니다.

선택 필드:

  • health_status - 포함할 health status ID 배열

  • not_health_status - 제외할 health status ID 배열

  • none_health_status - boolean, health status가 없는 문서를 필터링

  • any_health_status - boolean, health status가 있는 문서를 필터링

{
  "bool": {
    "must": {
      "terms": {
        "_name": "filters:health_status",
        "health_status": [1, 2]
      }
    }
  }
}

by_label_names#

라벨 이름으로 필터링하며, 다양한 매칭 모드와 범위가 지정된 라벨 와일드카드를 지원합니다. 선택 필드 중 적어도 하나는 반드시 제공해야 합니다.

선택 필드:

  • label_names - 모두 존재해야 하는 라벨 이름 배열

  • not_label_names - 제외할 라벨 이름 배열

  • or_label_names - 하나라도 일치하면 되는 라벨 이름 배열

  • none_label_names - 불리언, 라벨이 없는 문서를 필터링함

  • any_label_names - 불리언, 라벨이 하나 이상 있는 문서를 필터링함

"workflow::*"와 같은 범위 지정 라벨 와일드카드를 지원하여 "workflow::"로 시작하는 모든 라벨에 매칭합니다. 와일드카드는 Elasticsearch에서 접두사 쿼리로 변환됩니다.

정확한 일치를 사용하는 예시:

{
  "bool": {
    "_name": "filters:label_names",
    "must": [
      {
        "term": {
          "label_names": "advanced search"
        }
      },
      {
        "term": {
          "label_names": "GLQL"
        }
      }
    ]
  }
}

범위 지정 라벨 와일드카드를 사용하는 예시:

{
  "bool": {
    "_name": "filters:or_label_names",
    "should": [
      {
        "prefix": {
          "label_names": "workflow::"
        }
      },
      {
        "term": {
          "label_names": "backend"
        }
      }
    ],
    "minimum_should_match": 1
  }
}

Testing scopes#

Rails 콘솔에서 모든 scope를 테스트합니다.

search_service = ::SearchService.new(User.first, { search: 'foo', scope: 'SCOPE_NAME' })
search_service.search_objects

Permissions tests#

검색 코드는 SearchService#redact_unauthorized_results에서 최종 보안 검사를 수행합니다. 이를 통해 권한이 없는 결과가 조회 권한이 없는 사용자에게 반환되는 것을 방지합니다. 이 검사는 버그나 인덱싱 지연으로 인한 Elasticsearch 권한 데이터의 불일치를 처리하기 위해 Ruby에서 수행됩니다.

새로운 scope는 적절한 접근 제어를 보장하기 위해 가시성 스펙을 추가해야 합니다. 권한이 올바르게 적용되는지 테스트하려면, EE 스펙에서 'search respects visibility' 공유 예시를 사용하여 테스트를 추가하세요:

  • ee/spec/services/ee/search/global_service_spec.rb

  • ee/spec/services/ee/search/group_service_spec.rb

  • ee/spec/services/ee/search/project_service_spec.rb

Zero-downtime reindexing with multiple indices#

다중 인덱스 기능이 아직 완전히 구현되지 않았으므로 현재는 해당되지 않습니다.

현재 GitLab은 단일 버전의 설정만 처리할 수 있습니다. 설정 또는 스키마 변경 사항이 있으면 모든 것을 처음부터 다시 인덱싱해야 합니다. 재인덱싱에는 오랜 시간이 걸릴 수 있으므로 검색 기능 다운타임이 발생할 수 있습니다.

다운타임을 방지하기 위해 GitLab은 동시에 작동할 수 있는 다중 인덱스 지원을 개발 중입니다. 스키마가 변경될 때마다 관리자는 새 인덱스를 생성하고 해당 인덱스로 재인덱싱할 수 있으며, 검색은 계속해서 안정적인 기존 인덱스로 수행됩니다. 모든 데이터 업데이트는 두 인덱스 모두에 전달됩니다. 새 인덱스가 준비되면 관리자가 해당 인덱스를 활성으로 표시하여 모든 검색을 새 인덱스로 유도하고, 기존 인덱스를 제거할 수 있습니다.

이는 예를 들어 AWS로 또는 AWS에서 이동하는 등 새 서버로 마이그레이션할 때도 유용합니다.

현재 이 새로운 설계로의 마이그레이션 과정을 진행 중입니다. 지금은 모든 것이 단일 버전으로만 동작하도록 고정되어 있습니다.