Lucene: Una libreria efficiente per ricerche di testo Roberto Navigli, Fulvio D’Antonio Lo scenario Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 2 Lucene 3.1 • E’ una API (Application Programming Interface) – Estremamente efficiente e scalabile • Mette a disposizione le classi fondamentali per costruire un indicizzatore e un motore di ricerca • 100% Java, nessuna dipendenza, nessun config file • Fa parte del progetto Apache – Disponibile online: http://lucene.apache.org • Utilizzato da: – – – – – – – – Wikipedia, Wikimedia, ecc. Technorati Monster.com TheServerSide SourceForge Eclipse Beagle Molti progetti commerciali Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 3 Ancora su Lucene • Utilizzato in: – TREC (Text Retrieval Conference) – Sistemi di Document Retrieval a livello enterprise – Parte di motori web/basi di dati • Utilizzato da accademici per grandi progetti: – MIT (AI Lab) – Progetto Know-It-All Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 4 Lucene in breve my_doc1: Oggi ho assistito a una lezione su Lucene. Documento sorgente Lezione & Lucene Hits Hits Hits Document nome: my_doc1 term: oggi, ho, assistito, a, una,lezione, su, Lucene Query (risultati (risultati della ricerca) (risultati della ricerca) della ricerca) search() addDocument() IndexWriter IndexSearcher optimize() close() Indice Lucene Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 5 Conversione in Testo • Normalmente, è necessario convertire le risorse da indicizzare in formato testo se esse sono specificate mediante altri formati (es. Word, PDF, HTML, XML, ecc.) • Utilità di conversione: – PDFBox (PDF) – Jakarta POI (DOC, RTF, ecc.) – JTidy (HTML) Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 6 Documenti in Lucene • In Lucene Un documento è una unità di indicizzazione e ricerca (differente dal documento inteso come file) • Un documento è un insieme di campi • Ogni campo ha un nome e un valore testuale • Decidiamo noi quali informazioni inserire nel documento! JAVA Code Document d = new Document(); d.add(new Field(nome_campo, valore, storeable, indexable)); String Field.Store Lucene: API efficienti per ricerche indicizzate Field.Index 21/12/2015 Pagina 7 Campi di un Documento • Un campo di un documento specifica: – Il nome del campo (stringa) – Il valore testuale del campo (stringa) – Se il valore del campo deve essere memorizzato nel documento • Necessario per recuperare il valore del campo dai documenti che rispondono a una interrogazione – Se il valore del campo deve essere indicizzato • Necessario per la ricerca • E’ possibile richiedere l’analisi del testo prima dell’indicizzazione (ANALYZED, deprecato il vecchio TOKENIZED) JAVA Code Field f1 = new Field(“name”, “my_doc1”, Field.Store.YES, Field.Index.NO); Field f2 = new Field(“term”, “Lucene”, Field.Store.YES, Field.Index.NOT_ANALYZED); Field f3 = new Field(“term”, “Oggi ho assistito”, Field.Store.YES, Field.Index.ANALYZED); Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 8 Esempi di Campi Nome Field.Store Field.Index Telefono YES NOT_ANALYZED URL YES NOT_ANALYZED Data YES NOT_ANALYZED DocumentType YES NO DocumentPath NO NOT_ANALYZED DocumentTitle YES ANALYZED Text YES ANALYZED Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 9 Creazione dell’indice 1. Crea un oggetto di tipo IndexWriter: – – Esistono molti tipi diversi di costruttori a seconda delle esigenze. Uno dei più usati è: JAVA Code IndexWriter writer = new IndexWriter(indexDir, analizzatore, bCrea, maxFldLen); 1. Crea i documenti e aggiungili all’indice JAVA Code Document d = new Document(); d.add(new Field(“term”, “assistito”, Field.Store.NO, Field.Index.ANALYZED)); writer.addDocument(d); 2. Ottimizza e chiudi l’indice (fondamentale!) JAVA Code writer.optimize() writer.close(); Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 10 La classe Directory • Usata per la memorizzazione di indici • A livello astratti n oggetto Directory è una lista di file. – Random Access sia in lettura che scrittura – Aggiunta, cancellazione aggionamento di file • Lucene implementa diversi tipi di Directory: – RAM-based indices; – Indic memorizzati in database, via JDBC; – Indici su file; Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 11 Esempi di directory • In memoria RAM: – RAMDirectory dir= new RAMDirectory(); – Utile per creare indici “al volo”, l’indice non è persistente (viene perso una volta che l’esecuzione del programma è terminata) • Su file – FSDirectory dir = FSDirectory.open(new File("temp")); – Memorizzazione persistente – i dati sono disponibili anche dopo che l’esecuzione del programma è terminata Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 12 La classe Analyzer • Si occupa dell’estrazione di termini a partire da un testo in input – Istanzia un tokenizer, che suddivide il flusso di caratteri in token – Applica dei filtri ai singoli token • Può eliminare stopwords (parole frequenti senza contenuto informativo, es. a, the, of, in, etc.) • Può effettuare lo stemming (andare, andò -> and; bellissimo, bello, bellina -> bell) JAVA Code Analyzer analizzatore = new StandardAnalyzer(); IndexWriter writer = new IndexWriter(indexDir, analizzatore, bCrea, MaxFieldLength.UNLIMITED); Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 13 Analizzatori in Lucene • Analizzatori – WhitespaceAnalyzer: estrae i token separati da spazi (non modifica maiuscole/minuscole) – SimpleAnalyzer: tokenizza sulla base di spazi e caratteri speciali; applica il minuscolo ai token – StopAnalyzer: come SimpleAnalyzer ma elimina le stopwords (the, an, a, ecc.) – StandardAnalyzer: è il più completo (Whitespace+Stop+altri trattamenti) – SnowballAnalyzer: effettua anche lo stemming Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 14 Compatibilità con precedenti versioni di Lucene • Lucene è un progetto costantemente mantenuto e sviluppato • Occasionalmente, un cambio di versione, può causare incompatibilità all’interno di progetti pre-esistent – Es: il cambio da versione 2.4 a 3.1 di Lucene • Per ragioni di backward compatibility alcuni oggetti vanno istanziati specificando un campo di tipo “Version” – Es: • StopAnalyzer analyzer = new StopAnalyzer(Version.LUCENE_30); • Utilizzare, laddove non vi siano esigenze particolari, la versione LUCENE_30 Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 15 Esempi di analisi • The quick brown fox jumped over the lazy dogs • XY&Z Corporation – [email protected] Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 16 Analizzatori Personalizzati • E’ possibile creare il proprio analizzatore estendendo la classe Analyzer: JAVA Code class MyAnalyzer extends Analyzer { private Set stopWords = StopFilter.makeStopSet(StopAnalyzer.ENGLISH_STOP_WORDS); public TokenStream tokenStream(String fieldName, Reader reader) { TokenStream ts = new StandardTokenizer(reader); ts = new StandardFilter(ts); ts = new LowerCaseFilter(ts); ts = new StopFilter(ts, stopWords); return ts; } } Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 17 La classe Document • Rappresenta un documento virtuale della collezione – Può essere associato a qualunque oggetto informativo (email, pagina web, file, frammento di testo, ecc.) che si vuole recuperare in fase di ricerca • Virtuale perché il file sorgente è irrilevante per Lucene • E’ un insieme di campi – Un campo può rappresentare il contenuto del documento stesso o i meta-dati associati al documento JAVA Code String valore = d.get(“name”); String[] valori = d.getValues(“term”); List campi = d.getFields(); Field campo = getField(“name”); campi = d.getFields(“term”); d.removeField(“name”); d.removeFields(“term”); Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 18 Indicizzazione: Ricapitoliamo le Classi Fondamentali • IndexWriter – Si occupa del processo di indicizzazione • Analyzer – Si occupa dell’analisi dei documenti da indicizzare • Document – Rappresenta un singolo documento della collezione • Field – Rappresenta un campo del documento della collezione Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 19 Cercare in un Indice 1. Apri l’indice JAVA Code IndexSearcher is = new IndexSearcher(indexDir); 2. Crea la query JAVA Code QueryParser parser = new QueryParser(Version.LUCENE_30, “term”, analizzatore); Query q = parser.parse(“lezione”); 3. Effettua la ricerca JAVA Code ScoreDoc[] docs = searcher.search(q, <numHits>).scoreDocs; 4. Recupera i documenti JAVA Code for (ScoreDoc doc:docs) { Document d = is.doc(doc.doc); // ottiene il documento float score =doc.score; // punteggio del documento // ... } Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 20 Creare un Oggetto di Tipo Query • Tanti tipi di query, esempi di due modi differenti: 1. Effettuare il parsing di una query testuale (come in Google) JAVA Code QueryParser parser = new QueryParser(Version.LUCENE_30, “term”, analizzatore); Query q = parser.parse(“lezione AND Lucene”); 2. Creare l’oggetto costruendo l’espressione di ricerca istanziando e componendo classi che specializzano la classe Query JAVA Code BooleanQuery q = new BooleanQuery(); q.add(new TermQuery(new Term(“term”, “lezione”)), BooleanClause.Occur.MUST); q.add(new TermQuery(new Term(“term”, “Lucene”)), BooleanClause.Occur.MUST); • Si esplicita la query costruendola in modo programmatico • Non effettua alcuna analisi del testo della query (al contrario di QueryParser, che utilizza l’analizzatore in input) Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 21 Esempi di Query con QueryParser Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 22 Specializzazioni della classe Query • Per costruire un oggetto Query componendo in un’espressione istanze delle sue sottoclassi, ad es.: • TermQuery (cerca un termine) campo valore JAVA Code Query q = new TermQuery(new Term(“term”, “lezione”)); • ConstantScoreRangeQuery (cerca in un intervallo) campo limite inf. JAVA Code Query q = new ConstantScoreRangeQuery(“mese_pubblicazione”, “198805”, “198810”, true, true); limite sup. includi limite inf. e sup. • PrefixQuery (termini che iniziano con la stringa specificata) campo prefisso JAVA Code Query q = new PrefixQuery(new Term(“term”, “lez”)); Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 23 Pesatura dei Risultati di una Ricerca • Lucene calcola la rilevanza di un documento rispetto a una query – Usando una combinazione di modello booleano (per filtrare i documenti) e di Vector Space Model (per pesarli) – La pesatura è effettuata tenendo conto dei seguenti fattori: • tf(t): term frequency (il numero di termini che fanno match nel documento) • idf(t): inverse document frequency (in quanti documenti appare il termine rispetto al totale dei documenti?) • lengthNorm(t, d): numero di termini del campo di t che appaiono nel documento • coord: numero di termini che fanno match • http://lucene.apache.org/java/docs/scoring.html Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 24 Ricerca: Ricapitoliamo le Classi Fondamentali • IndexSearcher – Ha diversi metodi per la ricerca in un indice • Term – Unità di base per costruire un’interrogazione – Costituita da due elementi fondamentali: nome del campo e valore del campo • Query – E’ una classe astratta di cui esistono numerose classi concrete: TermQuery, BooleanQuery, PhraseQuery, RangeQuery, FilteredQuery, ecc. • Hits – E’ un contenitore di risultati (documenti) della ricerca con ranking associato Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 25 Operazioni sull’Indice: Aggiunta di Documenti • Aggiungere documenti all’indice: JAVA Code Document d = new Document(); d.add(new Field(“id”, “06”)); d.add(new Field(“name”, “Rome”, Field.Store.YES, Field.Index.NOT_ANALYZED)); d.add(new Field(“country”, “Italy”, Field.Store.YES, Field.Index.NO)); d.add(new Field(“text”, “Rome is the capital of Italy. Its river, the Tiber, etc...”, Field.Store.NO, Field.Index.TOKENIZED)); writer.addDocument(d); • Nota: – Documenti appartenenti allo stesso indice possono contenere campi differenti (questo permette di avere oggetti di tipo diverso indicizzati mediante lo stesso indice) JAVA Code Document d = new Document(); d.add(new Field(“id”, “0039”)); d.add(new Field(“name”, “Italy”, Field.Store.YES, Field.Index.NOT_ANALYZED)); d.add(new Field(“continent”, “Europe”, Field.Store.NO, Field.Index.NOT_ANALYZED)); writer.addDocument(d); Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 26 Operazioni sull’Indice: Eliminazione di Documenti (1) • Mediante la classe IndexReader: – es. eliminare il primo documento JAVA Code IndexReader reader = IndexReader.open(indexDir); reader.deleteDocument(0); – es. eliminare tutti i documenti aventi il campo city valorizzato con Amsterdam JAVA Code reader.deleteDocuments(new Term(“city”, “Amsterdam”)); – NOTA: per salvare le cancellazioni, è necessario chiudere l’IndexReader! JAVA Code reader.close(); Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 27 Operazioni sull’Indice: Eliminazione di Documenti (2) • Mediante la classe IndexWriter: – es. eliminare i documenti aventi il campo city valorizzato con Amsterdam: JAVA Code writer.deleteDocuments(new Term(“city”, “Amsterdam”)); – es. eliminare i documenti che rispondono all’interrogazione: JAVA Code writer.deleteDocuments(query); • E’ possibile aggiornare un documento (equivale a cancellare e aggiungere): JAVA Code writer.updateDocument(new Term(“city”, “Amsterdam”), newDoc); • NOTA: IndexReader non può essere utilizzato per cancellare se un IndexWriter è aperto sullo stesso indice Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 28 Span Query • Sono interrogazioni che forniscono informazioni riguardo alla posizione in cui un match ha avuto luogo all’interno di un documento • SpanTermQuery permette una interrogazione per termine (building block!) • SpanFirstQuery impone una posizione massima dell’occorrenza rispetto all’inizio di un campo • SpanNearQuery specifica span che devono essere uno “vicino” all’altro • SpanNotQuery, SpanOrQuery, SpanRegexQuery, ecc. Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 29 SpanTermQuery • E’ l’elemento di base di una SpanQuery: JAVA Code SpanTermQuery span = new SpanTermQuery(new Term(“term”, “antonio”)); Spans spans = span.getSpans(reader); while(spans.next()) { int docNumber = spans.doc(); Document doc = reader.document(docNumber); int primoToken = spans.start(); int ultimoToken = spans.end(); // e’ il primo token che segue il match // fa qualcosa con il documento } Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 30 SpanFirstQuery • Richiede che il match avvenga a distanza al più dist dal primo token del campo specificato JAVA Code SpanTermQuery span = new SpanTermQuery(new Term(“term”, “antonio”)); SpanFirstQuery first = new SpanFirstQuery(span, dist); Spans spans = first.getSpans(reader); while(spans.next()) { int docNumber = spans.doc(); Document doc = reader.document(docNumber); int primoToken = spans.start(); int ultimoToken = spans.end(); // e’ il primo token che segue il match // fa qualcosa con il documento } Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 31 SpanNearQuery • Effettua interrogazioni ponendo un limite di vicinanza per le varie clausole di tipo SpanQuery JAVA Code SpanTermQuery span1 = new SpanTermQuery(new Term(“term”, “antonio”)); SpanTermQuery span2 = new SpanTermQuery(new Term(“term”, “meucci”)); SpanQuery[] clauses = new SpanQuery[] { span1, span2 }; SpanNearQuery near = new SpanNearQuery(clauses, 2, true); Spans spans = near.getSpans(reader); distanza massima nell’ordine o no? while(spans.next()) { int docNumber = spans.doc(); Document doc = reader.document(docNumber); int primoToken = spans.start(); int ultimoToken = spans.end(); // e’ il primo token che segue il match // fa qualcosa con il documento } Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 32 Term Vector • In Lucene, è possibile rappresentare tutti i termini e i conteggi di occorrenze dei termini in uno specifico campo di un’istanza di Document: – (nomeCampo, (term1, termCount1), ..., (termn, termCountn)) • Queste informazioni vengono memorizzate in un campo solo se specificato esplicitamente: JAVA Code Field f = new Field(“term”, testo, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.YES); Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 33 Term Vector • Le opzioni sono: – Field.TermVector.NO • Non memorizza questa informazione – Field.TermVector.YES • Memorizza le coppie (termine, conteggio) – Field.TermVector.WITH_POSITIONS • Memorizza anche le posizioni dei token – Field.TermVector.WITH_OFFSETS • Memorizza anche le posizioni dei token al livello del carattere – Field.TermVector.WITH_POSITIONS_OFFSETS • Memorizza anche le posizioni dei token e al livello del carattere Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 34 Accedere ai Term Vector • Per accedere a un term vector, è necessario richiamare un apposito metodo della classe IndexReader: JAVA Code TermFreqVector tfv = reader.getTermFreqVector(docNumber, fieldName); TermFreqVector[] tfvs = reader.getTermFreqVectors(docNumber); String[] terms = tfv.getTerms(); int[] freqs = tfv.getTermFrequencies(); • Se è stata memorizzata anche l’informazione di posizione e/o offset, è possibile effettuare un cast alla classe TermPositionVector: JAVA Code TermPositionVector tpv = (TermPositionVector)tfv; For (int k = 0; k < terms.length; k++) { // lavora sul termine k-esimo int[] positions = tpv.getTermPositions(k); TermVectorOffsetInfo[] offsets = tpv.getOffsets(k); // ... } Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 35 Come ottenere il numero di un documento? • Dato il risultato di una interrogazione, è necessario chiamare il metodo id(): JAVA Trick Hits hits = is.search(q); for (int k = 0; k < hits.length(); k++) { int docNumber = hits.id(k); TermFreqVector tfv = reader.getTermFreqVector(docNumber, fieldName); // usa tfv... } Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 36 Perché utilizzare i TermFreqVector? • Permettono di espandere l’interrogazione originaria utilizzando termini dai documenti (Relevance Feedback) – L’utente o il sistema selezionano i documenti più rilevanti (es. i primi x documenti nella lista ordinata per punteggio) – Si ottengono i termini dai TermFreqVector di ciascun documento e si costruisce una nuova interrogazione – E’ possibile applicare un “boost” sulle frequenze dei termini Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 37 Boosting • Come aumentare l’importanza di un documento e/o di un campo in fase di indicizzazione? • Metodo setBoost() JAVA Code Document d = new Document(); Field f = new Field(“term”, “assistito”, Field.Store.NO, Field.Index.ANALYZED) d.setBoost(1.0); f.setBoost(0.9); d.add(f); writer.addDocument(d); • Il valore di boost è un float compreso tra 0.0 e 1.0 • Il valore di boost del documento e del campo viene utilizzato da Lucene per calcolare l’importanza del documento recuperato in fase di ricerca – http://lucene.apache.org/java/2_3_1/api/org/apache/lucene/search/Simi larity.html Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 38 Visualizzazione e diagnosi di un indice: Luke • Luke: uno strumento di visualizzazione e diagnostica degli indici Lucene – – – – – – Scorrere i documenti per id o per termine Visualizzare documenti Recuperare una lista ordinata dei termini più frequenti Eseguire ricerche e scorrere i risultati Eliminare documenti dall’indice Ottimizzare un indice Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 39 Sviluppi di Lucene • Nutch – Un motore di ricerca web che si basa su Lucene • Solr – Server di ricerca ad alte prestazioni costruito utilizzando Lucene • Mahout – Sottoprogetto Lucene con l’obiettivo di creare una suite di librerie scalabili di apprendimento automatico Lucene: API efficienti per ricerche indicizzate 21/12/2015 Pagina 40