Tutoriel Laravel 13 #8 : Moteur de recherche
vérifié le 2 septembre 2026 · 5 min
Un formulaire en GET, une requête where('title', 'like', "%$terme%") et une vue suffisent. Attention : sous SQLite, LIKE ignore la casse sur l’ASCII seulement. Chercher « demonstration » sans accent ne trouve pas « démonstration », alors que MySQL et MariaDB y parviennent.
Un moteur de recherche tient en trois pièces : un formulaire qui envoie la requête, une méthode qui interroge la base, une vue qui affiche les résultats. Ce chapitre les assemble, puis mesure ce que cette recherche sait faire, et ce qu’elle ne sait pas faire.
Le formulaire
Il est déjà dans le gabarit écrit au chapitre 5, en haut de chaque page :
<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>Trois détails comptent. La méthode est GET, pas POST : la recherche ne modifie rien, et l’adresse obtenue reste partageable et indexable. L’attribut value="{{ request('q') }}" conserve le terme saisi après l’envoi. Le <label> est masqué visuellement mais lu par les lecteurs d’écran.
Les anciennes versions de ce tutoriel plaçaient {{ csrf_field() }} dans ce formulaire. C’est inutile et nuisible : la protection CSRF ne concerne que les requêtes qui modifient l’état du serveur, et le jeton se retrouve exposé dans l’URL. Laravel ne vérifie d’ailleurs jamais le jeton sur une requête GET.
La route
Route::get('/recherche', [PostController::class, 'search'])->name('search');La méthode de recherche
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(),
]);
}Quatre points méritent l’attention.
La validation. validate() refuse une requête de plus de cent caractères ou d’un type inattendu. Sans elle, un visiteur peut envoyer ?q[]=x et provoquer une erreur PHP en passant un tableau là où une chaîne est attendue.
Le regroupement des conditions. La fonction anonyme passée à where() met les deux conditions orWhere entre parenthèses dans le SQL produit. Sans elle, la requête devient is_published = 1 AND title LIKE … OR content LIKE …, et la priorité de OR ferait remonter des brouillons dont le contenu correspond. C’est un bug discret, qui ne se voit qu’une fois un brouillon en base.
when(). La condition ne s’applique que si le terme n’est pas vide. Une recherche à vide affiche donc tous les articles plutôt qu’une page vide.
withQueryString(). Sans cet appel, les liens de pagination perdent le paramètre q et la page 2 affiche tous les articles au lieu des résultats.
La vue des résultats
@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$posts->total() donne le nombre de résultats sur l’ensemble des pages, pas seulement sur celle affichée.
Ce que cette recherche sait faire
Le comportement de LIKE dépend de la base de données, pas de Laravel. La différence est nette et vaut d’être connue avant de mettre un site en ligne. Douze articles intitulés « Article de démonstration » ont été interrogés le 2 septembre 2026 sur les deux moteurs.
| Terme recherché | SQLite 3.46.1 | MariaDB 11.8.9 (utf8mb4_unicode_ci) |
|---|---|---|
démonstration | trouve | trouve |
demonstration (sans accent) | ne trouve rien | trouve |
DÉMONSTRATION (majuscules accentuées) | ne trouve rien | trouve |
ARTICLE (majuscules sans accent) | trouve | trouve |
Autrement dit : sous SQLite, LIKE ignore la casse sur les caractères ASCII uniquement. Dès qu’un accent entre en jeu, la comparaison redevient stricte, et une recherche sans accent ne remonte rien. C’est une limite de l’implémentation par défaut de SQLite, documentée par le projet.
MariaDB et MySQL ne connaissent pas ce problème avec une collation utf8mb4_unicode_ci ou utf8mb4_general_ci : la comparaison ignore à la fois la casse et les accents.
Les contournements
Si vous restez sur SQLite et que le sujet compte, trois options, de la plus simple à la plus solide.
- Stocker une copie sans accent. Ajoutez une colonne
title_searchremplie avecStr::ascii($title)à l’enregistrement, et cherchez dedans après avoir appliqué la même transformation au terme saisi. - Passer à MySQL ou PostgreSQL en production. C’est de toute façon le choix habituel dès qu’un site reçoit du trafic.
- Utiliser un moteur de recherche dédié. Laravel Scout branche l’application sur Meilisearch, Typesense ou Algolia. Ils gèrent les accents, les fautes de frappe et la pertinence, ce que
LIKEne fera jamais.
Les limites assumées
Cette recherche est volontairement simple, et il faut savoir ce qu’elle ne fait pas.
- Elle ne tolère aucune faute de frappe : « larvel » ne trouvera pas « Laravel ».
- Elle ne classe pas par pertinence. Un article dont le titre correspond exactement ressort au même niveau qu’un article qui mentionne le terme une fois dans son contenu, le tri se faisant sur la date.
- Elle cherche dans le HTML brut du contenu. Chercher
strongremontera tous les articles contenant du texte en gras. - Le
LIKE '%terme%'ne peut pas utiliser d’index. Sur quelques milliers d’articles c’est indolore, au-delà, la requête ralentit proportionnellement à la taille de la table.
Pour un blog personnel, c’est suffisant. Au-delà, Scout est la marche suivante.
Le dernier chapitre ajoute la pagination et les articles similaires.