Retirar os acentos estraga a pesquisa — até o fazer dos dois lados
Publicado a 09/07/2026 · 16 min de leitura · Ferramentas de texto e idioma
Daniel Okonkwo — Programador front-end e redator de Tecnologia na OneKitly
Desempenho web · Formatos de ficheiro
Verificado a partir de 5 fontes
Retirar os diacríticos é um passo de normalização, não uma funcionalidade de pesquisa: só ajuda se a função idêntica correr sobre o índice e sobre a consulta. Faça a experiência: seis consultas contra seis nomes. Sem normalização, «cafe» encontra Cafe Central e «café» encontra Café Amélie: dois acertos, cada um a meio. Rebata só a consulta e «café» passa a encontrar Cafe Central e falha Café Amélie, a correspondência exata que antes obtinha, enquanto «müller» desce de um acerto para zero. Rebata só o índice e «café» não devolve nada. Rebata os dois lados e todas as grafias devolvem os dois cafés. Uma normalização unilateral não suaviza a correspondência; desloca o desencontro para outro sítio. O rebatimento em si é NFD seguido de apagar as marcas combinantes: é é o ponto de código único U+00E9, passa a U+0065 U+0301 depois da decomposição, e apagar U+0301 deixa e. Funciona para é, ö, å e ñ, e não faz absolutamente nada com ø, ł, ß, œ e ı, que são pontos de código únicos sem marca combinante e atravessam a NFD intactos. Esses exigem uma tabela por língua — ö alemão para oe, ß para ss — ou um Intl.Collator com sensibilidade base, que aproxima Ørsted de Orsted sem apagar nada.
Rebater os diacríticos é um passo de normalização, e a normalização só funciona quando a mesma função corre sobre o índice e sobre a consulta. NFC contra NFD com os pontos de código à vista, e as letras — ø, ł, ß, œ, ı — que saem do rebatimento intactas.
A normalização é uma função, e tem de correr dos dois lados
Aqui está todo o argumento numa experiência. Tome um índice de seis nomes — Café Amélie, Cafe Central, Zoë Müller, Zoe Miller, Łukasz Nowak, Ørsted Energi — e seis consultas: cafe, café, muller, müller, lukasz, orsted. Sem qualquer normalização, cafe encontra uma entrada e café encontra outra diferente, muller não encontra nada e müller encontra Zoë Müller. Seis consultas, dois acertos. É esse o problema que se pretende corrigir.
Aplique agora o rebatimento só à consulta, que é a mudança que se faz primeiro porque a consulta é o que se controla. O resultado é pior, não melhor. A consulta café rebate para cafe e corresponde a Cafe Central, enquanto Café Amélie — a entrada que encontrava perfeitamente há um instante — já não corresponde de todo, porque o índice continua a guardar a forma acentuada. A consulta müller perde o seu único acerto pela mesma razão. Dois acertos continuam a ser dois acertos, mas não são os mesmos dois, e uma das perdas era uma correspondência exata.
Rebata só o índice e acontece a imagem espelhada: cafe encontra agora os dois cafés, mas café não encontra nada, porque a consulta acentuada já não pode corresponder ao índice rebatido. Rebata os dois lados e a tabela finalmente comporta-se: as quatro grafias da consulta café devolvem as duas entradas, e as duas grafias de Müller devolvem a única entrada. A regra que isto estabelece não é «retire os acentos». É: seja qual for a função que aplicar, aplique a mesma, ao indexar e ao consultar, a partir do mesmo caminho de código. Uma normalização aplicada só de um lado não é uma versão suave do correto: é uma regra de correspondência diferente e em geral pior.
NFC, NFD e o que é realmente o rebatimento
O Unicode permite escrever uma letra acentuada de duas formas. Na forma composta, NFC, a letra é é um único ponto de código, U+00E9. Na forma decomposta, NFD, são dois: U+0065, o e simples, seguido de U+0301, o acento agudo combinante. Ambas se veem idênticas no ecrã. Ambas estão corretas. Não são a mesma cadeia: em JavaScript, o é composto tem comprimento 1 e o é decomposto comprimento 2, e a igualdade estrita entre ambos é falsa. Uma palavra como café tirada de uma fonte NFC e procurada num documento NFD simplesmente não aparece, e o programador vê uma pesquisa que falha sobre um texto que está a ler na página.
O rebatimento explora isto. Normaliza-se para NFD, o que separa toda a letra canonicamente decomponível numa letra base mais as suas marcas, e depois apaga-se tudo o que cai na categoria geral Unicode M: as marcas combinantes. é passa a U+0065 U+0301 e daí a e. ñ passa a U+006E U+0303 e daí a n. å passa a U+0061 U+030A e daí a a. É esse todo o mecanismo, e é por isso que a operação se chama com propriedade rebatimento e não remoção: não se tira nada do alfabeto, descarta-se uma distinção.
Uma consequência prática: antes de rebater seja o que for, normalize tudo para uma só forma. Se o seu índice está em NFC e o texto de entrada em NFD, rebater ambos para as mesmas letras base tapa a diferença por acidente, mas qualquer comparação feita antes do rebatimento, ou sobre um campo que decidiu não rebater, continuará errada. Normalize para NFC à entrada, como regra de armazenamento, e trate o rebatimento como um índice à parte construído por cima.
As letras que não levam marca nenhuma
O mecanismo tem um modo de falha evidente: se uma letra não tem decomposição canónica, a NFD deixa-a como está e não há marca combinante para apagar, por isso o rebatimento não faz nada. Execute-o e verá exatamente quais são essas letras. ß continua ß. ø continua ø. ł continua ł. œ continua œ. æ continua æ. ı continua ı. đ continua đ. Em cada um destes casos o diacrítico não é uma marca aplicada a uma letra base; o traço do l, a barra do o, a ligadura entre o o e o e fazem parte do desenho da letra, e o Unicode codifica cada uma como um caráter por direito próprio.
É por isso que a experiência dos seis nomes continua a não devolver nada para lukasz e orsted, mesmo depois de rebater os dois lados. Łukasz Nowak rebate para Łukasz Nowak, inalterado; Ørsted Energi rebate para Ørsted Energi, inalterado. As duas entradas que um utilizador tem menos probabilidade de escrever bem são precisamente as duas com que o rebatimento não ajuda. Uma palavra como Łódź é o caso mais claro: rebata-a e obtém Łodz, porque ó e ź se decompõem e ł não, pelo que o resultado não é nem o original nem a grafia ASCII que alguém procuraria.
A correção é um pequeno mapa explícito aplicado antes da passagem NFD: ł para l, ø para oe ou o, œ para oe, æ para ae, đ para d, ß para ss. A decomposição de compatibilidade, NFKD, resolve as ligaduras œ e æ mas não as letras com traço, por isso também não é uma resposta geral. Não há forma de contornar a pergunta sobre que língua se está a indexar, e é disso mesmo que tratam as três secções seguintes.
Alemão: oe, não o — e ss, não s
Os tremas alemães decompõem-se, por isso o rebatimento ingénuo produz alguma coisa. Produz a coisa errada. Größe rebate para Große: o trema desaparece, o s duro fica, e o resultado é uma palavra alemã real que significa outra coisa. Müller rebate para Muller, Öl para Ol. A convenção que os leitores alemães realmente esperam, codificada na DIN 5007 variante 2 e usada nas listas telefónicas, é o desdobramento em duas letras: ä para ae, ö para oe, ü para ue, ß para ss. Größe passa a Groesse, Müller a Mueller, Straße a Strasse.
O s duro merece o seu próprio parágrafo, porque se comporta como nenhuma outra letra desta lista. Não tem decomposição canónica, por isso a NFD deixa-o; a NFKC também o deixa, de modo que Straße continua Straße. Mas a sua passagem a maiúsculas em JavaScript dá a cadeia de dois caracteres SS, o que significa que um pipeline ingénuo de «maiúsculas e depois comparar» já o rebate bem, enquanto um rebatimento de acentos ingénuo não. A forma capital U+1E9E existe e volta a ß em minúsculas. Se o seu índice alemão for construído com maiúsculas e a sua consulta com remoção de acentos, ß corresponderá num sentido e não no outro: outra vez o problema unilateral, com outro disfarce.
Há uma forma de acertar sem escrever tabela nenhuma. Um Intl.Collator alemão com sensibilidade base já trata Grosse e Größe como iguais: é a colação alemã padrão, onde ß tem uma diferença secundária em relação a ss. Peça a variante de lista telefónica, a configuração regional de-u-co-phonebk, e Groesse e Größe também comparam como iguais, porque essa colação é precisamente a que trata ö como oe. O CLDR mantém essas tabelas; não tem de o fazer.
Escandinavo e turco: letras, não decorações
Em dinamarquês, norueguês e sueco, æ, ø e å são letras do alfabeto, e vêm depois do z. Um colador prova-o no ecrã: ordene A, Aa, Å, Ø e Z com um colador dinamarquês e obtém A, Z, Ø, Å, Aa; ordene essas mesmas cinco com um colador inglês e obtém A, Å, Aa, Ø, Z. Em dinamarquês, å não foi discretamente arrumado ao lado do a: tem a sua própria posição no fim, e o dígrafo Aa ordena-se com ele. Rebater å para a não retira, portanto, um acento: funde duas letras diferentes, e rebater ø para o funde outras duas.
O turco oferece o caso mais nítido, e é sobre maiúsculas e minúsculas, não sobre acentos. O turco distingue um ı sem ponto, U+0131, de um i com ponto, e correspondentemente uma maiúscula com ponto İ, U+0130, do I normal. A minúscula de I é ı e a minúscula de İ é i, mas apenas na configuração regional turca. Execute-o em JavaScript e a armadilha aparece: a cadeia İSTANBUL passada a minúsculas com as regras por omissão mede nove caracteres, porque İ se converte em i seguido do ponto sobrescrito combinante U+0307. Passada a minúsculas com toLocaleLowerCase("tr") mede oito, o istanbul simples que um utilizador escreveria. Um índice de pesquisa construído com a minúscula por omissão nunca encontrará essa consulta, e as duas cadeias veem-se idênticas no ecrã.
Ambos os casos apontam para o mesmo. As letras que estragam um rebatimento ingénuo são as que uma língua trata como membros de pleno direito do seu alfabeto, e a transformação que uma língua espera é uma propriedade da língua, não do caráter. É para isso que existe um argumento de configuração regional, e passá-lo não custa nada.
As palavras que a sua própria língua não o deixará rebater
O rebatimento é destrutivo de um modo fácil de provar em qualquer língua com diacríticos. Em português, avô e avó rebatem ambos para avo: o avô e a avó tornam-se o mesmo token, e a distinção que a língua marca com o acento desaparece por completo. Pêlo e pelo seguem o mesmo caminho. É exatamente o tipo de colisão que faz uma lista de resultados parecer avariada a quem a lê.
Isto não é um argumento contra o rebatimento. É um argumento a favor de conservar também a forma não rebatida. O padrão que funciona são dois campos: guarde o texto original tal como foi escrito, normalizado para NFC e mais nada, e construa ao lado um segundo campo rebatido para a correspondência. Ordene as correspondências exatas sobre o original acima das rebatidas, e o leitor que escreveu o acento obtém primeiro a entrada que queria, enquanto quem não o escreveu encontra na mesma alguma coisa. O rebatimento como índice adicional é uma funcionalidade; o rebatimento como substituição destrutiva dos seus dados é um erro de que só saberá mais tarde.
O que usar em vez de um rebatimento escrito à mão
Para comparar e ordenar, use um colador em vez de uma transformação. Um Intl.Collator com sensibilidade base declara cafe e café iguais, Muller e Müller iguais, Orsted e Ørsted iguais, e Lukasz e Łukasz iguais, incluindo as quatro letras que o rebatimento NFD não consegue tocar, porque as tabelas de colação sabem o que são essas letras. E fá-lo sem produzir uma cadeia intermédia estragada que depois tenha de ser guardada algures.
Para os slugs de URL, onde precisa mesmo de uma saída ASCII limitada, mantenha o rebatimento, mas conduza-o primeiro com um mapa de língua explícito e só depois com a NFD, e verifique o resultado. Um gerador de slugs é um dos poucos sítios onde um rebatimento destrutivo e irreversível é correto, porque um slug não é um dado: é uma etiqueta regenerável, e tem licença para perder distinções que o texto original carregava. Só garanta que o mapa é aplicado antes da NFD, ou ł e ø atravessarão o slug e sairão como caracteres inutilizáveis.
E seja qual for a sua decisão, escreva-a uma só vez. A causa mais frequente do erro do título não é um mau rebatimento: é um bom rebatimento implementado duas vezes, uma no indexador e outra na caixa de pesquisa, por duas pessoas, com seis meses de intervalo. Uma função, exportada de um só módulo, chamada dos dois lados.
| Letra | Pontos de código NFC | Pontos de código NFD | O rebatimento ingénuo dá | O que a língua exige |
|---|---|---|---|---|
| é | U+00E9 | U+0065 U+0301 | e | e serve para pesquisar, mas funde palavras que a língua distingue |
| ö | U+00F6 | U+006F U+0308 | o | oe em alemão; o é aceitável em sueco e finlandês |
| ß | U+00DF | U+00DF (sem decomposição) | ß, inalterado | ss |
| ø | U+00F8 | U+00F8 (sem decomposição) | ø, inalterado | oe; é uma letra própria, não um o decorado |
| å | U+00E5 | U+0061 U+030A | a | aa em dinamarquês e norueguês; uma letra distinta, ordenada depois do z |
| ı | U+0131 | U+0131 (sem decomposição) | ı, inalterado | i para um índice em alfabeto latino, mas nunca confundi-la com i dentro do turco |
| İ | U+0130 | U+0049 U+0307 | I | i, mas só toLocaleLowerCase("tr") o produz num único ponto de código |
| ł | U+0142 | U+0142 (sem decomposição) | ł, inalterado | l |
| œ | U+0153 | U+0153 (sem decomposição) | œ, inalterado | oe; NFKD dá-lo-ia, NFD não |
Perguntas frequentes
- Devo guardar o texto rebatido ou rebater na hora?
- Guarde-o, como campo adicional, e nunca como substituição. Rebater na hora significa rebater todo o índice em cada consulta, o que é lento, e torna demasiado fácil que um caminho de código rebata e outro se esqueça. Um campo rebatido guardado é barato, é construído uma vez pela mesma função que a caixa de pesquisa chama, e deixa o original intacto para a correspondência exata e para exibição. A única coisa que não deve fazer é rebater no lugar: uma vez desaparecida a forma acentuada da sua base de dados, já não consegue mostrar o nome corretamente, e nenhuma esperteza posterior a traz de volta.
- A NFKD é melhor do que a NFD aqui, já que também decompõe as ligaduras?
- Resolve um problema e cria vários. A NFKD converte de facto œ em oe e fi em fi, que é exatamente o que se quer num índice de pesquisa. Mas a decomposição de compatibilidade também reescreve os expoentes como algarismos normais, as letras latinas de largura completa como ASCII, o sinal de ohm como um ómega e vários caracteres de espaçamento como espaços simples. Num campo de exibição isso é destrutivo de formas que não pediu. Num campo de correspondência costuma ser aceitável e muitas vezes útil. Portanto: NFC para armazenar, NFKD como uma entrada do campo rebatido de correspondência se quiser o comportamento com ligaduras, e um mapa explícito para ø, ł e ß, que nenhuma das formas vai corrigir.
- A base de dados faz isto por mim se eu escolher a colação certa?
- Em boa medida sim, e costuma ser melhor resposta do que rebater no código da aplicação. Uma colação insensível a acentos implementa a mesma ideia do colador, ao nível onde a comparação acontece de facto, por isso o índice e a consulta são comparados sob uma só regra por construção. Duas ressalvas. Primeira, a colação é por coluna ou por comparação, por isso uma consulta que compare uma coluna colacionada contra uma expressão que rebateu à mão volta ao problema unilateral. Segunda, as colações insensíveis a acentos dependem da língua exatamente como descrito acima, por isso escolha a que corresponde ao seu conteúdo e não um valor por omissão genérico.
- Porque é que duas cadeias que parecem idênticas falham um teste de igualdade?
- Porque uma está composta e a outra decomposta. A igualdade estrita compara unidades de código, e um é composto é uma unidade enquanto um é decomposto são duas, por isso o teste falha embora a renderização seja idêntica ao pixel. É a surpresa Unicode mais frequente numa funcionalidade de pesquisa, e também a mais fácil de corrigir: normalize os dois operandos para NFC antes de comparar. Note que uma comparação sensível à configuração regional já declara as duas equivalentes, o que é um bom diagnóstico: se localeCompare devolver zero e a igualdade estrita devolver falso, encontrou um desencontro de normalização e não um erro de dados.
- Alguma vez é correto rebater o nome de uma pessoa?
- Para a correspondência, sim. Para exibição, não. Alguém chamada Zoë Müller tem direito a ver o seu nome bem escrito no ecrã, na fatura e no email, e um sistema que só guarda a forma rebatida não o consegue garantir faça o que fizer depois. Rebata para uma chave de pesquisa ao lado do registo, nunca por cima, e garanta que todos os caminhos de saída leem o campo original. É também esta a razão prática pela qual o padrão de dois campos vence: torna o caminho de exibição e o de correspondência estruturalmente diferentes, para que ninguém possa imprimir a chave de pesquisa por acidente.
Artigos que podem interessar-lhe
Todos os guias →Ferramentas relacionadas
Fontes
- 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
Detetaste um erro neste artigo?