커넥터 배포

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를 호출하려면 먼저 서드 파티 API를 초기화해야 합니다. Google Cloud Search 지원이 추가될 수 있습니다

Cloud Search에 대한 타사 지원을 초기화하려면 다음 안내를 따르세요.

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

  2. 사용 Google의 OAuth 2 Playground 액세스 토큰을 얻는 방법은 다음과 같습니다.

    1. 설정을 클릭하고 사용자 인증 정보 사용을 선택합니다.
    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
    

    타사 초기화가 완료되면 done 필드가 true(으)로 설정됨 예를 들면 다음과 같습니다.

    {
    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에 대한 인증된 액세스 권한이 필요합니다. 할당량도 충분해야 합니다 편의상 커넥터는 액세스 토큰을 사용할 수 있습니다. 개인 토큰을 사용하면 사용자 인증 정보를 얻을 수 있습니다

  1. GitHub에 로그인합니다.
  2. 오른쪽 상단에서 프로필 사진을 클릭합니다. 드롭다운 메뉴가 표시됩니다.
  3. 설정을 클릭합니다.
  4. 개발자 설정을 클릭합니다.
  5. 개인 액세스 토큰을 클릭합니다.
  6. Generate Personal access token을 클릭합니다.
  7. Note(메모) 필드에 'Cloud Search guide'(Cloud Search 튜토리얼)를 입력합니다.
  8. public_repo 범위를 확인합니다.
  9. 토큰 생성을 클릭합니다.
  10. 생성된 토큰을 기록해 둡니다. 커넥터가 GitHub 색인 생성을 수행할 수 있는 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();
}

ListingConnector SDK가 제공하는 각 레이어의 Cloud Search 큐를 활용하는 색인의 항목 상태를 추적합니다. 다음을 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();
}

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

이전 다음