diff --git a/jobeet/pt_BR/17.markdown b/jobeet/pt_BR/17.markdown new file mode 100644 index 0000000..40779f3 --- /dev/null +++ b/jobeet/pt_BR/17.markdown @@ -0,0 +1,646 @@ +Dia 17: Busca +=============== + +No dia 14, adicionamos alguns feeds para manter os usuários do Jobeet +atualizados com as novas ofertas de emprego. Hoje vamos ajudar você a melhorar +a experiência do usuário implementando a última das funcionalidades principais +do site do Jobeet: o motor de busca. + +A Tecnologia +------------ + +Antes entrarmos de cabeça no assunto, vamos falar um pouco sobre a história do +symfony. Defendemos muito as melhores práticas, como os testes e a +refatoração e nós também tentamos aplicá-las ao framework. Por exemplo, gostamos +do famoso lema "Não reinventar a roda". + +Na realidade, o symfony framework iniciou sua vida quatro anos atrás como sendo +a cola entre dois softwares Open-Source existentes: Mojavi e Propel. E todas as +vezes que precisávamos resolver um novo problema, procurávamos uma biblioteca +que já existia e que fizésse bem o trabalho antes de codificarmos nós mesmos +um do zero. + +Agora, queremos adicionar um motor de busca no Jobeet, e o Zend Framework +fornece uma excelente biblioteca, chamada +[Zend Lucene](http://framework.zend.com/manual/en/zend.search.lucene.html), +que é uma versão do projeto bastante conhecido Java Lucene. Em vez de criarmos +outro motor de busca para o Jobeet, que seria uma tarefa bem complexa, vamos +usar o Zend Lucene. + +Na página da documentação do Zend Lucene, a biblioteca é descrita assim: + +>... um motor de busca de texto de uso geral escrita completamente em PHP 5. +>Como ele armazena seu índice no sistema de arquivos e não precisa de um +>servidor de banco de dados, ele pode fornecer capacidades de busca para quase +>toda site baseado em PHP. O Zend_Search_Lucene tem suporte as seguintes +>funções: +> +> * Busca classificada - melhores resultados são retornados primeiramente +> * Vários tipos poderosos de pesquisa: pesquisas por frase, pesquisas +> booleanas, pesquisas com curingas, pesquisas por proximidade, pesquisas + por intervalos e muitas outras +> * Busca por campo específico (e.g. título, autor, conteúdo) + +- + +>**NOTE** +>O dia de hoje não é um tutorial sobre a biblioteca Zend Lucene, mas sim sobre +>como integrá-la no site do Jobeet; ou de modo geral, como integrar bibliotecas +>de terceiros em um projeto symfony. Se você quiser mais informações sobre essa +>tecnologia, consulte a +>[documentação do Zend Lucene](http://framework.zend.com/manual/en/zend.search.lucene.html). + +Instalando e Configurando o Zend Framework +------------------------------------------ + +A biblioteca Zend Lucene é parte do Zend Framework. Nós iremos instalar o +Zend Framework no diretório `lib/vendor/`, juntamente com o próprio symfony +framework. + +Primeiro, baixamos o +[Zend Framework](http://framework.zend.com/download/overview) e descompactamos +os arquivos, assim ficamos com um diretório `lib/vendor/Zend/`. + +>**NOTE** +>As explicações a seguir foram testadas na versão 1.10.3 do Zend Framework. + +- + +>**TIP** +>Você pode limpar o diretório removendo tudo exceto os seguintes arquivos +>e diretórios: +> +> * `Exception.php` +> * `Loader/` +> * `Autoloader.php` +> * `Search/` + +Depois, adicione o seguinte código à classe `ProjectConfiguration` para +proporcionar uma maneira simples de registrar o autoloader do Zend: + + [php] + // config/ProjectConfiguration.class.php + class ProjectConfiguration extends sfProjectConfiguration + { + static protected $zendLoaded = false; + + static public function registerZend() + { + if (self::$zendLoaded) + { + return; + } + + set_include_path(sfConfig::get('sf_lib_dir').'/vendor'.PATH_SEPARATOR.get_include_path()); + require_once sfConfig::get('sf_lib_dir').'/vendor/Zend/Loader/Autoloader.php'; + Zend_Loader_Autoloader::getInstance(); + self::$zendLoaded = true; + } + + // ... + } + +Indexando +--------- + +O motor de busca do Jobeet deve ser capaz de retornar todos os empregos que +corresponderem as palavras-chave informadoas pelo usuário. Antes de poder +buscar algo, um índice precisa ser criado para os empregos; para o Jobeet, ele +será armazenado no diretório `data/`. + + +O Zend Lucene fornece dois métodos para recuperar um índice dependendo se um +já existe ou não. Vamos criar um método helper na classe `JobeetJobPeer` que +retorna um índice existente ou cria um novo para a gente: + + +O Zend Lucene fornece dois métodos para recuperar um índice dependendo se um +já existe ou não. Vamos criar um método helper na classe `JobeetJobTable` que +retorna um índice existente ou cria um novo para nós: + + + [php] + + // lib/model/JobeetJobPeer.php + + + // lib/model/doctrine/JobeetJobTable.class.php + + static public function getLuceneIndex() + { + ProjectConfiguration::registerZend(); + + if (file_exists($index = self::getLuceneIndexFile())) + { + return Zend_Search_Lucene::open($index); + } + + return Zend_Search_Lucene::create($index); + } + + static public function getLuceneIndexFile() + { + return sfConfig::get('sf_data_dir').'/job.'.sfConfig::get('sf_environment').'.index'; + } + +### O método `save()` + +Toda vez que um emprego é criado, atualizado ou excluído, o índice precisa ser +atualizado. Altere `JobeetJob` para atualizar o índice sempre que um emprego +for serializado no banco de dados: + + + [php] + // lib/model/JobeetJob.php + public function save(PropelPDO $con = null) + { + // ... + + $ret = parent::save($con); + + $this->updateLuceneIndex(); + + return $ret; + } + + + [php] + public function save(Doctrine_Connection $conn = null) + { + // ... + + $ret = parent::save($conn); + + $this->updateLuceneIndex(); + + return $ret; + } + + +E crie o método `updateLuceneIndex()` que faz o trabalho em si: + + [php] + + // lib/model/JobeetJob.php + + + // lib/model/doctrine/JobeetJob.class.php + + public function updateLuceneIndex() + { + + $index = JobeetJobPeer::getLuceneIndex(); + + + $index = JobeetJobTable::getLuceneIndex(); + + + // remove as entradas existentes + foreach ($index->find('pk:'.$this->getId()) as $hit) + { + $index->delete($hit->id); + } + + // não indexa os empregos expirados nem os inativos + if ($this->isExpired() || !$this->getIsActivated()) + { + return; + } + + $doc = new Zend_Search_Lucene_Document(); + + // armazena a chave primária do emprego para identificá-lo nos resultados da busca + $doc->addField(Zend_Search_Lucene_Field::Keyword('pk', $this->getId())); + + // indexa os campos do emprego + $doc->addField(Zend_Search_Lucene_Field::UnStored('position', $this->getPosition(), 'utf-8')); + $doc->addField(Zend_Search_Lucene_Field::UnStored('company', $this->getCompany(), 'utf-8')); + $doc->addField(Zend_Search_Lucene_Field::UnStored('location', $this->getLocation(), 'utf-8')); + $doc->addField(Zend_Search_Lucene_Field::UnStored('description', $this->getDescription(), 'utf-8')); + + // adiciona o emprego no índice + $index->addDocument($doc); + $index->commit(); + } + +Como o Zend Lucene não é capaz de atualizar uma entrada existente, ela primeiro +é removida se o emprego já existe no índice. + +Indexar o próprio emprego é simples: a chave primária é armazenada para +referência futura quando estivermos buscando empregos e as colunas principais +(`position`, `company`, `location` e `description`) são indexados mas não +armazenados no índice pois usaremos os objetos reais para mostrar os resultados. + +### Transações do ##ORM## + +E se houver um problema na indexação de um emprego, ou se o emprego não for +salvo no banco de dados? Tanto o ##ORM## quanto o Zend Lucene irão lançar uma +exceção. Mas em algumas circunstâncias, poderemos ter um emprego salvo no banco +de dados sem um índice correspondente. Para prevenir que isso acontece, podemos +envolver as duas atualizações dentro de uma transação e fazer um roolback em +caso de erro: + + + [php] + // lib/model/JobeetJob.php + public function save(PropelPDO $con = null) + { + // ... + + if (is_null($con)) + { + $con = Propel::getConnection(JobeetJobPeer::DATABASE_NAME, Propel::CONNECTION_WRITE); + } + + $con->beginTransaction(); + try + { + $ret = parent::save($con); + + $this->updateLuceneIndex(); + + $con->commit(); + + return $ret; + } + catch (Exception $e) + { + $con->rollBack(); + throw $e; + } + } + + + [php] + // lib/model/doctrine/JobeetJob.class.php + public function save(Doctrine_Connection $conn = null) + { + // ... + + $conn = $conn ? $conn : $this->getTable()->getConnection(); + $conn->beginTransaction(); + try + { + $ret = parent::save($conn); + + $this->updateLuceneIndex(); + + $conn->commit(); + + return $ret; + } + catch (Exception $e) + { + $conn->rollBack(); + throw $e; + } + } + + +### `delete()` + +Também precisamos sobrescrever o método `delete()` para remover do índice a +entrada referente ao emprego excluído: + + + [php] + // lib/model/JobeetJob.php + public function delete(PropelPDO $con = null) + { + $index = JobeetJobPeer::getLuceneIndex(); + + foreach ($index->find('pk:'.$this->getId()) as $hit) + { + $index->delete($hit->id); + } + + return parent::delete($con); + } + + + [php] + // lib/model/doctrine/JobeetJob.class.php + public function delete(Doctrine_Connection $conn = null) + { + $index = JobeetJobTable::getLuceneIndex(); + + foreach ($index->find('pk:'.$this->getId()) as $hit) + { + $index->delete($hit->id); + } + + return parent::delete($conn); + } + + + +### Exclusão em Massa + +Sempre que carregamos as fixtures com o comando `propel:data-load`, o symfony +remove todos os registros de emprego chamando o método +`JobeetJobPeer::doDeleteAll()`. Vamos sobrescrever o comportamento padrão para +também apagar o índice completamente: + + [php] + // lib/model/JobeetJobPeer.php + public static function doDeleteAll($con = null) + { + if (file_exists($index = self::getLuceneIndexFile())) + { + sfToolkit::clearDirectory($index); + rmdir($index); + } + + return parent::doDeleteAll($con); + } + + +Buscando +-------- + +Agora que temos tudo no seu lugar, você pode recarregar os dados do fixture +para indexá-los: + + $ php symfony propel:data-load + +>**TIP** +>Para usuários Unix-like: como o índice é modificado pela linha de comando e +>também pela web, você precisa alterar as permissões do dirétorio index +>adequadamente de acordo com sua configuração: verifique se tanto o usuário +>que você usa na linha de comando quanto o usuário do servidor web possam +>escrever no diretório index. + +- + +>**NOTE** +>Você pode estar recebendo alguns alertas de erro sobre a classe `ZipArchive` +>se você não tiver a extensão `zip` compilada no seu PHP. Esse é um bug +>conhecido da classe `Zend_Loader`. + +A implementação da busca no frontend é muito fácil. Primeiro, crie uma rota: + + [yml] + job_search: + url: /search + param: { module: job, action: search } + +E a action correspondente: + + [php] + // apps/frontend/modules/job/actions/actions.class.php + class jobActions extends sfActions + { + public function executeSearch(sfWebRequest $request) + { + $this->forwardUnless($query = $request->getParameter('query'), 'job', 'index'); + + + $this->jobs = JobeetJobPeer::getForLuceneQuery($query); + + + $this->jobs = Doctrine_Core::getTable('JobeetJob') + ➥ ->getForLuceneQuery($query); + + } + + // ... + } + +>**NOTE** +>O novo método `forwardUnless()` encaminha o usuário para a action `index` do +>módulo `job` se a parâmetro `query` da requisição não existir ou for vazio. +> +>Ele é apenas um atalho para a seguinte declaração mais longa: +> +> if (!$query = $request->getParameter('query')) +> { +> $this->forward('job', 'index'); +> } + +O template também é bastante simples: + + [php] + // apps/frontend/modules/job/templates/searchSuccess.php + + +
+ $jobs)) ?> +
+ +A própria busca é delegada para o método `getForLuceneQuery()`: + + + [php] + // lib/model/JobeetJobPeer.php + static public function getForLuceneQuery($query) + { + $hits = self::getLuceneIndex()->find($query); + + $pks = array(); + foreach ($hits as $hit) + { + $pks[] = $hit->pk; + } + + $criteria = new Criteria(); + $criteria->add(self::ID, $pks, Criteria::IN); + $criteria->setLimit(20); + + return self::doSelect(self::addActiveJobsCriteria($criteria)); + } + + + [php] + // lib/model/doctrine/JobeetJobTable.class.php + public function getForLuceneQuery($query) + { + $hits = self::getLuceneIndex()->find($query); + + $pks = array(); + foreach ($hits as $hit) + { + $pks[] = $hit->pk; + } + + if (empty($pks)) + { + return array(); + } + + $q = $this->createQuery('j') + ->whereIn('j.id', $pks) + ->limit(20); + + $q = $this->addActiveJobsQuery($q); + + return $q->execute(); + } + + +Depois de pegarmos todos os resultados do índice do Lucene, excluímos os +empregos inativos e limitamos em `20` o número de resultados. + +Para fazer isso funcionar, atualize o layout: + + [php] + // apps/frontend/templates/layout.php +

Ask for a job

+
+ + +
+ Enter some keywords (city, country, position, ...) +
+
+ +>**NOTE** +>O Zend Lucene define um linguagem de pesquisa rica que suporta operações como +>Booleanos, caracteres curinga, pesquisa fuzzy e muito mais. Tudo está +>documentado no +>[manual do Zend Lucene](http://framework.zend.com/manual/en/zend.search.lucene.query-api.html) + +Testes Unitários +---------------- + +Que tipo de testes unitários precisamos criar para testar o motor de busca? +Obviamente não iremos testar a própria biblioteca Zend Lucene, mas sim sua +integração com a classe `JobeetJob`. + +Adicione os seguintes testes no fim do arquivo `JobeetJobTest.php` e não +esqueça de atualizar o número de testes no ínicio do arquivo para `7`: + + [php] + // test/unit/model/JobeetJobTest.php + $t->comment('->getForLuceneQuery()'); + $job = create_job(array('position' => 'foobar', 'is_activated' => false)); + $job->save(); + + $jobs = JobeetJobPeer::getForLuceneQuery('position:foobar'); + + + $jobs = Doctrine_Core::getTable('JobeetJob')->getForLuceneQuery('position:foobar'); + + $t->is(count($jobs), 0, '::getForLuceneQuery() does not return non activated jobs'); + + $job = create_job(array('position' => 'foobar', 'is_activated' => true)); + $job->save(); + + $jobs = JobeetJobPeer::getForLuceneQuery('position:foobar'); + + + $jobs = Doctrine_Core::getTable('JobeetJob')->getForLuceneQuery('position:foobar'); + + $t->is(count($jobs), 1, '::getForLuceneQuery() returns jobs matching the criteria'); + $t->is($jobs[0]->getId(), $job->getId(), '::getForLuceneQuery() returns jobs matching the criteria'); + + $job->delete(); + + $jobs = JobeetJobPeer::getForLuceneQuery('position:foobar'); + + + $jobs = Doctrine_Core::getTable('JobeetJob')->getForLuceneQuery('position:foobar'); + + $t->is(count($jobs), 0, '::getForLuceneQuery() does not return deleted jobs'); + +Testamos para que um emprego inativo ou um deletado não seja mostrado nos +resultados da busca; nós também verificamos para que os empregos que +correspondam ao critério informado apareçam nos resultados. + +Comandos +-------- + +Por fim, temos que criar um comando para limpar do índice as entradas obsoletas +(por exemplo quando o emprego expira) e otimizar o índice de tempos em tempos. +Como já temos um comando para limpeza, vamos atualizá-lo para adicionar essas +funcionalidades: + + [php] + // lib/task/JobeetCleanupTask.class.php + protected function execute($arguments = array(), $options = array()) + { + $databaseManager = new sfDatabaseManager($this->configuration); + + + // limpeza do índice do Lucene + $index = JobeetJobPeer::getLuceneIndex(); + + $criteria = new Criteria(); + $criteria->add(JobeetJobPeer::EXPIRES_AT, time(), Criteria::LESS_THAN); + $jobs = JobeetJobPeer::doSelect($criteria); + + + // limpeza do índice do Lucene + $index = JobeetJobTable::getLuceneIndex(); + + $q = Doctrine_Query::create() + ->from('JobeetJob j') + ->where('j.expires_at < ?', date('Y-m-d')); + + $jobs = $q->execute(); + + foreach ($jobs as $job) + { + if ($hit = $index->find('pk:'.$job->getId())) + { + $index->delete($hit->id); + } + } + + $index->optimize(); + + $this->logSection('lucene', 'Cleaned up and optimized the job index'); + + // Remove empregos obsoletos + + $nb = JobeetJobPeer::cleanup($options['days']); + + $this->logSection('propel', sprintf('Removed %d stale jobs', $nb)); + + + $nb = Doctrine_Core::getTable('JobeetJob')->cleanup($options['days']); + + $this->logSection('doctrine', sprintf('Removed %d stale jobs', $nb)); + + } + +Esse comando remove todos os empregos expirados do índice e depois faz a +otimização dele graças ao método `optimize()` embutido no Zend Lucene. + +Considerações Finais +-------------------- + +Ao longo do dia, implementamos um motor de busca completo com várias +funcionalidades em menos de uma hora. Toda vez que você quiser adicionar uma +nova funcionalidade nos seus projetos, verifique se essa solução já não existe +pronta em algum lugar. + +Primeiro, verifique se o que precisa já não é implementado nativamente no +[symfony framework](http://www.symfony-project.org/api/1_4/). Depois, verifique +os [plugins do symfony](http://www.symfony-project.org/plugins/). E não se +esqueça de verificar as +[bibliotecas do Zend Framework](http://framework.zend.com/manual/en/) e também +as do [ezComponent](http://ezcomponents.org/docs). + +Amanhã iremos usar alguns JavaScripts não obstrusivos para melhorar a +responsividade do motor de busca, atualizando os resultados em tempo real +enquanto o usuário estiver digitando na caixa de busca. É claro, será o momento +de falarmos sobre AJAX com o symfony. + +Tomorrow we will use some unobtrusive JavaScripts to enhance the responsiveness +of the search engine by updating the results in real-time as the user types in +the search box. Of course, this will be the occasion to talk about how to use +AJAX with symfony. + +Feedback +-------- +>**Dica - pt_BR** +>Este capítulo foi traduzido por **Rogerio Prado de Jesus**. +>Se encontrar algum erro que deseja corrigir ou quiser fazer algum comentário +>não deixe de enviar um e-mail para **rogeriopradoj [at] gmail.com** + +>**Tip - en** +>This chapter was translated by **Rogerio Prado Jesus**. +>If you find any errors to be corrected or you have any comments +>do not hesitate to send an email to **rogeriopradoj [at] gmail.com** + +__ORM__