部署連接器

本頁面的 Cloud Search 教學課程說明如何設定資料來源和內容連接器,以便為資料建立索引。如要從本教學課程的開頭開始,請參閱 Cloud Search 入門教學課程

建構連接器

將工作目錄變更為 cloud-search-samples/end-to-end/connector 目錄,然後執行下列指令:

mvn package -DskipTests

這項指令會下載建構內容連結器所需的必要依附元件,並編譯程式碼。

建立服務帳戶憑證

連接器需要服務帳戶憑證,才能呼叫 Cloud Search API。如要建立憑證,請按照下列步驟操作:

  1. 返回 Google Cloud 控制台
  2. 在左側導覽列中,按一下「憑證」。「憑證」頁面隨即顯示。
  3. 按一下「+ CREATE CREDENTIALS」下拉式清單,然後選取「Service account」。系統會顯示「建立服務帳戶」頁面。
  4. 在「Service account name」(服務帳戶名稱) 欄位中輸入「tutorial」。
  5. 記下服務帳戶 ID 值 (緊接在服務帳戶名稱之後)。 這個值稍後會用到。
  6. 按一下「建立」。系統會顯示「服務帳戶權限 (選用)」對話方塊。
  7. 按一下「繼續」。系統會顯示「將這個服務帳戶的存取權授予使用者 (選用)」對話方塊。
  8. 點按「完成」。系統會顯示「憑證」畫面。
  9. 在「服務帳戶」下方,按一下服務帳戶電子郵件地址。「服務帳戶詳細資料」頁面隨即顯示。
  10. 在「金鑰」下方,點按「新增金鑰」下拉式清單,然後選取「建立新的金鑰」。系統會顯示「建立私密金鑰」對話方塊。
  11. 點選「建立」。
  12. (選用) 如果出現「Do you want to allow downloads on console.cloud.google.com?」(是否允許在 console.cloud.google.com 上下載檔案?) 對話方塊,請按一下「Allow」(允許)
  13. 私密金鑰檔案會儲存到您的電腦中。記下下載檔案的位置。這個檔案用於設定內容連接器,以便在呼叫 Google Cloud Search API 時進行驗證。

初始化第三方支援

您必須先初始化 Google Cloud Search 的第三方支援,才能呼叫任何其他 Cloud Search API。

如要初始化 Cloud Search 的第三方支援功能,請按照下列步驟操作:

  1. 您的 Cloud Search 平台專案包含服務帳戶憑證。不過,為了初始化第三方支援,您必須建立 Web 應用程式憑證。如需建立網頁應用程式憑證的操作說明,請參閱「建立憑證」。完成這個步驟後,您應該會取得用戶端 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 tokens」。系統會傳回權杖。
  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
    

    第三方初始化完成後,會包含設為 truedone 欄位。例如:

    {
    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. 按一下「Personal access tokens」
  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 佇列追蹤索引中項目的狀態。這個函式會委派給範例連結器實作的 GithubRepository,以便存取 GitHub 的內容。

遍歷 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();
}

接著,部署搜尋介面。

返回 繼續