Implantar o conector

Esta página do tutorial do Cloud Search mostra como configurar uma fonte de dados e um conector de conteúdo para indexação de dados. Para começar do início deste tutorial, consulte o Tutorial para dar os primeiros passos com o Cloud Search.

Criar o conector

Mude o 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 exige credenciais da 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 "Credenciais" vai aparecer.
  3. Clique na lista suspensa + CRIAR CREDENCIAIS e selecione Conta de serviço. A página "Criar conta de serviço" vai aparecer.
  4. No campo Nome da conta de serviço, insira "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)" é exibida.
  7. Clique em CONTINUAR. A caixa de diálogo "Conceder aos usuários acesso a esta conta de serviço (opcional)" vai aparecer.
  8. Clique em CONCLUÍDO. A tela "Credenciais" vai aparecer.
  9. Em "Contas de serviço", clique no e-mail da conta de serviço. A página "Detalhes da conta de serviço" é exibida.
  10. Em "Chaves", clique na lista suspensa ADD KEY e selecione Criar nova chave. A caixa de diálogo "Criar chave particular" vai aparecer.
  11. Clique em CRIAR.
  12. (Opcional) Se a caixa de diálogo "Do you want to allow downloads on console.cloud.google.com?” aparecer, clique em Allow.
  13. Um arquivo de chave privada é salvo no seu computador. Anote o local do arquivo salvo. Esse arquivo é usado para configurar o 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 outras APIs do Cloud Search, é necessário inicializar o suporte de terceiros para o Google Cloud Search.

Para inicializar o suporte de terceiros ao Cloud Search:

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

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

    1. Clique em "Configurações" e marque a caixa 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 "Escopos", digite https://www.googleapis.com/auth/cloud_search.settings e clique em Autorizar. O playground do OAuth 2 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. Por exemplo:

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

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

  4. Use operations.get para verificar se o suporte de terceiros está 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 é concluída, ela contém 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 conteúdo usando o conector.

  1. Abra o Google Admin Console.
  2. Clique no ícone "Apps". A página "Administração de apps" vai aparecer.
  3. Clique em Google Workspace. A página "Administração de apps do Google Workspace" vai aparecer.
  4. Role a tela para baixo e clique em Pesquisa na nuvem. A página "Configurações do Google Workspace" aparece.
  5. Clique em Origens de dados de terceiros. A página "Fontes de dados" vai aparecer.
  6. Clique no + amarelo redondo. A caixa de diálogo "Adicionar nova fonte de dados" vai aparecer.
  7. No campo Nome de exibição, 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, pesquise o valor na página contas de serviço.
  9. Clique em ADICIONAR. A caixa de diálogo "A origem de dados foi criada" é exibida.
  10. Clique em OK. Anote o ID da origem da fonte de dados recém-criada. O ID da origem é usado para configurar o conector de conteúdo.

Gerar um token de acesso pessoal para a API do GitHub

O conector requer acesso autenticado à API do GitHub para ter cota suficiente. Para simplificar, o conector usa tokens de acesso pessoais em vez de OAuth. Os tokens pessoais permitem 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 Gerar token de acesso pessoal.
  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. Ele é usado pelo conector para chamar as APIs do GitHub e fornece 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, mude 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 que você criou anteriormente.
  7. Salve o arquivo.

Atualizar o esquema

O conector indexa conteúdo estruturado e não estruturado. Antes de indexar os dados, atualize o esquema da origem 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 a detectar mudanças 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 é criado.

Como iniciar o aplicativo

O ponto de entrada do 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 a GithubRepository, implementada pelo conector de exemplo, para acessar conteúdo do GitHub.

Como percorrer os repositórios do GitHub

Durante as travessias completas, o método getIds() é chamado para enviar itens que podem precisar ser indexados na fila.

O conector pode indexar vários repositórios ou organizações. Para minimizar o impacto de uma falha, um repositório do GitHub é percorrido por vez. Um ponto de verificação é retornado com os resultados da travessia contendo a lista de repositórios a serem indexados em chamadas subsequentes para getIds(). Se um erro ocorrer, 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 inseridos na 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 do hash é usado em transições subsequentes dos repositórios do GitHub. Esse valor fornece uma verificação simples para determinar se o conteúdo mudou sem precisar fazer upload de conteúdo extra. O conector enfileira todos os itens de forma cega. Se o item for novo ou o valor do hash tiver mudado, ele será disponibilizado para pesquisa na fila. Caso contrário, o item é considerado não modificado.

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

Processando a fila

Depois que a traversal completa é concluída, o conector começa a consultar a fila para encontrar itens que precisam ser indexados. O método getDoc() é chamado para cada item retirado da fila. O método lê o item do GitHub e o converte na representação adequada para indexação.

Como o conector é executado com dados em tempo real que podem ser alterados a qualquer momento, getDoc() também verifica se o item na fila ainda é válido e exclui 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 indexados pelo conector, o método indexItem() correspondente lida com a criação da representação do item para o Cloud Search. Por exemplo, para criar 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