|Home|      <<Prev<<      <-Back--      >>Next>>

Data mining : indexation et recherche


1. Intro -- généralités sur l'indexation et la recherche de documents

Dans le TP précédent nous avons vu comment récupérer des documents depuis le web à l'aide d'un web crawler. Une fois construite une telle collection de documents "en local", il est util de pouvoir chercher dedans ceux qui nous intéressent, et si possible d'obtenir ces résultats rapidement.
Une solution extrêmement simpliste (mais qui peut néanmoins être parfois très utile, par exemple si on est "pris de court") sous Unix est offerte par l'outil grep.
Travail à faire
L'outil grep (ou tout autre équivalent de recherche de sous-chaîne de caractères) se trouve néanmoins fortement limité lorsque les documents dans lesquels on cherche sont complexes, et qu'on fait des requêtes un peu plus élaborées.
Les solutions actuelles pour ces tâches passent par la construction d'une véritable base de données -- on l'appellera un index -- des documents qui sont d'abord analysés lexicalement (en anglais tokenized), avec leurs mots en forme canonique (en anglais, stemming désigne l'extraction de formes de base des mots, l'élimination de mots de liaison (prépositions, conjonctions, articles, etc.), etc.), dont on peut enregistrer des synonymes, et ainsi de suite.
Il existe déjà plusieurs bibliothèques de programmes offrant une partie ou toutes ces possibilités. Dans ce TP nous allons voir comment nous servir de la bibliothèque Lucene, en Java.

2. Bibliothèque Lucene

2.1 Présentation

La bibliothèque Lucene fait partie des projets Apache, et pour l'illustration des concepts de ce TP nous disposons de tout son code source (dans org/apache/lucene) de sa version 2.9.4. Regardez attentivement l'API de Lucene 2.9.4.
Les étapes nécessaires pour commencer à s'en servir avec par exemple les résultats du crawling (du TP précédent) sont les suivantes :

A. Construction d'un index Lucene

  1. construction d'un objet IndexWriter qui créera et écrira l'index
  2. pour chaque document à indexer:
    1. construction d'un objet Document (classe définie par Lucene)
    2. pour chaque champ "d'indexation" (contenu, url, id, etc.)
      1. construction d'un objet Field (classe définie par Lucene)
      2. rajout de cet objet Field à l'objet Document (méthode add())
    3. rajout de l'objet Document (ainsi créé et muni de ses champs) à l'IndexWriter créé au tout début ((méthode addDocument())
  3. optimisation et clôture de l'IndexWriter (méthodes avec ces noms -- qui finissent donc par écrira l'index (sur disque).

B. Recherche de documents à l'aide de cet index

  1. construction d'un objet IndexSearcher qui fera la recherche dans l'index créé comme ci-dessus
  2. construction d'un objet Query pour représenter la requête souhaitée
  3. construction d'un objet TopScoreDocCollector qui récupérera les résultats de la recherche dans l'ordre décroissant de leur score
  4. recherche proprement dite (méthode IndexSearcher.search() qui prend la requête et le collecteur
  5. affichage de résultats, en parcourant un tableau ScoreDoc[] fourni par le TopScoreDocCollector (rempli comme au-dessus).

2.1 Intéraction avec nos fichiers et programmes

Comment allons-nous "donner" nos documents (téléchargés par le crawler) à Lucene pour qu'elle les indexe ?
Lors des étapes A.2.2.1 et A.2.2.2, parmi les champs ainsi construits il y en aura un qui aura le contenu en entier (en texte, donc un String) de chaque document. Toutefois, on ne demandera pas à Lucene de le stocker, seulement de l'analyser. Ce qu'elle stockera ce sera l'identificateur du document (et son url).
Le crawler génère un fichier, appellé crawl.txt (et qui est dans le même sous-répertoire oû se trouvent les download_<N>.txt). Ce fichier énumère tout simplement ces identificateurs (un par ligne), les précédant de l'URL d'où chaque fichier a été téléchargé.
Travail à faire

À l'étape A.2.2.1 de la construction d'un index Lucene, ce sera la valeur de cet id qui sera donnée pour le champ nommé "id" lors de sa construction en tant qu'objet Field. Supposons qu'à l'étape A.2.1 on a dit
  Document luceneDoc = new Document();
alors pour illustrer tout ceci, on aura donc comme étapes A.2.2.1 et A.2.2.2:
   luceneDoc . add(new Field("id",docId,Field.Store.YES,Field.Index.NO));
Bien entendu, il y aura également un
  luceneDoc . add(new Field("content",leContenuDuFichier,Field.Store.NO,Field.Index.ANALYZED,...)
comme on a dit plus haut. On observe donc que lors de la construction d'un objet Field on précise en fait si on souhaite que Lucene le stocke, si on souhaite que le Lucene l'analyse et indexe selon lui, etc.

3. Écriture d'un programme Java simple utilisant Lucene

Il faut compléter quelques classes Java pour aboutir à un index.jar qui nous permettra d'indexer les documents téléchargés par le crawler.

3.1 Construction de l'index

Travail à faire dans le fichier source index/SimpleLuceneIndex.java (en suivant les indications données en commentaire là-dedans)
  1. Suivre pas à pas le constructeur (déjà écrit), et écrire sur une feuille de papier la marche des choses.
  2. public class SimpleLuceneIndex {
        private List       docIdList;
        private IndexWriter        indexWriter;
    
        public SimpleLuceneIndex(String docPath, String inputListFile, String indexPath) throws Throwable {
    	docIdList             = null;
    	setupDocIds(docPath+"/"+inputListFile);
    	
    	FSDirectory  indexDir = FSDirectory.open (new File(indexPath));
    	indexWriter           = new IndexWriter(indexDir,new  StandardAnalyzer(), true);
    
    	gatherAndIndexDocs(docPath);
    	indexWriter . optimize();
    	indexWriter . close();
        }
    }
    
  3. Écrivez le corps de la méthode readTextFile(String fullPathInputTextFile)
  4.     private String readTextFile(String fullPathInputTextFile) throws Throwable {
    	// Cette methode ouvre le fichier dont on donne le chemin complet,
    	// le lit en entier et renvoie ainsi son contenu sous la forme d'une
    	// chaine de caracteres
    
    	// TRAVAIL A FAIRE
    
    	// Declarer et instancier un nouveau Scanner avec un nouveau
    	// FileInputStream pour lire le fichier fullPathInputTextFile
    
    	StringBuilder text    = new StringBuilder();
    
    	String        NL      = System.getProperty("line.separator");
    	String        sepStr  = "";
    	boolean       qSetSep = true; 
    
    	// Tant que la methode hasNextLine() du Scanner renvoie vrai
    
    	////// obtenir la ligne suivante du fichier (avec nextLine())
    	////// et la rajouter a l'objet de type StringBuilder, mais
     	////// precedee de sepStr (qui au debut sera vide)
    	////// 
    	////// si qSetSep est vrai, mettez-le a faux et mettez 
    	////// sepStr a NL (comme cela, sepStr aura tout le temps
    	////// a partir de la seconde iteration la valeur de NL
    	////// pour bien separer le contenu des lignes) 
    
            // fin tant que 
    
    	// fermez le scanner
    
    	return text . toString();
        }
    
  5. Écrivez le corps de la méthode setupDocIds(String fullPathInputListFile)
  6.     private void setupDocIds(String fullPathInputListFile) 
    	throws Throwable {
    	// cette methode doit remplir docIdList avec la liste des 
    	// identificateurs des documents a traiter, en lisant le
    	// fichier de chemin donne en argument, qui contient des
    	// paires (url,docId) 
    	
    	docIdList    = new ArrayList();
    	System.out.println("setupListOfDocIds(): Attempting to open " 
    			   + fullPathInputListFile);
    
    	// Declarer et instancier un nouveau Scanner avec un nouveau
    	// FileInputStream pour lire le fichier fullPathInputListFile
    
    	// Tant que la methode hasNextLine() du Scanner renvoie vrai
     
            ////// utiliser String.split("\\s+") pour obtenir les deux
    	////// elements (url et docId) de chaque ligne
    
    	////// rajouter le second element a la docIdList 
    	
    	// fin tant que
    
    	// fermer le scanner
        }
    
  7. Écrivez le corps de la méthode gatherAndIndexDocs(String path)
  8.     private void gatherAndIndexDocs(String path) throws Throwable {
    	for (String docId : docIdList) {
    	    String       textFile  = path + "/download_" + docId + ".txt";
    	    System.out.println("gatherAndIndexDocs(): Processing doc " + textFile);
    	    String       docText   = readTextFile(textFile);
    
    	    // declarer et instancier un objet de type String nomme docUrl
    	    // avec le resultat de la lecture du fichier de chemin
    	    //         path + "/url_"       + docId + ".txt"
    
    	    // declarer et instancier un objet de type String nomme docTitle 
    	    // avec le resultat de la lecture du fichier de chemin
    	    //         path + "/title_"       + docId + ".txt"
    
    	    // declarer et instancier un objet de type String nomme docOutLk
    	    // avec le resultat de la lecture du fichier de chemin
    	    //         path + "/outlinks_"       + docId + ".txt"
    
    	    // declarer et instancier un objet de type String nomme rankScore
    	    // avec le resultat de la lecture du fichier de chemin
    	    //         path + "/rankscore_"       + docId + ".txt"
    
    	    Document     luceneDoc = new Document();
    	    luceneDoc . add(new Field("content",   docText,  
    				      Field.Store.NO, 
    				      Field.Index.ANALYZED,  
    				      Field.TermVector.YES));
    	    luceneDoc . add(new Field("url",       docUrl,   
    				      Field.Store.YES, 
    				      Field.Index.NO));
    	    // rajouter un autre nouveau champ nomme "id" contenant 
    	    // le docId, demandant son stockage, et sans indexation 
    	    // (i.e. comme pour docUrl)
    
    	    // faire la meme chose pour docTitle
    
    	    // faire la meme chose pour docOutLk
    
    	    // rajouter un autre nouveau champ nomme "rankscore" 
    	    // contenant le rankScore, demandant son stockage, et
    	    // precisant pour  l'indexation la valeur
    	    //       Field.Index.NOT_ANALYZED
    
    	    // appeler la methode addDocument() pour indexWriter avec 
    	    // comme argument l'objet luceneDoc (qu'on vient donc de
    	    // configurer plus haut)
    	}
        }
    

3.2 Recherche dans l'index

Travail à faire dans le fichier source index/SimpleLuceneSearcher.java (en suivant les indications données en commentaire là-dedans)

3.3 Programme principal

Le fichier source index/IndexSearch.java contient le programme principal, qui regarde le premier argument de la ligne de commande et construit notre objet afférent.
Travail à faire dans le répertoire principal de ce tp

4. Faire tourner le programme d'indexation et recherche

Travail à faire dans le shell

Solution

Des explications et le code pour la solution se trouve ici.
L'archive avec la solution se trouve ici et une fois téléchargée, vous pouvez la déarchiver ainsi:
  zcat archSolTp2.tgz | tar xvf -
Ensuite, il faut faire make dans le répertoire principal là où vous l'avez téléchargée et déarchivée, et puis vous pouvez faire tourner le tout comme suit.
On lance le server java (comme au premier tp, donc e.g. java -jar server.jar 8123, dans un shell différent) et on exécute les commandes suivantes.
mkdir testTp2
java -jar crawler.jar 8123 indexAll.html  testTp2
java -jar index.jar index testTp2 crawl.txt 
java -jar index.jar search testTp2/luceneIndex armstrong 5