/extension/myblog/design/myblog/stylesheets/white.css
/extension/myblog/design/myblog/stylesheets/black.css

Select your style :

A la une // Les blogs sur le développement Web, l'oenologie, Montpellier, etc...

Développement avancé avec eZ Find (partie 2 : Indexer des champs supplémentaires dans Solr)

Le billet précédent décrit les mécanismes bas niveaux d'eZ Find, et la façon dont les correspondances entre les attributs eZ Publish (noms, types de champs) et les champs Solr sont gérés. Ce billet décrit comment eZ Find peut considérablement faciliter le développement de certaines fonctionnalités (en évitant de complexes opérateurs de templates aux multiples requêtes SQL...), en ajoutant automatiquement des champs dans Solr lors de l'indexation d'un contenu, ré-exploitables par la suite pour la construction d'une facette par exemple ou pour profiter d'un filtre supplémentaire.

Etude de cas : un filtre par années, et par mois-années

L'exemple étudié a surtout une valeur pédagogique, puisqu'il s'agit d'un besoin générique et relativement simple à implémenter. Les listes d'actualités, ou les listes de billets sur les Blogs proposent généralement des filtres par années (2010) ou par mois / année (Janvier 2010) (comme sur ce blog par exemple dans la colonne de droite). Habituellement, pour ce genre de filtre on développe un opérateur de template permettant d'effectuer la requête SQL nécessaire, ce qui peut rapidement devenir très complexe. Il suffit de lire le code de l'opérateur de template eZArchive pour en comprendre les limites.

Il est relativement fréquent que ces manipulations SQL souffrent de carences fonctionnelles, comme la propagation des droits, la gestion des langues, la gestion de certaines subtilités entre MySql ou PostGreSql. Ce sont des problématiques à la charge du développeur, puisque l'on contourne les API pour exploiter directement du SQL. Par ailleurs, l'opérateur eZArchive montre une autre limite importante, puisqu'il se contente de travailler sur la date de publication 'publication_date' (par facilité), et ne permet donc pas d'exploiter un attribut de date spécifique à la classe.

Développer le filtre par années, et par mois-années avec eZ Find

Etape 1 : Indexer les années, mois / année vers Solr

Les settings d'eZ Find (ezfind.ini, à surcharger dans le ezfind.ini.append.php de votre extension) permettent de déléguer le traitement de l'indexation d'un datatype eZ Publish vers une classe PHP :

[SolrFieldMapSettings]
CustomMap[ezdate]=ezfSolrDocumentFieldDate
 

Notre classe PHP, nommée arbitrairement ezfSolrDocumentFieldDate hérite de la classe ezfSolrDocumentFieldBase et doit être ajoutée dans le dossier /extension/myextension/classes/ezfsolrdocumentfielddate.php avec le squelette suivante :

<?php
class ezfSolrDocumentFieldDate extends ezfSolrDocumentFieldBase
{
 public static function getFieldName( eZContentClassAttribute $classAttribute, $subAttribute = null, $context = 'search' )
 {
 // return the fieldname like : attr_mydate_d
 }
 
 public function getData()
 {
 // return the array keys (fieldname => value), like : array('attr_mydate_dt' => '2010-04-30T00:00:00Z')
 }
}
?>
 

Le rôle de la méthode getFieldName

Cette méthode est invoquée pour transformer les noms des attributs eZ Find vers les noms de champs Solr. Ainsi lorsqu'on construit une facette avec la syntaxe 'mycontentclass/mydateattribute', cette méthode reçoit 'mydateattribute' et doit retourner 'attr_mydateattribute_dt'. Nous allons donc implémenter cette fonction de la façon suivante :

  • Si un sous attribut est spécifié (par exemple 'mycontentclass/mydateattribute/year'), alors retourne le nom composé du sous attribut
  • Si aucun sous attribut n'est spécifié, alors exécute le code de la classe parent
  • Important : Pour profiter d'une certaine généricité, et éviter de nommer 'en dur' les champs Solr, nous utilisons les méthodes parents generateSubattributeFieldName et generateAttributeFieldName
const DEFAULT_SUBATTRIBUTE_TYPE = 'date';
 
public static function getFieldName( eZContentClassAttribute $classAttribute, $subAttribute = null, $context = 'search' )
{ 
  switch ( $classAttribute->attribute( 'data_type_string' ) )
  {
    case 'ezdate' :
    {
     if ( $subAttribute and $subAttribute !== '' )
     {
      // A subattribute was passed
      return parent::generateSubattributeFieldName( $classAttribute,
       $subAttribute,
       self::DEFAULT_SUBATTRIBUTE_TYPE );
     }
     else
     {
      // return the default field name here.
      return parent::generateAttributeFieldName( $classAttribute, self::getClassAttributeType( $classAttribute, null, $context ) );
     }
  } break;
    
    default:
    {} break;
  }
}
 

Le rôle de la méthode getData

Cette méthode est invoquée pour extraire les données d'eZ Publish, et les préparer pour leur indexation vers Solr. C'est donc dans cette méthode que l'on peut ajouter nos champs supplémentaires 'year' et 'yearmonth'. Pour faciliter la future exploitation de ces champs avec eZ Find, je souhaite pouvoir construire des facettes ou des filtres selon la syntaxe classique :

  • 'mycontentclass/mydateattribute/year', dont la transposition Solr serait 'subattr_date-year_dt'
  • 'mycontentclass/mydateattribute/yearmonth', dont la transposition Solr serait 'subattr_date-yearmonth_dt'
public function getData()
 {
  $contentClassAttribute = $this->ContentObjectAttribute->attribute( 'contentclass_attribute' );
 
  switch ( $contentClassAttribute->attribute( 'data_type_string' ) )
  {   
  case 'ezdate' :
  {
  $returnArray = array();
   
  // Get timestamp attribute value
  $value = $this->ContentObjectAttribute->metaData();
   
  // Generate the main filedName attr_XXX_dt 
  $fieldName = parent::generateAttributeFieldName( $contentClassAttribute,
  self::DEFAULT_ATTRIBUTE_TYPE );
 
  $returnArray[$fieldName] = parent::convertTimestampToDate( $value );
 
  // Generate the yearmonth subattribute filedName subattr_year_dt
  $fieldName = parent::generateSubattributeFieldName( $contentClassAttribute,
  'year',
  self::DEFAULT_SUBATTRIBUTE_TYPE );
 
  $year = date("Y", $value); // Get Year value : 2010
  $returnArray[$fieldName] = parent::convertTimestampToDate( strtotime($year.'-01-01') );
 
  // Generate the yearmonth subattribute filedName subattr_yearmonth_dt
  $fieldName = parent::generateSubattributeFieldName( $contentClassAttribute,
  'yearmonth',
  self::DEFAULT_SUBATTRIBUTE_TYPE );
 
  $month = date("n", $value); // Get Month value : 3
  $returnArray[$fieldName] = parent::convertTimestampToDate( strtotime($year.'-'.$month.'-01') );
   
  return $returnArray;
   
  } break;
 
  default:
  {} break;
  }
 }
}
 

A noter : $returnArray contient un tableau à clés, dont voici un exemple de sortie (effectuer avec var_dump) :

array(3) {
 ["attr_date_dt"]=>
 string(24) "2008-12-28T00:00:00.000Z"
 ["subattr_date-year_dt"]=>
 string(24) "2008-01-01T00:00:00.000Z"
 ["subattr_date-yearmonth_dt"]=>
 string(24) "2008-12-01T00:00:00.000Z"
}
 

A noter : Solr utilise le format de date ISO 8601, du type '2010-04-30T00:00:00Z'. La classe parent ezfSolrDocumentFieldBase propose la méthode convertTimestampToDate() pour convertir un format timestamp vers un format ISO 8601.

Le template de construction des facettes

Nos données par années et par mois-année sont maintenant disponibles. Il ne reste plus qu'à construire nos facettes avec la syntaxe habituelle :

{def $search_yearmonth=fetch( ezfind, search,
 hash( query , '',
 'facet', array( 
 hash('field', 'billet/date/year', 'sort', 'alpha', 'limit', 20 ),
 hash('field', 'billet/date/yearmonth', 'sort', 'alpha', 'limit', 20 )
 ),
 'class_id', array('billet'),
 'subtree_array', array(2)
 ))}
 
 {def $search_extras_year=$search_yearmonth['SearchExtras'].facet_fields[0].nameList|reverse}
 {def $search_extras_yearmonth=$search_yearmonth['SearchExtras'].facet_fields[1].nameList|reverse}
 {def $date_count = 0
 $date_ts = 0}
 
<li id="blog_block_10" class="colonne_block">
 <h1>Archives par années :</h1>
 <ul class="{$current_css} list">
 {foreach $search_extras_year as $facetID =&gt; $datevalue}
  {set $date_count = $search_yearmonth['SearchExtras'].facet_fields[0].countList[$facetID]}
  {set $date_ts = $datevalue|strtotime}
 <li><a href={concat('/Blogs/(year)/',$date_ts|datetime( 'custom', '%Y' ))|ezurl} title="Archives : {$date_ts|datetime( 'custom', '%Y' )} // {$date_count} Billet(s)">{$date_ts|datetime( 'custom', '%Y' )}</a></li>
 {/foreach}
 </ul>
</li>
<li id="blog_block_11" class="colonne_block">
 <h1>Archives par mois / années :</h1>
 <ul class="{$current_css} list">
 {foreach $search_extras_yearmonth as $facetID =&gt; $datevalue}
  {set $date_count = $search_yearmonth['SearchExtras'].facet_fields[1].countList[$facetID]}
  {set $date_ts = $datevalue|strtotime}
 <li><a href={concat('/Blogs/(year)/',$date_ts|datetime( 'custom', '%Y' ),'/(month)/',$date_ts|datetime( 'custom', '%n' ))|ezurl} title="Archives : {$date_ts|datetime( 'custom', '%F %Y' )} // {$date_count} Billet(s)">{$date_ts|datetime( 'custom', '%F %Y' )|upfirst}</a></li>
 {/foreach}
 </ul>
 
</li>
{undef $date_ts $date_count $search_yearmonth $search_extras_yearmonth}
 

A noter : Le fetch proposé est relativement basique, afin de faciliter la compréhension du mécanisme. Il faut bien sur faire évoluer le code pour obtenir le fonctionnel attendu (utiliser les filtres par exemple), mais ce n'est pas l'objet de ce billet puisque la documentation officielle détaille déjà la façon de procéder.

A noter : le 'sort', 'alpha' ne permet pas réellement de spécifier que l'on souhaite un tri alphabétique. Il s'agit surtout de spécifier que l'on ne souhaite pas un tri par 'count' (nombre d'items associés à la facette). Dans ce cas Solr applique un tri automatique 'croissant' en fonction de son index et du type de donnée (ce qui explique l'utilisation de l'opérateur reverse pour obtenir une liste décroissante).

Que boire avec ce billet ?

Guy Blanchard - Perrières "Les Vieilles"

Région : Bourgogne
Appellation : Vin de table de France
Domaine : Guy Blanchard
Couleur :
 
Stock : 1
Notation :
Prix : 12 €
Commentaire(s) : 0 Commentaire(s)

Une curiosité, un vin de "garage" à manipuler avec précaution. Travailler de façon naturelles (sans filtrage et sans sulfites additionnels) ce vin vivant et un peu trouble laisse une impression curieuse de manque de précision et de manque de netteté. Cependant après quelques heures d'aération et de respiration, le vin se met en place et dégage d'étonnants arômes de fruits frais, à déguster avec gourmandise. A carafer quelques heures avant dégustation.

Publié par : masev, le 17 mai 2010 02:09 pm

Très utile

Merci pour ces super tuto !
Je suis justement en train de jouer avec eZ Find et ça m'a énormément servi, de même que ta contrib sur projetcs.ez.no

Publié par : Seb, le 22 mai 2010 07:57 am

Merci

Article très clair, merci beaucoup !

Publié par : gandbox, le 22 mai 2010 09:08 pm

Tutos sur eZ Find

Merci pour les encouragements.

Si vous n'êtes pas à Berlin, on pourra en discuter à Bordeaux :
http://2010.rmll.info/Travailler-avec-eZ-Find-et-SolR.html

Publié par : nicoo, le 04 juillet 2010 08:37 pm

si je comprend bien ...

Bonjour,

Si je comprend bien, j'ai une classe qui contient un attribut "Région" qui me permet de catégoriser des articles apr régions. Si je souhaite ajouter un "filtre" (par facette) sur les régions, je dois procéder ainsi et créer ma propre méthode php pour indexer le champs "Région" des mes article et pouvoir ensuite créer des filtres avec eZFind ?

Merci pour ton retour,
Nico O

Publié par : gandbox, le 05 juillet 2010 12:35 pm

Attribut region

@nicoo : Pas forcement. Tout dépend de ton datatype "région"
S'il s'agit d'un keyword, ou d'un string, alors le "mapping" est natif
S'il s'agit d'une relation d'objet, alors il faut étendre la classe PHP proposée par défaut (en utilisant ma contrib par exemple : http://projects.ez.no/ezfsolrdocumentfieldobjectrelation
S'il s'agit d'un rangement dans des dossiers par région... alors cela peut se faire de plusieurs façon, dont l'ajout d'une classe PHP