고급 검색 개발 가이드

이 페이지에는 Elasticsearch를 사용하여 개발하고 작업하는 정보가 포함되어 있습니다.

Elasticsearch를 활성화하고 초기 색인을 수행하는 방법에 대한 정보는 Elasticsearch 통합 문서에 있습니다.

심층 탐구

2019년 6월, Mario de la Ossa는 GitLab 팀 멤버 전용으로 심층 탐구를 주최했습니다: https://gitlab.com/gitlab-org/create-stage/-/issues/1에서 GitLab Elasticsearch 통합에 대한 도메인 특정 지식을 나누기 위해 향후 이 코드베이스에서 작업할 수 있는 모든 사람과 함께 했습니다. 이 심층 탐구에 대한 YouTube 녹화Google 슬라이드PDF에서 찾을 수 있습니다. 이 심층 탐구에서 다룬 내용은 GitLab 12.0 기준으로 정확했으며, 특정 세부 정보가 변경되었을 수 있지만 여전히 좋은 소개 자료로 사용될 것입니다.

2020년 8월, 두 번째 심층 탐구가 진행되었는데, GitLab 특정 아키텍처에 대한 다중 인덱스 지원에 중점을 두었습니다. YouTube 녹화슬라이드가 제공됩니다. 이 심층 탐구에서 다룬 내용은 GitLab 13.3 기준으로 정확했습니다.

2024년 7월, Terri Chu가 고급 검색 기본 사항, 통합, 색인 및 검색에 대한 Lunch and Learn을 주최했습니다. Google 슬라이드 (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에서 상속된 create, update 및 destroy를 통해 실행됩니다.

초기 색인 작업이 완료되면 프로젝트 이외의 모든 모델에 대한 생성, 업데이트 및 삭제 작업은 Redis ZSET에서 추적됩니다. 정기적인 sidekiq-cron ElasticIndexBulkCronWorker는 이 대기열을 처리하여 여러 Elasticsearch 문서를 한 번에 업데이트합니다.

검색 쿼리는 ee/app/models/concerns/elastic에서 찾을 수 있는 관심사에 의해 생성됩니다. 이 관심사들은 액세스 제어를 담당하며, 보안 버그의 역사적인 원인이기도 하므로 주의를 기울이시기 바랍니다!

아키텍처

참고: 우리는 이 에픽에서 이러한 아키텍처 패턴을 이동하고 있습니다.

elasticsearch-rails에서 제공하는 전통적인 설정은 내부 프록시 클래스를 통해 통신하는 것입니다. 개발자는 모델별 논리를 모델에 포함시킬 모듈에 작성합니다 (예: SnippetsSearch). __elasticsearch__ 메서드는 프락시 객체를 반환합니다. 예를 들어:

  • Issue.__elasticsearch__Elasticsearch::Model::Proxy::ClassMethodsProxy의 인스턴스를 반환합니다.
  • Issue.first.__elasticsearch__Elasticsearch::Model::Proxy::InstanceMethodsProxy의 인스턴스를 반환합니다.

이러한 프록시 객체는 Elasticsearch 서버와 직접 통신합니다 (도표의 상단 부분 참조).

Elasticsearch 아키텍처

계획된 새 디자인에서 각 모델은 해당 모델에 대한 논리가 위치한 하위 클래스화된 프록시 객체 쌍을 갖게 됩니다. 예를 들어 SnippetElasticsearch::Model::Proxy::ClassMethodsProxy의 하위 클래스인 SnippetClassProxy가 되고, SnippetElasticsearch::Model::Proxy::InstanceMethodsProxy의 하위 클래스인 SnippetInstanceProxy가 됩니다.

__elasticsearch__은 여러 실제 프록시 객체를 추적하는 또 다른 프록시 객체를 나타냅니다. 적절한 인덱스로 메서드 호출을 전달합니다. 예를 들어:

  • model.__elasticsearch__.search는 읽기 작업이기 때문에 안정적인 하나의 인덱스로 전달됩니다.
  • model.__elasticsearch__.update_document는 모든 인덱스로 전달됩니다. 모든 인덱스를 최신 상태로 유지하기 위해.

버전 당 전역 구성은 이제 Elastic::(Version)::Config 클래스에 있습니다. 거기서 매핑을 변경할 수 있습니다.

사용자 정의 라우팅

사용자 정의 라우팅은 프로젝트와 관련된 문서 유형에 대해 Elasticsearch에서 사용됩니다. 라우팅 형식은 project_<project_id>입니다. 라우팅은 색인 및 검색 작업 중에 설정됩니다. 사용자 정의 라우팅을 사용하는 것의 일부 이점과 tradeoff는 다음과 같습니다:

  • 프로젝트 범위의 검색이 훨씬 빠릅니다.
  • 글로벌 및 그룹 범위의 검색에 너무 많은 샤드가 필요한 경우 라우팅이 사용되지 않습니다.
  • 샤드 크기 불균형이 발생할 수 있습니다.

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

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

분석기

path_analyzer

Blob의 경로를 색인할 때 사용됩니다. path_tokenizerlowercaseasciifolding 필터를 사용합니다.

예를 들어 path_tokenizer 설명은 아래를 참조하세요.

sha_analyzer

Blob 및 커밋에서 사용됩니다. sha_tokenizerlowercaseasciifolding 필터를 사용합니다.

나중에 나오는 sha_tokenizer 설명을 참조하세요.

code_analyzer

Blob의 파일 이름 및 내용을 색인할 때 사용됩니다. whitespace 토크나이저 및 word_delimiter_graph, lowercaseasciifolding 필터를 사용합니다.

whitespace 토크나이저는 토큰이 어떻게 분할되는지에 대한 더 많은 제어를 위해 선택되었습니다. 예를 들어 문자열 Foo::bar(4)는 올바르게 검색되기 위해 Foobar(4)와 같은 토큰을 생성해야 합니다.

토큰이 어떻게 분리되는지에 대한 설명은 code 필터를 참조하세요.

참고: Elasticsearch code_analyzer는 모든 코드 케이스를 고려하지 않습니다.

토크나이저

sha_tokenizer

이것은 의미있는 하위 문자열 검색을 가능하게 하는 [edgeNGram 토크나이저]를 사용하는 사용자 정의 토크나이저입니다 (최소 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"
    
  • Kibana 실행하여 로컬 Elasticsearch 클러스터와 상호 작용하십시오. 또는 Cerebro 또는 유사한 도구를 사용할 수 있습니다.
  • Elasticsearch 로그를 추적하려면 다음 명령을 실행하십시오:

    tail -f log/elasticsearch.log`
    

새로운 색인은 새로운 인덱스를 생성할 때 다음과 같은 필수 사항이 있어야 합니다:

  • project_idnamespace_id 필드(사용 가능한 경우)가 있어야 합니다. 이 필드 중 하나를 라우팅에 사용해야 합니다.
  • 효율적인 글로벌 및 그룹 검색을 위한 traversal_ids 필드. 필드에는 object.namespace.elastic_namespace_ancestry를 채워야 합니다.
  1. ee/lib/search/elastic/types/Search::Elastic::Types:: 클래스를 생성하십시오.
  2. 다음과 같은 클래스 메서드를 정의하십시오:
    • index_name: gitlab-<env>-<type> 형식으로 (예: gitlab-production-work_items)입니다.
    • mappings: 필드, 데이터 유형 및 분석기와 같은 인덱스 스키마를 포함하는 해시입니다.
    • settings: 복제본 및 토크나이저와 같은 인덱스 설정을 포함하는 해시입니다. 대부분의 경우 기본 설정이 충분합니다.
  3. 색인을 만드는 고급 검색 마이그레이션을 추가하려면 scripts/elastic-migration을 실행하고 지침을 따르십시오. 마이그레이션 이름은 Create<Name>Index 형식이어야 합니다.
  4. Elastic::MigrationCreateIndex 도우미 및 'migration creates a new index' 공유 예제를 사용하십시오.
  5. 대상 클래스를 Gitlab::Elastic::Helper::ES_SEPARATE_CLASSES에 추가하십시오.
  6. 색인 생성을 테스트하려면 콘솔에서 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)
   # 문자열을 역직렬화하고 초기화 호출
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에 대량 색인하기 위한 방법이 있으므로 인덱스에 데이터를 추가해야 합니다. 이는 백필과 인덱스 데이터가 최신 상태인지 계속해서 업데이트하는 것으로 구성됩니다.

백필은 Elastic::ProcessInitialBookkeepingService.track!()를 호출하여 색인되어야 하는 각 문서의 Search::Elastic::Reference 인스턴스로 수행됩니다.

지속적인 업데이트는 Elastic::ProcessBookkeepingService.track!()를 호출하여 작성/업데이트/삭제해야 하는 각 문서의 Search::Elastic::Reference 인스턴스로 수행됩니다.

데이터 백필

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

백필은 이전에 만든 Search::Elastic::Reference의 인스턴스와 함께 Elastic::ProcessInitialBookkeepingService.track!()를 실행해야 합니다. BackfillEpics 마이그레이션은 예제로 사용할 수 있습니다.

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

마이그레이션의 진행 상황을 확인하려면 다음 명령을 실행하여 로그를 추적하십시오:

tail -f log/elasticsearch.log

지속적인 업데이트

ActiveRecord 객체의 경우, 해당 모델에 ApplicationVersionedSearch concern을 포함하여 콜백에 따라 데이터를 색인화할 수 있습니다. 그게 적합하지 않은 경우, 색인화해야 하는 각 문서의 Search::Elastic::Reference 인스턴스로 Elastic::ProcessBookkeepingService.track!()를 호출하십시오.

일부 셀프 매니지드 인스턴스에서는 Elasticsearch를 사용하지 않을 수 있으므로 Gitlab::CurrentSettings.elasticsearch_indexing?use_elasticsearch?를 항상 확인하십시오. 또한 네임스페이스 제한이 가능합니다.

또한, 인덱스가 인덱싱 요청을 처리할 수 있는지 확인하십시오. 예를 들어, 현재 주요 릴리스에서 인덱스가 추가되었는지 확인하여 인덱스가 존재하는지 확인하십시오: Elastic::DataMigrationService.migration_has_finished?.

이전 및 삭제

프로젝트 및 그룹 전송 및 삭제는 고아 데이터를 방지하기 위해 인덱스를 업데이트해야 합니다.

project_id 필드를 포함하는 색인은 Search::Elastic::DeleteWorker를 사용해야 합니다. namespace_id 필드를 포함하지만 project_id 필드를 포함하지 않는 색인은 Search::ElasticGroupAssociationDeleteWorker를 사용해야 합니다.

  1. ElasticDeleteProjectWorkerexcluded_classes에 색인화된 클래스를 추가하십시오.
  2. 워커를 업데이트하여 인덱스에서 문서를 제거하십시오.

새로운 문서 유형 추가를 위한 권장 프로세스

다음 MR을 생성하고 Global Search 팀의 구성원이 검토하도록 하십시오:

  1. 인덱스 생성.
  2. 새로운 Elasticsearch 참조 생성.
  3. 기능 플래그 뒤에서 지속적인 업데이트 실행. 백필 이전에 플래그를 완전히 활성화하십시오.
  4. 데이터 백필 수행.

색인화가 완료되면, 인덱스가 검색 준비가 됩니다.

검색 서비스에 새로운 범위 추가

검색 데이터는 SearchController검색 API에 사용되며, 둘 다 SearchService를 사용하여 결과를 반환합니다. SearchService를 사용하여 SearchController검색 API 외부에서 결과를 반환할 수 있습니다.

검색 범위

SearchService글로벌, 그룹, 그리고 프로젝트 수준에서 검색 기능을 노출합니다.

새로운 범위는 다음 상수에 추가되어야 합니다:

  • 각 EE SearchService 파일의 ALLOWED_SCOPES (또는 allowed_scopes 메서드 재정의)
  • Gitlab::Search::AbuseDetectionALLOWED_SCOPES
  • (필요에 따라 EE 버전에서 재정의) Search::Navigationsearch_tab_ability_map 메서드

주의: 글로벌 검색은 특정 범위에 대해 비활성화될 수 있습니다. global_search_SCOPE_tab이라는 ops feature flag를 만들어 기본값을 true로 설정하고, SearchServiceglobal_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

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

  1. objects - Elasticsearch에서 변환된 페이지네이션된 데이터베이스 레코드 또는 PORO
  2. formatted_count - Elasticsearch에서 반환된 문서 수
  3. highlight_map - Elasticsearch에서 하이라이트 된 필드의 맵
  4. failed? - 실패가 발생했는지 여부
  5. error - Elasticsearch에서 반환된 오류 메시지
  6. 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 해시를 사용하여 query_hash에 추가되는 JSON을 구축합니다

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: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_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_idsnot_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_usernamenot_author_username으로 쿼리 수행.

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

target_branch 필드가 필요합니다. 옵션에서 target_branchnot_target_branch로 쿼리 수행.

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

source_branch 필드가 필요합니다. 옵션에서 source_branchnot_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을 필터링합니다.

참고: 로그인한 사용자의 예시입니다. 권한이 있는 사용자, 관리자, 외부 사용자, 익명 사용자에 따라 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_idtraversal_id 필드가 필요합니다. search_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
  }
}

Elasticsearch로 쿼리 보내기

쿼리는 ::Gitlab::Search::Client에서 Gitlab::Elastic::SearchResults로 전송됩니다. 결과는 Search::Elastic::ResponseMapper를 통해 Elasticsearch의 응답을 번역합니다.

모델 요구사항

모델은 to_ability_name 메소드에 응답하여 빨간색 로직이 Ability.allowed?(current_user, :"read_#{object.to_ability_name}", object)?를 확인할 수 있어야 합니다. 이 메소드는 존재하지 않는 경우 추가되어야 합니다.

모델은 N+1을 피하기 위해 preload_search_data 스코프를 정의해야 합니다.

권한 테스트

검색 코드에는 SearchService#redact_unauthorized_results에서 마지막 보안 확인이 있습니다. 이는 미승인된 결과가 사용자에게 반환되는 것을 방지합니다. 권한이 없는 사용자가 보기 권한이 없는 결과를 제대로 처리하기 위해 루비에서 확인이 이루어집니다. 이는 버그나 색인 지연으로 인한 Elasticsearch 권한 데이터의 불일치를 처리하기 위해 이루어집니다.

새로운 스코프는 적절한 접근 제어를 보장하기 위해 가시성 스펙을 추가해야 합니다. 권한이 제대로 적용되는지 테스트하기 위해 EE 스펙의 ['search respects visibility' shared example]https://gitlab.com/gitlab-org/gitlab/-/blob/a489ad0fe4b4d1e392272736b020cf9bd43646da/ee/spec/support/shared_examples/services/search_service_shared_examples.rb)를 추가하세요:

  • ee/spec/services/search/global_service_spec.rb
  • ee/spec/services/search/group_service_spec.rb
  • ee/spec/services/search/project_service_spec.rb ```

새로운 범위를 테스트하세요

루비 콘솔에서 새로운 범위를 테스트하세요.

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

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

다음 MR(Merge Request)을 생성하고 Global Search 팀의 구성원에게 검토를 받도록하세요:

  1. 새로운 범위 활성화합니다.
  2. 쿼리 빌더 생성합니다.
  3. 모든 모델 요구 사항 구현합니다.
  4. 기능 플래그 뒤에 새로운 범위를 Gitlab::Elastic::SearchResults에 추가합니다.
  5. 권한 테스트를 포함해야 하는 스펙 추가합니다.
  6. 새로운 범위를 테스트합니다.
  7. 고급 검색검색 API에 대한 문서를 업데이트합니다(해당하는 경우).

다중 인덱스로의 제로 다운타임 다시 색인

참고: 현재 다중 인덱스 기능이 완전히 구현되지 않았으므로 적용되지 않습니다.

현재 GitLab은 단일 버전의 설정만 처리할 수 있습니다. 어떤 설정/스키마 변경이든 모든 것을 처음부터 다시 색인해야 합니다. 다시 색인에는 시간이 오래 걸릴 수 있으므로 이로 인해 검색 기능이 다운될 수 있습니다.

이로 인한 다운타임을 피하려고 GitLab은 동시에 작동할 수 있는 여러 인덱스를 지원하도록 작업 중입니다. 스키마가 변경될 때마다 관리자는 새로운 인덱스를 생성하고 그로부터 다시 색인할 수 있으며, 검색은 여전히 이전 안정적인 인덱스로 계속됩니다. 모든 데이터 업데이트는 두 인덱스로 전달됩니다. 새 인덱스가 준비되면 관리자는 활성화할 수 있으며, 모든 검색은 새 인덱스로 이동되고 이전 인덱스는 제거됩니다.

이것은 예를 들어 새로운 서버로 마이그레이션하는 데도 유용합니다.

현재 우리는 이 새로운 디자인으로의 마이그레이션 과정 중입니다. 현재 모든 것은 단일 버전과 함께 작동하도록 되어 있습니다.

성능 모니터링

Prometheus

GitLab은 모든 웹/API 요청 및 Sidekiq 작업에 관한 Prometheus 지표를 내보냅니다. 이는 성능 추세를 진단하고 Elasticsearch 시간이 다른 작업을 하는 데 소요된 시간과 비교할 수 있도록 합니다.

인덱싱 대기열

GitLab은 또한 인덱싱 대기열에 대한 Prometheus 지표를 내보냅니다. 이를 통해 성능 병목 현상을 진단하고 GitLab 인스턴스나 Elasticsearch 서버가 업데이트의 양을 처리할 수 있는지 여부를 결정할 수 있습니다.

로그

모든 인덱싱은 Sidekiq에서 수행되므로 Elasticsearch 통합과 관련된 관련 로그는 대부분 sidekiq.log에서 찾을 수 있습니다. 특히 Elasticsearch에 요청을 하는 모든 Sidekiq 워커는 Elasticsearch에 대한 쿼리/쓰기에 소요된 요청 수와 시간을 로그로 남깁니다. 이는 클러스터가 인덱싱을 따라가고 있는지 이해하는 데 유용합니다.

Elasticsearch 검색은 페이지 로드나 API 요청을 통해 수행됩니다. 이는 Elasticsearch로 요청을 하는 모든 요청을 production_json.log에 남깁니다. 이 로그에는 검색이 느린 이유를 진단하는 데 도움이 되는 요청 수와 소요된 시간이 포함됩니다. 또한 데이터베이스 및 Gitaly 요청에 사용된 시간도 포함되어 있어 검색의 어느 부분이 성능이 나빠지고 있는지 진단하는 데 도움이 될 수 있습니다.

성능 문제를 진단하는 데 도움이 되는 Elasticsearch에 특화된 추가적인 로그가 elasticsearch.log에 기록됩니다.

성능 바

Elasticsearch 요청은 성능 바에 표시됩니다. 이는 개발 환경에서와 배포된 GitLab 인스턴스에서 모두 사용하여 검색 성능이 떨어지는지 진단하는 데 사용할 수 있습니다. 이는 사용자가 검색이 느린 이유를 진단하는 데 유용한 정확한 쿼리를 보여줍니다.

상관 ID 및 X-Opaque-Id

우리의 상관 ID는 모든 요청에서 Elasticsearch로 Rails에서 전달되어 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가 자동으로 해제됩니다.

“디스크 기반 샤드 할당 Elasticsearch 참조” 5.66.x에서 확인할 수 있습니다.