Comment créer un nuage de tags en PHP/MySQL ?

Les tags sont devenus un élément incontournable pour toute application Web 2.0 qui se respecte. Ils permettent une organisation qui va plus loin que la catégorisation classique (catégorie > sous-catégorie > etc.). Cette classification est appelée Folksonomie.

Un nuage de tags, quant à lui, est une façon de représenter un groupe de tags, en jouant visuellement sur la taille et la couleur de la police. Les exemples les plus marquants de nuages de tags sont ceux de Flickr, de Technorati ou encore de del.icio.us.

Dans ce billet, je vais tenter de vous montrer comment on peut réaliser son propre nuage de tags en PHP/MySQL.

Tout d’abord, vous avez besoin d’une table dans votre base de données, qui se présente à peu près de cette façon :

+---------+
| tags    |
+---------+
| id      |
| tag     |
| item_id |
+---------+

* id est l’identifiant unique du tag
* tag est le tag à proprement parler
* item_id est l’élément auquel vous attachez le tag, par exemple un billet de votre blog, un lien, une vidéo, etc. Il s’agit d’une clé étrangère vers l’identifiant unique de la table concernée.

Une fois remplie, votre table doit ressembler à quelque chose de ce genre :

+-----+------+---------+
| id  | tag  | item_id |
+-----+------+---------+
| 1   | fun  | 1       |
| 2   | html | 1       |
| 3   | css  | 1       |
| 4   | html | 2       |
| 5   | php  | 2       |
| 6   | css  | 3       |
| 7   | php  | 3       |
| 8   | php  | 4       |
+-----+------+---------+

Voyons maintenant le code PHP qui va nous permettre de recolter les données utiles à la création de notre nuage. Je vous épargne les lignes concernant la connexion à la base de données.

<?php

define("MIN_SIZE", 9);
define("MAX_SIZE", 36);

$result = mysql_query("SELECT tag, count(*) as number FROM tags GROUP BY tag ORDER BY tag") or die(mysql_error());

$min = MAX_INT;
$max = -MAX_INT;

while ($tag = mysql_fetch_assoc($result)) {
	if ($tag['number'] < $min) $min = $tag['number'];
	if ($tag['number'] > $max) $max = $tag['number'];
	$tags[] = $tag;
}

$min_size = MIN_SIZE;
$max_size = MAX_SIZE;

foreach ($tags as $tag) {
	$tag['size'] = intval($min_size + (($tag['number'] - $min) * (($max_size - $min_size) / ($max - $min))));
	$tags_extended[] = $tag;
}

?>

Les deux premières lignes servent à définir les tailles minimum et maximum. La taille minimum sera assignée aux élements ayant la plus petite fréquence (ici fun avec une fréquence 1) et la taille maximum aux éléments ayant la plus grande fréquence (ici php avec une fréquence 3). Ce sont des tailles en pixels, vous pouvez bien sûr changer ces valeurs ou adapter l’algorithme pour travailler en pourcentages.

Ensuite, on réalise une requête SQL qui extrait chaque tag distinct accompagné de sa fréquence.

La boucle while sert à récupérer la fréquence minimum et la fréquence maximum (dans notre exemple, respectivement 1 et 3). Avant la boucle, on associe à $min et $max des valeurs de départ (très grande pour le minimum et inversément) qui seront remplacées dès le premier passage dans la boucle.

Dans le foreach, on calcule la taille qui sera associée à chaque tag grâce à une formule magique. Cette formule ajoute à la taille minimum la fréquence moins la fréquence minimum fois un certain ratio. Ce ratio correspond à l’écart de tailles entre deux fréquences voisines. Dans notre exemple, la différence de taille entre deux fréquences voisines est de 12,5 (36-9/3-1).

Cela donne donc le résultat suivant : la taille des éléments de fréquence 1 est 9, la taille des éléments de fréquence 2 est 21 (9+12,5 arrondi à l’unité inférieure via intval), la taille des éléments de fréquence 3 est 36 (9+(2*12,5)). On est parfaitement retombé sur nos pattes.

Au niveau de l’affichage, il vous suffit de passer le tableau $tags_extended à votre vue et de boucler dessus pour afficher les tags, en utilisant un style en ligne pour les tailles :

<?php foreach ($tags_extended as $tag) : ?>
<a href="/tags/<?php echo $tag['tag']; ?>" style="font-size:<?php echo $tag['size']; ?>px" title="<?php echo $tag['tag']; ?>"><?php echo $tag['tag']; ?></a>
<?php endforeach; ?>

Si vous utilisez le moteur de templates Smarty (ce que je vous conseille), cela donne :

{foreach from=$tags_extended item=tag}
<a href="/tags/{$tag.tag}" style="font-size:{$tag.size}px" title="{$tag.tag}">{$tag.tag}</a>
{/foreach}

Voici le résultat auquel on arrive. Vous pouvez également voir le contenu des tableaux $tags et $tags_extended sur cette page.

Pour les techniciens, sachez que la complexité de cet algorithme est en O(2*n)n est le nombre de tags distincts dans la base de données.

Dans un prochain billet, j’essaierai de rendre ce nuage de tags accessible ! (je n’ai pas encore de solutions miracles mais j’y travaille)

46 Comments

  1. kNo''s avatar kNo' says:

    sympa. Pour améliorer les performances, il est recommandé d’ajouter un index sur le champ “tag”.

    Like

  2. Il faudrait que tu viennes une fois aux MySQL User Group

    Like

  3. vinch's avatar Vinch says:

    Pourquoi pas (même si je suis loin d’être un pro) ? Tu peux en dire un peu plus ?

    Like

  4. Il suffit d’être “utilisateur” et comprendre l’anglais 🙂
    les 2 premiers ont eu lieu en flandre. Le 3ème probablement à Leuven.

    pour la wallonnie, il faut trouver un lieu… une date…

    http://moosh.et.son.brol.be/blog/index.php/tag/mysql

    Like

  5. Mélissa's avatar Mélissa says:

    Euh… je crois que je reviendrais quand il sera moins tard et que j’aurais les idées plus claires.

    Like

  6. Unknown's avatar Anonymous says:

    Great Site, Yeah. Hope you can improve it again!

    Like

  7. NiKo's avatar NiKo says:

    Marrant, mon PHP 5.2 connait pas la constante MAX_INT.

    Like

  8. vinch's avatar Vinch says:

    J’ai entendu dire qu’il y avait plein de problèmes de retrocompatibilité dans PHP 5.2
    Ils ont certainement placé MAX_INT à la trappe…

    Like

  9. master3mx's avatar master3mx says:

    Est-ce normal que je n’ai pas tout compris ? 😦
    Je n’ai pas très bien compris à quoi correspondait GROUP By (dans la requête sql) ainsi que la totalité de foreach… 😥
    Help me…

    Like

  10. Gimi's avatar Gimi says:

    Je n’ai pas très bien compris à quoi correspondait GROUP By (dans la requête sql)

    A partir du moment où tu introduis une fonction de calcul dans le select (ici count()), et que ce count n’est pas la seule colonne (ici tag), il te faut spécifier la clause Group by.

    Like

  11. vinch's avatar Vinch says:

    Merci Gimi 😉

    Like

  12. Jero's avatar Jero says:

    coool
    j’ai bon espoir de réussir à faire des nuages de mots dans le système que je développe !
    merci,
    jérô

    Like

  13. Unknown's avatar Moeti says:

    très bon travail, par contre deux trois petits améliorations/corrections 🙂
    – je vois plutot un $min = $max = 1; (aucune valeur ne peut être nulle, en plus ca règle le problème de compatibilité)
    – heu l’affectation des constantes à des variables, pourquoi ?! ( $minsize et $maxsize )
    – plutot qu’allouer un nouveau tableau (extended), mieux vaut modifier l’actuel (charge mémoire!):
    foreach (array
    keys($tags) as $id) {
    $tag =& $tags[$id];
    $tag[‘size’] = intval($minsize + (($tag[‘number’] – $min) * (($maxsize – $min_size) / ($max – $min))));
    }
    – bug possible: $min = $max (d’où une division par 0): tu peux faire un $max++ ou bien tester ce cas avant la boucle…

    Like

  14. johnjohn's avatar johnjohn says:

    mercii tu va beaucoup m’aider 🙂

    Like

  15. morganusvitus's avatar morganusvitus says:

    The site looks great ! Thanks for all your help ( past, present and future !)

    Like

  16. Plougy's avatar Plougy says:

    Dans la requête effectué, je ne comprend pas la valeur numbers vu qu’il n’y pas de ligne dans la table de ce nom là…

    Like

  17. vinch's avatar Vinch says:

    Plougy, il s’agit d’un alias de count(*) :

    SELECT tag, count(*) as number FROM tags GROUP BY tag ORDER BY tag

    Like

  18. Plougy's avatar Plougy says:

    Merci! 😉

    Sinon comment gérer des tags pour différentes catégories (Vidéo, Blogs, News) ?

    Like

  19. Plougy's avatar Plougy says:

    Problème résolu Vinch ! 😉

    Like

  20. Yaya's avatar Yaya says:

    Bonjour,
    je souhaiterais installer votre système a mon site internet ttefois je souhaiterais savoir si je ne pouvais avoir les fichiers en zip car je ne suis pas programmeur.

    Merci a vous

    Like

  21. Unknown's avatar Djay says:

    Bonjour,

    J’ai réussi à faire ce nuage en SQL seulement !

    SELECT VT.tags, (
    ROUND((36 / tmp2.min ) * ( tmp2.min / tmp2.max ) * COUNT( VT.tags )
    )) AS valeur
    FROM video_tags AS VT, (

    SELECT MAX( tmp.nbre ) AS max, MIN( tmp.nbre ) AS min, (
    MIN( tmp.nbre ) / MAX( tmp.nbre )
    ) AS ratio
    FROM (

    SELECT tags, COUNT( tags ) AS nbre
    FROM video_tags
    GROUP BY tags
    )tmp
    )tmp2
    GROUP BY tags
    ORDER BY valeur DESC

    Requête générée en 0.0326 sec !!

    Like

  22. Unknown's avatar Djay says:

    Enfin c’est pas encore tout à fait au point, car ça ne gère pas encore la police MINIMALE, mais bon si on trouve une valeur inférieur à X on peut redefinir la police directement en PHP.

    Like

  23. vinch's avatar Vinch says:

    Cool ! Je proposerai certainement une version modifiée se basant sur tes requêtes SQL dans quelques temps 😉

    Like

  24. Unknown's avatar Djay says:

    Bon voici la requête terminée, je pense.
    Avec donc, la limitation pour la taille de police minimale.
    Et la selection aléatoire des tags à afficher !

    Dans ce cas, police maxi de 36, minimum de 6 et tirage aléatoire de 10 tags

    SELECT VT.tags,
    CASE WHEN ROUND( ( 36 / tmp2.min ) * ( tmp2.min / tmp2.max ) * COUNT( VT.tags ) ) >6
    THEN ROUND( ( 36 / tmp2.min ) * ( tmp2.min / tmp2.max ) * COUNT( VT.tags ) )
    ELSE 6
    END AS valeur
    FROM video_tags AS VT, (

    SELECT MAX( tmp.nbre ) AS max, MIN( tmp.nbre ) AS min, (
    MIN( tmp.nbre ) / MAX( tmp.nbre )
    ) AS ratio
    FROM (

    SELECT tags, COUNT( tags ) AS nbre
    FROM video_tags
    GROUP BY tags
    )tmp
    )tmp2
    GROUP BY tags
    ORDER BY RAND()
    LIMIT 0 , 10

    Like

  25. seb's avatar seb says:

    salut

    je voulais savoir pour la db en ce qui concerne l’item_id il serait possible de générer un classement pertinent du style
    SELECT * FROM tags WHERE MATCH(tags) -> AGAINST (‘+pertinence, -pertinence’ IN BOOLEAN MODE)
    ?
    Pour être plus précis, lorsqu’un article s’affiche, je voudrais que le nuage de tags classe par pertinence tous les tags qui seraient connexes à la thématique de l’article lu.
    Merci car j’avoue je sèche 😉

    Like

  26. ShaGa's avatar ShaGa says:

    Ma question va peut-être parraitre simpliste à beaucoup d’entre vous mais s’il était possible que quelqu’un m’explique plus en détail la solution en sql, ça serai sympa parce que j’avou que j’ai pas tout compris… :s

    Like

  27. Viktor Lebelge's avatar Viktor Lebelge says:

    Rien à voir mais je voulais te dire que j’aime vraiment bien ton “blog”.

    Like

  28. vinch's avatar Vinch says:

    Merci Viktor 😉

    Like

  29. klipso's avatar klipso says:

    Bonjour,
    j’aimerai integrer le systeme de tags dans le moteur de recherche de mon site , ou chaque mot clé devient un tags mais dans ce cas la je risque pas d’exploser ma base de donnée vu que j’ai beaucoup de visites donc beaucoup de recherche … une petite idée sur le probleme ?
    merci

    Like

  30. dangerous.sly's avatar dangerous.sly says:

    bien, bien, moi aussi j’ai mis 65535 et -65535 pour Maxint et -Maxint !!! mais ca marche bien…

    Par contre, c’est une liste infinie qu’on obtient ? je crois.

    Comment fait on pour limiter la talle des tags, par exemple, si je veut les 20 tags les plus usités ???

    Merci

    Like

  31. Danito's avatar Danito says:

    Tu rajoute à la fin de ta rquête :

    //—CODE —//

    LIMIT (0,29)

    //— FIN CODE —//

    Like

  32. Unknown's avatar SEBGO says:

    Je suis un elève qui cherche un emploi à l’etranger pour etre plus serieux dans mon travail.

    Like

  33. Très utile.
    Merci beaucoup pour ce partage.

    Like

  34. erick's avatar erick says:

    les nuages son cool

    Like

  35. erick's avatar erick says:

    tu pue le pette poilu

    Like

  36. erick's avatar erick says:

    les nuages son fait de base de gase carbuonique (pete de cheval)

    Like

  37. erick's avatar erick says:

    allo bye bye

    Like

  38. Séb's avatar Séb says:

    Thks !!!!
    Adapté et mis en place en 10 minutes chrono !
    Super

    Like

  39. kaoul's avatar kaoul says:

    slt je ve un code source en html/php: cmt céer des tags sur mon site svp c trés urgent et merci d’avance!!!!!!!!

    Like

  40. Super cette solution pour générer des nuages de tags, bien simpa 🙂 et prochainement réutilisé jpense 🙂

    Like

  41. Sim's avatar Sim says:

    Merci !! adapté et mis en place en un rien de temps !

    Question subsidiaire : et si on voulait limiter le nombre de tag aux 10 les plus courants ??

    Super et encore merci

    Like

  42. KubX's avatar KubX says:

    Merci beaucoup pour la formule magique 😀

    Like

  43. Arnaud's avatar Arnaud says:

    J’ai souci qui me bloque depuis longtemps sur les tags. Comment faire pour les inserer dans la BDD sachant que je veux taguer un articles avec plusieurs tags tout ca en une seule requete.

    Sur les sites qui proposent de d’ajouter des tags, il est souvent recommender d’insere une virgule apres chaque tag. C’est la ou mon cerveau ne capte pas comment faire. Merci Vinch

    Like

  44. Rhalmi's avatar Rhalmi says:

    Bon travaille merci pour le billet. Mais j ai une question: Comment remplir la table. J ai besoin du formulaire et la requete qui insert les tag..?

    Like

  45. SONIA's avatar SONIA says:

    Moi problème ce que je suis entraîne de créer un site et le client veut me demande de gréer un tagcloud qui
    permet d’afficher les nuages des mots clés a chaque visite dans le site,le mots clés en gras et à different couleurs………..!
    merçi à tous

    Like

Leave a reply to morganusvitus Cancel reply