eZ Find est une extension native d'eZ Publish, maintenant disponible dans les diverses installations du CMS. Mon précédent billet donne une courte définition du fonctionnement d'eZ Find, de son couplage avec Solr, et de sa relation avec les datatypes.
eZ Find est généralement présenté et vendu comme un moteur de recherche, et les utilisateurs (et développeurs) peuvent donc s'attendre à un mécanisme du type :
- Je saisie une expression libre
- J'envoie ma recherche
- J'obtiens une liste de résultat, et j'applique quelques tris (alphabétique, dates, pertinence) et quelques filtres disponibles (par rubriques, par facettes, etc.)
Cependant, le cadre d'exploitation d'eZ Find est plus vaste que ce schéma fonctionnel. Ce billet décrit un cas d'utilisation certes relativement inutile mais signification d'une utilisation alternative d'eZ Find : construire un nuage de tags.
A partir d'un exemple simple, on peut facilement en déduire d'autres cas d'utilisation qui facilitent énormément le développement de certains projets, comme par exemple les agrégateurs de contenus, les portails et autres mécanismes de navigations complexes dans un catalogue.
Comment construire un nuage de tags sur eZ Publish ?
La seule méthode un peu optimisée et fonctionnelle de procéder actuellement est l'utilisation d'un opérateur de template qui explore la base de données, et notamment la table ezkeyword. Le package ezwebin propose l'opérateur eztagcloud, qui est facile à déployer et à utiliser.
- Voici un exemple d'appel de l'opérateur dans un template
<div>
{eztagcloud( hash( 'class_identifier', 'billet', 'parent_node_id', 2 ) )}
</div>
Avantages de l'opérateur
Les fonctions fetch natives ne permettent pas de lister un ensemble de keywords en fonction des paramètres utiles (subtree, classes, etc.), c'est donc la seule façon "économique" et "optimisée" de procéder. Les opérateurs permettent souvent aux développeurs eZ Publish avancés d'optimiser certains traitements, en économisant le nombre de requêtes SQL par exemple, ou en facilitant certains algorithmes laborieux à transposer avec le langage de template (par exemple le calcul des pourcentages des styles CSS inline dans cet opérateur)
Inconvénients de l'opérateur
L'écriture de ce type d'opérateur est peu accessible aux développeurs occasionnels, et la manipulation du SQL est une pratique dangereuse si le modèle de données eZ Publish est mal maîtrisé (prise en compte des versions, des visibilités, des langues, des droits...). Par ailleurs cet opérateur encapsule la logique algorithmique du calcul des pourcentages transmis au "font-size" en style inline. Les amoureux du CSS full externe, ou de l'accessibilité devront donc adapter cet opérateur à leur besoin.
Comment construire un nuage de tags avec eZ Find ?
Comprendre le concept de facettes
Derrière ce terme "géométrique" se cache un concept finalement assez simple et naturel, que l'on pourrait appeler : "groupement des résultats pour un champs", à savoir :
- Supposons qu'un résultat de recherche contienne 100 billets
- Sur ces 100 billets, on peut lister 20 mots clés distincts associés
- Parmi ces 20 mots clés, le mot clés A est associé à 10 billets, le mot clés B est associé à 5 billets, et ainsi dessuite
On peut transposer cet exemple sur tous les attributs et meta données d'une classe (name, dates, auteur, attribut quelconque), et même obtenir N listes de facettes sur N attributs et méta différents
Construite le nuage de mots clés avec des facettes
Cet exemple de code montre comment construire sa requête eZ Find, récupérer les facettes résultantes sur l'attribut "tags" de type "keywords", et gérer le poids des keywords en fonction d'un algorithme simplifié (j'ai un peu triché sur cet aspect, puisque ce n'est pas l'objet de la démonstration).
{def $search_keywords=fetch( ezfind , search,
hash( query , '',
'facet', array(
hash('field', 'billet/tags', 'sort', 'alpha', 'limit', 100 )),
'class_id', array('billet'),
'filter', array('not', 'billet/tags:""'),
'subtree_array', array(2)
))}
{def $search_extras_keywords=$search_keywords['SearchExtras']}
{def $search_count_keywords=$search_keywords['SearchCount']}
<li id="blog_block_{$bloc_count}" class="colonne_block">
<h1>Tags ezfind :</h1>
<div class="tagclouds {$current_css}">
{foreach $search_extras_keywords.facet_fields[0].nameList as $facetID => $name}
{def $keyword_count = $search_extras_keywords.facet_fields[0].countList[$facetID]}
{def $percent = $keyword_count|div( $search_count_keywords )|mul( 200 )|floor|sum( 100 ) }
<a href={concat( $root_blog_node.url_alias, '/(tag)/', $name )|ezurl()} style="font-size: {$percent}%" title="{$keyword_count} billets taggés '{$name}' // ">{$name|wash()}</a>,
{undef $percent}
{/foreach}
</div>
</li>
{undef $search_extras_keywords $search_keywords $search_count_keywords}
Quelques astuces & clés de compréhensions
- La recherche sur une chaine vide (query , '') est une technique permettant d'explorer l'ensemble des contenus indexés, en appliquant uniquement les filtres et limitations utiles (class_id, subtree_array par exemple)
- L'opérateur 'NOT' n'est pas natif et nécessite un petit 'hack' proposé par Bruce Morrison, qui sera sans doute disponible dans les futures versions d'eZ Find
- Par défaut les facettes sur des datatypes 'keywords' sont présentés en minuscule. Ce n'est pas un bug, mais une fonctionnalité visant à homogénéiser la casse sur les syntaxes similaires (eZ Publish, ez Publish, Ez Publish, etc.). Cependant lorsqu'on est confiant dans la qualité de sa saisie (puisqu'on utilise une extension d'autocomplétion par exemple), il peut être souhaitable de ne pas forcer l'utilisation du minuscule. Pour cela il faut désactiver le filtre solr.LowerCaseFilterFactory dans le fichier /extension/ezfind/java/solr/conf/schema.xml
<!-- eZ Find: This field type is dedicated to ez publish keywords. -->
<fieldtype name="keyword" class="solr.TextField" positionIncrementGap="100">
<analyzer type="index">
<tokenizer class="solr.PatternTokenizerFactory" pattern=", *" />
<filter class="solr.TrimFilterFactory" />
<filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt"/>
<!--<filter class="solr.LowerCaseFilterFactory"/>-->
<filter class="solr.RemoveDuplicatesTokenFilterFactory"/>
</analyzer>
<analyzer type="query">
<tokenizer class="solr.PatternTokenizerFactory" pattern=", *" />
<filter class="solr.TrimFilterFactory" />
<filter class="solr.SynonymFilterFactory" synonyms="synonyms.txt" ignoreCase="true" expand="true"/>
<filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt"/>
<filter class="solr.LowerCaseFilterFactory"/>
<filter class="solr.RemoveDuplicatesTokenFilterFactory"/>
</analyzer>
</fieldtype>