Tutorial Laravel 13 #8: criar um motor de pesquisa
verificado a 2 Setembro 2026 · 5 min
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.
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:
<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ã.
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
Route::get('/recherche', [PostController::class, 'search'])->name('search');O método de pesquisa
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
@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')
@endsectionO $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_searchpreenchida comStr::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
LIKEnunca 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
strongdevolve 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.