Lucene: un motore di ricerca in Java

Con questo articolo viene introdotto Lucene, un interessante libreria per creare applicazioni in grado di memorizzare e cercare informazioni come un motore di ricerca

Introduzione

Lucene è la famosa libreria che permette di avere in Java un motore di ricerca per diverse tipologie di file. E’ un progetto opensource della Apache Software Foundation scritto da Doug Cutting. E’ una libreria estremamente flessibile che ci permette di inserire nelle nostre applicazioni le funzionalità di motore di ricerca. Per maggiori informazioni su download, licenze e quant’altro vi rimando al sito ufficiale http://lucene.apache.org/java/docs/index.html.

Caratteristiche

Lucene presenta una vasta collezione di interfacce che definiscono tutti i classici passi della ricerca (come vogliono i buoni libri di Information Retrieval).

Questa libreria quindi non è un vero e proprio motore di ricerca che noi richiamiamo dicendogli “Mi cerchi questa cosa?” e lui fa tutto in automatico. Proprio per questo motivo da una parte non viene subito capito dagli sviluppatori. Bisogna però rendersi conto della flessibilità delle API che vengono messe a disposizione da Lucene. Infatti una volta capito il modo in cui possiamo adattare i dati che vogliamo ricercare al pattern di Lucene, il suo utilizzo diventa molto semplice. Ci sono degli oggetti con i quali bisogna prendere confidenza, perchè saranno gli attori principali nell’utilizzo di questa libreria. L’immagine seguente riassume queste entità e le loro relazioni.

Apache Lucene

 

Come potete vedere la gerarchia delle nostre informazione parte dall'”INDEX”, ovvero dall’indice. In questo sono presenti diversi “DOCUMENT”, che rappresentano i vari documenti che sono stati indicizzati. Per ogni documento avremo diversi “FIELD”, ovvero una coppia di nome/valore che identifica un’informazione sul nostro documento.

Nel momento in cui noi dobbiamo creare un indice dobbiamo utilizzare la classe IndexWriter di Lucene, che ci permette appunto di aggiungere tutti i Document al nostro indice. Il Document stesso deve essere creato da noi, inserendo al suo interno i diversi Field che lo descrivono. Quando dobbiamo effettuare una ricerca ci dobbiamo basare sul QueryParser, che è la classe generatrice delle nostre Query. Come risultato di una query avremo un vettore di Document, che soddisfano i requisiti della nostra ricerca.

Indicizzazione

Per incominciare a capire questa interessante libreria dobbiamo vedere come funziona a livello di codice. In questo esempio abbiamo deciso di memorizzare un’informazione abbastanza semplice, ovvero un’email. Questa informazione ha una struttura che può essere resa abbastanza bene dalla seguente classe

public class EmailObject {

    private String oggetto;
    private String mittente;
    private String destinatario;
    private String testo;

    public String getOggetto() {
        return oggetto;
    }

    public void setOggetto(String oggetto) {
        this.oggetto = oggetto;
    }

    public String getMittente() {
        return mittente;
    }

    public void setMittente(String mittente) {
        this.mittente = mittente;
    }

    public String getDestinatario() {
        return destinatario;
    }

    public void setDestinatario(String destinatario) {
        this.destinatario = destinatario;
    }

    public String getTesto() {
        return testo;
    }

    public void setTesto(String testo) {
        this.testo = testo;
    }
}

Immaginiamo ora di avere un processo che ogni tot di tempo riceve una serie di email, estraendole magari da un server POP3. Quindi noi abbiamo a disposizione un vettore di EmailObject che dobbiamo indicizzare. In questo esempio utilizziamo il filesystem come repository del nostro indice, ma potremmo anche utilizzare una diversa strategia di memorizzazione (Database, RAM etc. etc.). Vediamo quindi cosa dobbiamo fare per inserire questi oggetti nel nostro indice.

import java.io.*;
import org.apache.lucene.search.*;
import org.apache.lucene.document.*;
import org.apache.lucene.search.*;
import org.apache.lucene.index.*;
import org.apache.lucene.store.*;
import org.apache.lucene.queryParser.*;
import org.apache.lucene.analysis.standard.*;

public class Indicizza {

    public static void main(String a[]) {
        //Costruiamo un oggetto email d'esempio	
        EmailObject email1 = new EmailObject();
        email1.setOggetto("Re: Ciao!");
        email1.setMittente("amicomio@server");
        email1.setDestinatario("io@server");
        email1.setTesto("Ma daiiiii!");
        try {
            FSDirectory dx = FSDirectory.getDirectory(new File("repository.dx"), true);
            IndexWriter writer = new IndexWriter(dx, new StandardAnalyzer(), true);
            Document doc = new Document();
            doc.add(new Field("oggetto", email1.getOggetto(), 
                    Field.Store.YES, Field.Index.NO));
            doc.add(new Field("destinatario", email1.getDestinatario(), 
                    Field.Store.YES, Field.Index.NO));
            doc.add(new Field("mittente", email1.getMittente(), 
                    Field.Store.YES, Field.Index.TOKENIZED));
            doc.add(new Field("content", email1.getTesto(), 
                    Field.Store.YES, Field.Index.TOKENIZED));
            writer.addDocument(doc);
            writer.optimize();
            writer.close();
        } catch (Exception e) {
        }
    }
}

Come potete vedere dal codice, dopo aver creato il repository utilizzando l’implementazione di Lucene FSDirectory, abbiamo iniziato a convertire gli EmailObject in Document, aggiungendo un Field per ogni campo dell’EmailObject. A questo punto il nostro indice è stato inizializzato con tutte le informazioni che gli abbiamo passato. Chiaramente solo quando viene inizializzato l’indice possiamo richiamare il metodo getDirectory() di FSDirectory passando come secondo parametro true. Così facendo infatti viene creato un nuovo indice oppure viene raso al suolo quello esistente.

Per un’applicazione reale dovremo fare prima un check sul path dell’indice che vogliamo usare e controllare l’esistenza dell’indice. Come potete vedere nella creazione del documento abbiamo specificato diverse cose. Prima fra tutte il campo, poi i dati e successivamente due valori che vanno a descrivere meglio l’informazione che dobbiamo memorizzare. Nel nostro caso abbiamo deciso di memorizzare tutte le informazioni del documento e di indicizzare soltanto i campi “mittente” e “content”.

Ricerca

Passiamo ora alla ricerca utilizzando Lucene. L’indice nel quale effettuiamo le nostre ricerche è lo stesso che abbiamo creato precedentemente. Di seguito vediamo l’implementazione di una classe che si collega all’indice sul filesystem ed effettua ricerche sui due indici che abbiamo creato, ovvero “mittente” e “content”.

import java.io.*;
import org.apache.lucene.search.*;
import org.apache.lucene.document.*;
import org.apache.lucene.search.*;
import org.apache.lucene.index.*;
import org.apache.lucene.store.*;
import org.apache.lucene.queryParser.*;
import org.apache.lucene.analysis.standard.*;

public class Ricerca {

    public static void main(String a[]) throws Exception {
        FSDirectory dx = FSDirectory.getDirectory(new File("repository.dx"), false);
        Searcher searcher = new IndexSearcher(dx);
        searchBySender(searcher, "amicomio@server");
        searchBySender(searcher, "prova@server");
        searchByText(searcher, "dai*");
        searcher.close();
    }

    public static void searchBySender(Searcher searcher, String sender) throws Exception {
        QueryParser qp = new QueryParser("mittente", new StandardAnalyzer());
        Query query = qp.parse(sender);
        Hits hits = searcher.search(query);
        int trovati = hits.length();
        if (trovati == 0) {
            System.out.println("Nessun risultato come mittente per [" + sender + "]");
        } else {
            System.out.println("Trovati risultati per [" + sender + "] come mittente:");
            for (int i = 0; i < trovati; i++) {
                Document doc = hits.doc(i);
                System.out.println("  " + (i + 1) + ". " + doc.get("mittente"));
            }
        }
    }

    public static void searchByText(Searcher searcher, String text) throws Exception {
        QueryParser qp = new QueryParser("content", new StandardAnalyzer());
        Query query = qp.parse(text);
        Hits hits = searcher.search(query);
        int trovati = hits.length();
        if (trovati == 0) {
            System.out.println("Nessun risultato nel testo per [" + text + "]");
        } else {
            System.out.println("Trovati risultati per [" + text + "] nel testo:");
            for (int i = 0; i < trovati; i++) {
                Document doc = hits.doc(i);
                System.out.println("  " + (i + 1) + ". " + doc.get("content"));
            }
        }
    }
}

Il codice che viene proposto è abbastanza semplice da analizzare. Per ogni ricerca che viene effettuata viene istanziato un oggetto Query, il quale fornito come parametro alla classe Searcher ci fornisce i risultati della nostra ricerca. Nel caso in cui vengono trovati dei risultati vengono elencate a schermo alcune informazioni. Eseguendo la classe appena riportata viene restituito il seguente output:

1	Trovati risultati per [amicomio@server] come mittente: 1. amicomio@server
2	Nessun risultato come mittente per [prova@server]
3	Trovati risultati per "dai*" nel testo: 1. Ma daiiiii!

Conclusioni

Lucene è un interessante progetto, che in questo articolo è stato appena introdotto. Per maggiori informazioni vi rimando alla documentazione ufficiale ed altri articoli.

Sito ufficiale
http://javatechniques.com/public/java/docs/basics/lucene-memory-search.html
http://www-128.ibm.com/developerworks/library/wa-lucene2/index.html

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.