커넥터 배포

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 플레이그라운드를 사용하여 액세스 토큰을 가져옵니다.

    1. 설정을 클릭하고 자체 인증 사용자 인증 정보 사용을 선택합니다.
    2. 1단계의 클라이언트 ID와 클라이언트 보안 비밀번호를 입력합니다.
    3. 닫기를 클릭합니다.
    4. 범위 필드에 https://www.googleapis.com/auth/cloud_search.settings를 입력하고 승인을 클릭합니다. OAuth 2 Playground가 승인 코드를 반환합니다.
    5. 승인 코드를 토큰으로 교환을 클릭합니다. 토큰이 반환됩니다.
  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
    

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

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

데이터 소스 만들기

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

  1. Google 관리 콘솔을 엽니다.
  2. 앱 아이콘을 클릭합니다. '앱 관리' 페이지가 표시됩니다.
  3. Google Workspace를 클릭합니다. 'Apps 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. 개인 액세스 토큰 생성을 클릭합니다.
  7. 메모 입력란에 '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();
}

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

이전 다음