Giuseppe.Rega
← Tutte le note

Il bug di ricerca che non fa rumore

Pubblicato il · 2 min di lettura

C’è una categoria di bug che preferisco a tutte le altre: quelli che non lanciano eccezioni, non finiscono nei log e restano in produzione per settimane perché il sistema, tecnicamente, funziona.

Questo è uno di quelli.

Il sintomo

Su un e-commerce, un cliente cerca «chianti» e non trova nulla. Cerca «Chianti» e trova nove prodotti. Nessun errore, nessun ticket: chi cerca in minuscolo pensa semplicemente che quel prodotto non ci sia, e se ne va.

La causa

Il codice era questo:

query = query.Where(p => p.Name.Contains(term));

Su SQL Server funziona, perché le collation predefinite sono insensibili alle maiuscole. Su PostgreSQL no: Contains viene tradotto in un LIKE, e LIKE in PostgreSQL distingue maiuscole e minuscole. Lo stesso identico codice C# ha due comportamenti diversi a seconda del database sotto, e nessuno dei due è segnalato.

La correzione

PostgreSQL ha ILIKE, che Npgsql espone come EF.Functions.ILike. Tokenizzando anche per parola, in modo che «vino rosso» trovi «Rosso di Montalcino — vino fermo»:

foreach (var token in SearchTextUtil.Tokenize(term))
{
    var pattern = $"%{SearchTextUtil.EscapeLikePattern(token)}%";
    query = query.Where(p => EF.Functions.ILike(p.Name, pattern)
                          || EF.Functions.ILike(p.Description, pattern));
}

Due dettagli che è facile saltare:

Escapare i wildcard dell’utente. In un pattern LIKE, % e _ sono caratteri speciali. Una ricerca per «100%» senza escape diventa un pattern che trova tutto. Non è un problema di sicurezza — i parametri restano parametrizzati — ma è un risultato sbagliato.

Nessun indice, per ora. Un ILIKE '%…%' non può usare un indice B-tree. Su qualche decina di prodotti la scansione sequenziale costa meno di quanto costerebbe mantenere un indice pg_trgm. Verso il migliaio di righe la valutazione cambia — e quello è il momento di rifarla, non prima.

Il danno collaterale

La parte che mi ha colpito di più non è stata la ricerca in sé: erano le statistiche. Il sistema registrava ogni ricerca con il numero di risultati, e usavo quei dati per capire quali prodotti mancassero a catalogo. Metà delle ricerche «senza risultati» non erano domande inevase: erano lo stesso bug, che mi stava suggerendo di comprare merce che avevo già.

Un bug silenzioso non costa solo la funzionalità che rompe. Costa anche tutte le decisioni prese guardando i dati che ha inquinato.