커넥터 배포

Cloud Search 튜토리얼의 이 페이지에서는 데이터 색인 생성을 위해 데이터 소스 및 콘텐츠 커넥터를 설정하는 방법을 보여줍니다. 이 튜토리얼의 시작 부분에서 시작하려면 Cloud Search 시작하기 튜토리얼을 참조하세요.

커넥터 빌드

작업 디렉터리를 cloud-search-samples/end-to-end/connector 디렉터리로 변경하고 다음 명령어를 실행합니다.

mvn package -DskipTests

이 명령어는 콘텐츠 커넥터를 빌드하는 데 필요한 종속 항목을 다운로드하고 코드를 컴파일합니다.

서비스 계정 자격 증명 만들기

커넥터가 Cloud Search API를 호출하려면 서비스 계정 사용자 인증 정보가 필요합니다. 사용자 인증 정보를 만들려면 다음 안내를 따르세요.

  1. Google Cloud 콘솔로 돌아갑니다.
  2. 왼쪽 탐색 메뉴에서 사용자 인증 정보를 클릭합니다. '사용자 인증 정보' 페이지가 표시됩니다.
  3. + 사용자 인증 정보 만들기 드롭다운 목록을 클릭하고 서비스 계정을 선택합니다. '서비스 계정 만들기' 페이지가 표시됩니다.
  4. 서비스 계정 이름 필드에 'tutorial'을 입력합니다.
  5. 서비스 계정 ID 값을 기록해 둡니다 (서비스 계정 이름 바로 뒤). 이 값은 나중에 사용됩니다.
  6. 만들기를 클릭합니다. '서비스 계정 권한 (선택사항)' 대화상자가 나타납니다.
  7. 계속을 클릭합니다. '사용자에게 이 서비스 계정에 대한 액세스 권한 부여(선택사항)' 대화상자가 나타납니다.
  8. 완료를 클릭합니다. '사용자 인증 정보' 화면이 표시됩니다.
  9. 서비스 계정에서 서비스 계정 이메일을 클릭합니다. '서비스 계정 세부정보' 페이지가 표시됩니다.
  10. 키에서 키 추가 드롭다운 목록을 클릭하고 새 키 만들기를 선택합니다. '비공개 키 만들기' 대화상자가 표시됩니다.
  11. 만들기를 클릭합니다.
  12. (선택사항) 'console.cloud.google.com에서 다운로드를 허용하시겠습니까?' 대화상자가 표시되면 허용을 클릭합니다.
  13. 비공개 키 파일이 컴퓨터에 저장됩니다. 다운로드한 파일의 위치를 기록해 둡니다. 이 파일은 Google Cloud Search API를 호출할 때 자체를 인증할 수 있도록 콘텐츠 커넥터를 구성하는 데 사용됩니다.

서드 파티 지원 초기화

다른 Cloud Search API를 호출하려면 먼저 Google Cloud Search에 대한 타사 지원을 초기화해야 합니다.

Cloud Search에 대한 서드 파티 지원을 초기화하려면 다음 단계를 따르세요.

  1. Cloud Search 플랫폼 프로젝트에는 서비스 계정 사용자 인증 정보가 포함되어 있습니다. 하지만 서드 파티 지원을 초기화하려면 웹 애플리케이션 사용자 인증 정보를 만들어야 합니다. 웹 애플리케이션 사용자 인증 정보를 만드는 방법에 대한 안내는 사용자 인증 정보 만들기를 참조하세요. 이 단계를 완료하면 클라이언트 ID와 클라이언트 보안 비밀번호 파일이 생성됩니다.

  2. Google의 OAuth 2 Playground를 사용하여 액세스 토큰을 가져오세요.

    1. 설정을 클릭하고 User your own auth credentials를 선택합니다.
    2. 1단계의 클라이언트 ID와 클라이언트 보안 비밀번호를 입력합니다.
    3. 닫기를 클릭합니다.
    4. 범위 필드에 https://www.googleapis.com/auth/cloud_search.settings를 입력하고 승인을 클릭합니다. OAuth 2 Playground가 승인 코드를 반환합니다.
    5. Exchange Authorization code for 토큰을 클릭합니다. 토큰이 반환됩니다.
  3. Cloud Search에 대한 타사 지원을 초기화하려면 다음 curl 명령어를 사용합니다. [YOUR_ACCESS_TOKEN]을 2단계에서 얻은 토큰으로 대체해야 합니다.

    curl --request POST \
    'https://cloudsearch.googleapis.com/v1:initializeCustomer' \
      --header 'Authorization: Bearer [YOUR_ACCESS_TOKEN]' \
      --header 'Accept: application/json' \
      --header 'Content-Type: application/json' \
      --data '{}' \
      --compressed
    

    요청이 성공하면 응답 본문에 operation 인스턴스가 포함됩니다. 예를 들면 다음과 같습니다.

    {
    name: "operations/customers/01b3fqdm/lro/AOIL6eBv7fEfiZ_hUSpm8KQDt1Mnd6dj5Ru3MXf-jri4xK6Pyb2-Lwfn8vQKg74pgxlxjrY"
    }
    

    실패하면 Cloud Search 지원팀에 문의하세요.

  4. operations.get을 사용하여 서드 파티 지원이 초기화되었는지 확인합니다.

    curl \
    'https://cloudsearch.googleapis.com/v1/operations/customers/01b3fqdm/lro/AOIL6eBv7fEfiZ_hUSpm8KQDt1Mnd6dj5Ru3MXf-jri4xK6Pyb2-Lwfn8vQKg74pgxlxjrY?key=
    [YOUR_API_KEY]' \
    --header 'Authorization: Bearer [YOUR_ACCESS_TOKEN]' \
    --header 'Accept: application/json' \
    --compressed
    

    서드 파티 초기화가 완료되면 true로 설정된 done 필드가 포함됩니다. 예를 들면 다음과 같습니다.

    {
    name: "operations/customers/01b3fqdm/lro/AOIL6eBv7fEfiZ_hUSpm8KQDt1Mnd6dj5Ru3MXf-jri4xK6Pyb2-Lwfn8vQKg74pgxlxjrY"
    done: true
    }
    

데이터 소스 만들기

그런 다음 관리 콘솔에서 데이터 소스를 만듭니다. 데이터 소스는 커넥터를 사용하여 콘텐츠 색인을 생성하기 위한 네임스페이스를 제공합니다.

  1. Google 관리 콘솔을 엽니다.
  2. 앱 아이콘을 클릭합니다. '앱 관리' 페이지가 표시됩니다.
  3. Google Workspace를 클릭합니다. '앱 Google Workspace 관리' 페이지가 표시됩니다.
  4. 아래로 스크롤하여 Cloud Search를 클릭합니다. 'Google Workspace 설정' 페이지가 표시됩니다.
  5. 서드 파티 데이터 소스를 클릭합니다. '데이터 소스' 페이지가 나타납니다.
  6. 노란색 둥근 + 아이콘을 클릭합니다. '새 데이터 소스 추가' 대화상자가 나타납니다.
  7. 표시 이름 필드에 'tutorial'을 입력합니다.
  8. 서비스 계정 이메일 주소 필드에 이전 섹션에서 만든 서비스 계정의 이메일 주소를 입력합니다. 서비스 계정의 이메일 주소를 모르는 경우 서비스 계정 페이지에서 값을 조회합니다.
  9. 추가를 클릭합니다. '데이터 소스 생성됨' 대화상자가 표시됩니다.
  10. *확인을 클릭합니다. 새로 만든 데이터 소스의 소스 ID를 기록해 둡니다. 소스 ID는 콘텐츠 커넥터를 구성하는 데 사용됩니다.

GitHub API용 개인 액세스 토큰 생성

충분한 할당량을 확보하려면 커넥터에 GitHub API에 대한 인증된 액세스 권한이 필요합니다. 커넥터는 편의상 OAuth 대신 개인 액세스 토큰을 활용합니다. 개인 토큰을 사용하면 OAuth와 유사하게 제한된 권한 집합을 가진 사용자로 인증할 수 있습니다.

  1. GitHub에 로그인합니다.
  2. 오른쪽 상단에서 프로필 사진을 클릭합니다. 드롭다운 메뉴가 나타납니다.
  3. 설정을 클릭합니다.
  4. 개발자 설정을 클릭합니다.
  5. 개인 액세스 토큰을 클릭합니다.
  6. Generate Personal access token을 클릭합니다.
  7. Note(메모) 입력란에 'Cloud Search 가이드'를 입력합니다.
  8. public_repo 범위를 확인합니다.
  9. 토큰 생성을 클릭합니다.
  10. 생성된 토큰을 기록해 둡니다. 이는 커넥터가 GitHub API를 호출하는 데 사용되며 색인 생성을 수행하기 위한 API 할당량을 제공합니다.

커넥터 구성

사용자 인증 정보와 데이터 소스를 만든 후 다음 값을 포함하도록 커넥터 구성을 업데이트합니다.

  1. 명령줄에서 디렉터리를 cloud-search-samples/end-to-end/connector/로 변경합니다.
  2. 텍스트 편집기로 sample-config.properties 파일을 엽니다.
  3. api.serviceAccountPrivateKeyFile 매개변수를 이전에 다운로드한 서비스 사용자 인증 정보의 파일 경로로 설정합니다.
  4. api.sourceId 매개변수를 이전에 만든 데이터 소스의 ID로 설정합니다.
  5. github.user 매개변수를 GitHub 사용자 이름으로 설정합니다.
  6. github.token 매개변수를 이전에 만든 액세스 토큰으로 설정합니다.
  7. 파일을 저장합니다.

스키마 업데이트

커넥터는 구조화된 콘텐츠와 구조화되지 않은 콘텐츠의 색인을 모두 생성합니다. 데이터의 색인을 생성하기 전에 데이터 소스의 스키마를 업데이트해야 합니다. 다음 명령어를 실행하여 스키마를 업데이트합니다.

mvn exec:java -Dexec.mainClass=com.google.cloudsearch.tutorial.SchemaTool \
    -Dexec.args="-Dconfig=sample-config.properties"

커넥터 실행

커넥터를 실행하고 색인 생성을 시작하려면 다음 명령어를 실행하세요.

mvn exec:java -Dexec.mainClass=com.google.cloudsearch.tutorial.GithubConnector \
    -Dexec.args="-Dconfig=sample-config.properties"

커넥터의 기본 구성은 googleworkspace 조직에 있는 단일 저장소의 색인을 생성하는 것입니다. 저장소의 색인을 생성하는 데 1분 정도 걸립니다. 초기 색인 생성 후 커넥터는 Cloud Search 색인에 반영되어야 하는 저장소 변경사항이 있는지 계속 폴링합니다.

코드 검토

나머지 섹션에서는 커넥터가 어떻게 만들어지는지 살펴봅니다.

애플리케이션 시작

커넥터의 진입점은 GithubConnector 클래스입니다. main 메서드는 SDK의 IndexingApplication를 인스턴스화하여 시작합니다.

GithubConnector.java
/**
 * Main entry point for the connector. Creates and starts an indexing
 * application using the {@code ListingConnector} template and the sample's
 * custom {@code Repository} implementation.
 *
 * @param args program command line arguments
 * @throws InterruptedException thrown if an abort is issued during initialization
 */
public static void main(String[] args) throws InterruptedException {
  Repository repository = new GithubRepository();
  IndexingConnector connector = new ListingConnector(repository);
  IndexingApplication application = new IndexingApplication.Builder(connector, args)
      .build();
  application.start();
}

SDK에서 제공하는 ListingConnector는 색인의 항목 상태를 추적하기 위해 Cloud Search 큐를 활용하는 순회 전략을 구현합니다. GitHub에서 콘텐츠에 액세스하기 위해 샘플 커넥터로 구현된 GithubRepository에 위임합니다.

GitHub 저장소 순회

전체 순회 중에 색인 생성이 필요할 수 있는 항목을 큐로 푸시하기 위해 getIds() 메서드가 호출됩니다.

커넥터는 여러 저장소나 조직의 색인을 생성할 수 있습니다. 장애의 영향을 최소화하기 위해 한 번에 하나의 GitHub 저장소를 순회합니다. 체크포인트는 이후 getIds() 호출에서 색인을 생성할 저장소 목록이 포함된 순회 결과와 함께 반환됩니다. 오류가 발생하면 색인 생성이 처음부터 시작하지 않고 현재 저장소에서 재개됩니다.

GithubRepository.java
/**
 * Gets all of the existing item IDs from the data repository. While
 * multiple repositories are supported, only one repository is traversed
 * per call. The remaining repositories are saved in the checkpoint
 * are traversed on subsequent calls. This minimizes the amount of
 * data that needs to be reindex in the event of an error.
 *
 * <p>This method is called by {@link ListingConnector#traverse()} during
 * <em>full traversals</em>. Every document ID and metadata hash value in
 * the <em>repository</em> is pushed to the Cloud Search queue. Each pushed
 * document is later polled and processed in the {@link #getDoc(Item)} method.
 * <p>
 * The metadata hash values are pushed to aid document change detection. The
 * queue sets the document status depending on the hash comparison. If the
 * pushed ID doesn't yet exist in Cloud Search, the document's status is
 * set to <em>new</em>. If the ID exists but has a mismatched hash value,
 * its status is set to <em>modified</em>. If the ID exists and matches
 * the hash value, its status is unchanged.
 *
 * <p>In every case, the pushed content hash value is only used for
 * comparison. The hash value is only set in the queue during an
 * update (see {@link #getDoc(Item)}).
 *
 * @param checkpoint value defined and maintained by this connector
 * @return this is typically a {@link PushItems} instance
 */
@Override
public CheckpointCloseableIterable<ApiOperation> getIds(byte[] checkpoint)
    throws RepositoryException {
  List<String> repositories;
  // Decode the checkpoint if present to get the list of remaining
  // repositories to index.
  if (checkpoint != null) {
    try {
      FullTraversalCheckpoint decodedCheckpoint = FullTraversalCheckpoint
          .fromBytes(checkpoint);
      repositories = decodedCheckpoint.getRemainingRepositories();
    } catch (IOException e) {
      throw new RepositoryException.Builder()
          .setErrorMessage("Unable to deserialize checkpoint")
          .setCause(e)
          .build();
    }
  } else {
    // No previous checkpoint, scan for repositories to index
    // based on the connector configuration.
    try {
      repositories = scanRepositories();
    } catch (IOException e) {
      throw toRepositoryError(e, Optional.of("Unable to scan repositories"));
    }
  }

  if (repositories.isEmpty()) {
    // Nothing left to index. Reset the checkpoint to null so the
    // next full traversal starts from the beginning
    Collection<ApiOperation> empty = Collections.emptyList();
    return new CheckpointCloseableIterableImpl.Builder<>(empty)
        .setCheckpoint((byte[]) null)
        .setHasMore(false)
        .build();
  }

  // Still have more repositories to index. Pop the next repository to
  // index off the list. The remaining repositories make up the next
  // checkpoint.
  String repositoryToIndex = repositories.get(0);
  repositories = repositories.subList(1, repositories.size());

  try {
    log.info(() -> String.format("Traversing repository %s", repositoryToIndex));
    Collection<ApiOperation> items = collectRepositoryItems(repositoryToIndex);
    FullTraversalCheckpoint newCheckpoint = new FullTraversalCheckpoint(repositories);
    return new CheckpointCloseableIterableImpl.Builder<>(items)
        .setHasMore(true)
        .setCheckpoint(newCheckpoint.toBytes())
        .build();
  } catch (IOException e) {
    String errorMessage = String.format("Unable to traverse repo: %s",
        repositoryToIndex);
    throw toRepositoryError(e, Optional.of(errorMessage));
  }
}

collectRepositoryItems() 메서드는 단일 GitHub 저장소의 순회를 처리합니다. 이 메서드는 큐로 푸시할 항목을 나타내는 ApiOperations 컬렉션을 반환합니다. 항목은 항목의 현재 상태를 나타내는 리소스 이름과 해시 값으로 푸시됩니다.

해시 값은 GitHub 저장소의 후속 순회에 사용됩니다. 이 값은 추가 콘텐츠를 업로드할 필요 없이 콘텐츠가 변경되었는지 확인하기 위해 가벼운 검사를 제공합니다. 커넥터는 모든 항목을 맹목적으로 큐에 추가합니다. 새 항목이거나 해시 값이 변경된 경우 큐에서 폴링에 사용할 수 있습니다. 그렇지 않으면 항목이 수정되지 않은 것으로 간주됩니다.

GithubRepository.java
/**
 * Fetch IDs to  push in to the queue for all items in the repository.
 * Currently captures issues & content in the master branch.
 *
 * @param name Name of repository to index
 * @return Items to push into the queue for later indexing
 * @throws IOException if error reading issues
 */
private Collection<ApiOperation> collectRepositoryItems(String name)
    throws IOException {
  List<ApiOperation> operations = new ArrayList<>();
  GHRepository repo = github.getRepository(name);

  // Add the repository as an item to be indexed
  String metadataHash = repo.getUpdatedAt().toString();
  String resourceName = repo.getHtmlUrl().getPath();
  PushItem repositoryPushItem = new PushItem()
      .setMetadataHash(metadataHash);
  PushItems items = new PushItems.Builder()
      .addPushItem(resourceName, repositoryPushItem)
      .build();

  operations.add(items);
  // Add issues/pull requests & files
  operations.add(collectIssues(repo));
  operations.add(collectContent(repo));
  return operations;
}

대기열 처리 중

전체 순회가 완료되면 커넥터는 색인을 생성해야 하는 항목에 대해 큐를 폴링하기 시작합니다. 대기열에서 가져온 각 항목에 대해 getDoc() 메서드가 호출됩니다. 이 메서드는 GitHub에서 항목을 읽고 색인 생성을 위한 적절한 표현으로 변환합니다.

커넥터가 언제든지 변경될 수 있는 실시간 데이터를 대상으로 실행되므로 getDoc()는 큐의 항목이 여전히 유효한지 확인하고 더 이상 존재하지 않는 모든 항목을 색인에서 삭제합니다.

GithubRepository.java
/**
 * Gets a single data repository item and indexes it if required.
 *
 * <p>This method is called by the {@link ListingConnector} during a poll
 * of the Cloud Search queue. Each queued item is processed
 * individually depending on its state in the data repository.
 *
 * @param item the data repository item to retrieve
 * @return the item's state determines which type of
 * {@link ApiOperation} is returned:
 * {@link RepositoryDoc}, {@link DeleteItem}, or {@link PushItem}
 */
@Override
public ApiOperation getDoc(Item item) throws RepositoryException {
  log.info(() -> String.format("Processing item: %s ", item.getName()));
  Object githubObject;
  try {
    // Retrieve the item from GitHub
    githubObject = getGithubObject(item.getName());
    if (githubObject instanceof GHRepository) {
      return indexItem((GHRepository) githubObject, item);
    } else if (githubObject instanceof GHPullRequest) {
      return indexItem((GHPullRequest) githubObject, item);
    } else if (githubObject instanceof GHIssue) {
      return indexItem((GHIssue) githubObject, item);
    } else if (githubObject instanceof GHContent) {
      return indexItem((GHContent) githubObject, item);
    } else {
      String errorMessage = String.format("Unexpected item received: %s",
          item.getName());
      throw new RepositoryException.Builder()
          .setErrorMessage(errorMessage)
          .setErrorType(RepositoryException.ErrorType.UNKNOWN)
          .build();
    }
  } catch (FileNotFoundException e) {
    log.info(() -> String.format("Deleting item: %s ", item.getName()));
    return ApiOperations.deleteItem(item.getName());
  } catch (IOException e) {
    String errorMessage = String.format("Unable to retrieve item: %s",
        item.getName());
    throw toRepositoryError(e, Optional.of(errorMessage));
  }
}

커넥터가 색인을 생성하는 각 GitHub 객체에 대해 해당하는 indexItem() 메서드가 Cloud Search의 항목 표현 작성을 처리합니다. 예를 들어 콘텐츠 항목의 표현을 빌드하려면 다음과 같이 합니다.

GithubRepository.java
/**
 * Build the ApiOperation to index a content item (file).
 *
 * @param content      Content item to index
 * @param previousItem Previous item state in the index
 * @return ApiOperation (RepositoryDoc if indexing,  PushItem if not modified)
 * @throws IOException if unable to create operation
 */
private ApiOperation indexItem(GHContent content, Item previousItem)
    throws IOException {
  String metadataHash = content.getSha();

  // If previously indexed and unchanged, just requeue as unmodified
  if (canSkipIndexing(previousItem, metadataHash)) {
    return notModified(previousItem.getName());
  }

  String resourceName = new URL(content.getHtmlUrl()).getPath();
  FieldOrValue<String> title = FieldOrValue.withValue(content.getName());
  FieldOrValue<String> url = FieldOrValue.withValue(content.getHtmlUrl());

  String containerName = content.getOwner().getHtmlUrl().getPath();
  String programmingLanguage = FileExtensions.getLanguageForFile(content.getName());

  // Structured data based on the schema
  Multimap<String, Object> structuredData = ArrayListMultimap.create();
  structuredData.put("organization", content.getOwner().getOwnerName());
  structuredData.put("repository", content.getOwner().getName());
  structuredData.put("path", content.getPath());
  structuredData.put("language", programmingLanguage);

  Item item = IndexingItemBuilder.fromConfiguration(resourceName)
      .setTitle(title)
      .setContainerName(containerName)
      .setSourceRepositoryUrl(url)
      .setItemType(IndexingItemBuilder.ItemType.CONTAINER_ITEM)
      .setObjectType("file")
      .setValues(structuredData)
      .setVersion(Longs.toByteArray(System.currentTimeMillis()))
      .setHash(content.getSha())
      .build();

  // Index the file content too
  String mimeType = FileTypeMap.getDefaultFileTypeMap()
      .getContentType(content.getName());
  AbstractInputStreamContent fileContent = new InputStreamContent(
      mimeType, content.read())
      .setLength(content.getSize())
      .setCloseInputStream(true);
  return new RepositoryDoc.Builder()
      .setItem(item)
      .setContent(fileContent, IndexingService.ContentFormat.RAW)
      .setRequestMode(IndexingService.RequestMode.SYNCHRONOUS)
      .build();
}

다음으로 검색 인터페이스를 배포합니다.

이전 다음