Capítulo 8 de 9

Tutorial Laravel 13 #8: criar um motor de pesquisa

verificado a 2 Setembro 2026 · 5 min

Resposta rápida

Um formulário em GET, uma consulta where('title', 'like', "%$terme%") e uma view chegam. Atenção: no SQLite, o LIKE ignora as maiúsculas apenas em ASCII. Procurar «demonstration» sem acento não encontra «démonstration», ao passo que o MySQL e o MariaDB encontram.

No fim deste capítulo, o teu blog terá uma pesquisa a funcionar, e saberás exatamente o que ela encontra e o que lhe escapa.

Um motor de pesquisa assenta em três peças: um formulário que envia a consulta, um método que interroga a base de dados e uma view que mostra os resultados. Este capítulo monta-as e mede depois o que esta pesquisa sabe fazer, e o que não sabe.

O formulário

Já está no template escrito no capítulo 5, no topo de cada página:

resources/views/layouts/app.blade.php
<form action="{{ route('search') }}" method="GET" role="search" class="flex gap-2">
    <label for="q" class="sr-only">Rechercher</label>
    <input id="q" type="search" name="q" value="{{ request('q') }}"
           placeholder="Rechercher…"
           class="rounded border border-gray-300 px-3 py-1.5 text-sm">
    <button type="submit" class="rounded bg-gray-900 px-3 py-1.5 text-sm text-white">Go</button>
</form>

Contam três detalhes. O método é GET, não POST: a pesquisa não altera nada, e o endereço obtido continua partilhável e indexável. O atributo value="{{ request('q') }}" mantém o termo escrito depois do envio. O <label> está escondido visualmente, mas é lido pelos leitores de ecrã.

Nada de token CSRF num formulário GET

As versões antigas deste tutorial colocavam {{ csrf_field() }} neste formulário. É inútil e prejudicial: a proteção CSRF só diz respeito aos pedidos que alteram o estado do servidor, e o token acaba exposto no URL. De resto, o Laravel nunca verifica o token num pedido GET.

A rota

routes/web.php
Route::get('/recherche', [PostController::class, 'search'])->name('search');

O método de pesquisa

app/Http/Controllers/PostController.php
use Illuminate\Http\Request;

public function search(Request $request): View
{
    $validated = $request->validate([
        'q' => ['nullable', 'string', 'max:100'],
    ]);

    $key = trim($validated['q'] ?? '');

    $posts = Post::published()
        ->when($key !== '', fn ($query) => $query->where(
            fn ($q) => $q->where('title', 'like', '%'.$key.'%')
                ->orWhere('content', 'like', '%'.$key.'%')
        ))
        ->with(['category', 'user'])
        ->latest('published_at')
        ->paginate(5)
        ->withQueryString();

    return view('search', [
        'key' => $key,
        'posts' => $posts,
        'categories' => Category::withCount('posts')->orderBy('name')->get(),
        'tags' => Tag::orderBy('name')->get(),
        'recentPosts' => Post::published()->latest('published_at')->take(5)->get(),
    ]);
}

Há quatro pontos que merecem atenção.

A validação. O validate() recusa uma consulta com mais de cem caracteres ou de um tipo inesperado. Sem ela, um visitante pode enviar ?q[]=x e provocar um erro de PHP, ao passar um array onde se espera uma string.

O agrupamento das condições. A função anónima passada ao where() põe as duas condições orWhere entre parênteses no SQL produzido. Sem ela, a consulta passa a ser is_published = 1 AND title LIKE … OR content LIKE …, e a prioridade do OR faria subir rascunhos cujo conteúdo corresponde. É um bug discreto, que só aparece quando há um rascunho na base.

when(). A condição só se aplica se o termo não estiver vazio. Uma pesquisa em branco mostra, portanto, todos os artigos em vez de uma página vazia.

withQueryString(). Sem esta chamada, os links de paginação perdem o parâmetro q e a página 2 mostra todos os artigos em vez dos resultados.

A view dos resultados

resources/views/search.blade.php
@extends('layouts.app')

@section('title', 'Recherche : '.$key)

@section('content')
    <h1 class="mb-6 text-2xl font-bold">
        Résultats pour « {{ $key }} »
        <span class="text-base font-normal text-gray-500">({{ $posts->total() }})</span>
    </h1>
    @include('partials.posts-list')
@endsection

O $posts->total() dá o número de resultados no conjunto das páginas, não apenas na que está a ser mostrada.

O que esta pesquisa sabe fazer

O comportamento do LIKE depende da base de dados, não do Laravel. A diferença é nítida e convém conhecê-la antes de pôr um site em linha. Doze artigos intitulados «Article de démonstration» foram interrogados a 2 de setembro de 2026 nos dois motores.

Termo pesquisado SQLite 3.46.1 MariaDB 11.8.9 (utf8mb4_unicode_ci)
démonstration encontra encontra
demonstration (sem acento) não encontra nada encontra
DÉMONSTRATION (maiúsculas com acento) não encontra nada encontra
ARTICLE (maiúsculas sem acento) encontra encontra

Por outras palavras: no SQLite, o LIKE ignora as maiúsculas apenas nos caracteres ASCII. Assim que entra um acento em jogo, a comparação volta a ser estrita, e uma pesquisa sem acento não devolve nada. É um limite da implementação predefinida do SQLite, documentado pelo próprio projeto.

O MariaDB e o MySQL não têm este problema com uma collation utf8mb4_unicode_ci ou utf8mb4_general_ci: a comparação ignora ao mesmo tempo as maiúsculas e os acentos.

Como contornar

Se ficares no SQLite e o assunto contar, há três opções, da mais simples à mais sólida.

  • Guardar uma cópia sem acentos. Acrescenta uma coluna title_search preenchida com Str::ascii($title) no momento do registo, e pesquisa nela depois de aplicares a mesma transformação ao termo escrito.
  • Passar para MySQL ou PostgreSQL em produção. É de qualquer forma a escolha habitual assim que um site recebe tráfego.
  • Usar um motor de pesquisa dedicado. O Laravel Scout liga a aplicação ao Meilisearch, ao Typesense ou ao Algolia. Tratam dos acentos, dos erros de escrita e da relevância, coisa que o LIKE nunca fará.

Os limites assumidos

Esta pesquisa é propositadamente simples, e convém saber o que ela não faz.

  • Não tolera nenhum erro de escrita: «larvel» não encontrará «Laravel».
  • Não ordena por relevância. Um artigo cujo título corresponde exatamente sai ao mesmo nível de outro que menciona o termo uma vez no conteúdo, porque a ordenação é feita pela data.
  • Pesquisa no HTML bruto do conteúdo. Procurar strong devolve todos os artigos que contêm texto a negrito.
  • O LIKE '%terme%' não consegue usar um índice. Em alguns milhares de artigos é indolor, acima disso, a consulta abranda proporcionalmente ao tamanho da tabela.

Para um blog pessoal, chega. Acima disso, o Scout é o degrau seguinte.

O último capítulo acrescenta a paginação e os artigos semelhantes.

Erros frequentes

Um token CSRF num formulário GET Inútil e prejudicial: a proteção CSRF só visa os pedidos que alteram o estado, e o token acaba exposto no URL. O Laravel nunca o verifica num GET.
Rascunhos nos resultados where('is_published', 1)->where(...)->orWhere(...) produz um SQL em que o OR ganha ao AND. Agrupa os dois orWhere numa função anónima passada ao where().
A página 2 dos resultados perde o termo pesquisado Acrescenta withQueryString() depois do paginate(), senão os links de paginação esquecem o parâmetro q.
Uma pesquisa sem acento não devolve nada É comportamento do SQLite, não do Laravel. Guarda uma cópia sem acentos com Str::ascii(), ou passa para MySQL, ou usa o Laravel Scout.
Newsletter

Os novos testes, tutoriais e projetos, por e-mail.

Testes reproduzíveis, código versionado, resultados datados. Nunca spam.