Implantar o conector

Nesta página do tutorial do Cloud Search, você aprende a configurar um conector de origem de dados e de conteúdo para indexação de dados. Para começar deste tutorial, consulte o Tutorial de primeiros passos do Cloud Search.

Criar o conector

Altere seu diretório de trabalho para o diretório cloud-search-samples/end-to-end/connector e execute este comando:

mvn package -DskipTests

O comando faz o download das dependências necessárias para criar o conector de conteúdo e compila o código.

Criar credenciais de conta de serviço

O conector requer credenciais de conta de serviço para chamar as APIs do Cloud Search. Para criar as credenciais:

  1. Volte para o console do Google Cloud.
  2. No painel de navegação à esquerda, clique em Credenciais. A página "Credencial" é exibida.
  3. Clique na lista suspensa + CRIAR CREDENCIAIS e selecione Conta de serviço. A página "Criar conta de serviço" é exibida.
  4. No campo Nome da conta de serviço, digite "tutorial".
  5. Anote o valor do ID da conta de serviço (logo após o nome da conta de serviço). Esse valor será usado mais tarde.
  6. Clique em CRIAR. A caixa de diálogo "Permissões da conta de serviço (opcional)" vai aparecer.
  7. Clique em CONTINUAR. A caixa de diálogo "Conceder aos usuários acesso a essa conta de serviço (opcional)" será exibida.
  8. Clique em CONCLUÍDO. A tela "Credenciais" será exibida.
  9. Em "Contas de serviço", clique no e-mail da conta. A página "Detalhes da conta de serviço" será exibida.
  10. Em "Chaves", clique na lista suspensa ADICIONAR CHAVE e selecione Criar nova chave. A caixa de diálogo "Criar chave privada" será exibida.
  11. Clique em CRIAR.
  12. (Opcional) Se a caixa de diálogo "Do you want to allow downloads on console.cloud.google.com?" for exibida, clique em Allow.
  13. Um arquivo de chave privada é salvo no seu computador. Anote o local do arquivo salvo. Esse arquivo é usado na configuração do conector de conteúdo para que ele possa se autenticar ao chamar as APIs do Google Cloud Search.

Inicializar o suporte de terceiros

Antes de chamar qualquer outra API do Cloud Search, você precisa inicializar o suporte de terceiros para o Google Cloud Search.

Para inicializar o suporte de terceiros para o Cloud Search:

  1. Seu projeto de plataforma do Cloud Search contém as credenciais da conta de serviço. No entanto, para inicializar o suporte de terceiros, é necessário criar credenciais de aplicativos da Web. Para instruções sobre como fazer isso, consulte Criar credenciais. Ao concluir esta etapa, você terá um ID e um arquivo de chave secreta do cliente.

  2. Use o OAuth 2 Playground do Google para receber um token de acesso:

    1. Clique em "Configurações" e marque a opção Usar suas próprias credenciais de autenticação.
    2. Insira o ID e a chave secreta do cliente da etapa 1.
    3. Clique em Fechar.
    4. No campo de escopos, digite https://www.googleapis.com/auth/cloud_search.settings e clique em Autorizar. O OAuth 2 Playground retorna um código de autorização.
    5. Clique em Trocar código de autorização dos tokens. Um token é retornado.
  3. Para inicializar o suporte de terceiros para o Cloud Search, use o seguinte comando curl. Substitua [YOUR_ACCESS_TOKEN] pelo token recebido na etapa 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
    

    Se funcionar, o corpo da resposta vai ter uma instância de operation. Exemplo:

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

    Se não conseguir, entre em contato com o suporte do Cloud Search.

  4. Use operations.get para verificar se o suporte de terceiros foi inicializado:

    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
    

    Quando a inicialização de terceiros estiver concluída, ela conterá o campo done definido como true. Exemplo:

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

Criar a fonte de dados

Em seguida, crie uma fonte de dados no Admin Console. A fonte de dados fornece um namespace para indexar o conteúdo usando o conector.

  1. Abra o Google Admin Console.
  2. Clique no ícone "Apps". A página "Administração de apps" é exibida.
  3. Clique em Google Workspace. A página "Apps Administração do Google Workspace" é exibida.
  4. Role para baixo e clique em Cloud Search. A página "Configurações do Google Workspace" é exibida.
  5. Clique em Origens de dados de terceiros. A página "Fontes de dados" é exibida.
  6. Clique no + amarelo redondo. A caixa de diálogo "Adicionar nova fonte de dados" será exibida.
  7. No campo Display name, digite "tutorial".
  8. No campo Endereços de e-mail da conta de serviço, insira o endereço de e-mail da conta de serviço que você criou na seção anterior. Se você não souber o endereço de e-mail da conta de serviço, procure o valor na página contas de serviço.
  9. Clique em ADICIONAR. A caixa de diálogo "Fonte de dados criada com sucesso" é exibida.
  10. Clique em *OK. Anote o ID da fonte recém-criada. O ID da fonte é usado para configurar o conector de conteúdo.

Gerar um token de acesso pessoal para a API GitHub

O conector requer acesso autenticado à API do GitHub para ter cota suficiente. Para simplificar, o conector usa tokens de acesso pessoais em vez do OAuth. Com os tokens pessoais, é possível fazer a autenticação como um usuário com um conjunto limitado de permissões, semelhante ao OAuth.

  1. Faça login no GitHub.
  2. No canto superior direito, clique na sua foto do perfil. Será exibido um menu suspenso.
  3. Clique em Configurações.
  4. Clique em Configurações do desenvolvedor.
  5. Clique em Tokens de acesso pessoal.
  6. Clique em Generate personal access token.
  7. No campo Observação, digite "Tutorial do Cloud Search".
  8. Verifique o escopo public_repo.
  9. Clique em Gerar token.
  10. Anote o token gerado. Ela é usada pelo conector para chamar as APIs do GitHub e fornece uma cota de API para realizar a indexação.

Configurar o conector

Depois de criar as credenciais e a fonte de dados, atualize a configuração do conector para incluir estes valores:

  1. Na linha de comando, altere o diretório para cloud-search-samples/end-to-end/connector/.
  2. Abra o arquivo sample-config.properties com um editor de texto.
  3. Defina o parâmetro api.serviceAccountPrivateKeyFile como o caminho do arquivo das credenciais de serviço que você fez o download anteriormente.
  4. Defina o parâmetro api.sourceId como o ID da fonte de dados que você criou anteriormente.
  5. Defina o parâmetro github.user como seu nome de usuário do GitHub.
  6. Defina o parâmetro github.token como o token de acesso criado anteriormente.
  7. Salve o arquivo.

Atualizar o esquema

O conector indexa conteúdo estruturado e não estruturado. Antes de indexar os dados, é necessário atualizar o esquema da fonte de dados. Execute o seguinte comando para atualizar o esquema:

mvn exec:java -Dexec.mainClass=com.google.cloudsearch.tutorial.SchemaTool \
    -Dexec.args="-Dconfig=sample-config.properties"

Executar o conector.

Para executar o conector e iniciar a indexação, execute o comando:

mvn exec:java -Dexec.mainClass=com.google.cloudsearch.tutorial.GithubConnector \
    -Dexec.args="-Dconfig=sample-config.properties"

A configuração padrão do conector é indexar um único repositório na organização googleworkspace. A indexação do repositório leva cerca de um minuto. Após a indexação inicial, o conector continua pesquisando alterações no repositório que precisam ser refletidas no índice do Cloud Search.

Como revisar o código

As seções restantes examinam como o conector foi criado.

Como iniciar o aplicativo

O ponto de entrada para o conector é a classe GithubConnector. O método main instancia e inicia o IndexingApplication do SDK.

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

O ListingConnector fornecido pelo SDK implementa uma estratégia de travessia que usa filas do Cloud Search para rastrear o estado dos itens no índice. Ele delega para GithubRepository, implementado pelo conector de amostra, para acessar o conteúdo do GitHub.

Como percorrer os repositórios do GitHub

Durante as travessias completas, o método getIds() é chamado para enviar itens que talvez precisem ser indexados para a fila.

O conector pode indexar vários repositórios ou organizações. Para minimizar o impacto de uma falha, um repositório do GitHub é transferido por vez. Um checkpoint é retornado com os resultados da travessia que contém a lista de repositórios a serem indexados nas chamadas seguintes para getIds(). Se ocorrer um erro, a indexação será retomada no repositório atual, em vez de começar do início.

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));
  }
}

O método collectRepositoryItems() processa a travessia de um único repositório do GitHub. Esse método retorna uma coleção de ApiOperations, que representa os itens a serem enviados para a fila. Os itens são enviados como um nome de recurso e um valor de hash que representa o estado atual do item.

O valor de hash é usado em travessias subsequentes dos repositórios do GitHub. Esse valor fornece uma verificação leve para determinar se o conteúdo foi alterado sem precisar fazer upload de outros conteúdos. O conector enfileira todos os itens às cegas. Se o item for novo ou o valor de hash tiver sido alterado, ele será disponibilizado para pesquisa na fila. Caso contrário, o item é considerado inalterado.

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;
}

Como processar a fila

Após a conclusão da travessia completa, o conector começa a pesquisar na fila os itens que precisam ser indexados. O método getDoc() é chamado para cada item extraído da fila. O método lê o item do GitHub e o converte na representação adequada para indexação.

Como o conector está sendo executado em dados ativos que podem ser alterados a qualquer momento, getDoc() também verifica se o item na fila ainda é válido e exclui todos os itens do índice que não existem mais.

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));
  }
}

Para cada um dos objetos do GitHub que o conector indexa, o método indexItem() correspondente cuida da criação da representação do item para o Cloud Search. Por exemplo, para construir a representação de itens de conteúdo:

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

Em seguida, implante a interface de pesquisa.

Anterior Próxima