Introduzione
Quando un’applicazione deve cercare parole all’interno di titoli, descrizioni o articoli, una semplice clausola LIKE può essere sufficiente per pochi dati, ma diventa poco pratica quando la tabella cresce. Una soluzione più adatta è la ricerca full-text, supportata sia da MySQL sia da MariaDB.
La ricerca full-text utilizza un indice specifico per analizzare il contenuto testuale e calcolare la rilevanza dei risultati. In questo modo è possibile cercare più parole, ordinare i record in base alla pertinenza e ignorare automaticamente termini troppo comuni o troppo brevi, secondo le impostazioni del database.
In questo tutorial realizzeremo una piccola ricerca di articoli. L’utente inserirà una frase, PHP interrogherà il database tramite PDO e i risultati saranno ordinati usando il punteggio restituito da MATCH ... AGAINST.
Codice completo
Per prima cosa creiamo una tabella con un indice full-text. L’indice deve essere applicato alle colonne sulle quali vogliamo effettuare la ricerca.
CREATE TABLE articoli (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
titolo VARCHAR(200) NOT NULL,
contenuto TEXT NOT NULL,
pubblicato TINYINT(1) NOT NULL DEFAULT 1,
creato_il DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FULLTEXT KEY ft_articoli (titolo, contenuto)
) ENGINE=InnoDB; Inseriamo alcuni dati di esempio:
INSERT INTO articoli (titolo, contenuto) VALUES
(
´Introduzione a PHP´,
´PHP è un linguaggio utilizzato per creare applicazioni web dinamiche.´
),
(
´Connessione a MariaDB´,
´PDO consente di collegare PHP a MariaDB in modo sicuro e portabile.´
),
(
´Sicurezza delle applicazioni web´,
´Le query parametrizzate aiutano a prevenire diversi attacchi informatici.´
); Il seguente file PHP contiene la connessione, la validazione dell’input e l’esecuzione della ricerca:
<?php
declare(strict_types=1);
$host = ´127.0.0.1´;
$dbname = ´blog´;
$user = ´app_user´;
$password = ´password_sicura´;
$query = trim((string) ($_GET[´q´] ?? ´´));
// Una ricerca troppo breve produce spesso risultati poco utili.
$messaggio = null;
$risultati = [];
if ($query !== ´´ && mb_strlen($query) < 3) {
$messaggio = ´Inserisci almeno tre caratteri.´;
} elseif ($query !== ´´) {
try {
$pdo = new PDO(
"mysql:host={$host};dbname={$dbname};charset=utf8mb4",
$user,
$password,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]
);
/*
* NATURAL LANGUAGE MODE valuta la rilevanza dei documenti
* senza interpretare operatori speciali nella frase cercata.
*/
$sql = "
SELECT
id,
titolo,
contenuto,
MATCH(titolo, contenuto)
AGAINST(:termine IN NATURAL LANGUAGE MODE) AS rilevanza
FROM articoli
WHERE pubblicato = 1
AND MATCH(titolo, contenuto)
AGAINST(:termine_filtro IN NATURAL LANGUAGE MODE)
ORDER BY rilevanza DESC, creato_il DESC
LIMIT 20
";
$stmt = $pdo->prepare($sql);
/*
* Usiamo due parametri nominati distinti perché alcuni driver
* PDO non consentono di riutilizzare lo stesso placeholder.
*/
$stmt->execute([
´termine´ => $query,
´termine_filtro´ => $query
]);
$risultati = $stmt->fetchAll();
if ($risultati === []) {
$messaggio = ´Nessun articolo trovato.´;
}
} catch (PDOException $e) {
// In produzione registrare l´errore, senza mostrarne i dettagli all´utente.
$messaggio = ´Si è verificato un problema durante la ricerca.´;
}
}
?>
<form method="get" action="">
<label for="q">Cerca un articolo</label>
<input
type="search"
id="q"
name="q"
value="<?= htmlspecialchars($query, ENT_QUOTES, ´UTF-8´) ?>"
minlength="3"
required
>
<button type="submit">Cerca</button>
</form>
<?php if ($messaggio !== null): ?>
<p><?= htmlspecialchars($messaggio, ENT_QUOTES, ´UTF-8´) ?></p>
<?php endif; ?>
<?php foreach ($risultati as $articolo): ?>
<article>
<h2>
<?= htmlspecialchars($articolo[´titolo´], ENT_QUOTES, ´UTF-8´) ?>
</h2>
<p>
<?= htmlspecialchars($articolo[´contenuto´], ENT_QUOTES, ´UTF-8´) ?>
</p>
<small>
Rilevanza:
<?= number_format((float) $articolo[´rilevanza´], 3) ?>
</small>
</article>
<?php endforeach; ?> Spiegazione
Creazione dell’indice
La clausola FULLTEXT KEY crea una struttura indicizzata sulle colonne titolo e contenuto. La definizione delle colonne nella query deve corrispondere a quella dell’indice: se l’indice usa due colonne, anche MATCH deve riferirsi alle stesse colonne e nello stesso ordine.
MATCH e AGAINST
MATCH(titolo, contenuto) AGAINST(...) restituisce un valore numerico chiamato rilevanza. Un valore maggiore indica, in generale, una corrispondenza più significativa. La stessa espressione viene usata nel filtro WHERE per eliminare i documenti che non contengono risultati pertinenti.
La modalità NATURAL LANGUAGE MODE interpreta il testo come una frase normale. Esiste anche BOOLEAN MODE, utile quando si vogliono utilizzare operatori come + per richiedere una parola, - per escluderla o * per una ricerca con prefisso.
SELECT id, titolo
FROM articoli
WHERE MATCH(titolo, contenuto)
AGAINST(´+PHP -sicurezza´ IN BOOLEAN MODE); Nel codice PHP il valore dell’utente viene passato tramite un prepared statement. Questo evita di concatenare direttamente l’input nella query. Tuttavia, i prepared statements non risolvono automaticamente ogni problema: il nome della modalità di ricerca, i nomi delle colonne e l’ordinamento devono rimanere elementi controllati dal programma.
Best practice
- Usare un indice FULLTEXT: senza indice, la ricerca full-text non funzionerà correttamente nelle configurazioni standard.
- Validare la lunghezza dell’input: ricerche di uno o due caratteri producono spesso risultati poco significativi.
- Limitare i risultati: un
LIMITprotegge l’applicazione dal caricamento di migliaia di righe. - Visualizzare l’output in sicurezza: usare
htmlspecialcharsper impedire che testo proveniente dal database venga interpretato come HTML. - Controllare la lingua e la configurazione: stopword, lunghezza minima delle parole e algoritmo di indicizzazione possono cambiare tra MySQL e MariaDB.
- Valutare BOOLEAN MODE: è utile per filtri avanzati, ma deve essere documentato chiaramente nell’interfaccia perché gli operatori modificano il significato della ricerca.
- Non mostrare dettagli delle eccezioni: i messaggi PDO possono contenere informazioni sulla struttura del database e devono essere registrati lato server.
- Analizzare le query reali: per dataset molto grandi è opportuno verificare il piano di esecuzione e misurare i tempi con dati realistici.
Riepilogo
La ricerca full-text rappresenta una soluzione efficace per trovare parole all’interno di contenuti testuali. MySQL e MariaDB forniscono questa funzionalità attraverso un indice FULLTEXT e le espressioni MATCH ... AGAINST.
Un’implementazione robusta combina indice, prepared statements, validazione dell’input, limite dei risultati e corretta codifica dell’output HTML. La modalità naturale è adatta alla ricerca libera, mentre la modalità booleana consente di costruire filtri più precisi.
Approfondisci con risorse ufficiali
- Documentazione MySQL sulla ricerca full-text: https://dev.mysql.com/doc/refman/en/fulltext-search.html
- Documentazione MariaDB sugli indici FULLTEXT: https://mariadb.com/kb/en/full-text-indexes/
- Documentazione PHP su PDO: https://www.php.net/manual/en/book.pdo.php
- Documentazione PHP su htmlspecialchars: https://www.php.net/manual/en/function htmlspecialchars.php
