Lucene: Una libreria efficiente per ricerche di testo
Roberto Navigli, Fulvio D’Antonio
Lo scenario
Lucene: API efficienti per ricerche indicizzate
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
Pagina 21
Esempi di Query con QueryParser
Lucene: API efficienti per ricerche indicizzate
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
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
24/06/2017
Pagina 40