Categorias
CakePHP Desenvolvimento Web PHP Projetos

Fulltext Search – Busca Textual com Postgres – Parte 3 (Final)

Encerramos aqui essa breve introdução sobre busca textual (fulltext search) com Postgres apresentando um plugin que pode te auxiliar (se você utiliza CakePHP + Postgres) na implementação da busca – caso não use, talvez sirva de inspiração para um fork.

A história desse plugin vem lá de 2015 quando precisei incluir em uma busca que desconsidera-se pequenos erros de grafia e permitia o uso de sinônimos. Tínhamos restrições de hardware para implementar a busca – era uma aplicação nova, com orçamento pequeno e dispunha de um servidor com apenas 2 GB de RAM. Usar Elasticsearch seria inviável.

Na época o CakePHP tinha recém chego a versão 3.0, com um ORM todo remodelado, muito mais flexível e extensível do que nas versões anteriores. Pensei: fácil, vou estender ele e adicionar suporte aos novos tipos de dados e índices (tsvector, GIN e GIST, como vimos anteriormente). Bom, na prática não era tão fácil, a extensibilidade ainda era pequena, havia muitas dependências acopladas. A solução foi criar um shell que era invocado a cada X minutos e regerava a tabela de buscas com os tipos e índices apropriados usando SQL puro. Funcionou e foi o suficiente porque o sistema era novo e não tinha milhões de registros – já prevíamos que escalando o banco de dados, precisaríamos de mais hardware para extrair a rotina de busca.

Mais de 6 anos se passaram, agora estamos com o CakePHP 4.2 e seu ORM muito mais flexível e desacoplado. E mais uma vez me foi dado o desafio de puxar uma busca que utilizava o Elasticsearch para dentro do banco de dados principal (Postgres). Assim nasceu o autopage/pg-search.

Considerando que você tenha uma instalação do CakePHP > 4.2.2, a instalação começa com composer:

$ composer require autopage/pg-search

Em seguida, carregue o plugin na sua aplicação:

$ bin/cake plugin load Autopage/PgSearch

Por último, precisamos configurar sua aplicação para utilizar o driver Postgres fornecido. Para isso, edite seu arquivo de configuração (ou variável de ambiente, se utilizar) config/app.php ou config/app_local.php:

// No ínicio do arquivo
use Autopage\PgSearch\Database\Driver\Postgres;
...

return [
...
// Dentro da configuração dos datasources
...
    'Datasources' => [
        'default' => [
            'driver' => Postgres::class,
...

Pronto, você já pode criar migrations (não depende disso), fixtures, Tables e querys com o CakePHP.

Uma migration poderia ser:

<?php
declare(strict_types=1);

use Migrations\AbstractMigration;
use Phinx\Util\Literal;

class CriaTabelaBuscas extends AbstractMigration
{
    /**
     * More information on this method is available here:
     * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
     * @return void
     */
    public function change()
    {
        $tabela = $this->table('buscas');
        $tabela
            ->addColumn('original_id', 'integer', ['null' => false, 'default' => null])
            ->addColumn('nome', 'string', ['limit' => 255, 'null' => false, 'default' => null])
            ->addColumn('data', 'date', ['null' => false, 'default' => null])
            ->addColumn('conteudo', 'text', ['null' => false, 'default' => null])
            ->addColumn('conteudo_fts', Literal::from('tsvector'), ['null' => false, 'default' => null])
            ->addTimestamps('criado', 'modificado')
            ->create();
    }
}

Para salvar registros, basta popular a entidade. Repare que criei duas colunas conteudo, uma text e outra tsvector. A ideia é que a primeira contenha a forma original que exibiremos ao usuário, enquanto a tsvector é preparada para execução da busca. Na hora de popular a entidade, atribua o mesmo valor em conteudo e conteudo_fts, o driver cuidará da conversão necessária na hora de persistir.

Já uma consulta ao banco poderia ser feita com:

// Não esqueça de sanitizar antes de passar na query abaixo
$termo = $this->request->getQuery('q');
$resultados = $this->Buscas->find()
    ->where(["conteudo_fts @@ phraseto_tsquery('{$termo}')"])
    ->order(['data' => 'desc'])
    ->all();

Ainda tem um behavior que adiciona um finder especial para busca, você deve vincular ele a tabela associada com a tabela de buscas – nesse exemplo da migration, o behavior seria ligado na tabela OriginalsTable. Veja as configurações disponíveis na documentação do projeto.

É isso, com essa sequência de artigos espero ter mostrado que é possível oferecer uma busca boa o suficiente para a maioria das aplicações sem precisar estourar orçamento com hardware ou serviços externos. E claro, que essa implementação não precisa ser nenhum bicho de sete cabeças.

Se tiver alguma dúvida ou encontrar algum problema, pode usar tanto a caixa de comentários abaixo quanto o github.

Por Cauan Cabral

Desenvolvedor com 2 dígitos de experiências, especialista em PHP e CakePHP mas com bagagens em JavaEE, Node.js, Javascript (ES6, jQuery, Angular) e Python. Interessado em automação, Machine Learning e cozinha.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.