Laravel Scout para busca full-text

1. Introdução ao Laravel Scout e busca full-text

O Laravel Scout é um pacote oficial do ecossistema Laravel que fornece uma abstração elegante para implementar busca full-text em suas aplicações. Diferente das buscas tradicionais com LIKE no SQL, que varrem tabelas inteiras e se tornam lentas com grandes volumes de dados, a busca full-text utiliza índices invertidos para retornar resultados relevantes em milissegundos.

Enquanto uma consulta WHERE title LIKE '%termo%' realiza uma varredura linear (e não utiliza índices tradicionais), o Scout opera sobre engines especializados como Algolia, Meilisearch, MySQL/PostgreSQL nativos ou engines personalizados. Isso permite:
- Busca por relevância com pontuação (scoring)
- Stemming (radicais de palavras)
- Tolerância a erros de digitação (typo tolerance)
- Filtros combinados com busca textual

Os drivers suportados oficialmente são: Algolia (SaaS), Meilisearch (self-hosted), MySQL/PostgreSQL nativos (via MATCH AGAINST), e a possibilidade de criar engines customizados.

2. Instalação e configuração inicial

A instalação é direta via Composer:

composer require laravel/scout

Após a instalação, publique o arquivo de configuração:

php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"

No arquivo config/scout.php, você encontrará as configurações principais:

<?php
return [
    'driver' => env('SCOUT_DRIVER', 'algolia'),
    'queue' => env('SCOUT_QUEUE', false),
    'after_commit' => env('SCOUT_AFTER_COMMIT', false),
    // ...
];

Para usar o driver MySQL/PostgreSQL nativo, instale o pacote complementar:

composer require laravel/scout

E configure seu .env:

SCOUT_DRIVER=database
DB_CONNECTION=mysql

Para MySQL, certifique-se de que suas tabelas tenham índices FULLTEXT:

ALTER TABLE posts ADD FULLTEXT INDEX fulltext_index (title, body);

3. Preparando modelos Eloquent para busca

Adicione o trait Searchable ao seu modelo:

<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;

class Post extends Model
{
    use Searchable;

    protected $fillable = ['title', 'body', 'category_id', 'published_at'];
}

Personalize o índice com searchableAs() e toSearchableArray():

public function searchableAs()
{
    return 'posts_index';
}

public function toSearchableArray()
{
    $array = [
        'id' => $this->id,
        'title' => $this->title,
        'body' => $this->body,
        'category' => $this->category->name,
        'published_at' => $this->published_at->timestamp,
    ];

    // Remova campos sensíveis ou irrelevantes
    unset($array['author_ip']);

    return $array;
}

4. Indexação e sincronização de dados

Importe registros existentes em massa:

php artisan scout:import "App\Models\Post"

Para sincronização automática, o Scout escuta eventos Eloquent (created, updated, deleted). Para ambientes de produção, recomenda-se usar filas:

// config/scout.php
'queue' => env('SCOUT_QUEUE', true),

Comandos úteis:

# Limpar índice
php artisan scout:flush "App\Models\Post"

# Sincronizar configurações do índice (Algolia/Meilisearch)
php artisan scout:sync-index-settings

5. Realizando buscas full-text

A sintaxe básica é extremamente simples:

$results = Post::search('Laravel Scout')->get();

Para paginação e ordenação:

$results = Post::search('busca avançada')
    ->where('category_id', 5)
    ->orderBy('published_at', 'desc')
    ->paginate(15);

Buscas com relevância personalizada:

$results = Post::search('framework PHP', function ($query, $callback) {
    $query->with(['attributes' => [
        'title' => ['boost' => 10],
        'body' => ['boost' => 2],
    ]]);
})->get();

6. Filtros avançados e consultas complexas

Combinando Scout com Eloquent para filtros avançados:

$results = Post::search('tutorial')
    ->where('category_id', 3)
    ->where('published_at', '>=', now()->subMonth()->timestamp)
    ->query(function ($builder) {
        $builder->with('author');
    })
    ->get();

Para filtros booleanos e intervalos numéricos:

$results = Post::search('avançado')
    ->where('is_featured', true)
    ->where('views', '>=', 1000)
    ->get();

Usando raw expressions para consultas específicas do driver (Meilisearch):

$results = Post::search('php')
    ->whereRaw([
        'filter' => 'category_id = 5 AND published_at > 1700000000'
    ])
    ->get();

7. Personalização e otimização de desempenho

Controle granular de indexação:

// Adicionar ao índice
$post->searchable();

// Remover do índice
$post->unsearchable();

// Condicional
$post->searchable() : $post->unsearchable();

Criando um engine personalizado:

<?php
namespace App\ScoutEngines;

use Laravel\Scout\Engines\Engine;

class CustomEngine extends Engine
{
    public function search($query)
    {
        // Implementação personalizada
    }
    // ...
}

Estratégias de cache:

$results = Cache::remember('search_results_' . md5($query), 3600, function () use ($query) {
    return Post::search($query)->get();
});

Para monitoramento, integre com Laravel Telescope:

composer require laravel/telescope

8. Considerações finais e boas práticas

Escolha do driver ideal:

Cenário Driver recomendado
Projeto pequeno/médio MySQL/PostgreSQL nativo
Alta performance e escalabilidade Meilisearch (self-hosted)
SaaS gerenciado, sem infra Algolia
Necessidades muito específicas Engine customizado

Limitações importantes:
- Campos não indexáveis: relacionamentos muitos-para-muitos não são automaticamente indexados
- Tamanho máximo de documento: Algolia limita a 10KB por registro
- O Scout não substitui consultas Eloquent complexas; use-o apenas para busca textual

Exemplo completo: API de busca com Laravel Sanctum:

<?php
namespace App\Http\Controllers\Api;

use App\Models\Post;
use Illuminate\Http\Request;

class SearchController extends Controller
{
    public function __invoke(Request $request)
    {
        $request->validate([
            'q' => 'required|string|min:2',
            'category' => 'nullable|exists:categories,id',
            'per_page' => 'nullable|integer|min:1|max:100',
        ]);

        $query = Post::search($request->q);

        if ($request->category) {
            $query->where('category_id', $request->category);
        }

        return response()->json(
            $query->paginate($request->per_page ?? 15)
        );
    }
}

Para integração com outras ferramentas da série, lembre-se de:
- Usar PHPStan para análise estática das consultas Scout
- Configurar Horizon para processar filas de indexação
- Versionar as configurações do índice no controle de versão

O Laravel Scout transforma a implementação de busca full-text de uma tarefa complexa em algo trivial, mantendo a flexibilidade para casos avançados. Comece com o driver nativo do banco de dados e migre para soluções mais robustas conforme sua aplicação cresce.

Referências