Afficher les articles connexes, triés par pertinence

Quelques noisettes et une goutte de php pour afficher les articles qui partagent le plus de mot-clés avec un autre article.

Les articles connexes

Il est très intéressant de pouvoir présenter à nos visiteurs les articles qui traitent du même sujet que celui qu’ils sont en train de lire. Cela peut se faire simplement en listant les articles présents dans la même rubrique. On peut aussi le faire plus finement, en présentant aux visiteurs la liste des articles qui partagent les mêmes mots-clés que l’article qu’ils sont en train de consulter. Ce sont les articles connexes (pas forcément dans la même rubrique, mais reliés par leurs mots-clés à l’article courant).

Avec Spip, il est simple de faire la liste des articles connexes. Pour chaque mot-clé rattaché à l’article consulté, on affiche la liste des articles qui ont ce mot-clé. Cela donne la noisette suivante à placer dans vos squelettes d’articles:

<ul>
<BOUCLE_Mots_De_Article_Contexte(MOTS){id_article}>[(#REM) Pour chaque mot clé de l'article]
	<BOUCLE_Articles_Connexes(ARTICLES){!id_article}{id_mot}{doublons}>[(#REM) Pour chaque article qui a ce mot clé]
	<li>
		<a href="#URL_ARTICLE" [title="(#DESCRIPTIF|textebrut|couper{200})"]>[(#TITRE|supprimer_numero)]</a>
	</li>
 
	</BOUCLE_Articles_Connexes>
</BOUCLE_Mots_De_Article_Contexte>
</ul>

Cette noisette qui contient une boucle dans une autre qui affiche le titre et un lien de chaque article qui a un mot-clé commun avec l’article de contexte [1]. Ainsi, on a la liste des articles connexes. On peut affiner en informant le visiteur des mots-clés communs grâce à cette autre noisette à placer dans la boucle _Articles_Connexes:

 
		<B_Mots_Communs>
		Nombre de mots-clés communs&nbsp;: [(#_Mots_Communs:TOTAL_BOUCLE)]. 
(Liste des mots-clés communs&nbsp;:
		<BOUCLE_Mots_Communs(MOTS){id_article=#_Contexte_Article:ID_ARTICLE}{id_article}{id_mot!=35}{par type}{par titre}{"&nbsp;- "}>
		<a href="#URL_MOT" [title="(#DESCRIPTIF|textebrut|couper{200})"]>[(#TITRE|supprimer_numero)]</a>
		</BOUCLE_Mots_Communs>
			)

Dans cette noisette, #_Contexte_Article:ID_ARTICLE signifie #ID_ARTICLE de la boucle _Contexte_Article, à ne pas confondre avec l’#ID_ARTICLE de la boucle dans laquelle on place la noisette (_Articles_Connexes). Pour en savoir plus sur la syntaxe des balises du type: #_Nom_Boucle:#NOMBALISE allez consulter la documentation de Spip sur les Balises non ambiguës.

Même chose pour #_Mots_Communs:TOTAL_BOUCLE.

Donc, vous devez adapter cette petite noisette à votre cas, si vous êtes dans une boucle globale qui se nomme _Contexte_Article, pas de problème; sinon, n’oubliez pas de changer son nom.

Tout ça, c’est très bien... Mais à condition d’avoir peu d’articles, et de mots-clés. Car, si comme moi, vous avez beaucoup d’articles et qu’ils ont chacun beaucoup de mots-clés; la liste des articles connexes devient rapidement interminable (songez qu’elle contient tous les articles qui ont au moins un mot-clé commun avec l’article visité !). Pour éviter d’avoir 200 articles listés à chaque fois on peut se contenter de mettre dans la boucle _Articles_Connexes un critère {0,20} qui liste les 20 premiers articles par mots-clés. Bien sûr si vous avez beaucoup de mots-clés, il faudra lister moins de 20 articles par mots-clés, et peut-être pas tous les mots-clés non plus... Beaucoup de contraintes finalement et une solution peu satisfaisante qui est de prendre les x premiers articles des n premiers mots-clés, sans tenir compte de la pertinence de ce choix (dans les x premiers articles, il peut y en avoir qui n’ont qu’un seul mot-clé commun avec celui visité, alors qu’on voudrai plutôt afficher ceux qui en ont le plus).

Comment trier les —trop nombreux— articles connexes par pertinence et n’afficher que les plus intéressants pour le visiteur ?

Principe de la contrib’

Les articles connexes les plus pertinents sont ceux qui ont le plus de mots-clés communs avec l’article consulté. C’est ceux-là que l’on veut voir s’afficher en premier.

La contrib’ adapte donc la noisette présentée plus haut, qui est classique, pour pouvoir compter le nombre de mots-clés communs associés à chaque article connexe.

Spip, permet depuis la version 1.9.2 de faire des tableaux et de les manipuler avec les balises #SET et #GET introduites avec Spip 1.9.1 (voir la documentation de Spip). Mais, pour bien comprendre comment les utiliser, il faudra au préalable comprendre comment faire sans les utiliser. Pour cela il faut utiliser du PHP (les impatients peuvent tout de suite aller lire la noisette pour Spip 1.9.2)

Noisette et noix de coco

Il faut toujours être dans un contexte d’article (donc que id_article soit défini).

On met en place le compteur pour chaque article qui a des mots-clés communs. Avec ce compteur on compte le nombre de mots-clés en commun. C’est dans un tableau en PHP. Voilà la noisette:

<BOUCLE_Mots_De_Article_Contexte(MOTS){id_article}>[(#REM) Pour chaque mot clé de l'article]
	<BOUCLE_Tableau_Articles_Connexes(ARTICLES){!id_article}{id_mot}>[(#Pour chaque article qui a ce mot clé]
<?php
# Un peu de PHP.
# on inclémente le compteur de pertinence:
# $pertinence est un tableau qui contient pour chaque id_article
# un compteur du nombre de mots-clés communs
$pertinence['[(#ID_ARTICLE|texte_script)]']++;
?>
	</BOUCLE_Tableau_Articles_Connexes>
</BOUCLE_Mots_De_Article_Contexte>

Le tableau $pertinence[ ] contient donc la liste des articles ayant des mots-clés communs avec notre article de contexte. Il contient aussi, pour chaque article de la liste, une valeur de pertinence qui correspond au nombre de mot-clés communs avec l’article de contexte.

On veut afficher les articles qui ont plus de 4 mots-clés communs (variable $maxcommun à changer selon vos goûts) avec notre article de contexte. On ne veut pas afficher plus de 20 articles (variable $maxarticles à changer selon vos goûts) au total.

Cette petite noisette grosse noix de coco est entrelacée dans du php: le tableau est trié de manière à ce que les articles les plus pertinents soient traités en premier. Puis, pour chaque élément du tableau, on va faire une boucle qui affiche un lien vers l’article, le nombre de mots-clés communs et la liste de ces mots-clés.

Et cela, seulement si la pertinence est suffisante (condition sur $maxcommun) et qu’il y a moins de 20 articles connexes affichés (condition sur $maxarticles)

<?php
# Un peu de PHP.
# on trie le tableau $pertinence[ ] afin que
# les id_articles qui ont le plus de mots-clés
# communs avec l'article du contexte soient en 1er
 
arsort ($pertinence);
 
#Puis pour chaque id_article:
 
$i=0;
$maxarticles=20;
$maxcommun=4;
while (list($ks,$vs) = each($pertinence)) {
#$ks contient id_article, $vs sa pertinence
	if ($vs > maxcommun-1) {
		$i++;
		if ($i==maxarticles) break;
?>
 
<ul>
 
<BOUCLE_Articles_Connexes(ARTICLES)>[(#REM) Liste absolument tous les articles]
 
<?php
# Un peu de PHP.
# Vérifie si l'article est celui du tableau
# Si oui, on effectue les actions de la boucle, sinon
# on passe à l'article suivant.
# ce petit morceau de PHP est en fait une condition de boucle
# comparable à {id_article=$ks} qui ne fonctionne pas dans Spip
		if ('[(#ID_ARTICLE|texte_script)]'==$ks) {
?>
	<li>
		<a href="#URL_ARTICLE" [title="(#DESCRIPTIF|textebrut|couper{200})"]>[(#TITRE|supprimer_numero)]</a> 
	<B_Mots_Communs>
		Nombre de mots-clés communs&nbsp;: [(#_Mots_Communs:TOTAL_BOUCLE)]. 
		(Liste des mots-clés communs&nbsp;:
	<BOUCLE_Mots_Communs(MOTS){id_article=#_Contexte_Article:ID_ARTICLE}{id_article}{id_mot!=35}{par type}{par titre}{"&nbsp;- "}>
		<a href="#URL_MOT" [title="(#DESCRIPTIF|textebrut|couper{200})"]>[(#TITRE|supprimer_numero)]</a>
	</BOUCLE_Mots_Communs>
			)
		</li>
<?php
# Un peu de PHP.
# fin de la condition {id_article=$ks}
# on passe donc à l'article suivant
		}
?>
 
</BOUCLE_Articles_Connexes>
 
<?php
# Un peu de PHP.
# fin de cet id_article ($ks)
# on passe donc au suivant
	}	
};
?>
</ul>

Cette noix de coco est complexe parce qu’elle est imbriquée dans du PHP...

À propos de l’intégration du PHP

J’ai découvert pas mal de choses sur l’intégration du PHP dans Spip. Ce qu’il faut principalement retenir ce sont les étapes du travail de Spip, telles qu’elles sont expliquées par ARNO* [2] ici: «Le PHP est interprété après les boucles».

Donc, quand on intègre du PHP dans Spip, on doit systématiquement le séparer du reste par des balises <?php et ?> qui indiquent où commence et où finit le PHP.

Si vous souhaitez, comme dans cette contrib’, utiliser des valeurs calculées par Spip, ou issues de la base de donnée dans votre script PHP, récupérez-les en suivant cette méthode: Comment récupérer une « variable spip » en une variable PHP ?

Autre découverte: on peut couper le script PHP par ces balises partout, même au milieu d’un test de condition, si on oublie pas de le finir un peu plus loin (toujours encadré par les balises <?php et ?>). C’est ce que je fais dans la seconde noisettex, en commençant un test sur le tableau («while (list($ks,$vs) = each($pertinence)) {» dont la fin se situe plus loin dans un autre « bloc » de PHP).

Dernière découverte et grosse difficulté (exposée par ARNO*): on ne peut pas récupérer de variable PHP dans Spip car les BOUCLE/#BALISE sont transformées en PHP avant que le contenu du fichier ne soit lu par le serveur web ! Heureusement, les contributeurs ont trouvé deux solutions.
-  l’une simple que j’ai adopté, qui est expliquée ici: utiliser une variable PHP pour sélectionner une rubrique (par exemple). Ce n’est pas très élégant, car cela m’oblige à faire une boucle dans laquelle l’ensemble des articles est sélectionné, ce qui coûte un peu au serveur web/MySQL.

-  L’autre est élégante et puissante, elle utilise l’environnement de la page: en allemand: Tricks mit HTTP-Vars (moi je ne parle pas l’allemand ! Mais, la voici en plus ou moins français: variable php dans un critère de boucle ?). Elle est trop complexe pour moi.

Sans PHP et en beaucoup plus rapide !...

(mise à jour du 12/05/2008)

La noix décrite plus haut fonctionne bien. Mais, comme elle contient du PHP, il y a des désavantages. Les principaux sont:

  1. Le PHP dans le squelette, ce qui en plus de ne pas être élégant, pose des problèmes d’efficacité avec le cache de Spip.
  2. Cette noix nécessite de faire plusieurs passages par une boucle (BOUCLE_Articles_Connexes) qui liste tous les articles. Sur de grosses bases de données, cela peut prendre des proportions considérables (vraiment...)!

Jusqu’à Spip 1.9.2 c’était la seule possible. En utilisant les #ARRAY de Spip, il est possible de faire sans PHP et donc d’éviter ces deux problèmes.

Le principe sera exactement le même: On va faire une première boucle imbriquée pour fabriquer un tableau qui liste les articles ayant des mots-clés communs avec une pondération (nombre de mots-clés communs). Puis nous allons afficher les articles ayant la plus forte pondération.

On ne peut éviter totalement de faire appel au PHP pour arriver à nos fins. Mais grâce aux filtres de Spip, nous pouvons éviter d’en mettre à l’intérieur du squelette. Les filtres sont appliqués sur les balises Spip. Des filtres personnalisés peuvent être définis dans le fichier mes_fonctions.php à placer dans le dossier squelettes. Des filtres peuvent aussi être des fonctions PHP. Nous allons utiliser les deux.

Si vous avez un fichier mes_fonctions.php, vous devrez rajouter le code ci-dessous dedans. Sinon, vous devrez en créer un que vous placerez dans votre dossier squelettes:

<?php
//Incrémente la valeur correspondant à une clé dans un tableau.
//Est utilisé pour incrémenter la pondération des articles.
function ponderation ($tableau,$cle) {
	$tableau[$cle]++;
	return $tableau;
}
?>
<?php
//Trie un tableau en fonction de ses valeurs
//Est nécessaire, car [(#GET{un_tableau}|asort)] ne semble pas fonctionner
function trie_tableau ($tableau) {
	arsort ($tableau);
	return $tableau;
}
?>

Nous y définissons deux filtres sur les tableaux. L’un incrémente la valeur correspondant à une clé d’un tableau, l’autre trie un tableau.

Reprenons notre squelette... Commençons par pondérer les articles en fonction de leurs mots-clés communs avec l’article de contexte:

[(#REM) Pour chaque mot clé de l'article]
<BOUCLE_Mots_De_Article_Contexte(MOTS){id_article}>
 
[(#REM) Pour chaque article qui a ce mot clé]
	<BOUCLE_Tableau_Articles_Connexes(ARTICLES){!id_article}{id_mot}>
[(#SET{tableau_pertinence,
	[(#GET{tableau_pertinence}
		|ponderation {#ID_ARTICLE}
	)]
})]
	</BOUCLE_Tableau_Articles_Connexes>
</BOUCLE_Mots_De_Article_Contexte>

À la place du PHP, nous utilisons l’un de nos filtres pour augmenter la pondération de nos articles dans un tableau Spip (notez que le tableau est en fait initialisé par le filtre). La balise #SET sert à affecter une valeur à une variable Spip (ici tableau_pertinence), #GET permet de récupérer le contenu de la variable.

L’objectif est de pouvoir utiliser un tableau Spip pour notre boucle BOUCLE_Articles_Connexes:

<BOUCLE_Articles_Connexes(ARTICLES){id_article IN #GET{un_tableau_spip}}>
</BOUCLE_Articles_Connexes>

Pour rendre notre tableau utilisable dans une boucle, il va falloir lui faire subir des transformations:

-  classer les articles par pondération (ils sont dans le désordre pour linstant)
-  Transformer le tableau pour qu’il soit une liste d’id_article seulement. Car les id_article sont pour l’instant les clés du tableau, les valeurs sont les pondérations elles-mêmes. Alors que la boucle aura besoin d’un tableau où les id_article seront des valeurs [3].

Voici la manœuvre:

[(#REM) On fabrique un tableau trié avec les id_article comme valeurs]
[(#SET{tableau_articles,
	[(#GET{tableau_pertinence}
		|trie_tableau
		|array_keys)]}
)]

Le premier filtre vient de notre fichier mes_fonctions.php, qui trie le tableau. Ce tableau subit un second filtre, qui est une fonction PHP qui renvoie un tableau avec seulement les clés d’un autre tableau: array_keys.

Nous aurons aussi besoin de connaître la pertinence maximum des articles. C’est la première valeur de notre tableau tableau_pertinence une fois trié:

[(#SET{pertinence_max,
	[(#GET{tableau_pertinence}
		|trie_tableau|reset)]}
)]

C’est reset qui retourne cette valeur dans notre variable pertinence_max (reset est une fonction PHP, utilisée comme filtre à l’instar de array_keys ci-dessus).

Passons aux choses sérieuses:

<BOUCLE_Articles_Connexes(ARTICLES){id_article IN #GET{tableau_articles}}{0,20}>
	#TITRE<br />
</BOUCLE_Articles_Connexes>

Cette boucle écrit les titres des 20 articles ({0,20}) ayant le plus de mots-clés communs avec l’article passé en contexte.

Mais pourquoi avoir récupéré pertinence_max ? Pour afficher la pertinence en pourcentage: (pondération de l’article*100)/pondération_max. Soit en Spip:

[(#GET{tableau_pertinence}
	|table_valeur{#ID_ARTICLE}
	|mult{100}
	|div{#GET{pertinence_max}}
)]

table_valeur donne la valeur stockée dans un tableau pour une clé donnée (ici #ID_ARTICLE). Pour le reste, ce sont des filtres mathématiques de Spip.

Voilà ! là noix qui découle de tout ceci et qui affiche les 20 articles connexes de l’id_article de contexte est:

[(#REM) Initialisation des tableaux]
#SET{tableau_pertinence,#ARRAY}
#SET{tableau_articles,#ARRAY}
#SET{pertinence_max,#ARRAY}
[(#REM) Pour chaque mot clé de l'article]
<BOUCLE_Mots_De_Article_Contexte(MOTS){id_article}>
 
[(#REM) Pour chaque article qui a ce mot clé]
	<BOUCLE_Tableau_Articles_Connexes(ARTICLES){!id_article}{id_mot}>
[(#SET{tableau_pertinence,
	[(#GET{tableau_pertinence}|ponderation{#ID_ARTICLE})]
})]
	</BOUCLE_Tableau_Articles_Connexes>
</BOUCLE_Mots_De_Article_Contexte>
[(#REM) On fabrique un tableau trié avec les id_article comme valeurs]
[(#SET{tableau_articles,
	[(#GET{tableau_pertinence}
		|trie_tableau
		|array_keys)]}
)]
[(#REM) On récupère la pondération maximale pour nos pourcentages]
[(#SET{pertinence_max,
	[(#GET{tableau_pertinence}
		|trie_tableau|reset)]}
)]
[(#REM) Cette boucle affiche le titre des 20 articles connexes, c'est-à-dire les 20 articles qui ont le plus de mots-clés communs avec celui du contexte.
Elle donne la pondération en pourcents.]
<BOUCLE_Articles_Connexes(ARTICLES){id_article IN #GET{tableau_articles}}{0,20}>
	#TITRE (pertinence: 
[(#GET{tableau_pertinence}
	|table_valeur{#ID_ARTICLE}
	|div{#GET{pertinence_max}}
	|mult{100}
)]		%)<br />
</BOUCLE_Articles_Connexes>

Notez l’apparition de :

[(#REM) Initialisation des tableaux]
#SET{tableau_pertinence,#ARRAY}
#SET{tableau_articles,#ARRAY}
#SET{pertinence_max,#ARRAY}

sur les conseils de Aurélie dans les forums qui permet d’initialiser les tableaux grâce à #ARRAY pour éviter des messages d’erreur (notamment quand il y a des articles sans mots-clés).

N’oubliez surtout pas de créer ou de compléter votre fichier mes_fonctions.php sinon, ça ne va pas fonctionner !

Voilà, j’espère que cela vous sera utile. Je trouve en tout cas très agréable de pouvoir proposer à mes visiteurs une liste restreinte d’articles connexes qui sont toujours pertinents, car ils partagent un nombre de mots-clés important.

Et vive la navigation transversale !

Footnotes

[1Le contexte est une notion fondamentale de Spip: c’est un endroit où id_article est défini (c’est-à-dire soit dans une boucle « articles », soit en recevant id_article par inclusion).

[2l’un des papas de Spip. Voir aussi L’histoire minuscule et anecdotique de SPIP.

[3Pour ceux qui ne savent pas, rappelons en simplifiant, que les tableaux de Spip, comme les tableaux PHP sont des suites d’éléments indexés par des clés. À chaque clé (unique) correspond une valeur (pas forcement unique, elle.).

Le logo de cet article est un extrait d’une image de notafish que vous pouvez retrouver là: Chaîne du pont-levis elle est sous license cc: by-sa.

updated on 2 October 2019

Discussion

14 discussions

  • 1
    Christophe

    Bonjour
    je viens d’installer sans difficulté cette superbe contrib’... Mais je voudrais aller encore plus loin... et trier les résultats trouvés par date... S’il est préférable de garder une hiérarchie par pertinence, on peut malgré cela classer par date au sein d’un même groupe (cad au sein des articles qui ont le même nombre de mot clé en commun). J’ai donc ajouter, tout simplement
    par dateinverse dans la boucle . En vain.
    Si quelqu’un a une idée, une piste, j’essaierai de bidouiller encore et encore...
    Cependant, merci encore pour cette contrib’ forte utile.

    • J’ai mis à jour l’article avec une solution sans PHP.
      Il est désromais possible de trier les articles par date. Mais attention, ils ne seront triés que par date, pas par pertinence !

    Reply to this message

  • ivandps

    Merci de cette formule qui me rejoint et à laquelle j’ajoute.

    -  navigation transversale = personnalisation du contenu pour l’utilisateur
    -  navigation transversale à la demande = personnalisation du contenu par l’utilisateur = web agile = vite !

    Reply to this message

  • ivandps

    Merci de cette formule qui me rejoins et à laquelle j’ajoute.

    -  navigation transversale = personnalisation du contenu pour l’utilisateur
    -  navigation transversale à la demande = personnalisation du contenu par l’utilisateur = web agile = vite !

    Reply to this message

  • Philippe

    J’ai comme l’impression que cela ne fonctionne plus sous une 1.9.2b

    Reply to this message

Comment on this article

Who are you?
  • [Log in]

To show your avatar with your message, register it first on gravatar.com (free et painless) and don’t forget to indicate your Email addresse here.

Enter your comment here

This form accepts SPIP shortcuts {{bold}} {italic} -*list [text->url] <quote> <code> and HTML code <q> <del> <ins>. To create paragraphs, just leave empty lines.

Add a document

Follow the comments: RSS 2.0 | Atom