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.