Soundex Français
Par Florent Bruneau le jeudi 15 mars 2007, 18:06:00 - Polytechnique.org - Lien permanent
Pour faire une recherche phonétique, on utilise souvent ce qu'on appelle une transcription soundex des mots. C'est une réécriture du mot, dans un alphabet restreint et sur un nombre de caractères restreint également. La plupart des algorithmes qu'on peut trouver sur internet sont conçus uniquement à la langue anglaise. Pour utiliser la recherche phonétique en français, il faut donc adapter cet algorithme.
L'implémentation française la plus courante utilise l'algorithme décrit par Frédéric Brouard. Malheureusement cet algorithme ne me satisfait pas vraiment, car il n'est finalement pas très adapté à langue française.
En effet cet algorithme a pas mal de défauts :
- il est très aléatoire dans le cas de dédoublement de consonnes. Par exemple, il ne permet de trouver que bananne (soundex
BNN) est une approximation de banane (soundexBN) - il est très peu sélectif sur les voyelles. Par exemple poulpe et palpa ont le même soundex (
PLP) alors que pour moi, poulpe et palpa sont deux mots éloignés, malgré leur consonnes communes. Par contre, il ne reconnaîtra pas Aymeric (soundexAYMR) comme étant un homophone de Emeric (soundexEMRC)... - il n'est pas vraiment capable de distinguer les conjugaisons. Par exemple palper (soundex
PLPR) et palpé (soundexPLP) sont éloignés l'un de l'autre.
J'ai donc réalisé une nouvelle implémentation de soundex qui est plus adaptée (à mon sens) à la langue française. Je n'ai évidemment pas pour ambition de réaliser une solution parfaite (ce qui est impossible à faire à partir d'un système automatisé sans dictionnaire).
Algorithme
Mon algorithme, comme je l'ai déjà dit, est une adaptation de celui de Frédéric Brouard. Il se compose de 2 étapes principales :
- le préformatage, qui consiste à transformer le chaîne de caractère brute, en une chaîne analysable.
- l'analyse de la chaîne, et la recherche de entités phoniques élémentaires qui la composent
Préformatage
Le préformatage est extrêmement simple :
- On converti la chaîne en majuscule
- On converti chaque caractère accentué vers son caractère non-accentué correspondant
- On filtre pour ne conserver que les lettres (de A à Z)
Ainsi après ce premier filtrage, Mac-Cartney devient MACCARTNEY et palpé sera PALPE
Traitement des données
Le traitement des données consiste à reconnaître les sonorités. Pour ceci on utilise la table de conversion suivante qui associe à chaque sonorité complexe un caractère :
| Combinaison | Caractère |
|---|---|
| G (prononcé GUE), C (prononcé Q), CK, K, QU, Q | K |
| CH, SH, SCH | 9 |
| ST | T |
| PF (en début de mot), PH | F |
| Z (en fin de mot) | ZE |
| Z, ZZ, C (prononcé SE) | S |
| G (prononcé JE) | J |
| EAU, AU | O |
| IN, UN, AIN, EIN (proncés UN) | 1 |
| AON, AOM, EN, AN (prononcés EN) | A |
| EY, EI, AY, AI, OE, OEU, EU, ER | E |
| OI | O |
| ILLE, I | Y |
| OU, OW | U |
| ON, OM | O |
| KN (en début de mot) | N |
Une fois qu'on a cette table de correspondances (elle n'est peut-être pas exhaustive... je n'hésiterais pas à l'améliorer), on applique toutes les modifications, on supprime les lettres qui sont présentes en doublon et finalement on supprime les H restants. Il faut également supprimer les caractères muets en fin de mot. En français, ce seront les X, T, D, S (et le L qui les précède si il existe) ou les E.
Ensuite, il faut faire un nettoyage sur la chaîne de caractère. Ce nettoyage consiste à rechercher les phonèmes importants et à supprimer les caractères muets ou peu audibles : on veut identifier ce qui fait la particularité d'une syllabe. Frédéric Brouard considère que toutes les voyelles (exceptés les Y précédés d'une voyelle) placées autre part qu'en début de mot sont insignifiantes... ça me paraît le gros point faible de sa méthode. Personnellement j'ai essayé d'identifier une liste de sonorité dominantes. Voici celles que j'ai actuellement :
| Sonorité | Informations |
|---|---|
| K, T, P | Ce sont les consonnes qui sont très marquées, qui rythment le mot |
| A, U, O, 1 | Ce sont les son qu'on entend le mieux, contrairement aux I ou aux E qui mettent plus en avant les consonnes. Le A est à mon avis particulier, car il est faible mais suffisamment marquant pour effacer des consonnes comme le R |
| Y | Y n'est marquant que lorsqu'il a un rôle de consonne : quand il permet de lier deux parties du mot, comme dans ''voyelle'', où il lie le ''vo'' et le ''elle''. Il est donc important lorsqu'il est encadré par une ou des voyelles. Dans les autres cas, c'est une voyelle faible |
| L | Contrairement aux K, T et P, le L permet d'adoucir le mot, en particulier lorsqu'il sert de liaison entre une consonne et une voyelle. Il est donc important de le conserver |
Ces choix sont relativement arbitraires. Le comportement de l'algorithme est alors simple :
- Si une voyelle est liée à une consonne de liaison (qui fait le lien entre la voyelle et une autre consonne, c'est souvent le cas du L ou du R, on exclut de ce cas les consommes fortes)
- Si en plus il s'agit d'une voyelle forte
- alors, on supprime la consonne de liaison et on garde la voyelle
Ensuite, on supprime les voyelles faibles (sauf si elles commencent le mot). Pour cette suppression, on considère le A comme une voyelle faible.
Pour terminer on coupe la chaîne obtenue à 4 caractères (mais il est parfaitement envisageable de prendre un soundex sur plus (ou moins) de caractères. Simplement, dans la plupart des cas, 4 caractères suffisent amplement à avoir un mot significatif.
Implémentation
Mon implémentation en PHP est téléchargeable ici. Elle est testable plus bas dans cette même page. Voici le code de la fonction soundex_fr (je ne garantis pas que le code disponible sur cette page reste constamment à jour, contrairement au lien de téléchargement) :[1]
function soundex_fr($sIn)
{
static $convVIn, $convVOut, $convGuIn, $convGuOut, $accents;
if (!isset($convGuIn)) {
$accents = array('É' => 'E', 'È' => 'E', 'Ë' => 'E', 'Ê' => 'E',
'Á' => 'A', 'À' => 'A', 'Ä' => 'A', 'Â' => 'A', 'Å' => 'A', 'Ã' => 'A',
'Ï' => 'I', 'Î' => 'I', 'Ì' => 'I', 'Í' => 'I',
'Ô' => 'O', 'Ö' => 'O', 'Ò' => 'O', 'Ó' => 'O', 'Õ' => 'O', 'Ø' => 'O',
'Ú' => 'U', 'Ù' => 'U', 'Û' => 'U', 'Ü' => 'U',
'Ç' => 'C', 'Ñ' => 'N', 'Ç' => 'S', '¿' => 'E',
'é' => 'e', 'è' => 'e', 'ë' => 'e', 'ê' => 'e',
'á' => 'a', 'à' => 'a', 'ä' => 'a', 'â' => 'a', 'å' => 'a', 'ã' => 'a',
'ï' => 'i', 'î' => 'i', 'ì' => 'i', 'í' => 'i',
'ô' => 'o', 'ö' => 'o', 'ò' => 'o', 'ó' => 'o', 'õ' => 'o', 'ø' => 'o',
'ú' => 'u', 'ù' => 'u', 'û' => 'u', 'ü' => 'u',
'ç' => 'c', 'ñ' => 'n');
$convGuIn = array( 'GUI', 'GUE', 'GA', 'GO', 'GU', 'SCI', 'SCE', 'SC', 'CA', 'CO',
'CU', 'QU', 'Q', 'CC', 'CK', 'G', 'ST', 'PH');
$convGuOut = array( 'KI', 'KE', 'KA', 'KO', 'K', 'SI', 'SE', 'SK', 'KA', 'KO',
'KU', 'K', 'K', 'K', 'K', 'J', 'T', 'F');
$convVIn = array( '/E?(AU)/', '/([EA])?[UI]([NM])([^EAIOUY]|$)/', '/[AE]O?[NM]([^AEIOUY]|$)/',
'/[EA][IY]([NM]?[^NM]|$)/', '/(^|[^OEUIA])(OEU|OE|EU)([^OEUIA]|$)/', '/OI/',
'/(ILLE?|I)/', '/O(U|W)/', '/O[NM]($|[^EAOUIY])/', '/(SC|S|C)H/',
'/([^AEIOUY1])[^AEIOUYLKTPNR]([UAO])([^AEIOUY])/', '/([^AEIOUY]|^)([AUO])[^AEIOUYLKTP]([^AEIOUY1])/', '/^KN/',
'/^PF/', '/C([^AEIOUY]|$)/', '/E(Z|R)$/',
'/C/', '/Z$/', '/(?<!^)Z+/', '/H/', '/W/');
$convVOut = array( 'O', '1\\3', 'A\\1',
'E\\1', '\\1E\\3', 'O',
'Y', 'U', 'O\\1', '9',-
'\\1\\2\\3', '\\1\\2\\3', 'N',
'F', 'K\\1', 'E',
'S', 'SE', 'S', '', 'V');
}
// Si il n'y a pas de mot, on sort immédiatement
if ( $sIn === '' ) return ' ';
// On supprime les accents-
$sIn = strtr( $sIn, $accents);
// On met tout en minuscule-
$sIn = strtoupper( $sIn );
// On supprime tout ce qui n'est pas une lettre
$sIn = preg_replace( '`[^A-Z]`', '', $sIn );
// Si la chaîne ne fait qu'un seul caractère, on sort avec.
if ( strlen( $sIn ) === 1 ) return $sIn . ' ';
// on remplace les consonnances primaires
$sIn = str_replace( $convGuIn, $convGuOut, $sIn );
// on supprime les lettres répétitives
$sIn = preg_replace( '`(.)\\1`', '$1', $sIn );
// on réinterprète les voyelles
$sIn = preg_replace( $convVIn, $convVOut, $sIn);
// on supprime les terminaisons T, D, S, X (et le L qui précède si existe)
$sIn = preg_replace( '`L?[TDX]?S?$`', '', $sIn );
// on supprime les E, A et Y qui ne sont pas en première position
$sIn = preg_replace( '`(?!^)Y([^AEOU]|$)`', '\\1', $sIn);
$sIn = preg_replace( '`(?!^)[EA]`', '', $sIn);
return substr( $sIn . ' ', 0, 4);
}
Tests
Avec cette implémentation, on a par exemple :
| Mot | Soundex | Mot | Soundex |
|---|---|---|---|
| Aymeric | EMRK | Emeric | EMRK |
| Banane | BNN | Bananne | BNN |
| Palper | PLP | Palpé | PLP |
| Palpa | PLP | Poulpe | PULP |
| Mario | MRYO | Marion | MRYO |
On a par contre des résultats moyens lorsque des H séparent des voyelles faibles : par exemple Mouahaha donne MU (contre M avec la version précédente).
N'hésitez pas à faire des tests et à me faire part des résultats qui semblent anormaux.
Conclusion
Cette version est loin d'être parfaite, mais elle est plus adaptée au français que celle de Frédéric Brouard. Elle n'est d'ailleurs pas terminée et risque d'évoluer dans un futur proche, en particuliers pour ce qui est des tables de transcriptions et de prédominances.
Commentaires
Pas mal! :-)
Juste une petite remarque, pourquoi s'arrêter à 4 caractères?
antico = anticonstitutionnellement = ATKO... et pourtant c'est bien différent! :-|
Y'a une faute de frappe dans $convVOut, il y a un - après le '9' qui fait tout foirer. ;-)
Les algorithmes de soundex ont été conçu pour analyser des annuaires de noms propres qui ont rarement plus de 3 à 4 syllabes (le problème étant par exemple de retrouver à la fois les Jack et les Jacques si on a entendu le nom, sans l'avoir lu). Avec 4 caractères, on représente correctement la plupart des noms propres, mais effectivement ce n'est pas adapté pour faire une recherche sonore sur un dictionnaire.
La taille à utiliser dépend des mots qu'on désire comparer et de la proximité recherchée : plus il y aura de caractères dans le soundex, plus la classe d'équivalence (l'ensemble des mots ayant le même soundex) sera réduite, et plus le risque de voir deux mots proches avoir des soundex différents augmente... tout est une question de compromis.
Merci pour les '-', c'est corrigé.
D'accord. J'ai simplement supprimé la réduction de taille à 4 caractères, et ça marche très bien. :-)
Très bon algorithme
seules chose dommages
- "ou" et "u" ne devraient peut être pas être confondus, leur diction est presque aussi différente en français que é et i.
- lettres muettes prises en compte & detail anormal :
grand = J
gran (faute d'orthographe) = JR
flan = FL
flanc = FLK
- sonorités non prises en compte
mail= ML
malheur = ML
(il est biensur normal que des mots n'ayant pas le meme orthographe donnent le meme code, c'est le but même de l'algorithme, mais EUR ne devrait il pas avoir un poids ? si l'on veut considerer que la sonorité de cette syllabe n'est pas déterminante, il faudrait peut être au moins que sa présence soit prise en compte, en créant un nouveau code pour toute syllabe faible mais obligatoirement prononçable, par exemple toute syllabe en fin de mot finissant par une consonne non muette et non prise en compte actuellement , ou plus simplement, conserver cette consonne ? > malheur => MLR )
dessert = desert = DS
problème de pluriel : le pluriel fait prendre en compte l'avant dernière lettre :
maillot = MLO
maillots = MLOT
malotru = MLOT
- si "SC" est precédé par une consonne suivie d'une voyelle, et suivi d'une voyelle, il se prononce "C" et non "SK"
descendre= DSKR
dessendre = DSR = désir
je sais que ce sont des cas particuliers qui ne sont pas faciles à prendre en compte, mais c'est justement sur ce genre de difficultés orthographiques que ce type d'algorithme est particulièrement utile
c'est toutefois un bon algorithme, et ce commentaire n'a pas vocation à le descendre, mais à apporter une remarque constructive ;)
Effectivement, l'algorithme n'est pas parfait. Ce sont de bons exemples pour chercher des améliorations. J'espère avoir un peu de temps pour le faire dans les semaines qui viennent.
Merci pour le commentaire.
J'avais fait quelques modifs déjà, et j'en ai refait un peu suite au commentaire de Stephane. ;-)
Principalement les 2 tableaux:
$convGuIn = array( 'GUI', 'GUE', 'GA', 'GO', 'GU', 'SCI', 'SCE', 'SC', 'CA', 'CO', 'CU', 'QU', 'Q', 'CC', 'CK', 'G', 'ST', 'PH');
$convGuOut = array( 'KI', 'KE', 'KA', 'KO', 'K', 'SI', 'SE', 'SK', 'KA', 'KO', 'KU', 'K', 'K', 'K', 'K', 'J', 'T', 'F');
et
'/([^AEIOUY1])[^AEIOUYLKTPN]([UAO])([^AEIOUY])/',
qui devient
'/([^AEIOUY1])[^AEIOUYLKTPNR]([UAO])([^AEIOUY])/',
et à la fin:
// on supprime les terminaisons T, D, S, X (et le L qui précède si existe)-
$sIn = preg_replace( '`L?[TDX]S?$`', '', $sIn );
(sorti le S des crochets)
J'ai également rajouté le EZ final qui devient E comme ER.
Si ça peut aider... Moi je dois mettre en production bientôt donc une fois la base soundex créée, difficile d'y toucher!
Merci beaucoup pour les modifications. Je les ai intégrées au script de l'article.
Merci à toi. :-)
J'ai fait une erreur par contre:
// on supprime les terminaisons T, D, S, X (et le L qui précède si existe)-
$sIn = preg_replace( '`L?[TDX]?S?$`', '', $sIn );
Sinon les mots au pluriel affiche le S final!
Pour les EZ finaux, j'ai simplement mis E(Z|R) au lieu de ER, mais il faut le déplacer avant dans la liste pour qu'il prenne le dessus:
$convVIn = array( '/E?(AU)/', '/([EA])?[UI]([NM])([^EAIOUY]|$)/', '/[AE]O?[NM]([^AEIOUY]|$)/',
'/[EA][IY]([NM]?[^NM]|$)/', '/(^|[^OEUIA])(OEU|OE|EU)([^OEUIA]|$)/', '/OI/',
'/(ILLE?|I)/', '/O(U|W)/', '/O[NM]($|[^EAOUIY])/', '/(SC|S|C)H/',
'/([^AEIOUY1])[^AEIOUYLKTPNR]([UAO])([^AEIOUY])/',
'/([^AEIOUY]|^)([AUO])[^AEIOUYLKTP]([^AEIOUY1])/', '/^KN/',
'/^PF/', '/C([^AEIOUY]|$)/', '/E(Z|R)$/',
'/C/', '/Z$/', '/(?<!^)Z+/', '/H/', '/W/');
$convVOut = array( 'O', '1\\3', 'A\\1',
'E\\1', '\\1E\\3', 'O',
'Y', 'U', 'O\\1', '9',
'\\1\\2\\3', '\\1\\2\\3', 'N',
'F', 'K\\1', 'E',
'S', 'SE', 'S', '', 'V');
comme ça bougez = bouger = BUJ
OK, merci, c'est mis à jour.
Le formulaire de test ne fonctionne pas et impossible de télécharger les sources!
Juste pour le signaler! sinon ca me semble un super travail!
Bonne continuation
Merci de l'avoir signaler !
J'ai récemment migrer ma configuration et effectivement, j'avais oublié quelques détails. Normalement c'est corrigé.
Salut, ça m'intéresse pour mon dictionnaire Keskiladi.
Pensez que c'est jouable de chercher en FULLTEXT sur un champs de bdd soundex pour chaque entrée ?
Salut, je confirme que cet algo semble meilleur que celui de Frederic BROUARD (qui était dèjà pas mal) cependant un exemple de cas qui ne semble pas bien fonctionner :
BORDEAUX => BORO
BORDEAU => BODO (idem BORDAU, BORDO ou BORDAU)
L'exception qui confirme la règle ! :-)
Je me sers des algo soundex pour chercher des villes/rues dans une base de données géographique (lorsque je n'ai pas de réponse exacte)
A+
Petite méthode pour retirer les accents
// On supprime les accents-
$sIn = preg_replace( "/&([a-z])[a-z]{2,6};/i", "\\\\1", html_entity_decode($sIn) );
Pourriez-vous réparer le lien de téléchargement de votre script svp ? Merci.
C'est réparé, merci de l'avoir signalé !
Bonjour, merci beaucoup pour ce script très utile et beaucoup plus approprié que sa version anglaise !
Je rencontre cependant un problème dans mon moteur de recherche :
Aix à le même soundex (E) que 20e ou 1er...
Si j'arrive à adapter votre code je le posterais ici.