Desenvolvimento

9 fev, 2017

Use o serviço de armazenamento de objeto para armazenar arquivos

Publicidade

Na Parte 01 desta série de artigos, eu mostrei como usar dois serviços importantes: conversão de documento e extração de palavra-chave. Também mostrei como usar esses serviços em um aplicativo real que permite que os usuários armazenem e indexem de forma inteligente os documentos PDF, para aumentar a eficiência das procuras.

Nesta parte de conclusão, vou introduzir o serviço de armazenamento de objeto do IBM® Bluemix™ que fornece uma infraestrutura de armazenamento de objeto confiável e compatível com os padrões. Vou mostrar como usar o serviço com PHP para criar um local ideal para seus uploads de PDF.

“Neste segmento de conclusão, vamos aprimorar a solução de armazenamento de documento, incluindo a procura de palavra-chave e uma recuperação de arquivo PDF e, em seguida, implementá-la no Bluemix”.

Também vou mostrar como criar um mecanismo de procura que utiliza os índices de texto do MongoDB para permitir a procura de palavra-chave no novo armazenamento de documento e, em seguida, realizar o processo de implementação no IBM Bluemix. Continue lendo, ainda tem muito mais!

Entenda e configure o serviço de armazenamento de objeto

O serviço de armazenamento de objeto facilita o armazenamento e a recuperação de dados não estruturados na nuvem do Bluemix. Ele oferece suporte para a API OpenStack Swift e segue a hierarquia de três camadas do Swift para organizar dados: contas, contêineres e objetos. Funciona da seguinte forma:

  • A unidade principal na hierarquia é uma conta. As contas correspondem a usuários. Para acessar uma conta, o usuário deve fornecer credenciais de autenticação;
  • Uma conta pode hospedar vários contêineres que, em geral, equivalem a pastas ou subdiretórios em um sistema de arquivos tradicional;
  • Cada contêiner pode armazenar vários objetos que podem ser arquivos ou dados. Os objetos podem ter metadados adicionais definidos pelo usuário. Geralmente, é possível armazenar um número ilimitado de objetos.

Para ver isso em ação, inicialize uma nova instância do serviço de armazenamento de objeto no Bluemix, efetuando login na sua conta. Procure e selecione o serviço de armazenamento de objeto.

Revise a descrição do serviço e clique para ativá-lo. Assegure-se de que o campo “Conectar a” esteja definido como “Deixar desvinculado” e que você esteja usando o “Plano grátis”. Inicialmente, essa instância do serviço é executada em um estado desvinculado. Semelhante à instância do serviço de Conversão de Documento na Parte 01, esse estado desvinculado permite que o aplicativo seja desenvolvido localmente com a própria instância do serviço hospedada remotamente no Bluemix.

Figura 1. Inicialização do serviço de armazenamento de objeto

A instância do serviço é inicializada e, após a conclusão, é apresentada uma página de informações do serviço. Exiba a barra de navegação à esquerda e clique no link “Credenciais do serviço” para visualizar a URL, a região, o nome de usuário, a senha e outras credenciais da instância do serviço. Anote todas essas credenciais porque elas serão necessárias nas próximas etapas.

Figura 2. Credenciais do serviço de armazenamento de objeto

Agora é possível fazer um teste com o serviço de armazenamento de objeto usando um cliente REST, como o Postman, para enviar algumas solicitações de exemplo. Comece enviando uma solicitação de POST à URL de autenticação para o serviço, com o nome do usuário do serviço e a senha. Se a autenticação for bem-sucedida, o servidor retornará um código de resposta 200 OK e um cabeçalho X-Subject-Token contendo um token de autenticação que deverá ser usado para as próximas solicitações.

Por exemplo, se a URL de autenticação do serviço for https://identity.open.softlayer.com/v3/auth/tokens, envie uma solicitação de POST com um corpo de conteúdo JSON contendo o nome do usuário do serviço e a senha. Caso seja autenticado, a resposta do servidor incluirá um cabeçalho X-Subject-Token contendo um token de autenticação. Aqui está um exemplo de solicitação e resposta:

Figura 3. Exemplo de solicitação e resposta para autenticação do serviço de armazenamento de objeto

A resposta também inclui uma série de terminais, um para cada um dos serviços disponíveis nesta implementação do OpenStack. Examine essa série de terminais até encontrar o terminal do serviço de armazenamento de objeto, como é mostrado abaixo. Essa URL do terminal será o destino de todas as próximas solicitações.

Figura 4. Terminal do serviço de armazenamento de objeto

Com o token de autenticação e a URL do terminal, é possível começar a interagir com o serviço de armazenamento de objeto usando a API Swift. Por exemplo, para incluir um novo contêiner, envie uma solicitação de PUT para a URL do terminal, incluindo o nome do novo contêiner no final da URL e lembrando-se de incluir um cabeçalho X-Auth-Token na solicitação com o token de autenticação. Portanto, se a URL do terminal for https://xyz.objectstorage.softlayer.net/AUTH_123, envie uma solicitação de PUT para https://xyz.objectstorage.softlayer.net/AUTH_123/container1, para criar um novo contêiner chamado “container1”. Aqui está um exemplo:

Figura 5. Exemplo de solicitação e resposta para criação de contêiner de armazenamento de objeto

Da mesma forma, se você desejar listar todos os contêineres na conta, é possível enviar uma solicitação de GET à URL do terminal correspondente sem nenhum parâmetro, neste exemplo, https://xyz.objectstorage.softlayer.net/AUTH_123, conforme mostrado abaixo:

Figura 6. Exemplo de solicitação e resposta para listagem de contêiner de armazenamento de objeto

Armazene os arquivos transferidos por upload na instância do serviço de armazenamento de objeto

Embora seja perfeitamente possível interagir com a API de armazenamento de objeto usando solicitações e respostas como as mostradas acima, uma solução mais fácil para desenvolvedores de aplicativo é usar o php-opencloud, um SDK de PHP para implementações baseadas em OpenStack. Esse SDK fornece um wrapper de PHP muito útil para os métodos da API Swift, portanto, basta chamar o método apropriado, por exemplo, createContainer() ou listContainers(). A biblioteca do cliente ficará responsável por formular a solicitação e decodificar a resposta. Porém, para depurar erros e, se desejar, executar alguma operação que ainda não tenha suporte no SDK, você precisará saber o que está acontecendo no plano de fundo.

O pacote do php-opencloud foi incluído no arquivo de dependência do editor, mostrado na Parte 01 deste artigo, portanto, ele já deve estar instalado no seu sistema de desenvolvimento. Antes de usá-lo, copie suas credenciais do serviço de armazenamento de objeto para o aplicativo PHP. Edite o arquivo $APP_ROOT/config.php e inclua as credenciais como no exemplo abaixo:

<?php
$config['settings']['object-storage']['url'] = "URL";
$config['settings']['object-storage']['region'] = "REGION";
$config['settings']['object-storage']['user'] = "USERNAME";
$config['settings']['object-storage']['pass'] = "PASSWORD";

Em seguida, use esta configuração para inicializar um novo cliente OpenStack usando o contêiner DI do Slim, incluindo o código abaixo no arquivo $APP_ROOT/public/index.php:

<?php
// Slim application initialization - snipped

// initialize dependency injection container
$container = $app->getContainer();

// add Object Storage service client
$container['objectstorage'] = function ($container) {
  $config = $container->get('settings');
  $openstack = new OpenStack\OpenStack(array(
    'authUrl' => $config['object-storage']['url'],
    'region'  => $config['object-storage']['region'],
    'user'    => array(
      'id'       => $config['object-storage']['user'],
      'password' => $config['object-storage']['pass']
  )));
  return $openstack->objectStoreV1();
};

Finalmente, atualize o manipulador de solicitação de POST /add no mesmo arquivo para usar esse cliente e salve o arquivo pdf atualizado na instância de armazenamento de objeto:

<?php
// Slim application initialization - snipped

// upload processor
$app->post('/add', function (Request $request, Response $response) {

  // get configuration
  $config = $this->get('settings');
  

  try {
    // check for valid file upload
    if (empty($_FILES['upload']['name'])) {
      throw new Exception('No file uploaded');
    }
    
    $finfo = new finfo(FILEINFO_MIME_TYPE);
    $type = $finfo->file($_FILES['upload']['tmp_name']);
    if ($type != 'application/pdf') {
      throw new Exception('Invalid file format');    
    }

    // convert uploaded PDF to text
    // connect to Watson document conversion API  
    // transfer uploaded file for conversion to text format
    $apiResponse = $this->converter->post(
      'v1/convert_document?version=2015-12-15', array('multipart' => array(
        array('name' => 'config', 
          'contents' => '{"conversion_target":"normalized_text"}'),
        array('name' => 'file', 
          'contents' => fopen($_FILES['upload']['tmp_name'], 'r'))
    )));
    
    // store response
    $text = (string)$apiResponse->getBody();
    unset($apiResponse);

    // extract keywords from text
    // connect to Watson/Alchemy API for keyword extraction 
    // transfer text content for keyword extraction
    // request JSON output
    $apiResponse = $this->extractor->post('text/TextGetRankedKeywords', 
      array('form_params' => array(
      'apikey' => $config['alchemy']['apikey'],
      'text' => strip_tags($text),
      'outputMode' => 'json'
    )));

    // process response
    // create keyword array
    $body = $apiResponse->getBody(); 
    $data = json_decode($body);
    $keywords = array();
    foreach ($data->keywords as $k) {
      $keywords[] = $k->text;
    }
    
    // save keywords to MongoDB
    $collection = $this->db->docs;
    $q = trim($_FILES['upload']['name']);
    $params = $request->getParams();
    $result = $collection->findOne(array('name' => $q));
    $doc = new stdClass;
    if (count($result) > 0) {
      $doc->_id = $result['_id'];
    }
    $doc->name = trim($_FILES['upload']['name']);
    $doc->keywords = $keywords;
    $doc->description = trim(strip_tags($params['description']));
    $doc->updated = time();
    $collection->save($doc);
    
    // save PDF to object storage
    $service = $this->objectstorage;
    $containers = $service->listContainers();
    
    $containerExists = false;
    foreach($containers as $c) {
      if ($c->name == 'documents') {
        $containerExists = true;
        break;
      }
    }
    
    if ($containerExists == false) {
      $container = $service->createContainer(array(
        'name' => 'documents'
      )); 
    } else {    
      $container = $service->getContainer('documents');
    }
      
    $stream = new Stream(fopen($_FILES['upload']['tmp_name'], 'r'));
    $options = array(
      'name'   => trim($_FILES['upload']['name']),
      'stream' => $stream,
    );
    $container->createObject($options);

    $response = $this->view->render($response, 'add.phtml', 
      array('keywords' => $keywords, 
        'object' => trim($_FILES['upload']['name']), 
        'router' => $this->router
    ));
    return $response;
    
  } catch (ClientException $e) {
    // in case of a Guzzle exception
    // display HTTP response content
    throw new Exception($e->getResponse());
  }

});

Uma parte desse código já foi mostrada na Parte 1: verificando o arquivo transferido por upload, convertendo-o em texto normalizado, extraindo palavras-chave e salvando-as em um banco de dados MongoDB. O restante do código começa com o uso do cliente de armazenamento de objeto para listar os contêineres disponíveis usando o método listContainers(). Em seguida, ele examina a lista de contêineres para verificar se existe um contêiner chamado documents e, se não existir, ele chamará o método createContainer() para criar um novo contêiner com esse nome. Ou, se o contêiner já existir, ele usará o método getContainer() para obter uma referência ao contêiner.

Quando uma referência ao contêiner documents for obtida, a próxima etapa será inicializar um novo fluxo a partir do documento pdf transferido por upload. Esse fluxo será passado ao método createObject() do contêiner como parte de uma array de opções, que inclui o nome desejado para o objeto no contêiner. O método createObject() é responsável por transferir e salvar o documento na instância de armazenamento de objeto como um objeto nomeado. Como você verá na Etapa 4, é possível usar o nome do objeto como uma chave para recuperar o documento pdf a qualquer momento.

Construa uma interface de procura

Depois de salvar os documentos e as palavras-chave, resta apenas desenvolver uma interface de procura que permita verificar rapidamente a lista de palavras-chave e encontrar documentos correspondentes.

Agora, lembre-se que as palavras-chave extraídas para cada pdf foram salvas como uma array de elementos de cadeia de caractere na propriedade keywords do documento do MongoDB correspondente. Para facilitar a procura dessa array, inclua o índice do texto na propriedade keywords usando um comando como este abaixo:

db.docs.createIndex({ keywords: "text" })

Aqui está um exemplo disso usando a interface do MongoLab:

Figura 7. Criação de índice de texto na coleção do MongoDB

Em seguida, inclua uma rota /search e um manipulador de retorno de chamada no aplicativo Slim no arquivo $APP_ROOT/public/index.php, conforme mostrado abaixo:

<?php
// Slim application initialization - snipped

$app->get('/search', function (Request $request, Response $response) {
  $params = $request->getQueryParams();
  $results = array();
  if (isset($params['q'])) {
    $q = trim(strip_tags($params['q']));
    if (!empty($q)) {
      $where = array(
        '$text' => array('$search' => $q) 
      );  
      $collection = $this->db->docs;
      $results = $collection->find($where)->sort(array('updated' => -1));    
    }
  }
  $response = $this->view->render($response, 'search.phtml', 
    array('router' => $this->router, 'results' => $results));
  return $response;
})->setName('search');

Esse retorno de chamada manipula as solicitações do terminal da URL /search e verifica se há uma cadeia de caractere de consulta na URL de solicitação. Caso seja encontrada uma cadeia de caractere de consulta, ele inicializará o cliente MongoDB e usará o método find()do cliente para gerar e executar uma consulta de procura do MongoDB no índice do texto. Primeiro, os resultados serão classificados mostrando os documentos com as atualizações mais recentes. O método find() retorna um cursor para essa coleção de resultados, e esse cursor é passado ao script de visualização para exibição.

Aqui está um exemplo desse script de visualização:

<div class="panel panel-default">
  <form method="get" 
    action="<?php echo $data['router']->pathFor('search'); ?>">
    <div class="input-group">
      <input type="text" name="q" class="form-control" 
        placeholder="Search for...">
      <span class="input-group-btn">
        <button type="submit" class="btn btn-default">Search</button>
      </span>
    </div>  
  </form>
</div>  

<?php if (isset($data['results']) && count($data['results'])): ?>
<h4>Search Results</h4>
<ul class="list-group row clearfix">
<?php foreach ($data['results'] as $doc): ?>
  <li class="list-group-item clearfix" style="border:none">
  <strong><?php echo $doc['name']; ?></strong> 
    <a href="<?php echo $data['router']->pathFor('download', 
      array('id' => $doc['name'])); ?>" 
      class="btn-sm btn-success">Download</a> <br /> 
  <?php echo $doc['description']; ?> <br /> 
  Last updated: <?php echo date('d M Y', $doc['updated']); ?> 
  <br /> 
  </li>
<?php endforeach; ?>
</ul>
<p>This operation made use of data generated through 
  <a href="http://www.ibm.com/smarterplanet/us/en/ibmwatson/">IBM Watson</a>
  and <a href="https://www.alchemyapi.com/">AlchemyAPI</a> services.</p>
<?php endif; ?>
</div>

Há dois componentes principais nesse script de visualização:

  • Um formulário de procura que contém um campo de entrada de texto para o usuário inserir uma ou mais palavras-chave. Ao enviar o formulário, os dados inseridos pelo usuário são enviados ao terminal da URL /search como uma solicitação de GET.
  • Um painel de resultados da procura que itera sobre a coleção de documentos do MongoDB retornada pelo manipulador /search e, para cada documento do resultado, exibe o nome, a descrição e a data da última atualização do documento. Cada entrada também contém um link para o terminal da URL /download que permite que o usuário faça o download do documento pdf correspondente a partir do serviço de armazenamento de objeto.

Aqui está um exemplo disso:

Figura 8. Formulário de procura e resultados

Recupere documentos a partir do armazenamento

O script de visualização mostrado na seção anterior inclui links para o terminal da URL /download, que deve permitir que os usuários façam download de um documento pdf a partir do serviço de armazenamento de objeto. Observe que a URL /download também inclui o nome do documento pdf.

Internamente, o manipulador de retorno de chamada do terminal /download deverá inicializar um novo cliente do Armazenamento de Objeto e, em seguida, usar os métodos do cliente para fazer o download do objeto binário correspondente, usando seu nome como segredo. Em seguida, esse objeto deverá ser enviado ao navegador do usuário como um fluxo para que possa ser salvo localmente.

Aqui está o código para o manipulador /download que executa todas as tarefas acima (clique aqui para ver a lista de documentos).

O manipulador acima inicializa um novo cliente do armazenamento de objeto e, em seguida, usa os métodos getContainer() e getObject() do cliente para obter uma referência ao contêiner documents e ao objeto especificado na URL de solicitação. Em seguida, ele usará o método download() do cliente para criar um fluxo contendo o arquivo pdf.

O objeto de resposta do Slim é modificado para incluir vários cabeçalhos, incluindo o Content-Type, Content-Disposition e o Content-Length, que informam ao navegador que há um objeto binário a seguir. Em seguida, o fluxo é anexado ao objeto de resposta com o método withBody() e a resposta completa é enviada ao cliente da solicitação.

Implementar no Bluemix

Neste momento, o aplicativo está concluído e pode ser implementado no Bluemix. Para fazer isso, primeiro crie o arquivo manifest do aplicativo em $APP_ROOT/manifest.yml, lembrando-se de usar um nome de host e de aplicativo exclusivo, anexando a ele uma cadeia de caracteres aleatória (como suas iniciais).

---
applications:
- name: pdf-keyword-search-[initials]
memory: 256M
instances: 1
host: pdf-keyword-search-[initials]
buildpack: https://github.com/cloudfoundry/php-buildpack.git
stack: cflinuxfs2

Por padrão, o buildpack de PHP do Cloud Foundry não inclui a extensão do MongoDB de PHP nem a extensão fileinfo (usada para validar uploads de arquivo pdf), portanto, deve-se configurar o buildpack para ativar essas extensões durante a implementação. Da mesma forma, deve-se configurar o buildpack para usar o diretório público do aplicativo como o diretório do servidor da web. Crie um arquivo $APP_ROOT/.bp-config/options.json com o conteúdo a seguir:

{
    "WEB_SERVER": "httpd",
    "PHP_EXTENSIONS": ["bz2", "zlib", "curl", "mcrypt", "mongo", "fileinfo"],
    "WEBDIR": "public",
    "PHP_VERSION": "{PHP_56_LATEST}"
}

Além disso, se você desejar que as credenciais dos serviços conversão de documento e armazenamento de objeto sejam obtidas automaticamente do Bluemix, atualize o script $APP_ROOT/public/index.php para usar a variável VCAP_SERVICES da seguinte forma:

<?php                
// include autoloader and configuration
require '../vendor/autoload.php';
require '../config.php';
                
// if BlueMix VCAP_SERVICES environment available
// overwrite with credentials from BlueMix
if ($services = getenv("VCAP_SERVICES")) {
  $services_json = json_decode($services, true);
  
  $config['settings']['document-conversion']['user'] = 
    $services_json["document_conversion"][0]["credentials"]["username"];
  $config['settings']['document-conversion']['pass'] = 
    $services_json["document_conversion"][0]["credentials"]["password"];

  $config['settings']['object-storage']['url'] = 
    $services_json["Object-Storage"][0]["credentials"]["auth_url"];
  $config['settings']['object-storage']['region'] = 
    $services_json["Object-Storage"][0]["credentials"]["region"];;
  $config['settings']['object-storage']['user'] = 
    $services_json["Object-Storage"][0]["credentials"]["userId"];;
  $config['settings']['object-storage']['pass'] = 
    $services_json["Object-Storage"][0]["credentials"]["password"];;
} 

// initialize application
$app = new \Slim\App($config);

// route callbacks - snipped

Agora é possível realizar o push do aplicativo para o Bluemix e, em seguida, ligar a ele os serviços de conversão de documento e armazenamento de objeto inicializados na Parte 01. Lembre-se de usar o ID correto para cada instância de serviço para assegurar-se de que elas sejam ligadas corretamente ao aplicativo.

shell> cf api https://api.ng.bluemix.net
shell> cf login
shell> cf push
shell> cf bind-service pdf-keyword-search-[initials] "Document Conversion-[id]"
shell> cf bind-service pdf-keyword-search-[initials] "Object Storage-[id]"
shell> cf restage pdf-keyword-search-[initials]

É possível começar a usar o aplicativo, navegando para o host especificado no manifest do aplicativo, por exemplo, http://pdf-keyword-search-[initials].mybluemix.net. Se aparecer uma página em branco ou outros erros, use o link na parte superior desta seção para depurar o código PHP e descobrir onde está o problema.

Conclusão

Este artigo focou na orquestração de vários serviços do Watson e do Bluemix para resolver um problema comum: filtrar rapidamente uma grande coleção de documentos para localizar somente aqueles que correspondem a determinadas palavras-chave. Ao combinar computação cognitiva com um armazenamento e uma infraestrutura em nuvem confiáveis e escaláveis e um pouco de PHP para fazer a ligação, foi demonstrado como é fácil para os desenvolvedores criarem soluções eficientes para procura e armazenamento de documentos na nuvem.

Se desejar experimentar os serviços discutidos neste artigo, comece com a demo live do aplicativo. Lembre-se que se trata de uma demo pública, portanto, tenha cuidado para não fazer upload de informações confidenciais (há também um botão “Reconfigurar sistema” muito útil, para apagar todos os dados transferidos por upload). Em seguida, faça download do código a partir de seu repositório GitHub e analise em detalhes para ver como tudo combina perfeitamente.

Este artigo focou somente um caso de uso específico, mas usando outros serviços do Bluemix e do Watson, é possível fazer mais combinações para criar outros aplicativos cognitivos para manipulação de documentos. Consulte os links na parte superior de cada seção para saber mais sobre o serviço de Conversão de Documento do Bluemix, a API de Extração de Palavra-chave AlchemyAPI, o serviço de armazenamento de objeto do Bluemix, a microestrutura Slim e outras ferramentas e técnicas usadas neste artigo.

Tenha uma boa codificação!