Lucene - Un motore di ricerca in java

Lucene - La libreria per l'indicizzazione e ricerca di dati
G.Morreale

Introduzione:

Lucene(http://lucene.apache.org/java/docs/ ) è una libreria per la ricerca e reperimento di informazioni.
E' un progetto open-source della Apache Software. 

La libreria astrae dalla tipologia e fonte di dati, è infatti possibile utilizzare le API per diversi scopi:Indicizzazione dati di file su disco, pdf, dati su db etc.

Ad esempio wikipedia utilizza lucene per le ricerche full-text.

Articolo riferito alla versione 2.4.0

Elementi chiave dell'architettura di Lucene
Document e Field

Per poter effettuare una ricerca con Lucene bisogna prima costruire la struttura sulla quale effettuare la ricerca tale struttura è composta dai seguenti elementi

  • Index - Indice che raccoglie i diversi document
  • Document - Rappresentazione dei documenti
  • Field - Elementi di documenti composti dalla coppia nome/valore.

Approfondimento

Il campo field contiene come appena menzionato una coppia nome/valore.
Per nome si intende sempre una Stringa che rappresenta la chiave per ottenere il valore.
Riguardo al valore oltre alla consueta Stringa si possono inserire dati binary(byte[]) e altri oggetti(Reader, TokenStream, non mi soffermo su questi vedi la javadoc per approfondire).

Quando si costruisce un campo inoltre si può indicare 

  • Se memorizzare o meno il valore nell'indice (Vedi campi Field.Store: COMPRESS, NO, YES)
Di solito si tende a non memorizzare nell'indice qui valori sui quali non si effettua la ricerca, e a indicizzare con compressione qualora i valori sono "grandi" (es. documenti di testo di diversi KB)

  • Se indicizzare o meno il valore e come (Vedi campi Field.Index: ANALYZED, ANALYZED_NO_NORMS,NO,,NOT_ANALYZED,NOT_ANALIZED_NO_NORMS)
I valori più usati, non indicati nella javadoc come Expert, sono
  • ANALYZED - Indicizza i token utilizzando l'analyzer(*)
  • NO - Non indicizzare del tutto
  • NOT_ANALYZED - Indicizza ma senza l'uso dell'analyzer(*)

(*)Riguardo l'analyzer ne parlo in seguito.


Costruire la struttura.
IndexWriter

Per costruire tale struttura si utilizza un IndexWriter che permette di aggiungere dei Document all'indice, ovviamente i Document dovranno già essere compilati con i corrispettivi field.

Esempio struttura per indicizzazione di una rubrica.

Supponiamo di avere un classe rubrica e di volere indicizzare le varie istanza nella struttura di lucene.

la classe

public class Rubrica {

    private String numero;
    private String nome;
    private String cognome;
    private String indirizzo;

    public String getCognome() {return cognome;}

    public void setCognome(String cognome) {this.cognome = cognome;}

    public String getIndirizzo(){return indirizzo;}

    public void setIndirizzo(String indirizzo){this.indirizzo = indirizzo;}

    public String getNome(){return nome;}

    public void setNome(String nome){this.nome = nome;}

    public String getNumero() {return numero;}

    public void setNumero(String numero)  {this.numero = numero;}
}

Essa sarà distribuita in una struttura simile alla seguente rappresentazione:

IndexRubrica

new Document()
cognome rossi
nome giovanni
numero 123123 123

new Document()
cognome verdi
nome mario
numero 789 789 789
..
new Document()
..
..
..

La forza di lucene stà nel fatto che la struttura è basata su degli oggetti generici (Document, Field) quindi svincolata da qualsiasi forma di dati proprietaria. Ciò consente l'adattabilità della libreria nei confronti delle diverse fonti dati.

L'Analyzer
SimpleAnalyzer,StopAnalyzer ,StandardAnalyzer

Quando si indicizza un Field lucene procede con la suddivisione del valore del campo in piccole parti chiamate token.
La suddivisione in token semplifica a lucene il processo di ricerca all'interno di un indice.

L'Analyzer viene usato sia per tokenizzare e filtrare in fase di indicizzazione, ma anche in fase di ricerca.
Quindi è opportuno utilizzare lo stesso analyzer sia in fase di indicizzazione che di ricerca.

Giusto per fare un esempio l'Analyzer è utile nel caso in cui si vuole ignorare all'interno di una ricerca token come "a" "il" "e" etc.

L'Analyzer è una classe astratta e le sue diverse implementazioni offrono diverse possibilità di suddividere in token e filtrare i dati in modo differente.

All'interno della libreria si trovano tre implementazioni:

  • SimpleAnalyzer - Suddivide il valore in token e converte l'input in soli caratteri minuscoli.
  • StopAnalyzer - Funziona come il precedente ma filtra l'indicizzazione su piccoli token che nella lingua inglese occorrono con alta probabilità (a, an, the, etc). E' possibile aggiungere o modificare l'array di token sul quale filtrare l'indicizzazione.
  • StandardAnalyzer - Come lo StopAnalizer con l'aggiunta di filtri su apostrofi, acronimi e altre parole che possono sporcare il risultato della ricerca.

Ciascuno di questi Analyzer può essere scelto in base alle proprie esigenze di progetto.
Inoltre all'interno di LuceneSandbox è possibile trovare Analyzer in diversi linguaggi.

Dove memorizzare la struttura.

La struttura sulla quale effettuare le ricerca deve essere resa persistente in un qualche modo:
La classe astratta di base è Directory

All'interno della core della libreria è possibile trovare le seguenti implementazioni.

DbDirectory - Implementazione basata su berkley db 4.3.
FSDirectory - Implementazione basata su file. 
JEDirectory - Implementazione basata su Berkley db JE.
RAMDirectory - Implementazione per utilizzare i dati in RAM.

Effettuare la ricerca sulla struttura.

Al fine di effettuare un ricerca all'intero di una index si utilizza la classe IndexSearcher, tale classe prende in input un oggetto Query che rappresenta la ricerca stessa.

Quando siamo di fronte a query complicate si può utilizzare il QueryParser che prende in input una stringa e costruisce l'oggeto Query da passare all'indexSearcher.

nota:
Il QueryParser ha un metodo statico per la costruzione della ricerca, ma tale metodo non è thread safe, quindi qualora necessario ogni thread deve avere la sua istanza di QueryParser


Esempio Riassuntivo
Creazione Indice e Ricerca

L'esempio seguente riassume e trasforma in codice gran parte dei concetti finora introdotti:

//------------------------------------------------
//--------------Creazione Struttura---------------
//------------------------------------------------

//creazione di un analyzer standard
Analyzer analyzer = new StandardAnalyzer();

//Memorizza l'indice in RAM:
//Per inserire ad esempio i dati su file, usare Directory dir = FSDirectory.getDirectory("path");
Directory directory = new RAMDirectory();
//Creazione istanza per la scrittura dell'indice
//Tale istanza viene fornita di analyzer, di un boolean per indicare se ricreare o meno da zero
//la struttura e di una dimensione massima (o infinita IndexWriter.MaxFieldLength.UNLIMITED)
IndexWriter iwriter = new IndexWriter(directory, analyzer, true, new IndexWriter.MaxFieldLength(25000));
//costruiamo un indice con solo 2 documenti

//creazione documento
Document doc = new Document();
String text = "Il cane corre dietro il gatto";
//creazione del campo con indicazione di memorizzazione(Store.YES) e indicizzazione con analyzer(ANALYZED)
Field field = new Field("testo", text, Field.Store.YES,Field.Index.ANALYZED))
//Aggiunta campo al documento
doc.add(field);
//creazione secondo campo con la data, non indicizzato.
field = new Field("data",new Date().toString(), Field.Store.YES, Field.Index.NO)
doc.add(field);
//aggiunta documento all'indice
iwriter.addDocument(doc);

//creazione secondo documento, come sopra
doc = new Document();
text = "il gatto è velocissimo";
Field field = new Field("testo", text, Field.Store.YES,Field.Index.ANALYZED))
doc.add(field);
field = new Field("data",new Date().toString(), Field.Store.YES, Field.Index.NO)
doc.add(field);
iwriter.addDocument(doc);

//chiusura indice (spostare il codice nella clausola finally!)
iwriter.close();

//----------------------------------------------
//--------------Ricerca-------------------------
//----------------------------------------------

//Creazione dell'oggetto per la ricerca indicando la struttura (directory) su cui lavorare e l'analyzer
IndexSearcher isearcher = new IndexSearcher(directory,analyzer);
//Catturiamo l'input dell'utente
String sentence = JOptionPane.showInputDialog("sentence");
//Creazione della query, viene indicato il campo di default sul quale effettuare la ricerca.
QueryParser parser = new QueryParser("testo", analyzer);
Query query = parser.parse("sentence ");

//Effettua la ricerca ottenendo l'oggetto TopDocs
TopDocs topDocs = isearcher.search(query,1000);
//Stampa del conteggio numero di hits.
System.out.println("Numero di hits " + topDocs.totalHits);

//Array dei risultati
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
int i = 1;
for (ScoreDoc sc : scoreDocs)
{
 System.out.print(i++ + "° - ");
 //Attraverso l'oggetto scoreDoc è possibile ottenere un indice che passato all metodo
 //indexSearcher.doc restituisce un Document dal quale estrarre i vari campi

 System.out.println(indexSearcher.doc(sc.doc).get("testo"));
}
indexSearcher.close();
directory.close();


Query e operatori di ricerca AND e OR

Supponendo che l'indice contiene i seguenti Document e Field

Document1
text = Il cane corre dietro il gatto
Document2
text = Il gatto corre veloce

Se settiamo come default Operator AND_OPERATOR
queryParser.setDefaultOperator(QueryParser.AND_OPERATOR);

otterremo sulla ricerca "cane gatto" solo il risultato
Il cane corre dietro il gatto

nel caso in cui si opta per OR_OPERATOR
si otterrano entrambi i risultati

Il cane corre dietro il gatto
Il gatto corre veloce

In quanto nel secondo caso anche se il matching è più debole viene comunque trovata una delle parole presenti nella query.

Conclusione

Attraverso questo articolo abbiamo mostrato come è semplice e nello stesso tempo flessibile l'approccio alla libreria Lucene

Riferimenti:
http://today.java.net/pub/a/today/2005/08/09/didyoumean.html

2 comments:

Anonymous said...

imparato molto

Zamby said...

ottimo