O Elasticsearch é um banco de dados orientado a documentos frequentemente listado como uma das ferramentas do Big Data. É desenvolvido em código Java, porém sua estrutura foi pensada para ser acessível por qualquer linguagem que capaz de comunicar-se por REST/HTTP. Para facilitar a vida dos programadores, existe uma série de clientes que encapsulam as operações, que normalmente seriam realizadas através de chamadas REST. Esse artigo irá apresentar o cliente para PHP, através de um exemplo prático pensado para um site de oferta de empregos.

A primeira atividade deve ser instalar o Elasticsearch em um servidor. Nesse exemplo vamos utilizar Linux, mas poderíamos também utilizar Windows. Em uma máquina com Java instalado, devemos baixar a última versão do site do Elasticsearch, desempacotá-la e executar o comando ./bin/elasticsearch. Se tudo ocorreu bem, podemos chamar localhost:9200 em um navegador, conforme ilustrado na Listagem 1 e o Elasticsearch irá retornar uma resposta JSON. Nessa resposta, o parâmetro name provavelmente irá variar para cada leitor, pois é escolhido de forma aleatória (em resumo, não se preocupe se a resposta JSON não for exatamente igual a Listagem 1).

Listagem 1. Criação do índice Elasticsearch.

  {
    "status" : 200,
    "name" : "Madeline Joyce",
    "version" : {
      "number" : "1.3.4",
      "build_hash" : "a70f3ccb52200f8f2c87e9c370c6597448eb3e45",
      "build_timestamp" : "2014-09-30T09:07:17Z",
      "build_snapshot" : false,
      "lucene_version" : "4.9"
    },
    "tagline" : "You Know, for Search"
  }

É importante conhecer alguns conceitos do Elasticsearch antes de um exemplo prático ser apresentado. Os conceitos principais são: índice, indexação e análise.

Um índice é o local onde serão armazenados os documentos enviados ao Elasticsearch. O processo de armazenamento de um documento é chamado de indexação, pois diferentemente dos bancos de dados tradicionais, as informações contidas nesse documento normalmente não serão armazenadas de forma crua, ou seja, não são guardadas na mesma forma que são enviadas ao Elasticsearch, mas antes são analisadas e transformadas. Essa análise é o que torna o Elasticsearch poderoso, já que através dela podemos comparar partes de texto e modificar as palavras contidas em um documento para que seja mais fácil encontrar termos semelhantes entre elas e os contidos durante uma busca.

A DevMedia possui os melhores cursos online de PHP do mercado, confira agora mesmo!

Primeiramente, devemos criar um índice que no exemplo desenvolvido nesse é artigo vamos chamar de “empregos”. Antes, porém, é recomendado fazer download do Sense - um plugin ou extensão para o navegador Chrome disponível na web Store do Google(ou de forma mais simples, vá às configurações do seu navegador e adicione a extensão Sense). Esse plugin torna mais fácil enviar comandos ao Elasticsearch.

Como ilustrado na Figura 1, o resultado será uma aplicação (acessível através do pequeno bonsai verde do lado da barra de endereços) que possui: uma caixa onde podemos definir o endereço de rede do servidor Elasticsearch ao qual queremos enviar um comando (o padrão é localhost:9200); uma janela onde podemos escrever o comando; e outra janela a direita onde será apresentada a resposta da execução desse comando.

Figura 1. Plugin Sense

A Listagem 2 apresenta a criação do índice. Um índice Elasticsearch pode ser entendido como um esquema do banco de dados relacional – ou seja, um ponto de acesso onde iremos criar as estruturas de armazenamento de dados -, e ainda que não seja o foco desse artigo, é importante explicar a estrutura do documento JSON que cria os índices. Nesse caso, estamos criando um analisador chamado sugestao_analizer, que vai permitir o desenvolvimento de uma feature de auto_complete na nossa aplicação PHP. Esse analisador é formado por duas partes principais: o tokenizer, que divide os textos presentes em cada campo em pedaços menos chamados tokens – nesse caso, divido nos espaços em branco já que estamos usando whitespace; e o filter, que transformará cada um dos tokens gerados na sua forma raiz, uma vez que estamos usando um filtro do tipo stemmer (remetendo a stem, em inglês raiz). Os analisadores são dependentes do domínio da aplicação que está sendo desenvolvida e fogem do escopo desse artigo. Mais informações podem ser encontradas na documentação oficial do Elasticsearch.

Listagem 2. Criação do índice Elasticsearch.

  PUT /empregos
  {
     "analysis": {
        "analyzer": { 
           "sugestao_analyzer": {
             "type": "custom",
             "tokenizer": "whitespace",
             "filter": [ "asciifolding", "stem_minimal_pt" ]
           }
        },
        "filter": 
    {
           "stem_minimal_pt": {
              "type": "stemmer",
              "language": "minimal_portuguese"
           }
        }
     }
  }

O próximo passo é adicionar um mapeamento para definir os campos, seus tipos e a forma que como cada um desses campos será analisado. A Listagem 3 apresenta o mapeamento com os seguintes campos que representam uma oferta de emprego: id, cidade, empresa, decricao e titulo. Todos os campos são do tipo string. É importante saber que o Elasticsearch permite mapear campos com outros tipos (por exemplo, long, integer, boolean) e definir mapeamentos mais complexos (como listas e objetos). Os campos cidade, empresa e titulo são chamados de multi-field, pois possuem subcampos que facilitam a vida do desenvolvedor pois a partir de um só valor enviado ao campo, outros valores serão criados de acordo com cada tipo de análise. Nesse exemplo, definimos esses campos como multi-field com o objetivo de permitir o armazenamento dessas informações de forma analisada e não analisada, definida respectivamente no campo padrão, ou seja, sem analisador e no campo raw, que leva o analisador not_analyzed. Como dito anteriormente, os campos que não possuem um analisador, são armazenados após processados pelo analisador padrão – standard. Para mais detalhes sobre o analisador standard visite a documentação oficial do Elasticsearch. Finalmente, o subcampo sugestao (parte do campo titulo) possui como analisador o sugestao_analizer definido anteriormente, e seu tipo é completion, para que seja possível criar uma função de autocomplete, explicada mais adiante nesse artigo.

Listagem 3. Mapeamento.

  PUT empregos/emprego/_mapping
  {
      "emprego" : {
         "properties" : {
           "id" : {
              "type" : "string"
           },
           "cidade" : {
              "type" : "string",                
              "fields" : {
                      "raw" : {
                    "type" : "string",
                    "index" : "not_analyzed"
                 }
                  }
           },
           "empresa" : {
              "type" : "string",                
              "fields" : {
                      "raw" : {
                    "type" : "string",
                    "index" : "not_analyzed"
                 }
                  }
           },
           "descricao" : {
              "type" : "string"
           },         
           "titulo" : {
              "type": "string",
              "index" : "analyzed",                
              "fields" : {
                      "sugestao" : {
                    "type" : "completion",
                    "analyzer" : "sugestao_analyzer"
                 },
                      "raw" : {
                    "type" : "string",
                    "index" : "not_analyzed"
                 }
              }
           }
        }
      }
  }

Antes de continuar, é importante notar que os comandos do Elasticsearch são sempre definidos em JSON, não importando se estamos utilizando a REST API, Java, PHP ou outra linguagem como cliente. Por isso, é importante que o desenvolvedor tenha em mente que cada chamada feita ao Elasticsearch será transformada em um texto que representa um comando no formato JSON. Na maioria dos comandos vamos definir o índice, o tipo que vamos acessar (ou seja, o nome do mapeamento) e um documento que contém uma série de valores de campos. Esse documento pode ser extremamente complexo, porém para os propósitos desse artigo considere comando REST definido na Listagem 4, contendo uma oportunidade de emprego que será inserida ao índice empregos criado anteriormente.

Listagem 4. Inserção um emprego.

  POST empregos/emprego
  {
  "cidade" : "Lins-SP",
  "empresa" : "XPTO",
  "titulo" : "Programador Java",
  "descricao" : "Precisamos de um programador Java que conheça PHP e Elasticsearch"
  }

Como o foco desse artigo é PHP, vamos para a obtenção do mesmo resultado, emulando a REST API através do cliente PHP. Para tal, é recomendado utilizar o framework composer que irá recuperar as dependências do cliente PHP e gerar um pacote para facilitar o gerenciamento dessas dependências. Mais detalhes sobre o composer pode ser encontrada em https://getcomposer.org/. Para nós, o importante é que será gerada uma pasta chamada vendor, que conterá o arquivo autoload.php, a única dependência a qual iremos fazer referência nos códigos a seguir.

Finalmente, podemos agora desenvolver o cliente PHP que se conectará ao índice que acabamos de criar. Como a maioria dos códigos desse artigo, vamos utilizar um array, que internamente será transformado em um documento JSON. Para cada servidor que nossa aplicação pode se conectar, vamos adicionar um elemento no array, contendo o IP e sua porta. Se não especificarmos o IP, o padrão será localhost e sem a porta o padrão será 9200. Na Listagem 5 escolhemos o IP 134.172.18.35 e a porta 9008.

Listagem 5. Criação do cliente

   require "vendor/autoload.php";
   
   $ELSEARCH_SERVER = array("134.172.18.35:9008");
   $conn = array();
   $conn["hosts"] = $ELSEARCH_SERVER;
   
   $client = new Elasticsearch\Client($conn);

Existem duas formas de estruturar os comandos que serão enviados ao cluster. É importante recordar que o objetivo desses códigos em PHP é emular o comportamento da API REST especificado na Listagem 4. Por isso, na Listagem 6 ilustramos como podemos inserir um documento ao índice empregos. Para tal, vamos usar o cliente criado na Listagem 5 e criar um array associativo em PHP que contém a mesma estrutura hierárquica de um documento JSON. Esse array será completado com variáveis para cidade, empresa, descrição e título - cujos valores poderiam vir, por exemplo, de um formulário -, e o comando index do objeto client que enviará os dados para o Elasticsearch.

Listagem 6. Inserção de um documento representado uma oferta de emprego.

   $params = array();
   //indice
   $params["index"] = "empregos";
   $params["type"]  = "emprego";   
   
   $params["body"] = array(
    "cidade" => $cidade,
    "empresa" => $empresa,
    "descricao" => $descricao,
    "titulo" => $titulo
   );
    
   try {
    //chamada ao server
    $results = $client->index($params);
   } catch (Exception $e) {
    echo "Caught exception: ",  $e->getMessage(), "\n";
   }

Na Listagem 7 vemos um código para recuperar todos os empregos do cluster. O objetivo nesse exemplo é traduzir uma consulta do tipo matchAll (para saber mais clique aqui ), para o código PHP. Essa consulta irá retornar todos os empregos que estão armazenados no índice empregos através de um array de hits, que poderá ser percorrido para manusear seus resultados. Cada hit presente nesse array conterá os valores do mapeamento definido anteriormente, e poderemos acessar uma posição específica do array através do nome do campo. Note que a estrutura do array associativo será traduzida para um arquivo JSON e a função search do objeto client irá enviar a consulta do Elasticsearch.

Listagem 7. Recuperar todos os empregos do cluster.

           
   //indice
   $searchParams['index'] = 'empregos';
   $searchParams['type']  = 'emprego';    
    
   //busca
   $searchParams['body']['match_all'] = ''
   
   //chamada ao server
   $results = $client->search($searchParams);
      $productIds = array();
   
   
      if (!empty($results['hits']['hits'])) {
          foreach ($results['hits']['hits'] as $hit) {
          echo '<br>';
          echo $hit['_source']['titulo'];
          echo '<br>';
          echo $hit['_source']['descricao'];
          echo '<br>';
          echo $hit['_source']['cidade'];
          echo '<br>';
          echo $hit['_source']['empresa'];=
          }
      }

No próximo exemplo, apresentado na Listagem 8, combina dois tipos – os mais simples do Elasticsearch - de busca: match, que retorna dados após realizar a análise do texto contido no campo buscado; term, que retornará apenas valores que sejam exatamente iguais ao item buscado. Essas duas consultas combinadas irão retornar apenas documentos que contenham a palavra “Java” no campo titutlo - ainda que não esteja com letra maiúscula como “java”, ou esteja no meio de uma frase “PHP é mais legal que Java” ou seja parte de uma outra palavra como “Javascript” – e exatamente o texto “Lins-SP” no campo cidade.

Listagem 8. Recuperar os empregos que contenham a palavra Java no campo titulo e que estejam localizados em Lins-SP.

  POST /empregos/emprego/_search
  {
      "query" : {
          "bool": {
        "should": [
          {
            "term" : {
              "cidade.raw" : "Lins-SP"
            }
          },  
          {
            "match" : {
              "titulo" : "Java"
            }
          }
        ]
      }
    }
  }

O código PHP da Listagem 9 modifica a estrutura da busca para ilustrar que também podemos utilizar um objeto array, no lugar de um array associativo. Essa consulta contém a mesma estrutura da Listagem 8 e retorna um array de hits que deverá ser percorrido para manusear os resultados, como ilustrado na Listagem 7.

Listagem 8. Recuperar os empregos que contenham a palavra Java no campo titulo e que estejam localizados em Lins-SP.

   //indice
   $searchParams['index'] = 'empregos';
   $searchParams['type']  = 'emprego';    
    
   $searchParams['body']['query']['bool']['should'][] = array(
    "match" => 'Java',
    "cidade.raw"  => 'Lins-SP'
   );
   
   //chamada ao server
   $results = $client->search($searchParams);
   $productIds = array();
   
      if (!empty($results['hits']['hits'])) {
          foreach ($results['hits']['hits'] as $hit) {
          //manusear resultados            
          }
      }

Um recurso muito interessante do Elasticsearch é criar uma função de auto complete. Podemos modificar a busca das listagens anteriores para buscar, por exemplo, por complementos para a palavra Java. Os resultados da busca são dependentes dos valores que estão armazenados no índice, mas essa busca poderia retornar valores como Java, JavaEE e JavaScript. Na Listagem 9 mostra-se como realizar uma busca com suggest no campo titulo.sugestao.

Listagem 9. Autocomplete com a REST API

  POST /empregos/_suggest
  {
          "sugestao_cidade" : {
           "text" : "Ja",
           "completion" : {
                      "field" : "titulo.sugestao"
           }
         }
  }

Na Listagem 10 apresenta a versão em PHP do código apresentado anteriormente para a sugestão. Nesse caso, o array irá passar ao campo titulo.sugestao do índice empregos o valor “Ja” e irá recuperar de uma lista de options os valores possíveis para completar a palavra.

Listagem 10. Recuperar sugestões para o campo titulo.sugestao.

  //query
   $query = 'Ja';
   
   //indice
   $searchParams['index'] = 'empregos';
   $searchParams['type']  = 'emprego';    
    
   //busca
   $searchParams['sugestao_categoria']['text'] = $query;
   $searchParams['sugestao_categoria']['text']['completion']['field'] = 'titulo.sugestao';
    
   //chamada ao server
   $results = $client->suggest($searchParams);
   
      if (!empty($results['sugestao_categoria'])) {
          foreach ($results['sugestao_categoria']['options'] as $option) {
            echo '<br>';
            echo $option['text'];
          }
      }

Finalmente, apresenta-se também como criar listas de valores agregados de acordo com a cidade referentes a uma oferta de emprego. Esses valores podem ser usados para criar listas laterais que contenham os valores de cidade e a respectiva quantidade de empregos, como por exemplo, “Campinas (12), Rio de Janeiro (50), São Paulo (100)”. O código da Listagem 11 apresenta-se como criar essa agregação usando a REST API.

Listagem 11. Agregação no campo cidades através da REST API.

  POST /empregos/emprego/_search
  {
      "query" : {
              ….parâmetros de consulta
      },
      "aggs" : {
          "contador_cidade": {
              "terms" : {
               "field" : "cidade" 
              }
     }
  }

Na Listagem 12 podemos ver que foi adicionado dois parâmetros aggs ao código da Listagem 8, posteriormente, em lugar dos hits, deveremos percorrer um array de aggregations, que contém um campo key para cada palavra e um campo doc_count para a quantidade de documentos da agregação.

Listagem 12. Recuperando uma agregação no campo de cidades.

   
   //indice
   $searchParams['index'] = 'empregos';
   $searchParams['type']  = 'emprego';    
    
   $searchParams['body']['query']['bool']['should'][] = array(
    "match" => 'Java'
   );
   
   $searchParams['body']['aggs']['contador_cidade']['terms']['field'] = 'cidade.raw';
   
   //chamada ao server
   $results = $client->search($searchParams);
     foreach ($results['aggregations']['contador_cidade']['buckets'] as $agg) {
          echo $agg['key'];
          echo $agg['doc_count'];
      }

Esse artigo introduziu o uso de Elasticsearch para PHP. O Elasticsearch é uma ferramenta muito poderosa para o desenvolvimento de aplicações baseadas em textos, como por exemplo, um site que contém ofertas de emprego. Atualmente, o Elasticsearch é usado por grandes players do mercado como Foursquare, Github e Globo.com, por isso seu conhecimento pode ser um importante diferencial. Mais interessante é saber que, conforme ilustrado nesse artigo, seu uso é simples e praticamente qualquer linguagem do mercado possui clientes que facilitam muito o desenvolvimento de aplicações baseadas nesse framework.

Para expandir o conhecimento do leitor e fixar o que foi apresentado até aqui, proponho o desafio de configurar o Elasticsearch e desenvolver os códigos necessários para inserir currículos que possam ser buscados e talvez combinados com as ofertas presentes no índice. Vai encarar?