Indexer : La boucle SPHINX

Avant de commencer :
Pour comprendre l’intérêt du plugin, nous invitions à lire l’article suivant Indexer : Introduction. La configuration et l’installation du serveur Sphinx sont expliquées dans l’article Indexer : Installation et Configuration ; l’usage de la recherche dans Indexer : Rechercher. Le paramétrage de l’indexation dans Indexer : Indexation.

Comment construire ses boucles SPHINX

Sur la page des résultats de recherche, on distinguera en général :
— la boucle principale, qui affiche les résultats de la recherche
— le rappel des éléments de la requête
— le nombre de résultats et les outils de tri
— les suggestions
— la pagination
— les facettes.

Le plugin Indexer offre des briques pour construire tous ces éléments en fonction des spécificités de votre système d’indexation.

À noter : Ce plugin est exploité sur de grosses bases de contenus, parfois réparties sur plusieurs sites SPIP, ce qui explique certains choix de structuration des données. Par conséquent, les résultats d’une recherche peuvent ne pas figurer dans la base de données du site que l’on est en train de consulter. Ceci implique que tous les éléments nécessaires à leur affichage (par exemple : titre, date, mots-clés, auteur, résumé, URL du contenu et URL du logo) doivent être stockés dans l’index — on n’ira pas faire de boucles SPIP à l’intérieur des boucles SPHINX.

La boucle principale

La boucle principale est celle qui va afficher les résultats de la recherche, sous forme de blocs avec titre et lien, et parfois un extrait où les mots de la requête sont surlignés (balise #SNIPPET.

Cette boucle doit prévoir deux grandes familles de critères :

1. quels sont les éléments de l’URL que l’on va transformer en filtres pour limiter le contenu (par date par auteur). Ce sont des critères de la forme
{filter #ENV*{auteur}, 'IN(properties.authors, @valeurs)', 'LENGTH(properties.authors) = 0'}

Cette syntaxe un peu compliquée se lit comme suit : « si l’URL contient une précision &auteur=Toto, ajouter un critère de recherche Sphinx demandant à ce que la propriété indexée properties.authors contienne la chaîne "Toto" ». La partie qui vient après la virgule correspond au cas particulier : « si l’URL contient une précision &auteur=-, limiter la recherche aux articles ayant properties.authors vide ».

Ces critères sont détaillés ci-dessous, dans la partie « filtres ».

2. Quelles facettes faut-il demander à Sphinx de nous renvoyer.

Ces critères ne servent pas à filtrer les résultats, mais permettent de demander à Sphinx de calculer les facettes qui permettront d’affiner par la suite la recherche. En règle générale, chaque filtre défini au point 1 sera accompagné par une facette équivalente. Mais ce n’est pas une obligation technique, et il est possible de prévoir un jeu limité de facettes, et des critères de filtrage qui seraient activés autrement que par des facettes (par exemple des cases à cocher programmées de manière classique).

Ces critères sont de la forme {facet auteur, properties.authors ORDER BY COUNT(*) DESC} ; éventuellement {facet auteur, properties.authors ORDER BY COUNT(*) DESC LIMIT 50} pour modifier le nombre maximal de facettes (par défaut, 20), ce qui peut être utile par exemple si les facettes représentent les années de publication.

On prendra l’habitude d’écrire d’abord la liste des filtres, puis la listes de facettes.

Le rappel des éléments de la requête

En général en tête de la page de recherche, on présente un formulaire de recherche qui rappelle au moins les termes de la requête qui vient d’être effectuée. Ce formulaire peut être incomplet, mais il est très important qu’il rappelle tous les éléments qui ont permis de construire la requête.

Ainsi par exemple si l’utilisatrice a saisi une requête sur les mots (full-texte) « Cheval » et la facette « Année : 2015 », il faut que ces deux éléments soient rappelés dans ce bloc. On se basera sur les paramètres de l’URL pour savoir quoi afficher. Chacun des paramètres qui peuvent constituer un filtre doit y figurer.

Par exemple, pour la facette « par année », on écrira au minimum :
[<p>Année: (#ENV{annee})</p>]

Mais ce rappel est aussi un endroit idéal pour ajouter l’interaction « supprimer la facette d’année », ce qui fait qu’on indiquera plutôt :
[<p><a href="[(#SELF|parametre_url{annee,''})]">x</a> Année: (#ENV{annee})</p>]

Le code se complique un peu, mais l’interface devient plus utilisable. Si on a beaucoup de filtres possibles, c’est un peu répétitif, et on peut passer par une boucle DATA pour lister les différents paramètres, tous traités de la même manière (cf. l’exemple dans content/sphinx.html).

Pour le full texte, par convention (hors cas particuliers), l’input texte libre correspondant est toujours présent, soit sous la forme du #FORMULAIRE_RECHERCHE{#SELF}, soit directement sous la forme :
<input type=search name=recherche value="[(#ENV{recherche}|attribut_html)]" placeholder="Saisir une requête…">

Ces éléments de rappel doivent impérativement se trouver en dehors de la boucle de recherche, de manière à apparaître dans tous les cas, que la recherche ait donné ou non des résultats.

Le nombre de résultats et les outils de tri

Dans la partie optionnelle avant la boucle principale, on va gérer l’affichage du nombre de résultats :
<h2>#GRAND_TOTAL documents trouvés (#TOTAL_BOUCLE affichés)</h2>

On peut aussi proposer ici [<p>(#SPHINX_MESSAGE)</p>] qui contiendra d’éventuelles suggestions d’orthographe.

On peut afficher le nombre de résultats trouvés, en sachant que, comme SPHINX coupe systématiquement à 1000 résultats, il faut éventuellement prévoir le cas où ce nombre est atteint.

[(#SPHINX_META|table_valeur{total}|=={1000}|?{'+ de '}) ][(#SPHINX_META|table_valeur{total}) ]résultats

Au même endroit on peut ajouter des infos de débogage, mais il n’est pas utile de les laisser en production : code technique de la requête Sphinx, et temps mis à répondre.

[<div><tt>(#SPHINX_QUERY|htmlspecialchars)</tt></div>]
requête effectuée en [(#SPHINX_META|table_valeur{time}|mult{1000})] ms

C’est ici aussi qu’on peut ajouter des boucles secondaires qui iront par exemple chercher les auteurs ou les mots-clés correspondant à le requête, de manière à suggérer un filtrage pertinent. Cette recherche peut être faite soit dans l’index sphinx, si vous avez indexés ces objets, soit avec une boucle de recherche SPIP normale (les tables auteurs et mots-clés étant en général plus petites que les tables articles, on n’a pas le même souci de performance). C’est ce que fait Le Monde diplomatique en proposant « Auteurs trouvés : le sous-commandant Marcos » lorsque l’on recherche le mot « Marcos ».

Les outils de tri en général sont par date et par pertinence ; il est bien sûr possible d’ajouter des tris sur d’autres critères en fonction des besoins (prix, notes, places disponibles…).

En termes de code, pas de surprise : il s’agit d’un critère SPIP classique dans la boucle SPHINX :
{par #GET{tri}}{inverse #GET{sens_tri}}

Pour l’interface, il faut élucider ce que l’utilisatrice a demandé ou pas, ce qu’on propose par défaut, etc.

<aside class="tri">
	[(#ENV{order}|match{date}|?{
		[(#SET{tri,date})]
		[(#SET{sens_tri,-1})]
		résultats triés par date
	})]
	<div>
	[(#GET{tri}|?{
		<a href="[(#SELF|parametre_url{order,''})]">trier par pertinence</a>
	,
		<a href="[(#SELF|parametre_url{order,date})]">trier par date</a>
	})]
	</div>
</aside>

La pagination

C’est du SPIP parfaitement standard : un critère {pagination 10} et une balise [<div class="pagination">(#PAGINATION)</div>] dans la partie optionnelle après de la boucle.

Les facettes

C’est l’apport le plus remarquable du moteur SPHINX : à chaque requête, il permet d’associer des facettes, qui représentent les dimensions secondaires associées à cette requête précise.

Ainsi par exemple si on demande des facettes d’auteurs, d’année et de mots-clés, SPHINX renverra un tableau de trois séries de facettes. La série des facettes d’auteurs indiquera jusqu’à 20 auteurs liés aux mots saisis dans la recherche, et pour chacun de ces auteurs, le nombre d’articles qui correspondent à cette saisie et à cet auteur.

Le tri des auteurs ou mots-clés est fait en général en fonction du nombre de résultats (mais on peut préférer par ordre alphabétique : cela se décide dans l’écriture du critère qui crée la facette). Le tri des dates est en général par ordre alphabétique (trier par nombre de résultats donne un affichage confus).

Les facettes permettent à la fois de 1) montrer quels sont les contextes dans lesquelles un mot ou une expression « full texte » apparaissent dans l’ensemble de la base (par exemple : quels sont les auteurs qui parlent le plus souvent de SPIP ?) ; et 2) dans le même temps, de créer des suggestions d’affinage de la recherche, par simple clic sur les mots en question, qui se transformeront en filtres.

Le code des facettes figure dans le squelette listes/sphinx_facettes.html et est inclus dans la partie optionnelle après de la boucle SPHINX par l’appel suivant :
[(#INCLURE{fond=liste/sphinx_facettes,facets=#SPHINX_FACETS,env})]

Des filtres spécifiques à des cas courants de comparaisons sont pré-programmés, ce qui évite de devoir connaître et écrire soi-même le code des tests.

Filtre pour un champ n’ayant qu’une valeur (mono-valué)

{filtermono test, champ, valeur(s)[, comparaison]}

Le test en premier permet de passer n’importe quelle valeur fixe ou dynamique (avec filtre ou pas) qui permettra de dire si on va ajouter le filtre ou pas. Le cas courant est de mettre la valeur d’un paramètre de l’URL, et s’il est présent et rempli, on ajoute le filtre.

La comparaison est optionnelle, et vaut « = » par défaut.

Le champ de valeur peut être une liste de plusieurs valeurs, et dans ce cas le test sera un « OU » sur chacune des comparaisons !

Exemples

// Les documents publiés par défaut, sinon ceux du statut demandé
{filtermono #ENV{statut,publie}, properties.statut, #ENV{statut,publie}}
// Les documents de 2014 ou 2013
{filtermono #ENV{annee}, year(date), #LISTE{2014,2013}}
// Les documents ayant au moins #ENV{favs} partages
{filtermono #ENV{favs}, length(properties.share), #ENV{favs}, '>='}

Filtre pour un champ JSON ayant plusieurs valeurs (multi-valué)

{filtermultijson test, champ, valeur(s)}

Mêmes principes que pour le critère précédent sauf que le critère cherche si les valeurs font partie du tableau « champ » (qui doit donc être une liste de plusieurs valeurs).

Si on donne plusieurs valeurs, le critère fera un « ET » entre les tests. Si l’une de ces valeurs est elle-même un tableau, le critère fera un « OU » (avec la commande Sphinx IN).

-  Si les valeurs sont #LISTE{mot1, mot2, mot3} : ça cherchera les documents qui ont mot1 ET mot2 ET mot3.
-  Si les valeurs sont #LISTE{mot1, #LISTE{mot2, mot3}} : ça cherchera les documents qui ont mot1 ET (mot2 OU mot3).

Exemples

// Un auteur précis parmi ceux du document
{filtermultijson #ENV{auteur}, properties.authors, #ENV{auteur}}
// Les documents ayant tous les tags demandés, par ex si tags[]=truc&tags[]=bidule
{filtermultijson #ENV{tags}, properties.tags, #ENV{tags}}

Filtre de distance

{filterdistance test, point1, point2, distance[, comparaison[, nom du champ]]}

Ce critère sélectionne uniquement les réponses qui font que la distance entre le point 1 et le point 2 correspond à la comparaison demandée avec la distance passée en paramètre.

La comparaison est optionnelle est vaut « <= » par défaut.
Le nom de la distance calculée est optionnelle et vaudra par défaut « distance_0 » pour la première distance demandée, puis « distance_1 », etc.

Ce paramètre permet de maîtriser le nom afin de pouvoir plus facilement
demander un tri par le nom voulu, et récupérer la valeur avec une balise.

Exemple

// Tous les documents qui sont à moins de 5km de Bordeaux, avec comme nom #DISTANCE
#SET{bordeaux, #ARRAY{lat, 44.83717, lon, -0.57403}}
#SET{point_document, #ARRAY{lat, properties.geo.lat, lon, properties.geo.lon}}
{filterdistance , testok, #GET{bordeaux}, #GET{point_document}, 5000, '<=', distance}

Tri des résultats

Comme dans les boucles SPIP habituelles, on peut utiliser le critère {par champ} pour trier les résultats, y compris avec les propriétés JSON {par properties.objet}.

Options pour les requêtes

{option nom, valeur}

Exemple

// Modifier la pondération des champs lors de la recherche libre
{option field_weights, "(title=10, content=5)"}

// Augmenter la limite du nombre de résultats (1000 par défaut)
{option max_matches, 999999}

Un exemple de boucle

<BOUCLE_recherche_sphinx(SPHINX)
	{index #ENV{source,spip}}
	{recherche #ENV*{recherche}}

	{filter #ENV{annee},  'YEAR(date) = @valeur' }
	{filter #ENV{tag},    'IN(properties.tags, @valeurs)',    'LENGTH(properties.tags) = 0'}
	{filter #ENV{auteur}, 'IN(properties.authors, @valeurs)', 'LENGTH(properties.authors) = 0'}

	{par #GET{tri}}{inverse #GET{sens_tri}}

	{facet auteur, properties.authors ORDER BY COUNT(*) DESC}
	{facet tag,    properties.tags ORDER BY COUNT(*) DESC}
	{facet annee, YEAR(date) ORDER BY date DESC}

	{pagination 10}
>

	[(#SET{properties,#PROPERTIES|json_decode{1}})]
	<li class='item'>
		<article class='entry article hentry'>
			<strong class='h3-like entry-title'>[(#SCORE|mult{100}|div{#GET{max}}|intval)%] <a href="#URI">#TITLE</a></strong>
			<p class="publication">
				[<time pubdate="pubdate" datetime="[(#DATE|date_iso)]">(#DATE|affdate_jourcourt)</time>][<span class="authors"><span class="sep">, </span> <:par_auteur:> (#GET{properties}|table_valeur{authors}|implode{', '})</span>]
			</p>
			[<div class="introduction entry-content">(#SNIPPET|sinon{#SUMMARY})</div>]
		</article>
	</li>

</BOUCLE_recherche_sphinx>
		</ul>
	</div>

	[<div class="pagination">(#PAGINATION)</div>]

	[(#INCLURE{fond=liste/sphinx_facettes,facets=#SPHINX_FACETS,env})]
	[(#INCLURE{fond=liste/sphinx_metas,meta=#SPHINX_META,env})]

</B_recherche_sphinx>
	<h2>Pas de résultat pour :</h2>
	<div><tt>#SPHINX_QUERY</tt></div>
	[<p>(#SPHINX_MESSAGE)</p>]

	[(#INCLURE{fond=liste/sphinx_metas,meta=#SPHINX_META,env})]

<//B_recherche_sphinx>

 Les criteres de boucle

  • index
  • recherche
  • filter
  • facet
  • pagination

Les balises

  • #SPHINX_META
  • #SPHINX_MESSAGE
  • #SPHINX_QUERY
  • #SPHINX_FACETS
  • #SNIPPET
  • #SUMMARY
  • #PROPERTIES
  • #DATE

Discussion

4 discussions

  • 9

    Hop, dans ma lancée, un autre bug rencontré avec le critère {filtermono}. Soit la boucle :

    <BOUCLE_test(SPHINX)
      {filtermono #ENV{id_objet}, properties.id_objet, #ENV{id_objet}}
    >

    Quand il y a un id_objet dans l’environnement, ça ne marche pas.

    Requête SQL :

    SELECT *, (properties.id_objet=230) as mono_0 FROM truc WHERE (mono_0=1)

    Erreur SQL :

    ERROR 1064 (42000): index truc: parse error: equal operation applied to part string operands

    En revanche, avec le critère {filtermultijson}, ça fonctionne bien :

    <BOUCLE_test(SPHINX)
      {filtermultijson #ENV{id_objet}, properties.id_objet, #LISTE{#ENV{id_objet}}}
    >

    Requête SQL :

    SELECT *, (IN(properties.id_objet, 230)) as multi_0 FROM truc WHERE (multi_0=1)
    • Merci ! J’ai exactement le même problème (avec id_rubrique).

      Mais le truc bizarre est que cela ne le fait que sur le serveur distant, pas sur le site local, où ma requete fonctionne parfaitement.

      Ca dépend peut être de la version de SPHINX, et dans ce cas ce serait un bug de SPHINX.

      Je vais tester avec filtermultijson dès que possible.

    • Moi j’ai l’impression que c’est parce que dans votre base il y a des valeurs en string, ou un mélange de valeurs string et int, non ? Et il peut pas faire la comparaison du coup ?

    • non, j’ai bien vérifié ces points : la requete interroge un int, et j’ai en base un int (en tout cas, [(#PROPERTIES|var_dump)] me montre un int.

    • la requete (qui marche en local mais pas sur mon site distant) :

      SELECT *, WEIGHT() AS score, (properties.objet='article') as mono_0, (properties.parents.ids[0]=1) as mono_1, SNIPPET(content, 'rigolo', 'limit=200','html_strip_mode=strip') AS snippet FROM spip WHERE (mono_0=1) AND (mono_1=1) AND (MATCH('rigolo')) LIMIT 0,10
    • Moi j’ai l’impression que c’est parce que dans votre base il y a des valeurs en string, ou un mélange de valeurs string et int, non ? Et il peut pas faire la comparaison du coup ?

      En effet pour moi il y a un mélange de int et de string.
      C’est l’objet « hierarchie » qui est en string → [(#PROPERTIES|var_dump)] :

      array (size=4)
        'objet' => string 'hierarchie' (length=10)
        'id_objet' => string 'mots' (length=4)
    • @Rastapopoulos : si le pb était un mélange int/string, comment ce ferait-il que la second requête Tcharlss, avec un IN, fonctionne ?

      @Tcharlss : tu est sur que ton ’id_objet’ contient « mots » ? ca devrait pas être un numéro plutot ?

    • @Tcharlss : qu’elle est ta version de SPHINX ?

      La version locale que j’ai est 2.2.11 -> cela marche.
      La version distante est 2.2.9 -> cela ne marche pas.

      Et le ticket http://sphinxsearch.com/bugs/view.php?id=2375 dit que le problème a été résolue en 2.2.11.

    • Ok donc ça a l’air d’être côté Sphinx en fait

    • @maieul : oui en local j’ai la version 2.2.9. Donc c’est résolu dans les versions postérieures, ticket fermé !

    Répondre à ce message

  • 2

    Un petit détail qui pourrait être mentionné dans la doc : les dates sont au format timestamp, donc pour trier sur ce critère il faut faire à l’inverse des boucles « traditionnelles » :

    * du plus récent au plus ancien : {par date}
    * du plus ancien au plus récent : {!par date}

    • Euh, comment ça se fait ? Parce que justement timestamp, c’est pareil : les dates les plus vieilles ont un nombre plus petit, donc si on classe par ordre croissant « par date », ça fait bien du plus vieux au plus récent à priori.

    • Ah mais je raconte n’importe quoi, tu as raison, ça marche bien.
      J’avais mal vérifié, désolé pour le bruit !

    Répondre à ce message

  • Un truc un peu surprenant qui devrait être mentionné dans la doc : sans le critère {pagination}, la boucle ne retourne que 20 résultats, même avec l’option max_matches.

    Donc pour afficher tous les résultats il faut ajouter un truc du genre {pagination 999999}

    Répondre à ce message

  • Bonjour,

    Je rencontre un drôle de problème, comme si des critères de la boucle étaient mis en cache.

    J’ai donc une saisie personnalisée utilisée dans un formulaire, elle contient une boucle SPHINX avec un critère {filtermultijson} qui doit être optionnel, donc pris en compte uniquement s’il y a rubriques dans l’environnement :

    <BOUCLE_truc(SPHINX)
    	{filtermultijson #ENV{rubriques}, properties.rubriques.ids, #ENV{rubriques}}
    >

    Cette saisie est inclue plusieurs fois dans mon formulaire, et je ne veux une restriction par rubriques que dans la 1re :

    $saisies = array(
    	array(
    		'saisie' => 'masaisie',
    		'options' => array(
    			'nom'       => 'test1',
    			'rubriques' => array(50),
    		),
    	),
    	array(
    		'saisie' => 'masaisie',
    		'options' => array(
    			'nom'       => 'test2',
    			'rubriques' => '',
    		),
    	),
    	array(
    		'saisie' => 'masaisie',
    		'options' => array(
    			'nom'       => 'test3',
    			'rubriques' => '',
    		),
    	),
    );

    Et là surprise, la restriction par rubriques est appliquée dans les 3 boucles !

    Dans la méthode runQuery(), j’ai fait un var_dump($result['query']['query']); pour voir ce qu’il se passait.
    Et effectivement, dans les 3 requêtes on retouve la même valeur dans le SELECT :

    1. requête 1 : (IN(properties.rubriques.ids, 50)) (ok)
    2. requête 2 : (IN(properties.rubriques.ids, 50)) (pas ok)
    3. requête 3 : (IN(properties.rubriques.ids, 50)) (pas ok)

    En revanche quand je donne explictement des valeurs à rubriques pour chaque saisie, elles sont correctement prises en compte. Par exemple en donnant respectivement 50, 100 et 150 :

    1. requête 1 : (IN(properties.rubriques.ids, 50)) (ok)
    2. requête 2 : (IN(properties.rubriques.ids, 100)) (ok)
    3. requête 3 : (IN(properties.rubriques.ids, 150)) (ok)

    Donc on dirait que ça prend automatiquement une valeur si la boucle est utilisée plusieurs fois, et qu’il n’y a pas de valeur donnée explicitement.

    Enfin, si je ne donne une valeur 100 qu’à la 2e saisie, j’obtiens ces requêtes :

    1. requête 1 : rien (ok)
    2. requête 2 : (IN(properties.rubriques.ids, 100)) (ok)
    3. requête 3 : (IN(properties.rubriques.ids, 100)) (pas ok)

    Donc ça semble ne remplir la valeur que s’il y en a une dans la boucle précédente.

    Pour finir, si je fais des copies des squelettes de ma saisie avec des noms différents, là ça fonctionne correctement.
    Bref, on dirait qu’il y a un espèce de cache quelque part qui entre en jeu.

    Répondre à ce message

Ajouter un commentaire

Avant de faire part d’un problème sur un plugin X, merci de lire ce qui suit :

  • Désactiver tous les plugins que vous ne voulez pas tester afin de vous assurer que le bug vient bien du plugin X. Cela vous évitera d’écrire sur le forum d’une contribution qui n’est finalement pas en cause.
  • Cherchez et notez les numéros de version de tout ce qui est en place au moment du test :
    • version de SPIP, en bas de la partie privée
    • version du plugin testé et des éventuels plugins nécessités
    • version de PHP (exec=info en partie privée)
    • version de MySQL / SQLite
  • Si votre problème concerne la partie publique de votre site, donnez une URL où le bug est visible, pour que les gens puissent voir par eux-mêmes.
  • En cas de page blanche, merci d’activer l’affichage des erreurs, et d’indiquer ensuite l’erreur qui apparaît.

Merci d’avance pour les personnes qui vous aideront !

Par ailleurs, n’oubliez pas que les contributeurs et contributrices ont une vie en dehors de SPIP.

Qui êtes-vous ?
[Se connecter]

Pour afficher votre trombine avec votre message, enregistrez-la d’abord sur gravatar.com (gratuit et indolore) et n’oubliez pas d’indiquer votre adresse e-mail ici.

Ajoutez votre commentaire ici

Ce champ accepte les raccourcis SPIP {{gras}} {italique} -*liste [texte->url] <quote> <code> et le code HTML <q> <del> <ins>. Pour créer des paragraphes, laissez simplement des lignes vides.

Ajouter un document

Suivre les commentaires : RSS 2.0 | Atom