lucasbrito-wdt / laravel-database-fts
Full-Text Search nativo para Laravel com suporte a PostgreSQL (pg_trgm) e MySQL (FULLTEXT), ACL e recursos avançados
Installs: 6
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/lucasbrito-wdt/laravel-database-fts
Requires
- php: ^8.1
- illuminate/console: ^10.0 || ^11.0 || ^12.0
- illuminate/database: ^10.0 || ^11.0 || ^12.0
- illuminate/support: ^10.0 || ^11.0 || ^12.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0
- phpunit/phpunit: ^10.0
README
Busca Full-Text nativa para Laravel com suporte a PostgreSQL (pg_trgm) e MySQL (FULLTEXT), incluindo ACL e busca parcial automática.
Características
- ✅ Multi-Driver: Suporta PostgreSQL (pg_trgm) e MySQL (FULLTEXT)
- ✅ Detecção Automática: Detecta automaticamente o banco de dados e usa o driver apropriado
- ✅ Motor nativo: Zero dependências externas, sem serviços adicionais
- ✅ Índices otimizados: GIN com gin_trgm_ops (PostgreSQL) ou FULLTEXT (MySQL)
- ✅ Busca parcial automática: Funciona com termos incompletos (ex: "adm" encontra "admin") - PostgreSQL
- ✅ Sem colunas extras: Não cria colunas adicionais, apenas índices
- ✅ Detecção automática: Lê campos da model automaticamente via trait
- ✅ Método customizado no Blueprint: Use
$table->searchableIndex()diretamente emSchema::table() - ✅ Helpers na migration gerada: Métodos
createSearchableIndex()edropSearchableIndex()disponíveis nas migrations geradas pelo comando - ✅ ACL: Controle de acesso baseado em visibilidade
- ✅ Similaridade configurável: Threshold ajustável para precisão vs recall
- ✅ Suporte a estruturas customizadas: Funciona com
Domains/*/Models/*e outros namespaces
Requisitos
- PHP >= 8.1
- Laravel >= 10.0
- PostgreSQL >= 12.0 (com extensão
pg_trgm- criada automaticamente) OU - MySQL >= 5.7 (com engine InnoDB ou MyISAM)
Instalação
composer require lucasbrito-wdt/laravel-database-fts
Publique o arquivo de configuração:
# Usando a tag específica php artisan vendor:publish --tag=fts-config # Ou usando a tag completa php artisan vendor:publish --tag=laravel-database-fts-config
Isso criará o arquivo config/fts.php na sua aplicação Laravel com todas as configurações padrão do pacote.
Configuração
O arquivo de configuração config/fts.php contém todas as opções:
return [ /* |-------------------------------------------------------------------------- | Driver de Busca |-------------------------------------------------------------------------- | | Define qual driver de busca será usado. Opções: | - 'auto': Detecta automaticamente baseado na conexão do banco de dados | - 'postgres': Força uso do driver PostgreSQL (pg_trgm) | - 'mysql': Força uso do driver MySQL (FULLTEXT) | | Recomendado: 'auto' para detecção automática baseada na conexão ativa. | */ 'driver' => env('FTS_DRIVER', 'auto'), /* |-------------------------------------------------------------------------- | Threshold de Similaridade |-------------------------------------------------------------------------- | | Threshold padrão para busca por similaridade. | Valores entre 0.0 e 1.0. Quanto menor, mais resultados serão retornados. | | Para PostgreSQL (pg_trgm): | - Controla a similaridade de trigramas (0.0 a 1.0) | - Valores menores retornam mais resultados | | Para MySQL (FULLTEXT): | - Se >= 0.3: usa NATURAL LANGUAGE MODE (busca mais precisa) | - Se < 0.3: usa BOOLEAN MODE (busca mais flexível) | */ 'similarity_threshold' => env('FTS_SIMILARITY_THRESHOLD', 0.2), /* |-------------------------------------------------------------------------- | Configurações de ACL (Access Control List) |-------------------------------------------------------------------------- | | Configurações para controle de acesso baseado em visibilidade. | */ 'acl' => [ 'column' => env('FTS_ACL_COLUMN', 'visibility'), 'ranking_multipliers' => [ 'public' => 1.2, 'internal' => 1.0, 'private' => 0.5, ], ], /* |-------------------------------------------------------------------------- | Métricas e Logging |-------------------------------------------------------------------------- | | Quando habilitado, todas as buscas são logadas com informações de | performance (termo, tempo de execução, quantidade de resultados, driver usado). | | Útil para monitoramento e otimização de queries de busca. | */ 'metrics' => [ 'enabled' => env('FTS_METRICS_ENABLED', true), 'log_channel' => env('FTS_LOG_CHANNEL', 'daily'), ], ];
Variáveis de Ambiente
Você pode configurar via .env:
# Driver de busca (auto, postgres, mysql) FTS_DRIVER=auto # Threshold de similaridade (0.0 a 1.0) FTS_SIMILARITY_THRESHOLD=0.2 # ACL FTS_ACL_COLUMN=visibility # Métricas FTS_METRICS_ENABLED=true FTS_LOG_CHANNEL=daily
Uso Rápido
Escolha o Método de Criação do Índice
| Método | Quando Usar | Exemplo |
|---|---|---|
$table->searchableIndex() |
Ao criar índice em tabela existente | Schema::table() |
make:searchable |
Para tabelas existentes ou múltiplas models | php artisan make:searchable Post |
| Trait PostgresFullTextMigration | Controle manual avançado | $this->addSearchableIndex($table, ...) |
| Helpers (migration gerada) | Só em migrations geradas pelo comando | $this->createSearchableIndex() |
1. Configurar o Model
Primeiro, adicione o trait Searchable e defina os campos pesquisáveis:
namespace App\Models; use Illuminate\Database\Eloquent\Model; use LucasBritoWdt\LaravelDatabaseFts\Traits\Searchable; class Post extends Model { use Searchable; protected static array $searchable = [ 'title', 'body', ]; }
Para estruturas customizadas (ex: Domains):
namespace App\Domains\Blog\Models; use Illuminate\Database\Eloquent\Model; use LucasBritoWdt\LaravelDatabaseFts\Traits\Searchable; class Post extends Model { use Searchable; protected static array $searchable = [ 'title', 'body', ]; }
2. Gerar Migration Automaticamente
Opção 1: Para uma model específica
Use o comando Artisan para gerar a migration para uma model específica. Não precisa passar os campos - eles são lidos automaticamente da model:
php artisan make:searchable Post
Opção 2: Para todas as models automaticamente
Gere migrations para todas as models que usam o trait Searchable de uma vez:
php artisan make:searchable --all
Ou simplesmente:
php artisan make:searchable
Este comando:
- 🔍 Busca automaticamente todas as models em
App\Models\,App\eApp\Domains\*\Models\ - ✅ Verifica quais usam o trait
Searchable - ✅ Verifica se têm o array
$searchabledefinido - 📝 Gera migrations para todas as models encontradas
- ⚠️ Ignora models que já têm migration existente
- 📊 Mostra resumo com sucessos e erros
Exemplo de saída:
Buscando todas as models que usam o trait Searchable...
Encontradas 3 model(s):
- App\Models\Post
- App\Domains\Auth\Models\User
- App\Domains\Blog\Models\Article
Deseja gerar migrations para todas essas models? (yes/no) [yes]:
✅ Post: Migration criada
✅ User: Migration criada
⚠️ Article: Migration já existe (2026_01_08_030322_add_searchable_index_to_articles_table.php)
Concluído! 2 migration(s) criada(s), 0 erro(s).
Detalhes do comando:
- Detecta automaticamente a model (busca em
App\Models\,App\eApp\Domains\{domain}\Models\{model}) - Verifica se a model usa o trait
Searchable - Lê os campos do array
$searchableautomaticamente - Cria migration que lê os campos da model na execução
- Cria índice GIN usando
gin_trgm_ops - Cria extensão
pg_trgmautomaticamente
A migration gerada lê automaticamente os campos da model quando executada!
3. Criar Índice no Schema::create()
⚠️ IMPORTANTE: Para usar searchableIndex() dentro de Schema::create(), você precisa criar o índice APÓS a tabela ser criada:
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { // Primeiro, cria a tabela Schema::create('posts', function (Blueprint $table) { $table->id(); $table->string('title'); $table->text('body'); $table->timestamps(); }); // Depois, cria o índice (após a tabela existir) Schema::table('posts', function (Blueprint $table) { $table->searchableIndex(['title', 'body']); // Ou com nome customizado // $table->searchableIndex(['title', 'body'], 'posts_custom_search_idx'); }); } public function down(): void { Schema::dropIfExists('posts'); } };
Alternativa: Use o trait PostgresFullTextMigration:
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; use LucasBritoWdt\LaravelDatabaseFts\Traits\PostgresFullTextMigration; return new class extends Migration { use PostgresFullTextMigration; public function up(): void { Schema::create('posts', function (Blueprint $table) { $table->id(); $table->string('title'); $table->text('body'); $table->timestamps(); }); // Cria o índice após a tabela ser criada Schema::table('posts', function (Blueprint $table) { $this->addSearchableIndex($table, ['title', 'body']); }); } public function down(): void { Schema::table('posts', function (Blueprint $table) { $this->dropSearchableIndex($table); }); Schema::dropIfExists('posts'); } };
Nota: O método createSearchableIndex() só está disponível nas migrations geradas pelo comando make:searchable. Para migrations manuais, use Schema::table() com searchableIndex() ou o trait PostgresFullTextMigration.
Para adicionar a uma tabela existente:
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { Schema::table('posts', function (Blueprint $table) { $table->searchableIndex(['title', 'body']); }); } public function down(): void { Schema::table('posts', function (Blueprint $table) { // O método dropSearchableIndex não está disponível no Blueprint // Use o helper da migration ou SQL direto }); } };
4. Usar Helpers na Migration
A migration gerada pelo comando make:searchable inclui métodos helper que podem ser usados manualmente:
use Illuminate\Database\Migrations\Migration; use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { $tableName = 'posts'; $fields = ['title', 'body']; // Usa o helper para criar o índice $this->createSearchableIndex($tableName, $fields); // Ou com nome customizado // $this->createSearchableIndex($tableName, $fields, 'posts_custom_idx'); } public function down(): void { $tableName = 'posts'; // Usa o helper para remover o índice $this->dropSearchableIndex($tableName); // Ou com nome customizado // $this->dropSearchableIndex($tableName, 'posts_custom_idx'); } /** * Helper para criar índice GIN com pg_trgm para busca por similaridade. * Pode ser chamado manualmente se necessário. */ protected function createSearchableIndex( string $tableName, array $fields, ?string $indexName = null ): void { // Implementação automática na migration gerada } /** * Helper para remover índice de busca por similaridade. * Pode ser chamado manualmente se necessário. */ protected function dropSearchableIndex( string $tableName, ?string $indexName = null ): void { // Implementação automática na migration gerada } }
5. Usar Trait PostgresFullTextMigration (Método Legado)
Alternativamente, você pode usar a trait PostgresFullTextMigration diretamente na sua migration:
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; use LucasBritoWdt\LaravelDatabaseFts\Traits\PostgresFullTextMigration; class CreatePostsTable extends Migration { use PostgresFullTextMigration; public function up(): void { Schema::create('posts', function (Blueprint $table) { $table->id(); $table->string('title'); $table->string('body'); $table->timestamps(); // Adiciona índice para busca por similaridade $this->addSearchableIndex($table, ['title', 'body']); }); } public function down(): void { Schema::table('posts', function (Blueprint $table) { $this->dropSearchableIndex($table); }); Schema::dropIfExists('posts'); } }
6. Métodos Disponíveis para Criar Índices
O pacote oferece três formas de criar índices de busca:
Opção 1: Método Customizado no Blueprint (Mais Simples) ⭐
Use diretamente em Schema::create() ou Schema::table():
Schema::create('posts', function (Blueprint $table) { $table->id(); $table->string('title'); $table->text('body'); // Cria índice automaticamente $table->searchableIndex(['title', 'body']); // Com nome customizado // $table->searchableIndex(['title', 'body'], 'posts_custom_idx'); });
Vantagens:
- ✅ Mais simples e direto
- ✅ Funciona com method chaining
- ✅ Não precisa de traits ou imports extras
- ✅ Cria extensão
pg_trgmautomaticamente
Opção 2: Helpers na Migration (Gerada pelo Comando)
A migration gerada pelo make:searchable inclui métodos helper:
public function up(): void { // Helper para criar índice $this->createSearchableIndex('posts', ['title', 'body']); // Com nome customizado // $this->createSearchableIndex('posts', ['title', 'body'], 'custom_idx'); } public function down(): void { // Helper para remover índice $this->dropSearchableIndex('posts'); // Com nome customizado // $this->dropSearchableIndex('posts', 'custom_idx'); }
Vantagens:
- ✅ Disponível automaticamente na migration gerada pelo comando
make:searchable - ✅ Permite controle total sobre a criação do índice
- ✅ Os métodos
createSearchableIndex()edropSearchableIndex()são incluídos automaticamente
Nota: Esses métodos helper só estão disponíveis nas migrations geradas pelo comando make:searchable. Para migrations manuais, use Schema::table() com searchableIndex() ou o trait PostgresFullTextMigration.
Opção 3: Trait PostgresFullTextMigration (Método Legado)
Para compatibilidade com código existente:
use LucasBritoWdt\LaravelDatabaseFts\Traits\PostgresFullTextMigration; class CreatePostsTable extends Migration { use PostgresFullTextMigration; public function up(): void { Schema::create('posts', function (Blueprint $table) { $this->addSearchableIndex($table, ['title', 'body']); }); } }
7. Buscar
Busca simples:
$results = Post::search('gestão corporativa')->paginate(10);
Busca com threshold customizado:
// Threshold menor = mais resultados (menos preciso) $results = Post::search('gest', 0.1)->get(); // Threshold maior = menos resultados (mais preciso) $results = Post::search('gestão', 0.5)->get();
Busca com filtro ACL:
$results = Post::search('termo', null, ['public', 'internal'])->get();
Busca parcial automática:
// Funciona mesmo com termo incompleto $results = Post::search('adm')->get(); // Encontra "admin", "administrador", etc.
Como Funciona a Migration Automática
A migration consolidada (2024_01_01_000000_add_searchable_indexes_to_all_tables.php) é carregada automaticamente pelo FtsServiceProvider e:
-
Detecta automaticamente todas as models que:
- Estendem
Illuminate\Database\Eloquent\Model - Usam o trait
LucasBritoWdt\LaravelDatabaseFts\Traits\Searchable - Têm o array
$searchabledefinido e não vazio
- Estendem
-
Busca em múltiplos namespaces:
App\Models\*App\*App\Domains\*\Models\*(estrutura de domínios)
-
Cria índices automaticamente:
- Verifica se o índice já existe (idempotente)
- Cria apenas índices que não existem
- Usa a mesma expressão imutável do índice para garantir compatibilidade
-
É segura para executar múltiplas vezes:
- Usa
CREATE INDEX IF NOT EXISTS - Verifica existência antes de criar
- Não duplica índices
- Usa
Vantagens:
- ✅ Zero configuração - funciona automaticamente
- ✅ Detecta novas models automaticamente
- ✅ Idempotente - pode executar múltiplas vezes sem problemas
- ✅ Suporta estruturas customizadas (Domains, etc.)
Funcionalidades Avançadas
Detecção Automática de Campos
A migration gerada pelo comando make:searchable lê automaticamente os campos do array $searchable da model quando executada. Isso significa:
- ✅ Não precisa passar campos no comando
- ✅ Não precisa editar a migration manualmente
- ✅ Se você mudar os campos na model, basta recriar a migration
- ✅ Funciona com qualquer estrutura (App\Models, Domains, etc.)
Como funciona:
- O comando
make:searchable Postgera uma migration - A migration busca automaticamente a model que corresponde à tabela
- Lê o array
$searchablevia Reflection - Cria o índice com os campos encontrados
Suporte a Estruturas Customizadas
O pacote detecta automaticamente models em:
App\Models\*App\*App\Domains\*\Models\*(estrutura de domínios)
Exemplo com Domains:
// app/Domains/Auth/Models/User.php namespace App\Domains\Auth\Models; use LucasBritoWdt\LaravelDatabaseFts\Traits\Searchable; class User extends Model { use Searchable; protected static array $searchable = ['name', 'email']; }
# O comando encontra automaticamente
php artisan make:searchable User
ACL (Access Control List)
Filtre resultados por visibilidade:
// Busca apenas em itens públicos e internos $results = Post::search('termo', null, ['public', 'internal'])->get();
Como Funciona
O pacote detecta automaticamente o banco de dados e usa o driver apropriado:
PostgreSQL (pg_trgm)
- Divide strings em trigramas (grupos de 3 caracteres)
- Calcula similaridade entre strings usando a função
similarity() - Usa índice GIN com
gin_trgm_opspara busca rápida - Combina múltiplos campos usando concatenação para busca unificada
MySQL (FULLTEXT)
- Usa índices FULLTEXT nativos do MySQL
- Busca usando
MATCH() AGAINST()para relevância - Suporta modos NATURAL LANGUAGE e BOOLEAN
- Ranking automático por relevância
Recursos disponíveis:
- PostgreSQL: Termos parciais (ex: "adm" encontra "admin"), erros leves de digitação
- MySQL: Busca por palavras completas com ranking de relevância
- Ambos: Múltiplos campos simultaneamente
Threshold de Similaridade
O threshold controla quantos resultados serão retornados:
- 0.0 - 0.2: Muitos resultados, menos preciso (padrão: 0.2)
- 0.3 - 0.5: Balanceado
- 0.6 - 1.0: Poucos resultados, muito preciso
Ajuste conforme necessário:
// Mais resultados Post::search('termo', 0.1)->get(); // Menos resultados, mais precisos Post::search('termo', 0.4)->get();
SearchService Multi-Model
Busque em múltiplos models simultaneamente com ranking unificado:
use LucasBritoWdt\LaravelDatabaseFts\Services\SearchService; $results = app(SearchService::class) ->register(Post::class) ->register(Document::class) ->register(Ticket::class) ->search('fluxo de caixa', null, ['public', 'internal']);
Os resultados são ordenados por score (similaridade) independente do model de origem.
Estrutura do Banco de Dados
O pacote cria automaticamente os índices apropriados para cada banco:
PostgreSQL:
- Extensão
pg_trgm(se não existir) - Índice GIN com
gin_trgm_opsna expressão concatenada
MySQL:
- Índice FULLTEXT nas colunas especificadas
Não cria colunas extras - apenas índices para performance.
Exemplo de Índice Criado
O pacote cria índices apropriados para cada banco de dados:
PostgreSQL
CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE INDEX IF NOT EXISTS posts_search_trgm_idx ON posts USING GIN ( (COALESCE(title::text, '') || ' ' || COALESCE(body::text, '')) gin_trgm_ops );
Por que não usar concat_ws?
- A função
concat_wsnão éIMMUTABLEno PostgreSQL - Índices requerem funções imutáveis
- A solução usa
COALESCEe concatenação||que são imutáveis
MySQL
CREATE FULLTEXT INDEX posts_search_ft_idx
ON posts (title, body);
Fluxo Completo de Uso
Opção A: Usando o Comando Artisan (Recomendado)
- Configure o Model:
class Post extends Model { use Searchable; protected static array $searchable = ['title', 'body']; }
- Gere a Migration:
php artisan make:searchable Post
- Execute a Migration:
php artisan migrate
A migration automaticamente:
- Encontra a model
Post - Lê os campos
['title', 'body']do array$searchable - Cria o índice com esses campos
- Use a Busca:
Post::search('termo')->get();
Opção B: Criando Manualmente no Schema::create()
- Configure o Model:
class Post extends Model { use Searchable; protected static array $searchable = ['title', 'body']; }
- Crie a Migration Manualmente:
Schema::create('posts', function (Blueprint $table) { $table->id(); $table->string('title'); $table->text('body'); $table->timestamps(); // Adiciona índice de busca $table->searchableIndex(['title', 'body']); });
- Execute a Migration:
php artisan migrate
- Use a Busca:
Post::search('termo')->get();
Diferenças entre Drivers
PostgreSQL (pg_trgm)
- ✅ Suporta busca parcial (ex: "adm" encontra "admin")
- ✅ Suporta busca por similaridade (tolerante a erros de digitação)
- ✅ Funciona bem com termos curtos
- ⚠️ Requer extensão
pg_trgm
MySQL (FULLTEXT)
- ✅ Busca nativa FULLTEXT do MySQL
- ✅ Ranking automático por relevância
- ✅ Suporta modos NATURAL LANGUAGE e BOOLEAN
- ⚠️ Requer palavras completas (não suporta busca parcial como pg_trgm)
- ⚠️ Funciona melhor com palavras de 3+ caracteres
- ⚠️ Tem lista de stopwords que pode afetar resultados
Quando NÃO usar
Esta solução não é adequada para:
- ❌ Busca semântica / IA
- ❌ Busca em logs massivos
- ❌ Multitenancy extremo com shards
Nesses casos, Elasticsearch / Meilisearch são mais adequados.
Comparação com Outras Soluções
| Solução | Quando Usar |
|---|---|
| Este pacote (PostgreSQL pg_trgm / MySQL FULLTEXT) | Busca parcial (PostgreSQL), autocomplete, simplicidade, zero infra extra |
| PostgreSQL FTS (tsvector) | Busca por palavras completas, stemming, múltiplos idiomas |
| Meilisearch / Elasticsearch | Busca semântica, autocomplete avançado, escalabilidade extrema |
Troubleshooting
Erro: "function similarity does not exist"
A extensão pg_trgm não está habilitada. O pacote tenta criá-la automaticamente, mas se falhar, execute manualmente:
CREATE EXTENSION IF NOT EXISTS pg_trgm;
Erro: "Não foi possível encontrar os campos pesquisáveis"
A migration não conseguiu encontrar a model ou ler os campos. Verifique:
- A model usa o trait
Searchable? - A model define o array
$searchable? - O namespace da model está em
App\Models\,App\ouApp\Domains\*\Models\?
Solução: Você pode passar os campos manualmente na migration usando uma das opções:
Opção 1: Método customizado no Blueprint (Recomendado):
Schema::table('posts', function (Blueprint $table) { $table->searchableIndex(['title', 'body']); });
Opção 2: Trait PostgresFullTextMigration:
use LucasBritoWdt\LaravelDatabaseFts\Traits\PostgresFullTextMigration; class AddSearchableIndexToPostsTable extends Migration { use PostgresFullTextMigration; public function up(): void { Schema::table('posts', function (Blueprint $table) { $this->addSearchableIndex($table, ['title', 'body']); }); } }
Opção 3: Helper na migration gerada pelo comando:
// Só disponível em migrations geradas por: php artisan make:searchable Post $this->createSearchableIndex('posts', ['title', 'body']);
Performance lenta
Certifique-se de que o índice foi criado:
\d+ posts -- Lista índices da tabela
Verifique se o índice *_search_trgm_idx existe.
Muitos/poucos resultados
Ajuste o threshold de similaridade:
// Mais resultados Post::search('termo', 0.1)->get(); // Menos resultados Post::search('termo', 0.4)->get();
Model não encontrada em estrutura customizada
Se sua model está em um namespace customizado que não é detectado automaticamente, você pode:
- Passar o namespace completo no comando (se o Laravel suportar)
- Ou criar a migration manualmente passando os campos
Contribuindo
Contribuições são bem-vindas! Por favor, abra uma issue ou pull request.
Licença
MIT License. Veja o arquivo LICENSE para mais detalhes.
Referências
Este pacote é baseado nas melhores práticas documentadas em: