- 심층 탐구
- 지원되는 버전
- 개발 환경 설정
- 유용한 Rake 작업
- 어떻게 작동하나요?
- 기존 분석기 및 토크나이저
- 주의 사항
- Elasticsearch에 새 문서 유형 추가하기
- 여러 인덱스를 이용한 제로 다운타임 재색인
- 성능 모니터링
- 문제 해결
고급 검색 개발 지침
이 페이지는 Elasticsearch 개발 및 작업에 대한 정보를 포함합니다.
Elasticsearch를 활성화하고 초기 인덱싱을 수행하는 방법에 대한 정보는 Elasticsearch 통합 문서에 있습니다.
심층 탐구
2019년 6월, Mario de la Ossa는 GitLab Elasticsearch 통합에 대한 심층 탐구를 주최했습니다 (GitLab 팀원 전용: https://gitlab.com/gitlab-org/create-stage/-/issues/1
). 이는 이 코드베이스의 해당 부분에서 작업할 수 있는 분들과 도메인 특정 지식을 공유하기 위함이었습니다.
녹화는 YouTube에서 확인할 수 있으며, 슬라이드는 Google Slides와 PDF로 제공됩니다. 이번 심층 탐구에서 다룬 모든 내용은 GitLab 12.0 기준으로 정확했으며, 특정 세부정보는 변경되었을 수 있지만 여전히 좋은 소개가 될 것입니다.
2020년 8월, 두 번째 심층 탐구가 개최되어 다중 인덱스 지원을 위한 GitLab 전용 아키텍처에 중점을 두었습니다. 녹화는 YouTube에서 확인할 수 있으며, 슬라이드는 제공됩니다. 이번 심층 탐구에서 다룬 모든 내용은 GitLab 13.3 기준으로 정확했습니다.
2024년 7월, Terri Chu는 고급 검색 기초, 통합, 인덱싱 및 검색에 대한 Lunch and Learn을 주최했습니다. Google Slides (GitLab 팀원 전용)과 녹화는 YouTube에서 (GitLab 팀원 전용) 확인할 수 있습니다. 이번 심층 탐구에서 다룬 모든 내용은 GitLab 17.0 기준으로 정확했습니다.
지원되는 버전
버전 요구 사항을 참조하세요.
Elasticsearch 쿼리에 중대한 변경을 하는 개발자는 모든 지원되는 버전에서 기능을 테스트해야 합니다.
개발 환경 설정
Elasticsearch GDK 설정 지침을 참조하세요.
유용한 Rake 작업
-
gitlab:elastic:test:index_size
: 현재 인덱스가 사용하는 공간과 인덱스에 있는 문서 수를 알려줍니다. -
gitlab:elastic:test:index_size_change
: 인덱스 크기를 출력하고, 재인덱싱을 수행한 후 다시 인덱스 크기를 출력합니다. 인덱싱 크기 개선을 테스트할 때 유용합니다.
또한, 테스트를 위해 대규모 리포지토리나 여러 포크가 필요한 경우, 이 지침을 따라 하시기 바랍니다.
어떻게 작동하나요?
Elasticsearch 통합은 외부 인덱서에 의존합니다. 우리는 Go로 작성된 인덱서를 제공합니다.
사용자는 Rake 작업을 통해 초기 인덱싱을 트리거해야 하지만, 이 작업이 완료되면
GitLab 자체가 생성, 업데이트 및 삭제 시 after_
콜백을 통해 재인덱싱을 트리거합니다. 에서 상속됩니다
/ee/app/models/concerns/elastic/application_versioned_search.rb
.
초기 인덱싱이 완료된 후, 모든 모델의 생성, 업데이트 및 삭제 작업은 프로젝트를 제외하고(참조 #207494)
Redis의 ZSET
에 기록됩니다.
정기적인 sidekiq-cron
ElasticIndexBulkCronWorker
가 이 큐를 처리하며,
Bulk Request API를 사용하여 한 번에 여러 Elasticsearch 문서를 업데이트합니다.
검색 쿼리는
ee/app/models/concerns/elastic
에서 발견된
concerns에 의해 생성됩니다. 이 concerns는 접근 제어도 책임지며,
역사적으로 보안 버그의 원인이 되었으므로 주의 깊게 다뤄야 합니다!
아키텍처
참고: 우리는 이 에픽에서 이 아키텍처 패턴을 벗어나고 있습니다.
전통적인 설정은 elasticsearch-rails
에 의해 제공되며, 내부 프록시 클래스를 통해 통신합니다.
개발자는 모델이 포함할 모듈에 모델별 로직을 작성합니다(예: SnippetsSearch
).
__elasticsearch__
메소드는 프록시 객체를 반환합니다. 예를 들면:
-
Issue.__elasticsearch__
는Elasticsearch::Model::Proxy::ClassMethodsProxy
의 인스턴스를 반환합니다. -
Issue.first.__elasticsearch__
는Elasticsearch::Model::Proxy::InstanceMethodsProxy
의 인스턴스를 반환합니다.
이러한 프록시 객체는 Elasticsearch 서버와 직접 통신합니다(다이어그램의 상단 반).
계획된 새로운 디자인에서는 각 모델이 해당 서브 클래스화된 프록시 객체 쌍을 가지며, 이 객체 내에 모델별 로직이 위치합니다.
예를 들어, Snippet
은 Elasticsearch::Model::Proxy::ClassMethodsProxy
의 서브클래스인 SnippetClassProxy
를 가지며,
Snippet
은 Elasticsearch::Model::Proxy::InstanceMethodsProxy
의 서브클래스인 SnippetInstanceProxy
를 가집니다.
__elasticsearch__
는 여러 실제 프록시 객체를 추적하는 또 다른 계층의 프록시 객체를 나타냅니다.
적절한 인덱스로 메소드 호출을 전달합니다. 예를 들면:
-
model.__elasticsearch__.search
는 읽기 작업이므로 하나의 안정적인 인덱스로 전달됩니다. -
model.__elasticsearch__.update_document
는 모든 인덱스로 전달되어 모든 인덱스를 최신 상태로 유지합니다.
버전별 전역 구성은 이제 Elastic::(Version)::Config
클래스에 있습니다.
거기에서 매핑을 변경할 수 있습니다.
사용자 정의 라우팅
사용자 정의 라우팅은 프로젝트에 연관된 문서 유형에 사용됩니다.
라우팅 형식은 project_<project_id>
입니다. 라우팅은 인덱싱 및 검색 작업 중에 설정됩니다. 사용자 정의 라우팅을 사용함으로써 얻을 수 있는 이점과 단점은 다음과 같습니다:
- 프로젝트 범위 검색이 훨씬 빠릅니다.
- 글로벌 및 그룹 범위 검색에 과도한 샤드가 발생할 경우 라우팅이 사용되지 않습니다.
- 샤드 크기 불균형이 발생할 수 있습니다.
기존 분석기 및 토크나이저
다음 분석기 및 토크나이저는
ee/lib/elastic/latest/config.rb
에서 정의됩니다.
분석기
path_analyzer
블롭의 경로를 인덱싱할 때 사용됩니다. path_tokenizer
와 lowercase
, asciifolding
필터를 사용합니다.
아래에서 path_tokenizer
의 설명을 참조하여 예를 확인하세요.
sha_analyzer
블롭 및 커밋에서 사용됩니다. sha_tokenizer
와 lowercase
, asciifolding
필터를 사용합니다.
아래에서 sha_tokenizer
의 설명을 참조하여 예를 확인하세요.
code_analyzer
블롭의 파일 이름 및 내용을 인덱싱할 때 사용됩니다. whitespace
토크나이저와
word_delimiter_graph
, lowercase
, 및 asciifolding
필터를 사용합니다.
whitespace
토크나이저는 토큰 분할 방법에 대한 더 많은 제어를 위해 선택되었습니다. 예를 들어 문자열 Foo::bar(4)
는 Foo
및 bar(4)
와 같은 토큰을 생성해야 올바르게 검색할 수 있습니다.
토큰이 분할되는 방법에 대한 설명은 code
필터를 참조하세요.
주의:
Elasticsearch code_analyzer
는 모든 코드 사례를 고려하지 않습니다.
토크나이저
sha_tokenizer
이것은
edgeNGram
토크나이저
를 사용하여 SHAs를 하위 집합으로 검색할 수 있도록 하는 사용자 지정 토크나이저입니다(최소 5자).
예시:
240c29dc7e
는 다음과 같이 변환됩니다:
240c2
240c29
240c29d
240c29dc
240c29dc7
240c29dc7e
path_tokenizer
이것은
path_hierarchy
토크나이저
를 사용하여 입력된 경로가 얼마만큼 주어지든 관계없이 경로를 찾을 수 있도록 하기 위해 reverse: true
를 사용하는 사용자 지정 토크나이저입니다.
예시:
'/some/path/application.js'
는 다음과 같이 변환됩니다:
'/some/path/application.js'
'some/path/application.js'
'path/application.js'
'application.js'
주의 사항
- 검색에는 자체 분석기가 있을 수 있습니다. 분석기를 편집할 때 확인하는 것을 잊지 마세요.
-
Character
필터(토큰 필터와는 달리)는 항상 원래 문자를 대체합니다. 이러한 필터는 정확한 검색에 방해가 될 수 있습니다.
Elasticsearch에 새 문서 유형 추가하기
데이터를 Elasticsearch의 기존 인덱스 중 하나에 추가할 수 없는 경우, 새 인덱스를 설정하고 채우기 위한 다음 지침을 따르세요.
권장 사항
-
Elasticsearch가 실행되고 있는지 확인하세요:
curl "http://localhost:9200"
-
Elasticsearch의 로그를 확인하려면 다음 명령을 실행하세요:
tail -f log/elasticsearch.log`
새 문서 유형 추가에 대한 권장 프로세스 를 참조하여 롤아웃 구조를 확인하세요.
인덱스 생성
NOTE
모든 새로운 인덱스는 다음을 가져야 합니다:
-
project_id
및namespace_id
필드(사용 가능한 경우). 라우팅을 위해 필드 중 하나를 사용해야 합니다. - 효율적인 전역 및 그룹 검색을 위한
traversal_ids
필드.object.namespace.elastic_namespace_ancestry
로 필드를 채우세요.
-
ee/lib/search/elastic/types/
에Search::Elastic::Types::
클래스를 생성합니다. - 다음 클래스 메서드를 정의합니다:
-
index_name
: 포맷gitlab-<env>-<type>
(예:gitlab-production-work_items
). -
mappings
: 필드, 데이터 유형 및 분석기를 포함하는 인덱스 스키마의 해시. -
settings
: 복제본과 토크나이저와 같은 인덱스 설정을 포함하는 해시. 기본값은 대부분의 경우에 충분합니다.
-
-
인덱스를 생성하기 위해 새로운 고급 검색 마이그레이션을 추가합니다
scripts/elastic-migration
를 실행하고 지침을 따르세요.
마이그레이션 이름은 형식Create<Name>Index
이어야 합니다. -
사양 파일에 대해
Elastic::MigrationCreateIndex
도우미와'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 .
새로운 Elastic 참조 생성
ee/lib/search/elastic/references/
에 Search::Elastic::References::
클래스를 생성합니다.
참조는 Elasticsearch에서 집계 작업을 수행하는 데 사용됩니다.
이 파일은 Search::Elastic::Reference
에서 상속받고 다음 메서드를 정의해야 합니다:
include Search::Elastic::Concerns::DatabaseReference # 각 문서에 대한 해당 데이터베이스 레코드가 있는 경우
override :serialize
def self.serialize(record)
# 참조의 문자열 표현
end
override :instantiate
def self.instantiate(string)
# 문자열을 역직렬화하고 initialize 호출
end
override :preload_indexing_data
def self.preload_indexing_data(refs)
# `Search::Elastic::Concerns::DatabaseReference`가 포함된 경우 이 메서드를 제거합니다
# 그렇지 않으면 refs를 반환합니다
end
def initialize
# 인스턴스 변수를 사용하여 초기화
end
override :identifier
def identifier
# 참조를 식별하는 방법
end
override :routing
def routing
# 선택 사항: Elasticsearch에서 문서를 라우팅할 식별자
end
override :operation
def operation
# `:index`, `:upsert` 또는 `:delete` 중 하나
end
override :serialize
def serialize
# 참조의 문자열 표현
end
override :as_indexed_json
def as_indexed_json
# 이 참조의 문서 표현을 포함하는 해시
end
override :index_name
def index_name
# 인덱스 이름
end
def model_klass
# `Search::Elastic::Concerns::DatabaseReference`가 포함된 경우 모델 클래스를 설정
end
인덱스에 데이터를 추가하기 위해, 새로운 참조 클래스의 인스턴스가 Elastic::ProcessBookkeepingService.track!()
에서 호출되어
인덱싱을 위한 참조의 큐에 데이터를 추가합니다.
크론 작업자는 큐에 있는 참조를 가져와 Elasticsearch에 항목을 일괄 인덱싱합니다.
인덱싱 작업이 작동하는지 테스트하기 위해, 참조 클래스의 인스턴스를 사용하여
Elastic::ProcessBookkeepingService.track!()
를 호출하고 Elastic::ProcessBookkeepingService.new.execute
를 실행합니다.
로그에 업데이트가 표시됩니다. 인덱스에서 문서를 확인하려면 이 명령을 실행하세요:
curl "http://localhost:9200/gitlab-development-<type>/_search"
데이터 일관성
이제 인덱스를 가지고 있고 새로운 문서 유형을 Elasticsearch에 대량으로 인덱싱하는 방법이 마련되었으므로 인덱스에 데이터를 추가해야 합니다. 이는 데이터를 다시 채우고 인덱스 데이터가 최신 상태로 유지되도록 지속적인 업데이트를 수행하는 것으로 구성됩니다.
데이터 다시 채우기는 인덱싱되어야 하는 각 문서에 대해 Search::Elastic::Reference
인스턴스를 사용하여 Elastic::ProcessInitialBookkeepingService.track!()
를 호출하여 수행합니다.
지속적인 업데이트는 인덱싱, 생성 또는 삭제가 필요한 모든 문서에 대해 Search::Elastic::Reference
인스턴스를 사용하여 Elastic::ProcessBookkeepingService.track!()
를 호출하여 수행합니다.
데이터 다시 채우기
새로운 고급 검색 마이그레이션을 추가하여 scripts/elastic-migration
를 실행하고 지침에 따라 데이터를 다시 채웁니다.
다시 채우기는 인덱싱되어야 하는 각 문서에 대해 이전에 생성된 Search::Elastic::Reference
인스턴스를 사용하여 Elastic::ProcessInitialBookkeepingService.track!()
를 실행해야 합니다. BackfillEpics
마이그레이션을 예제로 사용할 수 있습니다.
다시 채우기를 테스트하려면 콘솔에서 Elastic::MigrationWorker.new.perform
을 여러 번 실행하여 인덱스가 채워졌는지 확인하세요.
마이그레이션 진행 상황을 보려면 로그를 확인하세요:
tail -f log/elasticsearch.log
지속적인 업데이트
ActiveRecord
객체의 경우, 콜백을 기반으로 데이터를 인덱싱하기 위해 모델에 ApplicationVersionedSearch
컨cern을 포함할 수 있습니다. 그렇지 않은 경우 문서가 인덱싱되어야 할 때마다 Search::Elastic::Reference
의 인스턴스를 사용하여 Elastic::ProcessBookkeepingService.track!()
를 호출하세요.
Gitlab::CurrentSettings.elasticsearch_indexing?
및 use_elasticsearch?
를 항상 확인하세요. 일부 셀프 관리 인스턴스에서는 Elasticsearch가 활성화되어 있지 않으며 네임스페이스 제한을 활성화할 수 있습니다.
또한 인덱스 요청을 처리할 수 있는지 확인하세요. 예를 들어, 현재 주요 릴리스에서 추가된 경우 인덱스가 존재하는지 확인하고 인덱스를 추가하는 마이그레이션이 완료되었는지 확인하세요: Elastic::DataMigrationService.migration_has_finished?
.
이전 및 삭제
프로젝트 및 그룹 이전 및 삭제는 고아 데이터가 발생하지 않도록 인덱스를 업데이트해야 합니다.
project_id
필드를 포함하는 인덱스는 Search::Elastic::DeleteWorker
를 사용해야 합니다. namespace_id
필드가 있지만 project_id
필드가 없는 인덱스는 Search::ElasticGroupAssociationDeleteWorker
를 사용해야 합니다.
-
ElasticDeleteProjectWorker
에서excluded_classes
에 인덱스 클래스 추가 - 인덱스에서 문서를 제거하도록 워커 업데이트
새 문서 유형 추가를 위한 권장 프로세스
다음 MRs를 생성하고 Global Search 팀의 구성원에게 검토를 요청하세요:
- 인덱스 생성.
- 새 Elasticsearch 참조 생성.
- 기능 플래그 뒤에서 지속적인 업데이트 수행. 다시 채우기 전에 플래그를 완전히 활성화하세요.
- 데이터 다시 채우기.
인덱싱이 완료된 후, 인덱스가 검색을 위해 준비됩니다.
검색 서비스에 새로운 범위 추가하기
검색 데이터는 SearchController
와
Search API에서 사용할 수 있으며, 두 가지 모두 SearchService
를 사용하여 결과를 반환합니다.
SearchService
는 SearchController
및 Search API
외부에서도 결과를 반환하는 데 사용할 수 있습니다.
검색 범위
SearchService
는 전역,
그룹, 및 프로젝트 수준에서의 검색을 제공합니다.
새로운 범위는 다음 상수에 추가해야 합니다:
- 각 EE
SearchService
파일의ALLOWED_SCOPES
(또는allowed_scopes
메서드 오버라이드) -
Gitlab::Search::AbuseDetection
의ALLOWED_SCOPES
-
Search::Navigation
의search_tab_ability_map
메서드. 필요시 EE 버전에서 오버라이드
참고:
전역 검색은 범위에 대해 비활성화할 수 있습니다. 기본값이 true
인 global_search_SCOPE_tab
라는 ops 기능 플래그를 생성하고, 이를 SearchService
에서 global_search_enabled_for_scope?
메서드에 추가하세요.
결과 클래스
사용 가능한 검색 결과 클래스는 다음과 같습니다:
검색 유형 | 검색 수준 | 클래스 |
---|---|---|
기본 검색 | 전역 | Gitlab::SearchResults |
기본 검색 | 그룹 | Gitlab::GroupSearchResults |
기본 검색 | 프로젝트 | Gitlab::ProjectSearchResults |
고급 검색 | 전역 | Gitlab::Elastic::SearchResults |
고급 검색 | 그룹 | Gitlab::Elastic::GroupSearchResults |
고급 검색 | 프로젝트 | Gitlab::Elastic::ProjectSearchResults |
정확한 코드 검색 | 전역 | Search::Zoekt::SearchResults |
정확한 코드 검색 | 그룹 | Search::Zoekt::SearchResults |
정확한 코드 검색 | 프로젝트 | Search::Zoekt::SearchResults |
모든 검색 유형 | 모든 수준 | Search::EmptySearchResults |
결과 클래스는 다음 데이터를 반환합니다:
-
objects
- Elasticsearch에서 데이터베이스 레코드 또는 PORO로 변환된 페이지네이션된 결과 -
formatted_count
- Elasticsearch에서 반환된 문서 수 -
highlight_map
- Elasticsearch에서 강조된 필드의 맵 -
failed?
- 실패가 발생했는지 여부 -
error
- Elasticsearch에서 반환된 오류 메시지 -
aggregations
- (선택적) Elasticsearch에서의 집계
새로운 범위는 Gitlab::Elastic::SearchResults
클래스 내에서 다음 메서드에 대한 지원을 추가해야 합니다:
objects
formatted_count
highlight_map
failed?
error
쿼리 작성
쿼리 빌더 프레임워크는 Elasticsearch 쿼리를 작성하는 데 사용됩니다.
쿼리는 다음을 사용하여 작성됩니다:
-
Search::Elastic::Queries
의 쿼리 -
::Search::Elastic::Filters
의 하나 이상의 필터 - (선택적)
::Search::Elastic::Aggregations
의 집계 -
::Search::Elastic::Formats
의 하나 이상의 형식
새로운 범위는 Search::Elastic::QueryBuilder
를 상속하는 새로운 쿼리 빌더 클래스를 생성해야 합니다.
필터
아래의 필터는 Elasticsearch 쿼리를 구축하는 데 사용할 수 있습니다. 필터를 사용하려면 인덱스에 매핑된 필드가 필요합니다. 필터는 options
해시를 사용하여 JSON을 작성하고, 이는 query_hash
에 추가됩니다.
by_type
type
필드가 필요합니다. 옵션에서 doc_type
으로 쿼리합니다.
{
"term": {
"type": {
"_name": "filters:doc:is_a:milestone",
"value": "milestone"
}
}
}
by_group_level_confidentiality
current_user
와 group_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:non_confidential",
"value": false
}
}
},
{
"bool": {
"must": [
{
"term": {
"confidential": {
"_name": "filters:confidential",
"value": true
}
}
},
{
"bool": {
"should": [
{
"term": {
"author_id": {
"_name": "filters:confidential:as_author",
"value": 1
}
}
},
{
"term": {
"assignee_id": {
"_name": "filters:confidential:as_assignee",
"value": 1
}
}
},
{
"terms": {
"_name": "filters:confidential:project:membership:id",
"project_id": [
12345
]
}
}
]
}
}
]
}
}
]
}
}
by_label_ids
label_ids
필드가 필요합니다. 옵션에서 label_names
으로 쿼리합니다.
{
"bool": {
"must": [
{
"terms": {
"_name": "filters:label_ids",
"label_ids": [
1
]
}
}
]
}
}
by_archived
archived
필드가 필요합니다. 옵션에서 search_level
과 include_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_group_level_authorization
current_user
, group_ids
, traversal_id
, search_level
필드가 필요합니다. search_level
로 쿼리하고 사용자가 각 그룹에 대해 가진 권한에 따라 namespace_visibility_level
을 필터링합니다.
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
}
}
그룹
[
{
"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
필드가 필요합니다. *_access_level
필드를 지원합니다. search_level
로 쿼리하고 선택적으로
project_ids
, group_ids
, features
, 및 current_user
를 옵션에 넣습니다.
필터링은 다음에 적용됩니다:
- 글로벌, 그룹 또는 프로젝트에 대한 검색 수준
- 그룹 및 프로젝트에 대한 직접적인 멤버십 또는 그룹에 대한 직접 액세스를 통한 공유 멤버십
-
features
를 통해 전달된 모든 기능 액세스 수준
참고: 예시는 로그인한 사용자에 대해 표시됩니다. JSON은 권한이 있는 사용자, 관리자, 외부 사용자 또는 익명 사용자에게는 다를 수 있습니다.
글로벌
{
"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
}
}
]
Elasticsearch에 쿼리 전송하기
쿼리는 Gitlab::Elastic::SearchResults
에서 ::Gitlab::Search::Client
로 전송됩니다.
결과는 Elasticsearch로부터 응답을 변환하기 위해 Search::Elastic::ResponseMapper
를 통해 파싱됩니다.
모델 요구 사항
모델은 to_ability_name
메서드에 응답해야 하며, 이를 통해 레닥션 로직이 Ability.allowed?(current_user, :"read_#{object.to_ability_name}", object)?
를 확인할 수 있습니다.
해당 메서드는 존재하지 않을 경우 추가되어야 합니다.
모델은 N+1을 방지하기 위해 preload_search_data
스코프를 정의해야 합니다.
권한 테스트
Search 코드에는 SearchService#redact_unauthorized_results
에 최종 보안 검사가 있습니다.
이 검사는 권한이 없는 결과가 이를 볼 수 있는 권한이 없는 사용자에게 반환되지 않도록 방지합니다.
검사는 Elasticsearch 권한 데이터의 불일치로 인한 버그나 인덱싱 지연을 처리하기 위해 Ruby에서 수행됩니다.
새로운 스코프는 적절한 접근 제어를 보장하기 위해 가시성 스펙을 추가해야 합니다.
권한이 적절하게 시행되고 있는지 테스트하려면 EE 스펙 내에서 'search respects visibility'
공유 예제를 사용하여 테스트를 추가하세요:
ee/spec/services/search/global_service_spec.rb
ee/spec/services/search/group_service_spec.rb
ee/spec/services/search/project_service_spec.rb
새로운 스코프 테스트
Rails 콘솔에서 새로운 스코프를 테스트하세요.
search_service = ::SearchService.new(User.first, { search: 'foo', scope: 'SCOPE_NAME' })
search_service.search_objects
새로운 문서 유형에 대한 검색 구현 권장 프로세스
다음 MRs를 생성하고 Global Search 팀의 구성원에게 검토를 받으세요:
- 새로운 스코프 활성화.
- 쿼리 빌더 생성.
- 모든 모델 요구 사항 구현.
- 기능 플래그 뒤에 있는
Gitlab::Elastic::SearchResults
에 새 스코프 추가. - 권한 테스트를 포함해야 하는 스펙 추가.
- 새 스코프 테스트.
- 고급 검색 및 검색 API(해당되는 경우)에 대한 문서 업데이트.
여러 인덱스를 이용한 제로 다운타임 재색인
현재 GitLab은 단일 버전의 설정만 처리할 수 있습니다.
모든 설정/스키마 변경은 처음부터 모든 것을 재색인해야 합니다. 재색인 작업은 오랜 시간이 걸릴 수 있어 검색 기능 중단을 초래할 수 있습니다.
다운타임을 피하기 위해 GitLab은 동시에 작동할 수 있는 여러 인덱스를 지원하기 위해 작업하고 있습니다.
스키마가 변경될 때, 관리자는 새로운 인덱스를 생성하고 그것으로 재색인할 수 있으며, 검색은 계속해서 구형 안정 인덱스로 향합니다.
모든 데이터 업데이트는 두 인덱스로 전달됩니다.
새 인덱스가 준비되면 관리자는 이를 활성화로 표시할 수 있으며, 이는 모든 검색을 해당 인덱스로 방향 전환하고 구형 인덱스를 제거합니다.
이는 새로운 서버로 마이그레이션하는 데도 유용합니다. 예를 들어, AWS로의 이동/이동 시 도움이 됩니다.
현재 우리는 이 새로운 디자인으로 마이그레이션하는 과정에 있습니다. 현재 모든 것은 단일 버전으로 작동하도록 하드와이어되어 있습니다.
성능 모니터링
Prometheus
GitLab은 Prometheus 메트릭을 내보내어 모든 웹/API 요청 및 Sidekiq 작업에 대한 요청 수와 시간과 관련된 정보를 제공하며, 이는 성능 추세를 진단하고 Elasticsearch 시간이 전체 성능에 미치는 영향을 비교하는 데 도움이 됩니다.
인덱싱 큐
GitLab은 인덱싱 큐에 대한 Prometheus 메트릭도 내보내어 성능 병목 현상을 진단하고 GitLab 인스턴스 또는 Elasticsearch 서버가 업데이트 양을 따라잡을 수 있는지를 판단하는 데 도움을 줍니다.
로그
모든 인덱싱은 Sidekiq에서 발생하므로 Elasticsearch 통합과 관련된 많은 로그를 sidekiq.log
에서 찾을 수 있습니다. 특히, Elasticsearch에 요청을 하는 모든 Sidekiq 작업자는 요청 수와 Elasticsearch에 쿼리/쓰기하는 데 소요된 시간을 기록합니다. 이는 클러스터가 인덱싱을 따라잡고 있는지를 이해하는 데 유용할 수 있습니다.
Elasticsearch를 검색하는 것은 요청을 처리하는 일반 웹 작업자를 통해 이루어집니다. 페이지를 로드하거나 API 요청을 만들어 Elasticsearch에 요청을 하는 경우, 요청 수와 production_json.log
에 기록된 소요 시간을 기록합니다. 이 로그는 데이터베이스 및 Gitaly 요청에 소요된 시간을 포함하여, 검색의 어느 부분이 성능이 저조한지를 진단하는 데 도움을 줄 수 있습니다.
Elasticsearch에 특정한 추가 로그는 elasticsearch.log
로 전송되며, 이는 성능 문제를 진단하는 데 도움이 될 수 있는 정보를 포함할 수 있습니다.
성능 바
Elasticsearch 요청은 Performance Bar
에 표시되며, 이는 개발 중 로컬 및 배포된 GitLab 인스턴스 모두에서 저조한 검색 성능을 진단하는 데 사용될 수 있습니다. 이는 실행된 정확한 쿼리를 보여주어, 검색 속도가 느린 이유를 진단하는 데 유용합니다.
상관 ID 및 X-Opaque-Id
우리의 상관 ID는 Rails에서 Elasticsearch로 모든 요청과 함께 전달되며, X-Opaque-Id
헤더로, 이는 클러스터 내에서 요청을 GitLab로 추적하는 데 도움이 됩니다.
문제 해결
Elasticsearch 쿼리 디버깅
ELASTIC_CLIENT_DEBUG
환경 변수는 개발 또는 테스트 환경에서 Elasticsearch 클라이언트의 디버그 옵션을 활성화합니다. 코드 또는 테스트에서 생성된 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
파일에 추가하세요:
# 디스크 할당기 끄기
cluster.routing.allocation.disk.threshold_enabled: false
혹은
# 자체 한도 설정
cluster.routing.allocation.disk.threshold_enabled: true
cluster.routing.allocation.disk.watermark.flood_stage: 5gb # ES 6.x 전용
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.6 및 6.x_ |