Tutorial Laravel 13 #8: un motore di ricerca per il blog
verificato il 2 Settembre 2026 · 5 min
Bastano un form in GET, una query where('title', 'like', "%$terme%") e una vista. Attenzione: su SQLite LIKE ignora le maiuscole solo sull'ASCII. Cercare «demonstration» senza accento non trova «démonstration», mentre MySQL e MariaDB ci riescono.
Un motore di ricerca sta in tre pezzi: un form che invia la query, un metodo che interroga il database, una vista che mostra i risultati. Questo capitolo li mette insieme, poi misura che cosa questa ricerca sa fare, e che cosa non sa fare.
Il form
È già nel layout scritto al capitolo 5, in cima a ogni pagina:
<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>Contano tre dettagli. Il metodo è GET, non POST: la ricerca non modifica niente, e l’indirizzo che ne esce resta condivisibile e indicizzabile. L’attributo value="{{ request('q') }}" conserva il termine digitato dopo l’invio. Il <label> è nascosto alla vista ma viene letto dagli screen reader.
Le vecchie versioni di questo tutorial mettevano {{ csrf_field() }} in questo form. È inutile e dannoso: la protezione CSRF riguarda solo le richieste che modificano lo stato del server, e il token finisce esposto nell’URL. Del resto Laravel non verifica mai il token su una richiesta GET.
La rotta
Route::get('/recherche', [PostController::class, 'search'])->name('search');Il metodo di ricerca
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(),
]);
}Quattro punti meritano attenzione.
La validazione. validate() rifiuta una query più lunga di cento caratteri o di un tipo inatteso. Senza, un visitatore può inviare ?q[]=x e provocare un errore PHP passando un array dove ci si aspetta una stringa.
Il raggruppamento delle condizioni. La funzione anonima passata a where() mette le due condizioni orWhere tra parentesi nell’SQL prodotto. Senza, la query diventa is_published = 1 AND title LIKE … OR content LIKE …, e la precedenza di OR farebbe risalire bozze il cui contenuto corrisponde. È un bug discreto, che si vede solo quando in database c’è una bozza.
when(). La condizione si applica solo se il termine non è vuoto. Una ricerca a vuoto mostra quindi tutti gli articoli, non una pagina vuota.
withQueryString(). Senza questa chiamata i link di paginazione perdono il parametro q e la pagina 2 mostra tutti gli articoli invece dei risultati.
La vista dei risultati
@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() restituisce il numero di risultati su tutte le pagine, non solo su quella visualizzata.
Che cosa sa fare questa ricerca
Il comportamento di LIKE dipende dal database, non da Laravel. La differenza è netta e vale la pena conoscerla prima di mettere un sito online. Il 2 settembre 2026 sono stati interrogati dodici articoli intitolati «Article de démonstration» sui due motori.
| Termine cercato | SQLite 3.46.1 | MariaDB 11.8.9 (utf8mb4_unicode_ci) |
|---|---|---|
démonstration | trova | trova |
demonstration (senza accento) | non trova nulla | trova |
DÉMONSTRATION (maiuscole accentate) | non trova nulla | trova |
ARTICLE (maiuscole senza accento) | trova | trova |
In altre parole: su SQLite LIKE ignora le maiuscole solo sui caratteri ASCII. Appena entra in gioco un accento il confronto torna stretto, e una ricerca senza accento non restituisce niente. È un limite dell’implementazione predefinita di SQLite, documentato dal progetto stesso.
MariaDB e MySQL non hanno questo problema con una collation utf8mb4_unicode_ci o utf8mb4_general_ci: il confronto ignora sia le maiuscole sia gli accenti.
Come aggirare il problema
Se resti su SQLite e la cosa ti interessa, hai tre strade, dalla più semplice alla più solida.
- Salvare una copia senza accenti. Aggiungi una colonna
title_searchriempita conStr::ascii($title)al salvataggio, e cerca lì dentro dopo aver applicato la stessa trasformazione al termine digitato. - Passare a MySQL o PostgreSQL in produzione. È comunque la scelta abituale appena un sito riceve traffico.
- Usare un motore di ricerca dedicato. Laravel Scout collega l’applicazione a Meilisearch, Typesense o Algolia. Gestiscono accenti, errori di battitura e rilevanza, cose che
LIKEnon farà mai.
I limiti accettati
Questa ricerca è volutamente semplice, e conviene sapere che cosa non fa.
- Non tollera nessun errore di battitura: «larvel» non troverà «Laravel».
- Non ordina per rilevanza. Un articolo il cui titolo corrisponde esattamente esce allo stesso livello di uno che cita il termine una volta sola nel contenuto, dato che l’ordinamento è per data.
- Cerca nell’HTML grezzo del contenuto. Cercare
strongfarà risalire tutti gli articoli che contengono del grassetto. - Il
LIKE '%terme%'non può usare un indice. Su qualche migliaio di articoli è indolore, oltre, la query rallenta in proporzione alla dimensione della tabella.
Per un blog personale è più che sufficiente. Oltre, il passo successivo è Scout.
L’ultimo capitolo aggiunge la paginazione e gli articoli correlati.