Une fois qu'il a fini de télécharger le tout, exécutez la commande suivante
grep -i armstrong test1Tp2/download_* | cut -d: -f1
Vous devrez voir quelques noms de fichiers, par exemple
test1Tp2/download_10.txt
test1Tp2/download_11.txt
test1Tp2/download_9.txt
et si vous faites less pour un d'entre eux
au hasard, vous allez retrouver du texte qui mentionne le nom du cycliste (l'outil cut permet de séparer
des mots sur chaque ligne, ici selon les deux points, et on affiche ainsi seulement les noms des fichiers)
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
- construction d'un objet IndexWriter qui créera et écrira l'index
- pour chaque document à indexer:
- construction d'un objet Document (classe définie par Lucene)
- pour chaque champ "d'indexation" (contenu, url, id, etc.)
- construction d'un objet Field (classe définie par Lucene)
- rajout de cet objet Field à l'objet Document (méthode add())
- rajout de l'objet Document (ainsi créé et muni de ses champs) à l'IndexWriter
créé au tout début ((méthode addDocument())
- 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
- construction d'un objet IndexSearcher qui fera la recherche dans l'index créé comme ci-dessus
- construction d'un objet Query pour représenter la requête souhaitée
- construction d'un objet TopScoreDocCollector qui récupérera les résultats de la recherche
dans l'ordre décroissant de leur score
- recherche proprement dite (méthode IndexSearcher.search() qui prend la requête et le collecteur
- 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
- Allez dans le shell dans le répertoire test1Tp2 et regarder les fichiers ainsi que le
contenu de crawl.txt. Vous allez y voir quelque chose comme
http://localhost:8123/data/indexClean.html 0
http://localhost:8123/data/biz-01.html 1
...
et le fichier download_1.txt par exemple contiendra bien ce qu'il y a dans biz-01.html, mais
juste le texte brut, sans balises html.
À 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)
- Suivre pas à pas le constructeur (déjà écrit), et écrire sur
une feuille de papier la marche des choses.
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();
}
}
- Écrivez le corps de la méthode readTextFile(String
fullPathInputTextFile)
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();
}
- Écrivez le corps de la méthode setupDocIds(String fullPathInputListFile)
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
}
- Écrivez le corps de la méthode gatherAndIndexDocs(String path)
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)
- Écrivez le corps du constructeur SimpleLuceneSearcher(), tout en gardant le fichier
index/SimpleLuceneIndex.java ouvert également, pour référence
class SimpleLuceneSearcher {
private final String searchFieldName = "content";
private final String docIdFieldName = "id";
private final String docUrlFieldName = "url";
private final String docTitleFieldName = "title";
private final String docLinksFieldName = "outlinks";
public SimpleLuceneSearcher(String indexPath, String queryString,
String maxHitsString)
throws Throwable {
int maxHits = Integer . valueOf(maxHitsString);
IndexSearcher indexSearcher = new IndexSearcher
(FSDirectory.open(new File(indexPath)));
//TRAVAIL A FAIRE
// declarez un objet de type TopScoreDocCollector, a obtenir
// avec la methode create() (qui demande bien le tri par
// score entre autres)
// declarez un objet de type Query, a obtenir avec la methode
// parse() d'un nouveau QueryParser
//// ce nouveau QueryParser est a construire avec deux
//// arguments:
//// . le searchFieldName (donc le nom du champ de
//// recherche, c'est-a-dire celui
//// qui a ete indexe auparavant
//// (on l'a demande quand on avait)
//// fait new Field en mentionnant
//// Field.Index.ANALYZED dans
//// index/SimpleLuceneIndex.java
//// dans gatherAndIndexDocs())
//// . l'analyseur standard (comme celui utilise dans le
//// constructeur de
//// index/SimpleLuceneIndex.java)
// appelez la methode search() pour l'IndexSearcher, avec
// l'objet de type Query et le collecteur ainsi construits.
// déclarez un objet ScoreDoc[] hits et mettez dedans
// ce que renvoie .topDocs().scoreDocs, appele pour l'objet
// collecteur -- autrement dit, le resultat de la recherche
//// regardez dans le fichier approprie pour trouver la methode
//// topDocs() (indication: regardez qui est la classe parent
//// de notre collecteur)
//// regardez egalement dans fichier definissant la classe
//// TopDocs, pour trouver le membre scoreDocs
// affichez d'abord, par courtoisie, le nombre de documents
// ainsi trouves (hits.length)
// pour chaque element de hits, mettons d'indice kHit (donc de
// (0 a hits.length - 1)
//// recuperer l'entier luceneDocId depuis la donnee-membre
//// hits[kHit].doc;
//// recuperer le score dans un String luceneScore depuis la
//// donnee-membre hits[kHit].score, avec String.valueOf()
//// recuperer le Document luceneDoc en appelant
//// indexSearcher.doc(luceneDocId);
//// recuperer le titre du document (ici ce sera directement
//// un String) avec luceneDoc.get(docTitleFieldName)
//// afficher les resultats, une ligne par document:
//// numero d'ordre du document
//// url du document
//// score du document
//// titre du document
// fin boucle pour
}
}
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
- Compilez le tout (avec make) pour obtenir le bon index.jar
4. Faire tourner le programme d'indexation et recherche
Travail à faire dans le shell
- Assurez-vous d'avoir toujours en place le résultat du crawling dans test1Tp2
- Lancez la construction de l'index avec la commande
java -jar index.jar index test1Tp2 crawl.txt
(rappel: le fichier crawl.txt a été crée par le crawler et contient
la liste des ids).