コネクタのデプロイ

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 のサードパーティ サポートを初期化します。

サードパーティ サポートを初期化する手順は次のとおりです。

  1. Cloud Search プラットフォーム プロジェクトでウェブ アプリケーションの認証情報を作成します。認証情報を作成するをご覧ください。クライアント ID とクライアント シークレットが必要です。
  2. OAuth 2.0 Playground を使用してアクセス トークンを取得します。
    1. [OAuth 2.0 Configuration](設定アイコン)をクリックし、[Use your own OAuth credentials] をオンにします。
    2. クライアント ID とクライアント シークレットを入力します。
    3. [scopes] フィールドに「https://www.googleapis.com/auth/cloud_search.settings」と入力し、[Authorize APIs] をクリックします。
    4. [Exchange authorization code for tokens] をクリックします。
  3. 次の curl コマンドを実行します。[YOUR_ACCESS_TOKEN] はトークンに置き換えます。

    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が含まれます。 失敗した場合は、Cloud Search サポートにお問い合わせください。

  4. operations.get を使用して初期化を確認します。

    curl 'https://cloudsearch.googleapis.com/v1/operations/<var>operation_name</var>?key=[YOUR_API_KEY]' \
    --header 'Authorization: Bearer [YOUR_ACCESS_TOKEN]' \
    --header 'Accept: application/json' \
    --compressed
    

    donetrue の場合、初期化は完了しています。

データソースを作成する

次に、管理コンソールでデータソースを作成します。データソースは、コネクタを使用してコンテンツをインデックス登録するための名前空間を提供します。

  1. Google 管理コンソールを開きます。
  2. [アプリ] アイコンをクリックします。[アプリの管理] ページが表示されます。
  3. [Google Workspace] をクリックします。[アプリ Google Workspace 管理] ページが表示されます。
  4. 下にスクロールして [Cloud Search] をクリックします。[Google Workspace の設定] ページが表示されます。
  5. [サードパーティのデータソース] をクリックします。[データソース] ページが表示されます。
  6. 丸い黄色の [\+] をクリックします。[新しいデータソースを追加] ダイアログが表示されます。
  7. [表示名] フィールドに「tutorial」と入力します。
  8. [サービス アカウントのメールアドレス] フィールドに、前のセクションで作成したサービス アカウントのメールアドレスを入力します。サービス アカウントの メールアドレスがわからない場合は、 サービス アカウント ページで値を確認してください。
  9. [追加] をクリックします。[データソースを作成しました] ダイアログが表示されます。
  10. [**OK**] をクリックします。 新しく作成したデータソースのソース ID をメモします。ソース ID は、コンテンツ コネクタを構成するために使用されます。

GitHub API の個人アクセス トークンを生成する

コネクタには、十分な割り当てを確保するために、GitHub API への認証済みアクセスが必要です。簡略化のため、コネクタは OAuth の代わりに個人アクセス トークンを利用します。個人トークンを使用すると、OAuth と同様に、限られた権限セットを持つユーザーとして認証できます。

  1. GitHub にログインします。
  2. 右上にあるプロフィール写真をクリックします。プルダウン メニューが表示されます。
  3. [設定] をクリックします。
  4. [デベロッパー向けの設定] をクリックします。
  5. [Personal access tokens] をクリックします。
  6. [Generate personal access token] をクリックします。
  7. [Note] フィールドに「Cloud Search tutorial」と入力します。
  8. [public_repo] スコープをオンにします。
  9. [Generate token] をクリックします。
  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 リポジトリは一度に 1 つずつ走査されます。チェックポイントは、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();
}

次に、検索インターフェースをデプロイします。

前へ 次へ