But : définir dans son projet Eclipse les dépendances vers les jars Lucene nécessaires sans référence à des chemins "en dur"
Étapes :
Les variables Eclipse sont définies au niveau du Workspace et utilisables ensuite dans tous les projets. Ceci permet de livrer des projets allégés et qui fonctionnent sur n'importe quel poste du moment que la variable en question est définir.
Rappel de l'objectif : Objectif : Permettre la recherche de mots et expressions dans "Voyage au bout de la nuit" avec une indexation au chapitre.
Note, les jars suivants de lucene ont été utilisés :
/* * Cours Java / POO * M2 Pro Ingénierie Multilingue * INALCO */ package fr.crim.a2012.saxigraph; import java.io.File; import java.io.IOException; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field.Store; import org.apache.lucene.document.IntField; import org.apache.lucene.document.TextField; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.index.IndexWriterConfig.OpenMode; import org.apache.lucene.store.FSDirectory; import org.apache.lucene.util.Version; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.SAXNotRecognizedException; import org.xml.sax.SAXNotSupportedException; import org.xml.sax.helpers.DefaultHandler; /** * Une spécialisation de la classe DefaultHandler indexe les chapitres * (balises <div>) d'un texte en TEI. * * @author Pierre DITTGEN */ public class ChapitreTeiLuceneIndexer extends DefaultHandler { private StringBuilder text = new StringBuilder(); private boolean dansBalise; private int noChapitre; private IndexWriter writer; /** * Constructeur * @param writer Indexwriter Lucene */ public ChapitreTeiLuceneIndexer(IndexWriter writer) { this.writer = writer; } /** * Méthode appelée au début de l'analyse du document */ @Override public void startDocument() { noChapitre = 0; } /** * Méthode appelée sur la lecture de contenu texte */ @Override public void characters(char[] ch, int start, int length) { if (dansBalise) { text.append(ch, start, length); } } /** * Méthode appelée sur une balise ouvrante */ @Override public void startElement(String uri, String localName, String qName, Attributes attrs) { if ("div".equals(localName)) { dansBalise = true; text.setLength(0); noChapitre++; } } /** * Méthode appelée sur une balise fermante */ @Override public void endElement(String uri, String localName, String qName) { if ("div".equals(localName)) { dansBalise = false; indexeChapitre(text.toString(), noChapitre); } } /** * Indexe le contenu texte d'un chapitre dans Lucene * @param contenu Le contenu texte du chapitre * @param numero Le numéro du chapitre */ private void indexeChapitre(String contenu, int numero) { // Création d'un nouveau document Lucene Document doc = new Document(); // Ajout de ses champs doc.add(new IntField("numero", numero, Store.YES)); doc.add(new TextField("contenu", contenu, Store.YES)); // Ajout dans l'index try { writer.addDocument(doc); } catch (IOException ioe) { System.err.println("Exception lors de l'écriture du document Lucene"); ioe.printStackTrace(); } } /** * Retourne le nombre de chapitres indexés */ public int getNbChapitres() { return noChapitre; } /** * Méthode principale * @param args paramètres de la ligne de commande (ignorés) * @throws SAXException En cas de XML non valide * @throws ParserConfigurationException Peu probable * @throws IOException Si le fichier XML n'est pas trouvé par exemple * @throws SAXNotSupportedException * @throws SAXNotRecognizedException */ public static void main(String[] args) throws IOException, SAXNotRecognizedException, SAXNotSupportedException, ParserConfigurationException { if (args.length != 1) { System.err.println("usage: ChapitreTeiLuceneIndexer fichiertei"); System.exit(1); } // Nom du fichier TEI à indexer passé en paramètre // de ligne de commande File teiFile = new File(args[0]); if (!teiFile.exists()) { System.err.println("Le fichier à indexer « "+teiFile+" » est introuvable"); System.exit(1); } // Ouverture de l'index Lucene Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_42); IndexWriterConfig iwc = new IndexWriterConfig(Version.LUCENE_42, analyzer); iwc.setOpenMode(OpenMode.CREATE); IndexWriter writer = new IndexWriter(FSDirectory.open(new File("indexes")), iwc); // Création du "DocumentHandler" ChapitreTeiLuceneIndexer indexer = new ChapitreTeiLuceneIndexer(writer); // On lance l'analyse SAXParserFactory spf = SAXParserFactory.newInstance(); spf.setNamespaceAware(true); spf.setValidating(false); spf.setFeature("http://xml.org/sax/features/validation", false); try { long start = System.currentTimeMillis(); SAXParser saxParser = spf.newSAXParser(); saxParser.parse(teiFile, indexer); long delay = System.currentTimeMillis() - start; int nbChapitres = indexer.getNbChapitres(); System.out.println(nbChapitres + " chapitre(s) indexé(s) en " + delay + "ms."); } catch (SAXException se) { System.err.println("Erreur d'analyse XML"); se.printStackTrace(); } finally { writer.close(); } } }
package fr.crim.a2012.saxigraph; import java.io.File; import java.io.IOException; import java.util.Scanner; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.document.Document; 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.search.highlight.Highlighter; import org.apache.lucene.search.highlight.InvalidTokenOffsetsException; import org.apache.lucene.search.highlight.QueryScorer; import org.apache.lucene.search.highlight.SimpleHTMLFormatter; import org.apache.lucene.search.highlight.TextFragment; import org.apache.lucene.search.highlight.TokenSources; import org.apache.lucene.store.FSDirectory; import org.apache.lucene.util.Version; /** * Une classe de recherche dans les chapitres d'un document TEI * * @author Pierre DITTGEN * */ public class ChapitreTeiLuceneSearcher { /** * À l'instar de la fonction normalize-space en XSL, retire les espaces * en début et fin de chaine et remplace les espaces multiples en une seule * espace en cours de chaîne * @param str La chaîne à traiter */ private static String normalizeSpace(String str) { return str.replaceAll("\\s+", " ").trim(); } /** * @param args Paramètres de la ligne de commande * @throws IOException * @throws ParseException * @throws InvalidTokenOffsetsException */ public static void main(String[] args) throws IOException, InvalidTokenOffsetsException { // Ouverture de l'index Lucene File indexDir = new File("indexes"); IndexReader indexReader = DirectoryReader.open(FSDirectory.open(indexDir)); Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_42); IndexSearcher searcher = new IndexSearcher(indexReader); // L'analyseur de requête qui s'applique par défaut sur le champ "contenu" QueryParser parser = new QueryParser(Version.LUCENE_42, "contenu", analyzer); // La requête Query query; // Pour le "surlignage" des résultats SimpleHTMLFormatter htmlFormatter = new SimpleHTMLFormatter("**", "**"); TokenStream tokenStream; TextFragment[] frag; // Invite de commande Scanner in = new Scanner(System.in, "UTF-8"); while (true) { System.out.print("\n> "); String line = in.nextLine(); if ("quit".equals(line)) { System.out.println("Bye!"); break; } // Requête Long start = System.currentTimeMillis(); String q = line; try { query = parser.parse(q); } catch (ParseException e) { System.out.println("Requête invalide !\n"); continue; } TopDocs hits = searcher.search(query, 10000); int totalHits = hits.totalHits; Long delay = System.currentTimeMillis() - start; // Non trouvé :-( if (totalHits == 0) { System.out.println("L'expression recherchée n'a pas été trouvée" + " (" + delay + "ms)"); continue; } // Trouvé ! System.out.println("L'expression recherchée a été trouvée dans " +totalHits+" chapitres (en "+delay+"ms)"); // On itère sur les résultats Highlighter highlighter = new Highlighter(htmlFormatter, new QueryScorer(query)); for (int i=0; i<totalHits; i++) { // Récupération du document ScoreDoc sdoc = hits.scoreDocs[i]; int docid = sdoc.doc; Document doc = searcher.doc(docid); String text = doc.get("contenu"); // Affichage "- chapitre 24" System.out.println("- chapitre " + doc.get("numero")); // Affichage des extraits : tokenStream = TokenSources.getAnyTokenStream(indexReader, docid, "contenu", analyzer); frag = highlighter.getBestTextFragments(tokenStream, text, false, 10); for (int j = 0; j < frag.length; j++) { if ((frag[j] != null) && (frag[j].getScore() > 0)) { System.out.println("... " + normalizeSpace(frag[j].toString()) + " ..."); } } } } } }
Classe Query
Objectif : Indexer le texte en TEI au niveau <p> ou <said> (1 document Lucene = le contenu d'une balise <p> ou d'une balise <said>) avec les champs suivants :
Tester différentes recherches sur les indexes générés
Indexer le texte comme spécifié (on peut partir de la classe ChapitreTeiIndexer)
Écrire une classe pour tester les recherches. On pourra partir du squelette suivant :
package fr.crim.a2012.saxigraph; import java.io.File; import java.io.IOException; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.Term; import org.apache.lucene.queryparser.classic.ParseException; import org.apache.lucene.queryparser.classic.QueryParser; import org.apache.lucene.search.BooleanClause.Occur; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.NumericRangeQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.Sort; import org.apache.lucene.search.SortField; import org.apache.lucene.search.SortField.Type; import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.TopDocs; import org.apache.lucene.search.highlight.InvalidTokenOffsetsException; import org.apache.lucene.store.FSDirectory; import org.apache.lucene.util.Version; /** * Une classe de recherche dans les chapitres d'un document TEI * * @author Pierre DITTGEN */ public class ChapitrePlusTeiLuceneSearcher { private static IndexSearcher searcher; private static DirectoryReader reader; /** * À l'instar de la fonction normalize-space en XSL, retire les espaces * en début et fin de chaine et remplace les espaces multiples en une seule * espace en cours de chaîne * @param str La chaîne à traiter */ private static String normalizeSpace(String str) { return str.replaceAll("\\s+", " ").trim(); } /** * Lance la requête passée en paramètre et affiche le nombre de résultats * ainsi que les 10 premiers résultats * * @param title Titre de la requête * @param query Requête Lucene * @throws IOException */ private static void runQuery(String title, Query query) throws IOException { System.out.println("\nRequête : [" + title + "]"); long start = System.currentTimeMillis(); TopDocs hits = searcher.search(query, 10000, new Sort(new SortField("numero", Type.INT))); int totalHits = hits.totalHits; long delay = System.currentTimeMillis() - start; System.out.println(totalHits + " résultat(s) en " + delay + "ms."); int nb = Math.min(10, totalHits); for (int i=0; i<nb; i++) { Document doc = reader.document(hits.scoreDocs[i].doc); String text = normalizeSpace(doc.get("contenu")); int noChapitre = Integer.parseInt(doc.get("numero")); System.out.println((i+1)+". ch"+noChapitre+" "+truncate(text, 70)); } } /** * Renvoie les n premiers caractères d'un texte passé en paramètre en * accolant "[...]" en fin de chaîne * @param str * @param maxsize */ private static String truncate(String str, int maxsize) { if (str.length() < maxsize) { return str; } str = str.substring(0, maxsize); // On essaie de couper au mot int wsIdx = str.lastIndexOf(' '); if (wsIdx != -1) { str = str.substring(0, wsIdx+1); } return str + "[...]"; } /** * @param args Paramètres de la ligne de commande * @throws IOException * @throws ParseException * @throws InvalidTokenOffsetsException */ public static void main(String[] args) throws IOException, ParseException, InvalidTokenOffsetsException { // Ouverture de l'index Lucene et création du searcher File indexDir = new File("indexes"); reader = DirectoryReader.open(FSDirectory.open(indexDir)); Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_42); searcher = new IndexSearcher(reader); // Pour l'analyse des requêtes QueryParser queryParser = new QueryParser(Version.LUCENE_42, "contenu", analyzer); // Recherche n°1 : "mort" dans les balises <p> // 86 résultats BooleanQuery query1 = new BooleanQuery(); // ... runQuery("Mort dans les balises <p>", query1); // Recherche n°2 : "mort" dans les balises <said> // 8 résultats BooleanQuery query2 = new BooleanQuery(); // ... runQuery("Mort dans les balises <said>", query2); // Recherche n°3 : copain ou mère dans les balises <said> // 36 résultats BooleanQuery query3 = new BooleanQuery(); // ... runQuery("copain ou mère dans les balises <said>", query3); // Recherche n°4 : copain et mère dans les balises <said> // 1 résultat BooleanQuery query4 = new BooleanQuery(); // ... runQuery("Copain et mère dans les balises <said>", query4); // Recherche n°5 : voyage dans les chapitres 10 à 23 // 9 résultats BooleanQuery query5 = new BooleanQuery(); // ... runQuery("voyage dans les chapitre 10 à 23", query5); // Recherche n°6 : voyage dans les chapitres 12, 15 et 20 // 6 résultats BooleanQuery query6 = new BooleanQuery(); // ... runQuery("voyage dans les chapitres 12, 15 et 20", query6); // Recherche n°7 : vie dans les balises <p> des chapitres pairs // 83 résultats BooleanQuery query7 = new BooleanQuery(); // ... runQuery("vie dans les chapitres pairs", query7); } }
Notes :
La classe TermQuery permet d'effectuer une recherche de terme dans un champ
TermQuery q = new TermQuery(new Term("nomduchamp","motachercher"));
La classe BooleanQuery permet de composer une requête en assemblant des sous-requêtes avec des opérateurs booléens (OR : Occur.SHOULD, AND : Occur.MUST, NOT : Occur.MUST_NOT)
// Création d'une requête booléenne avec des ET BooleanQuery bq = new BooleanQuery(); bq.add(q1, Occur.MUST); ... bq.add(qn, Occur.MUST);
Une booleanQuery peut elle-même être composée de booleanQuery
Wiki: 2012-2013
Wiki: 2013-04-24
Anonymous
Pour la prochaine séance [17/04/2013]
Terminer l'exercice en implémentant les 7 requêtes prévues. On ne s'interdira pas de d'utiliser d'autres types de requêtes que les TermQuery et BooleanQuery. Il n'est pas interdit non plus d'ajouter des champs d'indexation qui pourrait simplifier les requêtes ensuite.
Merci d'envoyer votre projet complet (arborescence de fichier + fichiers .classpath et .project à la racine). Pensez à référencer vos jars Lucene en utilisant la variable LUCENE_42.