部署連接器

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

建構連接器

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

mvn package -DskipTests

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

建立服務帳戶憑證

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

  1. 返回 Google Cloud 控制台
  2. 在左側導覽列中,按一下「Credentials」。系統隨即會顯示「憑證」頁面。
  3. 按一下「+ CREATE CREDENTIALS」 下拉式清單,然後選取「Service account」。畫面上會顯示「建立服務帳戶」頁面。
  4. 在「服務帳戶名稱」欄位中輸入「tutorial」。
  5. 記下服務帳戶 ID 值 (緊接在服務帳戶名稱後方)。這個值稍後會用到。
  6. 按一下「建立」。畫面上會顯示「服務帳戶權限 (選用)」對話方塊。
  7. 點選「繼續」。系統隨即會顯示「授予使用者這個服務帳戶的存取權 (選用)」對話方塊。
  8. 點按「完成」。系統會隨即顯示「憑證」畫面。
  9. 在「服務帳戶」下方,按一下服務帳戶電子郵件。「服務帳戶詳細資料」頁面小工具。
  10. 按一下「金鑰」下方的「新增金鑰」下拉式清單,然後選取「建立新的金鑰」。畫面上會顯示「建立私密金鑰」對話方塊。
  11. 點按「建立」。
  12. (選用) 如果畫面上顯示「Do you you want to allow download 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 平台專案含有服務帳戶憑證。不過,為了初始化第三方支援,您必須建立網頁應用程式憑證。如需建立網頁應用程式憑證的操作說明,請參閱建立憑證。完成這個步驟後,您應該擁有用戶端 ID 和用戶端密鑰檔案。

  2. 使用 Google 的 OAuth 2 Playground 取得存取權杖:

    1. 按一下「設定」,然後勾選「使用您自己的驗證憑證」
    2. 輸入步驟 1 的用戶端 ID 和用戶端密鑰。
    3. 點選「關閉」
    4. 在範圍欄位中輸入 https://www.googleapis.com/auth/cloud_search.settings,然後按一下「Authorize」(授權)。OAuth 2 Playground 會傳回授權碼。
    5. 按一下「Exchange License code for token」。傳回權杖。
  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. 按一下「應用程式」圖示。「Apps 管理」頁面隨即顯示。
  3. 點按「Google Workspace」。系統隨即會顯示「Google Workspace 管理應用程式」頁面。
  4. 向下捲動,然後按一下「Cloud Search」。「Google Workspace 設定」頁面會隨即顯示。
  5. 按一下「第三方資料來源」。「資料來源」頁面隨即顯示。
  6. 按一下黃色圓形的 +,畫面上會出現「新增資料來源」對話方塊。
  7. 在「顯示名稱」欄位中輸入「tutorial」。
  8. 在「Service account email addresses」欄位中,輸入您在上一節建立的服務帳戶電子郵件地址。如果您不知道服務帳戶的電子郵件地址,請前往服務帳戶頁面查詢值。
  9. 點選「新增」。系統會顯示「已成功建立資料來源」對話方塊。
  10. 按一下 *「確定」。記下新建資料來源的「來源 ID」。來源 ID 可用來設定內容連接器。

產生 GitHub API 的個人存取權杖

連接器需要經過 GitHub API 的驗證存取權,才能擁有足夠的配額。為簡單起見,連接器會使用個人存取權杖而非 OAuth。個人權杖可讓使用者以類似 OAuth 的有限權限組合進行驗證。

  1. 登入 GitHub。
  2. 按一下右上角的個人資料相片。系統隨即會顯示下拉式選單。
  3. 點選「Settings」(設定)
  4. 按一下「開發人員設定」
  5. 按一下「Personal access token」
  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();
}

接下來,請部署搜尋介面。

返回 繼續