Togliere gli accenti rompe la ricerca — finché non lo fai da entrambi i lati
Pubblicato il 09/07/2026 · 15 min di lettura · Strumenti testo e lingua
Daniel Okonkwo — Sviluppatore front-end e redattore Tech presso OneKitly
Performance web · Formati di file
Verificato su 5 fonti
Togliere i segni diacritici è un passaggio di normalizzazione, non una funzione di ricerca: aiuta solo se la funzione identica gira sull'indice e sulla query. Fai l'esperimento: sei query contro sei nomi. Senza normalizzazione, «cafe» trova Cafe Central e «café» trova Café Amélie: due risultati, ciascuno a metà giusto. Ripiega solo la query e «café» ora trova Cafe Central e manca Café Amélie, cioè proprio la corrispondenza esatta che prima otteneva, mentre «müller» scende da un risultato a zero. Ripiega solo l'indice e «café» non restituisce più nulla. Ripiega entrambi i lati e ogni grafia restituisce entrambi i caffè. Una normalizzazione unilaterale non ammorbidisce la corrispondenza; sposta soltanto il disallineamento altrove. Il ripiegamento in sé è NFD seguito dalla cancellazione dei segni combinanti: é è il punto di codice singolo U+00E9, diventa U+0065 U+0301 dopo la decomposizione, e cancellare U+0301 lascia e. Funziona per é, ö, å e ñ, e non fa assolutamente nulla per ø, ł, ß, œ e ı, che sono punti di codice singoli senza segno combinante e attraversano NFD intatti. Per quelli serve una tabella per lingua — ö tedesco verso oe, ß verso ss — oppure un Intl.Collator con sensibilità base, che avvicina Ørsted e Orsted senza cancellare nulla.
Ripiegare i segni diacritici è un passaggio di normalizzazione, e la normalizzazione funziona solo se la stessa funzione gira sull'indice e sulla query. NFC contro NFD con i punti di codice in chiaro, e le lettere — ø, ł, ß, œ, ı — che escono dal ripiegamento intatte.
La normalizzazione è una funzione, e deve girare da entrambi i lati
Ecco l'intero argomento in un esperimento. Prendi un indice di sei nomi — Café Amélie, Cafe Central, Zoë Müller, Zoe Miller, Łukasz Nowak, Ørsted Energi — e sei query: cafe, café, muller, müller, lukasz, orsted. Senza alcuna normalizzazione, cafe trova una voce e café ne trova un'altra, muller non trova nulla e müller trova Zoë Müller. Sei query, due risultati. È questo il problema che si vuole risolvere.
Ora applica il ripiegamento alla sola query, che è la modifica che si fa per prima perché la query è ciò che si controlla. Il risultato è peggiore, non migliore. La query café si ripiega in cafe e corrisponde a Cafe Central, mentre Café Amélie — la voce che un attimo prima trovava perfettamente — non corrisponde più affatto, perché l'indice conserva ancora la forma accentata. La query müller perde il suo unico risultato per la stessa ragione. Due risultati restano due risultati, ma non sono gli stessi due, e una delle perdite era una corrispondenza esatta.
Ripiega solo l'indice e accade l'immagine speculare: cafe trova ora entrambi i caffè, ma café non trova nulla, perché la query accentata non può più corrispondere all'indice ripiegato. Ripiega entrambi i lati e la tabella finalmente si comporta: le quattro grafie della query café restituiscono entrambe le voci, e le due grafie di Müller restituiscono l'unica voce. La regola che ne discende non è «togli gli accenti». È: qualunque funzione applichi, applica la stessa, in fase di indicizzazione e in fase di query, dallo stesso percorso di codice. Una normalizzazione applicata da un solo lato non è una versione più debole della cosa giusta: è una regola di corrispondenza diversa, e in genere peggiore.
NFC, NFD e che cos'è davvero il ripiegamento
Unicode consente di scrivere una lettera accentata in due modi. In forma composta, NFC, la lettera é è un unico punto di codice, U+00E9. In forma decomposta, NFD, sono due: U+0065, la e semplice, seguita da U+0301, l'accento acuto combinante. Entrambe appaiono identiche a schermo. Entrambe sono corrette. Non sono la stessa stringa: in JavaScript la é composta ha lunghezza 1 e la é decomposta lunghezza 2, e l'uguaglianza stretta fra le due è falsa. Una parola come café presa da una sorgente NFC e cercata in un documento NFD semplicemente non compare, e chi sviluppa vede una ricerca che fallisce su un testo che legge benissimo sulla pagina.
Il ripiegamento sfrutta proprio questo. Si normalizza in NFD, che scompone ogni lettera canonicamente decomponibile in una lettera base più i suoi segni, poi si cancella tutto ciò che appartiene alla categoria generale Unicode M: i segni combinanti. é diventa U+0065 U+0301 e quindi e. ñ diventa U+006E U+0303 e quindi n. å diventa U+0061 U+030A e quindi a. È tutto qui il meccanismo, ed è per questo che l'operazione si chiama propriamente ripiegamento e non rimozione: non si toglie nulla all'alfabeto, si scarta una distinzione.
Una conseguenza pratica: prima di ripiegare qualsiasi cosa, normalizza tutto a un'unica forma. Se il tuo indice è in NFC e il testo in entrata è in NFD, ripiegarli entrambi sulle stesse lettere base copre la differenza per caso, ma ogni confronto fatto prima del ripiegamento, o su un campo che hai scelto di non ripiegare, resterà sbagliato. Normalizza in NFC all'ingresso, come regola di archiviazione, e tratta il ripiegamento come un indice separato costruito sopra.
Le lettere che non portano alcun segno
Il meccanismo ha un modo di fallimento evidente: se una lettera non ha decomposizione canonica, NFD la lascia stare e non c'è alcun segno combinante da cancellare, quindi il ripiegamento non fa nulla. Eseguilo e vedi esattamente quali sono quelle lettere. ß resta ß. ø resta ø. ł resta ł. œ resta œ. æ resta æ. ı resta ı. đ resta đ. In ognuno di questi casi il segno diacritico non è un segno applicato a una lettera base; il tratto della l, la sbarra della o, il legamento fra la o e la e fanno parte del disegno della lettera, e Unicode codifica ciascuna come un carattere a pieno titolo.
Ecco perché l'esperimento dei sei nomi continua a non restituire nulla per lukasz e orsted anche dopo aver ripiegato entrambi i lati. Łukasz Nowak si ripiega in Łukasz Nowak, immutato; Ørsted Energi si ripiega in Ørsted Energi, immutato. Le due voci che un utente ha meno probabilità di digitare correttamente sono proprio le due su cui il ripiegamento non aiuta. Una parola come Łódź è il caso più chiaro: ripiegala e ottieni Łodz, perché ó e ź si decompongono e ł no, quindi il risultato non è né l'originale né la grafia ASCII che qualcuno cercherebbe.
La correzione è una piccola tabella esplicita applicata prima del passaggio NFD: ł verso l, ø verso oe oppure o, œ verso oe, æ verso ae, đ verso d, ß verso ss. La decomposizione di compatibilità, NFKD, risolve i legamenti œ e æ ma non le lettere sbarrate, quindi non è nemmeno lei una risposta generale. Non c'è modo di aggirare la domanda su quale lingua si stia indicizzando, ed è proprio di questo che parlano le tre sezioni seguenti.
Tedesco: oe, non o — e ss, non s
Le dieresi tedesche si decompongono, quindi il ripiegamento ingenuo produce qualcosa. Produce la cosa sbagliata. Größe si ripiega in Große: la dieresi sparisce, la esse dura resta, e il risultato è una vera parola tedesca che significa altro. Müller si ripiega in Muller, Öl in Ol. La convenzione che i lettori tedeschi si aspettano davvero, codificata nella DIN 5007 variante 2 e usata negli elenchi telefonici, è lo scioglimento in due lettere: ä verso ae, ö verso oe, ü verso ue, ß verso ss. Größe diventa Groesse, Müller diventa Mueller, Straße diventa Strasse.
La esse dura merita un paragrafo a sé, perché si comporta come nessun'altra lettera di questo elenco. Non ha decomposizione canonica, quindi NFD la lascia; anche NFKC la lascia, così Straße resta Straße. Ma il suo passaggio a maiuscolo in JavaScript dà la stringa di due caratteri SS, il che significa che una pipeline ingenua di «maiuscolo e poi confronto» la ripiega già correttamente, mentre un ripiegamento ingenuo degli accenti no. La forma capitale U+1E9E esiste e torna a ß in minuscolo. Se il tuo indice tedesco è costruito con il maiuscolo e la tua query togliendo gli accenti, ß corrisponderà in una direzione e non nell'altra: ancora il problema unilaterale, con un altro travestimento.
C'è un modo per farlo bene senza scrivere alcuna tabella. Un Intl.Collator per il tedesco con sensibilità base tratta già Grosse e Größe come uguali: è la collazione tedesca standard, in cui ß ha una differenza secondaria rispetto a ss. Chiedi la variante da elenco telefonico, la locale de-u-co-phonebk, e anche Groesse e Größe risultano uguali, perché quella collazione è proprio quella che tratta ö come oe. Il CLDR mantiene quelle tabelle; tu non devi.
Scandinavo e turco: lettere, non decorazioni
In danese, norvegese e svedese, æ, ø e å sono lettere dell'alfabeto, e vengono dopo la z. Un collator lo dimostra a schermo: ordina A, Aa, Å, Ø e Z con un collator danese e ottieni A, Z, Ø, Å, Aa; ordina le stesse cinque con un collator inglese e ottieni A, Å, Aa, Ø, Z. In danese, å non è stato discretamente archiviato accanto alla a: ha una posizione propria alla fine, e il digramma Aa si ordina insieme a lei. Ripiegare å in a non toglie quindi un accento, fonde due lettere diverse, e ripiegare ø in o ne fonde altre due.
Il turco offre il caso più netto, e riguarda le maiuscole più che gli accenti. Il turco distingue una ı senza punto, U+0131, da una i con il punto, e corrispondentemente una maiuscola con il punto İ, U+0130, dalla I normale. La minuscola di I è ı e la minuscola di İ è i, ma solo nella locale turca. Eseguilo in JavaScript e la trappola si vede: la stringa İSTANBUL messa in minuscolo con le regole predefinite è lunga nove caratteri, perché İ viene mappata su i seguita dal punto sovrascritto combinante U+0307. Messa in minuscolo con toLocaleLowerCase("tr") è lunga otto, il semplice istanbul che un utente digiterebbe. Un indice di ricerca costruito con il minuscolo predefinito non troverà mai quella query, e le due stringhe appaiono identiche a schermo.
Entrambi i casi puntano nella stessa direzione. Le lettere che rompono un ripiegamento ingenuo sono quelle che una lingua tratta come membri a pieno titolo del proprio alfabeto, e la trasformazione che una lingua si aspetta è una proprietà della lingua, non del carattere. È a questo che serve un argomento di locale, e passarlo non costa nulla.
Le parole che la tua stessa lingua non ti lascerà ripiegare
Il ripiegamento è distruttivo in un modo facile da dimostrare in qualunque lingua con segni diacritici. In italiano, è si ripiega in e: il verbo essere e la congiunzione diventano lo stesso token, e sono due delle parole più frequenti della lingua. Papà si ripiega in papa e però si ripiega in pero. Sono esattamente le collisioni che fanno sembrare rotta una lista di risultati a chi la legge.
Questo non è un argomento contro il ripiegamento. È un argomento a favore del conservare anche la forma non ripiegata. Lo schema che funziona sono due campi: conserva il testo originale esattamente come è stato scritto, normalizzato in NFC e nient'altro, e costruiscigli accanto un secondo campo ripiegato per la corrispondenza. Ordina le corrispondenze esatte sull'originale sopra quelle ripiegate, e il lettore che ha digitato l'accento ottiene per primo la voce che intendeva, mentre chi non l'ha digitato trova comunque qualcosa. Il ripiegamento come indice aggiuntivo è una funzionalità; il ripiegamento come sostituzione distruttiva dei tuoi dati è un bug che scoprirai più avanti.
Che cosa usare al posto di un ripiegamento scritto a mano
Per confrontare e ordinare, usa un collator invece di una trasformazione. Un Intl.Collator con sensibilità base dichiara cafe e café uguali, Muller e Müller uguali, Orsted e Ørsted uguali, e Lukasz e Łukasz uguali, comprese le quattro lettere che il ripiegamento NFD non riesce a toccare, perché le tabelle di collazione sanno che cosa sono quelle lettere. E lo fa senza produrre una stringa intermedia rovinata che poi va conservata da qualche parte.
Per gli slug degli URL, dove hai davvero bisogno di un output ASCII limitato, tieni pure il ripiegamento, ma guidalo prima con una mappa di lingua esplicita e poi con NFD, e controlla il risultato. Un generatore di slug è uno dei pochi posti in cui un ripiegamento distruttivo e irreversibile è corretto, perché uno slug non è un dato: è un'etichetta rigenerabile, ed è autorizzata a perdere distinzioni che il testo originale portava. Assicurati soltanto di applicare la mappa prima di NFD, altrimenti ł e ø attraverseranno lo slug e ne usciranno come caratteri inutilizzabili.
E qualunque cosa decidi, scrivila una volta sola. La causa più frequente del bug nel titolo non è un ripiegamento sbagliato: è un buon ripiegamento implementato due volte, una nell'indicizzatore e una nella casella di ricerca, da due persone, a sei mesi di distanza. Una funzione, esportata da un solo modulo, chiamata da entrambi i lati.
| Lettera | Punti di codice NFC | Punti di codice NFD | Il ripiegamento ingenuo dà | Che cosa richiede la lingua |
|---|---|---|---|---|
| é | U+00E9 | U+0065 U+0301 | e | e va bene per la ricerca, ma fonde parole che la lingua tiene distinte |
| ö | U+00F6 | U+006F U+0308 | o | oe in tedesco; o è accettabile in svedese e finlandese |
| ß | U+00DF | U+00DF (nessuna decomposizione) | ß, immutato | ss |
| ø | U+00F8 | U+00F8 (nessuna decomposizione) | ø, immutato | oe; è una lettera a sé, non una o decorata |
| å | U+00E5 | U+0061 U+030A | a | aa in danese e norvegese; una lettera distinta, ordinata dopo la z |
| ı | U+0131 | U+0131 (nessuna decomposizione) | ı, immutato | i per un indice in alfabeto latino, ma mai da confondere con i all'interno del turco |
| İ | U+0130 | U+0049 U+0307 | I | i, ma solo toLocaleLowerCase("tr") lo produce in un solo punto di codice |
| ł | U+0142 | U+0142 (nessuna decomposizione) | ł, immutato | l |
| œ | U+0153 | U+0153 (nessuna decomposizione) | œ, immutato | oe; NFKD lo darebbe, NFD no |
Domande frequenti
- Devo salvare il testo ripiegato o ripiegare al volo?
- Salvalo, come campo aggiuntivo, e mai come sostituzione. Ripiegare al volo significa ripiegare l'intero indice a ogni query, il che è lento, e rende fin troppo facile che un percorso di codice ripieghi e un altro se ne dimentichi. Un campo ripiegato salvato costa poco, è costruito una volta dalla stessa funzione che chiama la casella di ricerca, e lascia l'originale intatto per la corrispondenza esatta e per la visualizzazione. L'unica cosa da non fare è ripiegare sul posto: una volta sparita la forma accentata dal tuo database non puoi più mostrare il nome correttamente, e nessuna astuzia successiva la riporta indietro.
- NFKD è meglio di NFD qui, dato che decompone anche i legamenti?
- Risolve un problema e ne crea diversi. NFKD trasforma davvero œ in oe e fi in fi, che è proprio ciò che vuoi in un indice di ricerca. Ma la decomposizione di compatibilità riscrive anche gli apici come cifre normali, le lettere latine a larghezza piena come ASCII, il segno dell'ohm come un omega e vari caratteri di spaziatura come spazi semplici. In un campo di visualizzazione è distruttivo in modi che non hai chiesto. In un campo di corrispondenza è di solito accettabile e spesso utile. Quindi: NFC per l'archiviazione, NFKD come input di un campo di corrispondenza ripiegato se vuoi il comportamento sui legamenti, e una mappa esplicita per ø, ł e ß, che nessuna delle due forme risolverà.
- Lo fa il database per me se scelgo la collazione giusta?
- In larga misura sì, ed è di solito una risposta migliore del ripiegare nel codice applicativo. Una collazione insensibile agli accenti implementa la stessa idea del collator, al livello in cui il confronto avviene davvero, così indice e query sono confrontati sotto un'unica regola per costruzione. Due avvertenze. Primo, la collazione vale per colonna o per confronto, quindi una query che confronta una colonna collazionata con un'espressione che hai ripiegato tu torna al problema unilaterale. Secondo, le collazioni insensibili agli accenti dipendono dalla lingua esattamente come descritto sopra, quindi scegli quella che corrisponde ai tuoi contenuti e non un valore predefinito generico.
- Perché due stringhe che sembrano identiche falliscono un test di uguaglianza?
- Perché una è composta e l'altra decomposta. L'uguaglianza stretta confronta unità di codice, e una é composta è un'unità mentre una é decomposta ne è due, quindi il test fallisce anche se la resa è identica al pixel. È la sorpresa Unicode più frequente in una funzione di ricerca, ed è anche la più facile da correggere: normalizza entrambi gli operandi in NFC prima di confrontare. Nota che un confronto sensibile alla locale dichiara già le due equivalenti, il che è una buona diagnosi: se localeCompare restituisce zero e l'uguaglianza stretta restituisce falso, hai trovato un disallineamento di normalizzazione e non un errore nei dati.
- È mai giusto ripiegare il nome di una persona?
- Per la corrispondenza, sì. Per la visualizzazione, no. Una persona che si chiama Zoë Müller ha diritto a vedere il proprio nome scritto correttamente sullo schermo, sulla fattura e nell'email, e un sistema che conserva solo la forma ripiegata non può garantirlo, qualunque cosa faccia dopo. Ripiega verso una chiave di ricerca accanto al record, mai sopra di esso, e assicurati che ogni percorso di uscita legga il campo originale. È anche la ragione pratica per cui lo schema a due campi vince: rende il percorso di visualizzazione e quello di corrispondenza strutturalmente diversi, così nessuno può stampare per sbaglio la chiave di ricerca.
Articoli che potrebbero interessarti
Tutte le guide →Strumenti correlati
Fonti
- Unicode Consortium — Unicode Standard Annex #15: Unicode Normalization Forms — canonical and compatibility decomposition
- Unicode Consortium — Unicode Technical Standard #10: Unicode Collation Algorithm — collation strength and secondary differences
- Unicode Consortium — CLDR — Common Locale Data Repository, locale collation tables and the German phonebook variant
- MDN Web Docs — Intl.Collator — the sensitivity option and locale-aware comparison
- W3C — Internationalization Activity — character encoding, normalization and string matching on the web
Hai notato un errore in questo articolo?