Apache Lucene : créer un moteur de recherche pour votre site Web

De nos jours, si l’on pense à un moteur de recherche, Google est le premier à venir à l’esprit. Les exploitants de sites Web utilisent également Google sous la forme d’un moteur de recherche personnalisé (CSE) pour offrir rapidement et facilement aux utilisateurs une fonction de recherche pour leur propre contenu. Mais ce n’est bien sûr pas la seule option et pour de nombreux exploitants de sites Web, ce n’est pas la meilleure pour offrir aux visiteurs une recherche plein texte. À la place, vous pouvez utiliser Lucene, un projet open source gratuit d’Apache.

De nombreuses entreprises ont intégré Apache Lucene, en ligne ou hors ligne. Wikipédia avait implémenté Lucene comme fonction de recherche jusqu’à il y a quelques années et utilise maintenant Solr, qui est tout de même basé sur Lucene, et les recherches sur Twitter sont aussi entièrement basées sur Lucene. Le projet, que Doug Cutting a débuté comme un passe-temps à la fin des années 1990, s’est depuis transformé en un logiciel qui bénéficie à des millions de personnes au quotidien.

Qu’est-ce que Lucene ?

Lucene est une bibliothèque de programmes publiée par l’Apache Software Foundation. C’est un logiciel gratuit et libre, n’importe qui peut l’utiliser et le modifier. À l’origine, Lucene a été écrit entièrement en Java, mais il existe maintenant aussi des ports vers d’autres langages de programmation. Avec Apache Solr et Elasticsearch, il existe des extensions puissantes qui apportent encore plus de possibilités à la fonction de recherche.

Lucene est une recherche plein texte. Cela signifie tout simplement qu’un programme recherche un ou plusieurs termes définis par l’utilisateur dans une série de documents texte. Cela montre que Lucene n’est pas seulement utilisé dans le contexte du World Wide Web, même si les fonctions de recherche sont omniprésentes sur le Web. Lucene peut également être utilisé pour les archives, les bibliothèques ou même le PC à domicile. Lucene ne recherche pas seulement des documents HTML, mais travaille aussi avec des emails ou même des fichiers PDF.

Le facteur décisif pour la recherche est un index, c’est le cœur de Lucene : tous les termes de tous les documents sont stockés ici. Un tel « Inverted Index » n’est en principe qu’un tableau dans lequel, pour chaque terme, la position correspondante est enregistrée. Pour construire un index, il faut d’abord procéder à une extraction. Tous les termes doivent être extraits de tous les documents et sauvegardés dans l’index. Lucene permet aux utilisateurs de configurer cette extraction individuellement. Lors de la configuration, les développeurs décident quels champs ils veulent inclure dans l’index. Pour mieux comprendre le processus, il nous faut prendre du recul.

Les objets avec lesquels Lucene travaille sont des documents sous toutes leurs formes. Cependant, les documents eux-mêmes contiennent, du point de vue de Lucene, des champs. Ces champs contiennent, par exemple, le nom de l’auteur, le titre du document ou le nom du fichier de l’auteur. Chaque champ possède un nom unique et une valeur. Par exemple, le champ nommé titre peut avoir la valeur « Apache Lucene user manual ». Lors de la création de l’index, vous pouvez décider quelles métadonnées vous voulez inclure.

Lors de l’indexation des documents, on procède à ce qu’on appelle une « tokenisation ». Pour une machine, un document est en effet d’abord un assemblage de données. Même si l’on s’éloigne du niveau des bits et que l’on se tourne vers le contenu lisible par l’homme, un document est avant tout constitué d’une chaîne de caractères : lettres, ponctuation et espaces.

À partir de cette quantité de données, des segments sont créés avec la tokenisation, les termes (la plupart du temps des mots simples), peuvent finalement être recherchés. La façon la plus simple d’exécuter une telle tokenisation est d’utiliser la méthode White-Space : un terme se termine lorsqu’un espace (un espace blanc) apparaît. Toutefois, cela n’est pas utile si les termes fixes sont composés de plusieurs mots, par exemple « veille de Noël ». Des dictionnaires sont également utilisés à cet effet, qui peuvent également être implémentés directement dans le code Lucene.

Lors de l’analyse des données, dont la tokenisation fait partie, Lucene effectue également une normalisation. Cela signifie que les termes sont transformés en une forme standardisée, par exemple, toutes les lettres majuscules sont écrites en minuscules. En outre, Lucene crée un ordre de tri. Ceci fonctionne via différents algorithmes, par exemple via la mesure TF-IDF. En tant qu’utilisateur, vous voulez probablement d’abord obtenir les résultats les plus pertinents ou les plus récents, les algorithmes du moteur de recherche Lucene rendent cela possible.

Pour que les utilisateurs puissent trouver quoi que ce soit, ils doivent alors entrer un terme de recherche dans une ligne de texte. Les termes sont appelés « Query » dans le contexte de Lucene. Le mot anglais pour requête indique que l’entrée ne doit pas seulement consister en un ou plusieurs mots, mais peut aussi contenir des modificateurs tels que AND, OR ou + et - ainsi que des caractères génériques. Le QueryParser, une classe au sein de la bibliothèque du programme, traduit l’entrée en une demande de recherche concrète pour le moteur de recherche. Le QueryParser fournit également aux développeurs des options de paramétrage. L’analyseur peut être configuré de manière à être exactement adapté aux besoins de l’utilisateur.

Ce que Lucene a apporté de complètement nouveau dès sa sortie est l’indexation incrémentale. Avant Lucene, seule l’indexation par lots était possible. Alors que seuls des index complets peuvent être implémentés, un index peut être mis à jour avec l’indexation incrémentale. Des entrées individuelles peuvent être ajoutées ou supprimées.

Lucene versus Google & Co ?

La question semble justifiée : pourquoi construire votre propre moteur de recherche alors qu’il existe déjà Google, Bing ou d’autres moteurs de recherche ? Bien sûr, y répondre n’est pas si évident, après tout, vous devez tenir compte de l’application individuelle. Mais une chose est importante pour comprendre : lorsque nous parlons de Lucene comme d’un moteur de recherche, c’est juste une appellation simplifié.

En fait, il s’agit plus précisément d’une bibliothèque de recherche d’information (Information Retrieval Library). Lucene est donc un système avec lequel on peut chercher et trouver de l’information. C’est aussi le cas de Google et d’autres moteurs de recherche, mais ces derniers se limitent à l’information provenant du World Wide Web. Vous pouvez par contre utiliser Lucene dans n’importe quel scénario et le configurer en fonction de vos besoins précis. Par exemple, vous pouvez intégrer Lucene au sein d’autres applications.

En résumé

Apaches Lucene, contrairement aux moteurs de recherche Web, n’est pas un logiciel prêt à l’emploi : afin de bénéficier des capacités du système, vous devez d’abord programmer votre propre moteur de recherche. Nous vous montrons les premières étapes dans notre tutoriel sur Lucene.

Lucene, Solr, Elasticsearch, quelles sont les différences ?

Les débutants se demandent notamment quelle est la différence entre Apache Lucene d’une part et Apache Solr et Elasticsearch d’autre part. Ces deux derniers sont basés sur Lucene : l’ancien produit est un moteur de recherche pur. Alors que Solr et Elasticsearch sont des serveurs de recherche complets qui étendent encore plus le champ d’application de Lucene.

Note

Si vous n’avez besoin que d’une seule fonction de recherche pour votre site Web, Solr ou Elasticsearch sont probablement plus adaptés. Ces deux systèmes sont spécifiquement conçus pour une utilisation sur le Web.

Apache Lucene : le tutoriel

Lucene est, dans sa version originale, basé sur Java : ceci permet d’utiliser le moteur de recherche pour différentes plateformes en ligne et hors ligne, si vous savez comment faire. Nous vous expliquons étape par étape comment construire votre propre moteur de recherche avec Apache Lucene.

Note

Ce tutoriel traite de Lucene basé sur Java. Le code a été testé sous la version Lucene 7.3.1 et la version JDK 8. Nous travaillons avec Eclipse sous Ubuntu. Les étapes individuelles peuvent être différentes lorsqu’on utilise d’autres environnements de développement et systèmes d’exploitation.

Installation

Pour pouvoir travailler avec Apache Lucene, Java doit être installé sur votre système. Tout comme Lucene, vous pouvez également télécharger gratuitement le Java Development Kit (JDK) sur le site officiel. Vous devriez également installer un environnement de développement que vous pouvez utiliser pour écrire le code pour Lucene. De nombreux développeurs font confiance à Eclipse, mais il existe de nombreuses autres offres open source. Ensuite, vous pouvez télécharger Lucene à partir de la page du projet. Choisissez la version de base (Core Version) du programme.

Vous n’avez pas besoin d’installer Lucene. Il suffit de décompresser le téléchargement à l’emplacement souhaité. Vous créez ensuite un nouveau projet dans Eclipse ou un autre environnement de développement et ajoutez Lucene comme bibliothèque. Pour cet exemple, nous utilisons trois bibliothèques, qui sont toutes incluses dans le paquet d’installation :

  • …/lucene-7.3.1/core/lucene-core-7.3.1.jar
  • …/lucene-7.3.1/queryparser/lucene-queryparser-7.3.1.jar
  • …/lucene-7.3.1/analysis/common/lucene-analyzers-common-7.3.1.jar

Si vous utilisez une version différente ou si vous avez modifié la structure des dossiers, vous devez adapter les spécifications en conséquence.

Conseil

Sans connaissance de base de Java et de la programmation en général, les étapes suivantes sont difficiles à suivre. Cependant, si vous avez déjà une connaissance de base de ce langage de programmation, travailler avec Lucene est un bon moyen de développer vos compétences.

Indexation

Le cœur d’un moteur de recherche basé sur Lucene est l’index. Sans index, vous ne pouvez pas proposer de fonction de recherche. C’est donc la première étape : nous créons une classe Java pour l’indexation.

Mais avant de construire le mécanisme d’indexation proprement dit, nous créons deux classes pour vous aider avec le reste. La classe index et la classe de recherche que nous utiliserons plus tard.

package tutorial;
public class LuceneConstants {
    public static final String CONTENTS = "contents";
    public static final String FILE_NAME = "filename";
    public static final String FILE_PATH = "filepath";
    public static final int MAX_SEARCH = 10;
}

Cette information sera importante plus tard pour déterminer avec précision les champs.

package tutorial;
import java.io.File;
import java.io.FileFilter;
public class TextFileFilter implements FileFilter {
    @Override
    public boolean accept(File pathname) {
        return pathname.getName().toLowerCase().endsWith(".txt");
    }
}

Avec cela, nous implémentons un filtre qui lit correctement nos documents. À ce stade, vous pouvez voir que notre moteur de recherche ne fonctionnera plus tard que pour les fichiers txt. Tous les autres formats sont ignorés par l’exemple simple.

Note

Pour débuter une classe, vous importez d’abord les autres classes. Celles-ci font déjà partie de votre installation Java ou sont disponibles grâce à l’intégration de bibliothèques externes.

Créez maintenant la classe réelle pour l’indexation.

package tutorial;
import java.io.File;
import java.io.FileFilter;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Paths;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
public class Indexer {
    private IndexWriter writer;
    public Indexer(String indexDirectoryPath) throws IOException {
        Directory indexDirectory = 
            FSDirectory.open(Paths.get(indexDirectoryPath));
        StandardAnalyzer analyzer = new StandardAnalyzer();
        IndexWriterConfig iwc = new IndexWriterConfig(analyzer);
        writer = new IndexWriter(indexDirectory, iwc);
    }
    public void close() throws CorruptIndexException, IOException {
        writer.close();
    }
    private Document getDocument(File file) throws IOException {
        Document document = new Document();
        TextField contentField = new TextField(LuceneConstants.CONTENTS, new FileReader(file));
        TextField fileNameField = new TextField(LuceneConstants.FILE_NAME,
            file.getName(),TextField.Store.YES);
        TextField filePathField = new TextField(LuceneConstants.FILE_PATH,
            file.getCanonicalPath(),TextField.Store.YES);
        document.add(contentField);
        document.add(fileNameField);
        document.add(filePathField);
        return document;
    }    
    private void indexFile(File file) throws IOException {
        System.out.println("Indexing "+file.getCanonicalPath());
        Document document = getDocument(file);
        writer.addDocument(document);
    }
    public int createIndex(String dataDirPath, FileFilter filter) 
        throws IOException {
        File[] files = new File(dataDirPath).listFiles();
        for (File file : files) {
            if(!file.isDirectory()
                && !file.isHidden()
                && file.exists()
                && file.canRead()
                && filter.accept(file)
            ){
                indexFile(file);
            }
        }
        return writer.numDocs();
    }
}

Diverses étapes sont effectuées au cours de l’élaboration du code : vous définissez l’IndexWriter à l’aide de StandardAnalyzer. Lucene offre différentes classes d’analyse, qui peuvent toutes être trouvées dans la bibliothèque correspondante.

Conseil

Voir la documentation d’Apache Lucene pour toutes les classes incluses dans le téléchargement.

En outre, le programme lit les fichiers et définit les zones à indexer. À la fin du code, les fichiers index sont créés.

Fonction de recherche

Bien sûr, l’index seul ne vous est d’aucune utilité. Vous devez donc également établir une fonction de recherche.

package tutorial;
import java.io.IOException;
import java.nio.file.Paths;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
public class Searcher {
	
   IndexSearcher indexSearcher;
   QueryParser queryParser;
   Query query;
   
   public Searcher(String indexDirectoryPath) 
      throws IOException {
      Directory indexDirectory = 
         FSDirectory.open(Paths.get(indexDirectoryPath));
      IndexReader reader = DirectoryReader.open(indexDirectory);
      indexSearcher = new IndexSearcher(reader);
      queryParser = new QueryParser(LuceneConstants.CONTENTS,
         new StandardAnalyzer());
   }
   
   public TopDocs search( String searchQuery) 
      throws IOException, ParseException {
      query = queryParser.parse(searchQuery);
      return indexSearcher.search(query, LuceneConstants.MAX_SEARCH);
   }
   public Document getDocument(ScoreDoc scoreDoc) 
      throws CorruptIndexException, IOException {
      return indexSearcher.doc(scoreDoc.doc);	
   }
}

Deux classes Lucene importées sont particulièrement importantes dans le code : IndexSearcher et QueryParser. Pendant que la première effectue une recherche dans l’index créé, QueryParser est responsable de la traduction de la requête de recherche en informations que la machine peut comprendre.

Vous disposez maintenant d’une classe pour l’indexation et d’une classe pour rechercher l’index, mais vous ne pouvez pas encore exécuter une demande de recherche spécifique avec l’une ou l’autre de ces classes. Par conséquent, vous avez maintenant besoin d’une cinquième classe.

package tutorial;
import java.io.IOException;
import org.apache.lucene.document.Document;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
public class LuceneTester {
	
   String indexDir = "/home/Index/";
   String dataDir = "/home/Data/";
   Indexer indexer;
   Searcher searcher;
   public static void main(String[] args) {
      LuceneTester tester;
      try {
         tester = new LuceneTester();
         tester.createIndex();
         tester.search("YourSearchTerm");
      } catch (IOException e) {
         e.printStackTrace();
      } catch (ParseException e) {
         e.printStackTrace();
      }
   }
   private void createIndex() throws IOException {
      indexer = new Indexer(indexDir);
      int numIndexed;
      long startTime = System.currentTimeMillis();	
      numIndexed = indexer.createIndex(dataDir, new TextFileFilter());
      long endTime = System.currentTimeMillis();
      indexer.close();
      System.out.println(numIndexed+" File indexed, time taken: "
         +(endTime-startTime)+" ms");		
   }
   private void search(String searchQuery) throws IOException, ParseException {
      searcher = new Searcher(indexDir);
      long startTime = System.currentTimeMillis();
      TopDocs hits = searcher.search(searchQuery);
      long endTime = System.currentTimeMillis();
   
      System.out.println(hits.totalHits +
         " documents found. Time :" + (endTime - startTime));
      for(ScoreDoc scoreDoc : hits.scoreDocs) {
         Document doc = searcher.getDocument(scoreDoc);
            System.out.println("File: "
            + doc.get(LuceneConstants.FILE_PATH));
      }  
   }
}

Vous devez adapter au moins trois entrées dans ces classes finales, car vous spécifiez ici les chemins d’accès aux documents originaux et aux fichiers index, ainsi que le mot recherché.

  • String indexDir : insérez ici le chemin d’accès au dossier dans lequel vous voulez stocker les fichiers d’index.
  • String dataDir : à ce stade, le code source attend le chemin d’accès au dossier dans lequel sont stockés les documents à rechercher.
  • tester.search : entrez votre terme de recherche ici.

Puisque les trois cas sont des chaînes de caractères, vous devez mettre les expressions entre guillemets. Sous Windows aussi, vous utilisez des barres obliques normales au lieu des barres obliques inverses pour les chemins.

Pour tester le programme, copiez quelques fichiers en clair dans le répertoire spécifié comme dataDir. Assurez-vous que les extensions de fichier sont « .txt » . Vous pouvez maintenant démarrer le programme, dans Eclipse par exemple, cliquez sur la flèche verte dans la barre de menu.

Note

Le code de programme présenté n’est qu’un projet de démonstration pour montrer comment Lucene fonctionne. Par exemple, il manque une interface utilisateur graphique dans ce programme : vous devez entrer le terme de recherche directement dans le code source et le résultat n’est disponible que via la console.

Lucene Query Syntax

Les moteurs de recherche, même ceux que vous connaissez sur le Web, ne permettent généralement pas uniquement la recherche d’un seul terme. Avec certaines méthodes, vous pouvez en effet enchaîner des termes, rechercher des phrases ou exclure des mots de manière individuelle. Bien entendu, Apache Lucene offre également ces possibilités : avec Lucene Query Syntax, vous pouvez rechercher des expressions complexes, même dans plusieurs champs.

  • Single Term : entrez un terme simple tel quel. Contrairement à Google et les autres moteurs de recherche célèbres, Lucene suppose que vous savez comment le terme est correctement écrit. Si vous faites une faute d’orthographe, vous obtiendrez alors un résultat négatif. Exemple : voiture
  • Phrase : les phrases sont des séquences de mots définies. Ce ne sont pas seulement les termes individuels de la phrase qui sont déterminants, mais aussi l’ordre dans lequel ils apparaissent. Exemple : "Ma voiture est rouge".
  • Wildcard Searches : ils remplacent un ou plusieurs caractères dans votre requête de recherche. Les caractères génériques peuvent être utilisés à la fin et au milieu d’un terme, mais pas au début.
    • ? : Le point d’interrogation remplace exactement un caractère. Exemple : Au?o
    • * : L’astérisque ne remplace aucun caractère ou un nombre infini de caractères. Par exemple, d’autres formes d’un terme peuvent également être recherchées, telles que le pluriel. Exemple : Auto*
  • Regular Expression Searches : vous pouvez utiliser des expressions régulières pour rechercher plusieurs termes en même temps, dont certains ont des similitudes et d’autres diffèrent les uns des autres. Contrairement aux caractères génériques, vous définissez exactement les différences à prendre en compte. Pour ce faire, vous utilisez des barres obliques et des crochets. Exemple : /[MS]ein/
  • Fuzzy Searches : vous effectuez une recherche vague si, par exemple, vous voulez avoir une tolérance aux erreurs. Vous utilisez la distance Damerau-Levenshtein (une formule qui évalue les similitudes) pour déterminer l’ampleur de l’écart. Pour ce faire, utilisez le tilde. Des distances de 0 à 2 sont autorisées. Exemple : Auto~1
  • Proximity Searches : même si vous voulez permettre une approximation pour les phrases, utilisez le tilde. Par exemple, vous pouvez spécifier que vous voulez rechercher deux termes même s’il y a 5 autres mots entre eux. Exemple : "Auto rouge"~5
  • Range Searches : dans ce type de requête, vous recherchez entre deux termes dans une zone spécifique. Bien qu’une telle recherche n’ait guère de sens pour le contenu général d’un document, elle peut s’avérer très utile dans le traitement de certains domaines tels que les auteurs ou les titres. Le tri s’effectue selon un ordre lexicographique. Pendant que vous clarifiez une zone inclusive avec des crochets, vous utilisez des crochets bouclés pour exclure la zone déterminée par les deux termes de recherche de la requête. Utilisez TO pour délimiter les deux termes. Exemple : [Allende TO Borges] ou {Byron TO Shelley}, respectivement
  • Boosting : Lucene vous permet de donner des termes ou des expressions plus pertinentes que les autres dans votre recherche. Ceci vous permet d’influencer le tri des résultats. Vous définissez le facteur d’amplification avec le circonflexe suivie d’une valeur. Exemple : Auto^2 rouge
  • Boolean Operators : vous utilisez des opérateurs logiques pour créer des connexions entre les termes d’une requête de recherche. Les opérateurs doivent toujours être écrits en majuscules afin que Lucene ne les évalue pas comme des termes de recherche normaux.
    • AND : avec l’esperluette, les deux termes doivent apparaitre dans le document pour qu’un résultat apparaisse. Vous pouvez aussi utiliser deux esperluettes consécutives au lieu de l’expression. Exemple : Voiture && rouge
    • OR : le lien « ou » est le cas par défaut lorsque vous insérez simplement deux termes l’un après l’autre. L’un des deux termes doit apparaitre, mais il également possible que les deux soient contenus dans le même document. Vous créez la liaison ou avec OR, || ou en ne saisissant aucun opérateur. Exemple : Voiture rouge
    • + : avec le signe « plus », vous établissez un certain cas du lien « ou ». Si vous placez le caractère directement devant un mot, ce terme doit apparaitre, tandis que l’autre est facultatif. Exemple : +Voiture rouge
    • NOT : le lien « non » exclut certains termes ou expressions de la recherche. Vous pouvez remplacer l’opérateur par un point d’exclamation ou placer un signe moins juste avant le terme à exclure. Vous ne pouvez pas utiliser l’opérateur NOT avec un seul terme ou une seule expression. Exemple : Voiture rouge -bleu
  • Grouping : les parenthèses peuvent être utilisées pour regrouper les termes dans les requêtes de recherche. Pour créer des entrées plus complexes, par exemple, vous devez lier un terme à l’un des deux termes suivants : Voiture ET (rouge OU bleu).
  • Escaping Special Characters : pour utiliser les caractères qui peuvent être utilisés pour la Lucene Query Syntax dans les termes de recherche, combinez-les avec une barre oblique inverse. Par exemple, vous pouvez inclure un point d’interrogation dans une requête de recherche sans que l’analyseur ne l’interprète comme un caractère de remplacement : "Où est ma voiture\?"

Apache Lucene : avantages et inconvénients

Lucene est un outil puissant pour établir une fonction de recherche sur le Web, dans des archives ou sur des applications. Les adeptes de Lucene apprécient de pouvoir construire un moteur de recherche très rapide grâce à l’indexation, qui peut également être adapté dans les moindres détails à leurs propres besoins. Puisqu’il s’agit d’un projet open source, Lucene n’est pas seulement disponible gratuitement, mais il est aussi constamment développé par une grande communauté.

Vous pouvez donc l’utiliser non seulement en Java mais aussi en PHP, Python et autres langages de programmation. Et cela conduit aussi au seul inconvénient : la connaissance de la programmation est absolument nécessaire. La recherche plein texte n’est donc pas la bonne chose pour tout le monde. Si vous n’avez besoin que d’une fonction de recherche pour votre site web, vous êtes mieux servi par d’autres offres.

Avantages Inconvénients
Disponible pour différents langages de programmation Nécessite des connaissances en programmation
Open Source  
Rapide et léger  
Ranked Searching  
Requêtes de recherche complexes possibles  
   
Cet article vous a-t-il été utile ?
Page top