Captcha: Una JSP con controllo Immagine

Effettuare su pagine web controlli basati sulle immagini
G.Morreale

Introduzione:

Diverse e diverse volte sicuramente vi siete trovati davanti una pagina di registrazione, login, o download nella quale viene richiesta la digitazione dei caratteri presenti in un immagine.

Tale test ha lo scopo di verificare se l'utente è un essere umano e non un computer.

L'acronimo inglese CAPTCHA riassume in una parola il concetto appena esposto.

"Completely Automated Public Turing test to tell Computers and Humans Apart" (Test di Turing pubblico e completamente automatico per distinguere computer e umani)


Implementare tale controllo in una pagina JSP

Al fine di inserire tale controllo all'interno di una pagina JSP, è necessario l'uso di una libreria in grado di generare l'immagine e fornire in un qualche modo il dato generato sottoforma di variabile.

La libreria che viene utilizzata nell'esempio è scaricabile da:

L'Esempio

Creiamo un progetto Web, e generiamo due pagine JSP:

  • index.jsp - che contiene il controllo
  • protetta.jsp - che è visualizzabile solo se l'utente riporta correttamente il contenuto dell'immagine su un campo di testo.

Aggiungiamo tra le librerie di progetto il jar scaricato dal precedente link.

Dividiamo ora l'esempio in due parti:

  • La generazione dell'immagine
  • Il controllo

La generazione dell'immagine

L'immagine è generata da una servlet la cui classe si trova all'interno della libreria katpcha.
Tale classe esattamente si trova nel seguente percorso:

com.google.code.kaptcha.servlet.KaptchaServlet

Bisogna però rendere disponibile la servlet configurando correttamente l'elemento e il suo mapping sul file xml.
Tale configurazione è fattibile inserendo le seguenti righe nel file web.xml

    <servlet>
        <servlet-name>Kaptcha</servlet-name>
        <servlet-class>com.google.code.kaptcha.servlet.KaptchaServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>Kaptcha</servlet-name>
        <url-pattern>/Kaptcha</url-pattern>
    </servlet-mapping>


Il primo nodo serve a dichiarare una servlet di nome Kaptcha corrispondente alla classe com.google.code.kaptcha.servlet.KaptchaServlet

Il secondo nodo invece serve a indicare su quale url deve rispondere la servlet.

La servlet è quindi ora configurata, è quindi possibile effettuare già il deploy dell'applicazione (es. su localhost) e richiamare l'url Kaptcha al fine di generare un immagine (es. prova con http://localhost/ContextPathProgetto/Kaptcha).

L'output dovrebbe essere simile a questo:


Quindi il codice della pagina index.jsp potrebbe essere simile al seguente:

<%@page contentType="text/html"%>
<%@page pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
   "http://www.w3.org/TR/html4/loose.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Esempio Captcha</title>
    </head>
    <body>
    <h1>Esempio Captcha</h1>
    <img src="Kaptcha" alt=""/>    
    <br />
    <form action="protetta.jsp">
        <label>Ricopia correttamente il testo: </label><br />
        <input type="text" name="kaptcha" value="" />
    </form>   
    </body>
</html>

Banalmente viene richiamata la servlet per generare l'immagine e viene mostrata all'utente una textbox per inserire il valore.
Il metodo action richiama la pagina protetta nella quale inseriremo il controllo.


Il controllo

Partiamo dal presupposto che la servlet Kaptcha non appena genera l'mmagine inserisce il corrispettivo testo all'interno di una variabile di sessione.
Tale variabile di sessione è identificabile dalla chiave com.google.code.kaptcha.Constants.KAPTCHA_SESSION_KEY

Quindi adesso credo risulti ovvio come chiudere il cerchio effettuando il controllo tra il testo digitato e quello generato.
Non si fà altro che ottenere il parametro GET o POST inserito dall'utente e confrontarlo con quello inserito dalla servlet Captcha nella sessione.

In caso di controllo positivo si procede tenendo conto che l'utente è un essere umano, in caso contrario si richiede la digitazione del testo o si  ridirige il client verso una pagina di errore.

Riassumendo il tutto con del codice sorgente:

<%@page contentType="text/html"%>
<%@page pageEncoding="UTF-8"%>
<%
//ottenimento del valore generato dal captcha sottoforma di String
String kaptchaExpected = (String) session.getAttribute(com.google.code.kaptcha.Constants.KAPTCHA_SESSION_KEY);
//estrazione input dell'utente
String kaptchaReceived = request.getParameter("kaptcha");
//Matching
if (kaptchaReceived == null || !kaptchaReceived.equalsIgnoreCase(kaptchaExpected))
{
  response.sendError(500, "Non hai ricopiato correttamente il testo nell'immagine");
  return;
}
%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Pagina Protetta dal Controllo</title>
    </head>
    <body>
        <h1>Pagina Protetta dal Controllo</h1>                
    </body>
</html>


Conclusione

Questo è un esempio base, le applicazioni e le varianti sono diverse, ma il concetto resta invariato.

Riguardo la libreria utilizzata per la generazione dei codici è possibile effettuare delle customizzazioni sull'immagine generata(vedi http://code.google.com/p/kaptcha/wiki/ConfigParameters ).

Glassfish VS Tomcat

Glassfish VS Tomcat
G.Morreale

Introduzione:

Spesso sui forum si fà confusione riguardo all'uso di Glassfish o Tomcat, richiedendo se conviene usare uno piuttosto che un altro.
Guardando i log del blog spesso arriva gente che effettua proprio la ricerca "Glassfish VS Tomcat" oppure "Glassfish o Tomcat" oppure ancora "Glassfish contro Tomcat", quindi è chiaro che un newbie che si avvicina al mondo Java EE faccia un pò di confusione circa questa scelta.

La disputa è subito chiarita.. E' un confronto che non ha senso!

Glassfish è un Fuoristrada 4x4, Tomcat è un'utilitaria.
Se devo andare in città e in percorsi da "fuori strada" uso il 4x4 altrimenti se vado solo in città mi muovo con un utilitaria in quanto il suo uso è più agevole.

Tornando sugli aspetti tecnici..

Partiamo dal presupposto che la piattaforma Java EE  è composta da diverse e diverse tecnologie: JSP, Servlet, JMS, MDB, EJB, JPA etc etc.(vedi http://java.sun.com/javaee/technologies/

Tomcat è in grado di supportare solo una piccola parte di Java EE, essenzialmente quella relativa alle JSP e alle Servlet, motivo per cui Tomcat è definito un Servlet Container.

nota:
Le JSP sono anch'esse delle servlet


Un Application server Java EE come Glassfish invece supporta per intero Java EE.
Glassfish (che nella sua versione commerciale con supporto sun si chiama Sun Application Server) è l'implementazione di riferimento per le tecnologie Java EE.

Ci sono comunque diverse alternative a Glassfish:


Il confronto Glassfish vs uno dei server nell'elenco ha più senso rispetto al titolo dell'articolo!!

Un Application Server è in grado di fare le stesse cose che è in grado di fare Tomcat, in quanto è anche un servlet container, motivo per cui spesso si trovano dei confronti di prestazioni tra le funzionalità comuni di Tomcat e Glassfish
(date un occhio 
http://raibledesigns.com/rd/entry/glassfish_2_vs_tomcat_6
oppure  
http://www.pneumonoultramicroscopicsilicovolcanoconiosis.org/blog/glassfish-vs-tomcat
)

Conclusione

Io personalmente preferisco applicare il ragionamento dell'esempio del fuoristrada vs utilitaria, nel senso che prendo dal garage l'auto il cui uso è più vicino alle esigenze del progetto.

Non è inoltre da escludere, in un architettura complessa, l'uso di entrambi i tipi di server, ad esempio tomcat per il front-end e glassfish per il back-end.


SVN Branch

Svn Branch con Netbeans
G.Morreale

Introduzione:


Il branch serve a creare delle linee di sviluppo parallele.



Se, ad esempio, sviluppiamo un software e a un certo punto viene richiesto di implementare una feature sperimentale ma vogliamo mantenere la versione principale disponibile per gli altri sviluppatori che ogni tanto si trovano a risolvere dei bug.
In questo caso si crea un branch e si implementa la feature sperimentale sul branch stesso. Gli altri sviluppatori, che lavorano sulla linea principale, non saranno "disturbati" dalle modifiche effettuate sul branch riguardanti la feature sperimentale richiesta.
Se alla fine si decide di rendere la feature sperimentale disponibile sulla linea principale si procede con un operazione di merge, al fine di fondere la linea principale con il branch.

La linea principale prende il nome di Trunk (tronco)
Le linee parallele sono i branch.

Netbeans e i Branch.
(Articolo relativo a Netbeans 6.1)

Netbeans consente di interagire con svn attraverso un integrazione nell'ide.
I comandi da utilizzare per lavorare con i branch sono

  • CopyTo - Copia l'intero progetto o un singolo file/package su un altra directory(creando il branch).
  • SwitchTo - Serve a indicare con quale branch (o trunk) si intende lavorare.
  • Merge Changes - Serve a fondere diversi branch(o trunk).



Esempio

(L'esempio presuppone la configurazione di svn sul proprio netbeans ide - vedi http://subversion.netbeans.org/faq/index.html)

Creiamo un progetto java qualsiasi.
Tasto destro sul progetto - Versioning - Import into Subversion Repository

Impostare url, nome utente e password.
Riguardo la repository folder: cliccare su browse, scegliere la directory il cui nome corrisponde a quello del progetto, cliccare su "Into new folder" e creare la directory trunk. Conferma l'operazione.

Si è creata la linea principale.

Adesso procediamo creando il branch.

nota:
Ogni operazione: copy to, switch to copy, update, commit può essere relativa all'intero progetto o a un singolo elemento(File ad esempio)

Tasto destro su un file di progetto sul quale si intende effettuare il branch e i relativi test.
Scegliere Subversion -> Copy To, 
Cliccare su browse, Creare cliccando su "Into new folder" la directory branches nello stesso livello di trunk, Creare un altra directory all'interno con il nome del branch (es. branch1), Confermare cliccando su ok.

Con quest'ultima operazione si è creato il branch, ovvero una linea di sviluppo parallela equivalente a quella attuale, ma al momento l'ide effettuera tutte le varie operazioni (Es. update, commit) sul trunk e non sul branch.

Per attivare il branch quindi, usare il comando Switch to Copy sul file spostato sul branch e selezionare il branch precedentemente creato.

Da ora in poi si lavora sul branch e non sulla linea principale.

nota:
Tale operazione è stata effettuata sul singolo file, quindi le operazioni svn sul file sul quale si è finora operato saranno effettuate sul branch, mentre gli altri file continueranno a lavorare sul trunk

Per capire su quale branch si lavora, è possibile attivare la visualizzazione del branch, cliccando su View -> Show Versioning label.

E' possibile adesso effettuare delle modifiche sul file(es. scrivendo "modifica branch 1").
Effettuare il commit.
Effettuare lo switch copy to sulla linea principale e verificare che la modifica riportata sul branch1 non è presente.

Per verificare l'operazione di merging del branch con la linea principale è possibile usare il comando "Merge Changes"
Selezionare il branch1 e cliccare su merge.
Adesso il file in questione conterrà anche le modfiche del branch.


Conclusione

Vi invito a leggere http://svnbook.red-bean.com/ al fine di approfondire le varie nozioni relative a questo utilissimo tool per il controllo di versione.


Riferimento:



Lucene SpellChecker - "Il Forse Cercavi"

Lucene - Suggerire una query..
G.Morreale

Introduzione:

I moderni motori di ricerca, riescono a proporre una versione "corretta" di una query presunta erronea.

Quante volte vi sarà capitato su google di effettuare un errore di digitazione e google vi ha risposto, ad esempio, con un 

"Forse Cercavi: Java" 

dopo aver digitato 'jav'.

Gli approcci per raggiungere tale obiettivo sono diversi:

  • Minimum Edit distance: Calcolare il numero di inserimenti, cancellazioni e sostituzioni necessarie a trasformare una stringa(Quella presupposta erronea) in un'altra(quella presupposta corretta)

  • Similiarity Key: Basato su un dizionario dove sotto la stessa chiave ci sono le stringhe similiari.

  • Letter n-gram: n indica il numero di lettere all'interno di una sotto-sequenza di una parola. Es. 3-grams per la parola lucene "luc", "uce", "cen","ene". Confrontare due n-gram può dare con buona approssimazione un suggerimento. Per approfondimenti (http://en.wikipedia.org/wiki/N-gram)


Lucene Spell Checker:

All'interno pacchetto di librerie scaricabili da:  http://www.apache.org/dyn/closer.cgi/lucene/java/
è possible trovare una libreria chiamata "lucene-spellchecker-2.4.0.jar" (Si trova nel seguente path: lucene-2.4.0\contrib\spellchecker).

Tale libreria consente di raggiungere l'obiettivo di suggerire una query "vicina" a quella introdotta inizialmente in input.
Quindi qualora il numero di risultati di una query è 0 oppure al di sotto di un certa soglia lo spellchecker potrà suggerire una nuova query.

Lo spellchecker utilizza il metodo letter-ngram, il suo compito è quello di analizzare un suo indice, vediamo in seguito come costruirlo, al fine di calcolare una query similiare in grado di restituire dei risultati "buoni".

La query suggerita dallo spellChecker può essere data in pasto nuovamente ai metodi di ricerca.

Come visto nel precedente articolo la fasi salienti dell'uso della libreria sono 2: Costruzione e Scrittura Indice, e ricerca all'interno dell'indice.
Anche nel caso dello Spell Checker si distinguono due fasi:

  • Scrittura del dizionario
  • Ricerca all'interno del dizionario


Scrittura del dizionario

Il dizionario dello SpellChecker viene rappresentato dalla classe LuceneDictionary.
Esso viene costruito a partire da un indice.

Adesso costruiamo un package che si occupa dei vari step dell'indicizzazione e costruzione dizionario.
La classe astratta IndexMaker contiene il campo indexDirectory che rappresenta la locazione sulla quale verrà memorizzato l'indice e il metodo generateIndex che dato un gruppo di oggetti Document si occupa della generazione dell'indice.

BaseIndexMaker estende IndexMaker al fine di implementare concretamente il metodo generateIndex.
Il costruttore inoltre prevede il settaggio di alcuni parametri per l'indicizzazione:Analyzer, MaxFieldLength(numero massimo di elementi dell'indice).

La classe SpellIndexMaker è in grado di svolgere le stesse funzioni di BaseIndexMaker, ma in più è in grado di generare il dizionario per lo spellChecking(metodo generateSpellIndex(String fieldname).
Il dizionario viene creato su un determinato campo del document.

Di seguito il codice delle 3 classi presenti nel diagramma e appena descritte


  • IndexMaker

package index;

import org.apache.lucene.document.Document;
import org.apache.lucene.store.Directory;

public abstract class IndexMaker 
{
    protected Directory indexDirectory;

    public IndexMaker(Directory dir)
    {
        this.indexDirectory = dir;
    }
    
    public abstract void generateIndex(Document[] documentArray, boolean append);

}

  • BaseIndexMaker

package index;

import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.SimpleAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriter.MaxFieldLength;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.LockObtainFailedException;

public class BaseIndexMaker extends IndexMaker
{
    Analyzer analyzer;
    MaxFieldLength maxFieldLength;
            
    public BaseIndexMaker(Directory indexDirectory)
    {
        super(indexDirectory);
        analyzer = new SimpleAnalyzer();
        this.maxFieldLength = IndexWriter.MaxFieldLength.UNLIMITED;
    }

    public BaseIndexMaker(Directory indexDirectory, Analyzer analyzer)
    {
        super(indexDirectory);
        this.analyzer = analyzer;
    }
    
    public BaseIndexMaker(Directory indexDirectory, Analyzer analyzer, MaxFieldLength maxFieldLength)
    {
        super(indexDirectory);
        this.analyzer = analyzer;
        this.maxFieldLength = maxFieldLength;
    }
    
    /**
     * Genera un indice a partire da un array di document.
     * L'indice viene creato sulla directory indicata nel costruttore
     * @param documentArray - Array di Document da inserire nell'indice
     * @param append - Indica se inserire in modalità append o creare un nuovo indice da zero.
     */
    @Override
    public void generateIndex(Document[] documentArray, boolean append )
    {
         try
        {   //inizializzazione dell'IndexWriter         
            IndexWriter indexWriter = new IndexWriter(indexDirectory, analyzer, !append, maxFieldLength);

            //Inserimento documenti all'interno dell'indice
            for (Document d : documentArray)
            {
                indexWriter.addDocument(d);
            }
            //committ delle modifiche
            indexWriter.commit();
            //chiusura del writer.
            indexWriter.close();

        } 
         catch (CorruptIndexException ex)
        {
            Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, null, ex);            
        } catch (LockObtainFailedException ex)
        {
            Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, null, ex);            
        } catch (IOException ex)
        {
            Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, null, ex);            
        }
    }
}

  • SpellIndexMaker

package index;

import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.search.spell.LuceneDictionary;
import org.apache.lucene.search.spell.SpellChecker;
import org.apache.lucene.store.Directory;

/**
 * Tale classe è in grado di costruire un indice su una determinata directory a partire
 * da un array di document.
 * Inoltre è in grado di costruire un dizionario partendo dalla directory dell'indice al fine
 * di poterlo utilizzare in fase di spell checking(Ricerca query similiari).
 */
public class SpellIndexMaker extends BaseIndexMaker {

    Directory dictionaryDirectory;
    
    public SpellIndexMaker(Directory indexDirectory, Directory dictionaryDirectory)
    {
        super(indexDirectory);
        this.dictionaryDirectory = dictionaryDirectory;
    }
      
    public void generateSpellIndex(String fieldName)
    {       
        IndexReader indexReader = null;
        try
        {   
            //apertura dell'indice
            indexReader = IndexReader.open(indexDirectory);
            //inizializzazione dizionario su un dato campo
            LuceneDictionary dictionary = new LuceneDictionary(indexReader, fieldName);
            //inizializzazione oggetto spellchecker
            SpellChecker spellChecker = new SpellChecker(dictionaryDirectory);
            //scrittura del dizionario sulla Directory
            spellChecker.indexDictionary(dictionary);
            
        } catch (CorruptIndexException ex)
        {
            Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, null, ex);            
        } catch (IOException ex)
        {
            Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, null, ex);            
        } finally
        {
            if (indexReader != null)
            {
                try
                {
                    indexReader.close();
                } catch (IOException ex)
                {
                    Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, null, ex);                    
                }
            }
        }
    }

}

Dando un occhio ai commenti e al codice stesso non credo sia difficile cogliere la struttura e il significato dei vari step.
Al fine di utilizzare il codice si possono utilizzare le seguenti righe di codice.


//Connessione e ottenimento array di document dal db
//E' possibile utilizzare qualsiasi altro metodo che generi un insieme di Document
String dbUrl = "jdbc:mysql://localhost:3306/mydb?user=peppe&password=sql";
Document[] documentArray = dao.DAO.getContenutiDocument(dbUrl);

//Inizializzazione campi necessari all'indicizzazione
String path = "c:\\luceneIndex"; //path dell'indice.
String pathDict = "c:\\luceneIndex\\Dictionary";//path del dizionario per lo spellchecking.

//Ottenimento oggetto Directory dalla stringa path
Directory dir = (FSDirectory.getDirectory(path));                        
Directory dirDict = FSDirectory.getDirectory(pathDict);

//Inizializzazione IndexMaker           
SpellIndexMaker spellIndexMaker = new SpellIndexMaker(dir, dirDict);
//Generazione indice (Vedi nel path c:\\luceneIndex la creazione dei file dopo l'esecuzione)
spellIndexMaker.generateIndex(documentArray, false);
//Generazione dizionario.
spellIndexMaker.generateSpellIndex(fieldName);

Ricerca e SpellChecking

In maniera del tutto analoga al precedente step(Indicizzazione e Scrittura del dizionario) costruiamo un package che si occupa della ricerca all'interno di un indice e dell'eventuale suggerimento all'interno del dizionario qualora la ricerca non soddisfa le aspettative:

La classe astratta SearchEngine pone le basi per una classe adatta alla ricerca all'interno di un indice; essa è dotata del campo su cui effettuare la ricerca (defaultField), della Directory dell'indice, del numero massimo di hits per il risultato della ricerca, e infine il tipo di operatore, AND o OR. Il metodo da implementare è un metodo che data una stringa effettua la ricerca ritornando oggetti di tipo SearchResult.

La classe SearchResult infatti incapsula il Document su cui la ricerca è stata vincente e il relativo punteggio di affinità per la ricerca.

La classe SimpleSearchEngine implementa il metodo di ricerca e ne aggiunge uno in grado di recepire in input una query rappresentata non più da una stringa ma da un oggetto Query proprio delle librerie di Lucene.

SuggestAndSearchEngine estende le precedenti funzionalità e aggiunge un metodo in grado di suggerire delle query similiari.
Nota bene che suggest suggerisce solo la query similiare, se si vogliono ottenere i risultati "similiari" bisogna dare tale query nuovamente in pasto al metodo search.

Vediamo direttamente il codice:

  • SearchResult 

package search;

import org.apache.lucene.document.Document;

public class SearchResult
{
    //Document contenente il risultato
    Document doc;
    //Punteggio della ricerca, più è altro più la ricerca è affine
    float Score;   
    
    public SearchResult(Document doc, float score)
    {
        this.doc = doc;
        this.Score = score;
    }

    public float getScore()
    {
        return Score;
    }

    public Document getDoc()
    {
        return doc;
    }
 
}

  • SearchEngine 

package search.engine;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.queryParser.QueryParser.Operator;
import org.apache.lucene.store.Directory;
import search.SearchResult;

public abstract class SearchEngine 
{
    //campo field sul quale effettaure la ricerca
    protected String defaultField;
    //istanza della Directory sulla quale risiede l'indice
    protected Directory indexDirectory;
    //Numero di hits
    protected int maxHits;
    //Operatore di ricerca (AND o OR)
    protected Operator operator;
    
    //Analyzer per tokenizzare e filtrare la ricerca
    protected  Analyzer analyzer;
    
    public abstract SearchResult[] search(String queryString);
}

  • SimpleSearchEngine 

package search.engine;

import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.SimpleAnalyzer;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.queryParser.QueryParser.Operator;
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.store.Directory;
import search.SearchResult;

public class SimpleSearchEngine extends SearchEngine
{
    public SimpleSearchEngine(String defaultField, Directory directory, int maxHits, Operator op, Analyzer analizyer)
    {
        this.defaultField = defaultField;
        this.indexDirectory = directory;
        this.maxHits = maxHits;
        this.operator = op;
        this.analyzer = analizyer;
    }

    /**
     * Metodo per la ricerca data una query all'interno dell'index specificato 
     * nel costruttore
     * @param query - Oggetto Query di Lucene
     * @return
     */
    public SearchResult[] search(Query query)
    {
        IndexSearcher is = null;
        SearchResult[] ret = null;
        try
        {
            //Inizializzazione dell'oggetto chiave della ricerca
            is = new IndexSearcher(indexDirectory);
            //Ricerca indicando il numero massimo di hits da ottenere
            TopDocs topDocs = is.search(query, maxHits);
            //Estrazione risultati
            ScoreDoc[] scoreDocs = topDocs.scoreDocs;
            int len = topDocs.totalHits > maxHits ? maxHits : topDocs.totalHits;
            ret = new SearchResult[len];

            int i = 0;
            for (ScoreDoc sc : scoreDocs)
            {
                ret[i++] = new SearchResult(is.doc(sc.doc), sc.score);
            }

        } catch (Exception e)
        {
            Logger.getLogger(this.getClass().getName()).log(Level.INFO, "Error in searching..", "");
        } finally
        {
            if (is != null)
            {
                try
                {
                    is.close();
                } catch (IOException ex)
                {
                    Logger.getLogger(SimpleSearchEngine.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
            return ret;
        }

    }

/**
 * Metodo per la ricerca all'intero di un index.
 * La location(Directory) dell'index è specificata tramite il costruttore
 * @param queryString - stringa rappresentante la query, viene convertita 
 * in Query attraverso il QueryParser
 * @return - Insiemi oggetti SearchResult contenenti Document e relativo 
 * punteggio di affinità
 */
    public SearchResult[] search(String queryString)
    {
        try
        {
            //Costruzione della query partendo da una stringa
            QueryParser queryParser = new QueryParser(this.defaultField, new SimpleAnalyzer());
            //Tipo di ricerca (AND o OR)
            queryParser.setDefaultOperator(operator);
            //parsing vero e proprio della stringa
            Query query = queryParser.parse(queryString);
            return search(query);
        } catch (ParseException ex)
        {
            Logger.getLogger(SimpleSearchEngine.class.getName()).log(Level.SEVERE, null, ex);
            return null;
        }
    }
}


  • SuggestAndSeachEngine 


package search.engine;


import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryParser.QueryParser.Operator;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.spell.SpellChecker;
import org.apache.lucene.store.Directory;

public class SuggestAndSeachEngine extends SimpleSearchEngine
{
    //Directory sulla quale si trova il dizionario
    private Directory spellDictionaryDir;

    public SuggestAndSeachEngine(String defaultField, Directory indexDirectory, int maxHits, Operator op,Analyzer analyzer ,Directory spellDictionaryDir)
    {
        super(defaultField, indexDirectory, maxHits, op, analyzer);
        this.spellDictionaryDir = spellDictionaryDir;
    }

    /**
     * Data una singola parola il sistema costruisce al massimo nsuggesion Query 
     * contenenti parole similiari a quella proposta in input.
     * @param word - Input sul quale effettuare l'elaborazione
     * @param nsuggestion - Numero massimo di suggerimenti e quindi di query da restituire
     * @return - Query per le parole similiari a word
     */    
    public List<Query> suggest(String word, int nsuggestion)
    {
        List<Query> ret = new ArrayList<Query>();
        try
        {
            //Inizializzazione oggetto chiave del metodo!
            SpellChecker spellChecker = new SpellChecker(spellDictionaryDir);
            //Se la query string esiste nel dizionario vuol dire che non ha 
            //senso cercare un suggerimento!
            if (!spellChecker.exist(word))
            {
                //ricerca di parole similiari all'interno del dizionario
                String[] similiarWords = spellChecker.suggestSimilar(word, nsuggestion);
                if (similiarWords.length != 0)
                {
                    for (String s : similiarWords)
                    {
                        //date le parole similiari si procede alla costruzione delle query
                        ret.add(new TermQuery(new Term(defaultField, s)));
                    }                    
                }
            }
        } catch (IOException ex)
        {
            Logger.getLogger(SuggestAndSeachEngine.class.getName()).log(Level.SEVERE, null, ex);
        } finally
        {
            return ret;
        }
    }
}

Utilizzare il codice per una ricerca e relativo suggerimento

Vi invito a leggere direttamente il codice e i relativi commenti.
Il meccanismo è abbastanza semplice: Si richiede un input all'utente, si inizializza il motore di ricerca e si richiedono dei risultati.
Se il numero dei risultati è al di sotto di 1, allora si procede con la creazione di query alternative utilizzando lo spellChecking.
Una di queste query, se presente, viene data nuovamente in pasto la motore di ricerca per conseguire dei risultati basati su di essa.

//Catturiamo una stringa per effettuare la ricerca
String sentence = JOptionPane.showInputDialog("sentence");

//Inizializzazione motore di ricerca, indicando
//il campo su cui ricercare, la Directory dell'indice, il tipo di operatore, l'Analyzer e la directory del dizionario.
SuggestAndSeachEngine se = new SuggestAndSeachEngine(fieldName, dir, 10, QueryParser.Operator.OR, new SimpleAnalyzer(), dirDict);

//Richiesta risultati relativi alla query catturata con l'input dialog.
SearchResult[] res = se.search(sentence);

//Stampa a video risultati
int i = 1;
if (res != null)
{
  for (SearchResult s : res)
  {
     System.out.println(i++ + "° " + s.getDoc().get("descrizione") + " - " + s.getScore());
  }
}
//Se il numero di risultati ottenuti è meno di 1 si procede con lo spell checking
if (res.length < 1)
{
        //Richiesta query di suggerimento
List<Query> suggested = se.suggest(sentence, 2);
//stampa query suggerite
System.out.println(suggested);
//Se esiste almento un suggerimento..
if (suggested != null && suggested.size() > 0)
{
//effettuiamo una nuova ricerca con la prima query suggerita
res = se.search(suggested.get(0));
i = 1;
//stampa risultati con la nuova query.
for (SearchResult s : res)
{
  System.out.println(i++ + "° " + s.getDoc().get("descrizione") + " - " + s.getScore());
}
}
}
 
Migliorare la Qualità dei Risultati

Al fine di migliorare la qualità dei risultati è possibile avvalersi di un suggerimento che tiene conto della frequenza con la quale appare il suggerimento nell'indice (non nel dizionario).

Utilizzando

public String[] suggestSimilar(String arg0, int arg1, IndexReader arg2, String arg3, boolean arg4) throws IOException


Questo overloading è possibile avvalersi di due criteri per l'ottenimento dei risultati.


  1. La distanza di editing tra l'input e il suggerimento
  2. La popolarità del suggerimento all'interno dell'indice originale (ribadisco: non nel dizionario).



E le query Composte?

Per semplicità fin ora si è considerato un suggerimento su una singola parola, il metodo suggest infatti è in grado di fornire suggerimenti su una query composta da una singola parola.

Adesso cercherò di spiegare come generare un suggerimento su query composte da diverse parole 
(es. input:program jav - suggerimento: programma java)

L'idea di base è quella di suddividere l'input in diversi token e ottenere il suggerimento per ciascuno di essi.
La suddivisione in token potrebbe essere fatta con i canonici metodi offerti dalle librerie standard (es. StringTokenizer o metodo String.split(regx)) ma ciò non tiene conto delle modalità di tokenizzazione utilizzate in fase di creazione indice e ricerca.
Infatti in queste fasi si è fatto uso dell'Analyzer, che ha proprio il compito di gestire la suddivisione delle stringhe composte in token tenendo conto di diversi criteri (più o meno complessi a seconda della classe specifica scelta (SimpleAnalyzer, StandardAnalyzer etc.))

Quindi la suddivisione in token presuppone l'uso dell'Analyzer che attraverso il metodo

public abstract TokenStream tokenStream(String arg0, Reader arg1)


Ci consente di navigare tra i token ottenuti.


Bene, visto però che il metodo suggest realizzato nella classe SuggestAndSearchEngine ritorna diverse Query(diversi suggerimenti), è opportuno creare un metodo suggest in grado di ottenere un solo suggerimento per input.

Tale metodo sarà di supporto al metodo finale suggestComposite la cui realizzazione è obiettivo del paragrafo.

Ne incollo l'implementazione (Da inserire nella classe SuggestAndSearchEngine )


/**

 * Data una singola parola è in grado di generare il suggerimento

 * @param word - input su cui generare il suggerimento

 * @return - Il suggerimento, esso viene proposto sottoforma di Term in modo

 * da permettere la costruzione di Query composte da più termini

 */

    public Term suggest(String word)

    {

        Term term = null;

        try

        {

            //Inizializzazione oggetto chiave del metodo!

            SpellChecker spellChecker = new SpellChecker(spellDictionaryDir);

            //Se la query string esiste nel dizionario vuol dire che non ha 

            //senso cercare un suggerimento!

            if (!spellChecker.exist(word))

            {

                //ricerca di parole similiari all'interno del dizionario

                IndexReader indexReader = IndexReader.open(indexDirectory);

                String[] similiarWords = spellChecker.suggestSimilar(word, 1,indexReader,defaultField,true);

                //String[] similiarWords = spellChecker.suggestSimilar(word, 1);

                if (similiarWords.length != 0)

                {

                    //data la parole similiari si procede alla costruzione del Term

                    term = new Term(defaultField, similiarWords[0]);

                }

            }

        } catch (IOException ex)

        {

            Logger.getLogger(SuggestAndSeachEngine.class.getName()).log(Level.SEVERE, null, ex);

        } finally

        {

            return term;

        }

    }



Anche il metodo suggestComposite per semplicità fornisce un solo suggerimento per input.

Ecco la firma del metodo    


public Query suggestComposite(String queryString)


Tale metodo quindi, 

  • Suddivide in token l'input
  • Per ogni token richiede un suggerimento
  • Concatena i vari suggerimenti all'interno di una query (tale query è una PhraseQuery, addatta alla composizione con più termini)

Implementazione e relativi commenti:

    /**
     * Data una query composta da diverse parole il metodo restituisce un 
     * oggetto Query contenente la query di suggerimento
     * @param queryString - input composto anche da diverse parole
     * @return - Query suggerita
     */
    public Query suggestComposite(String queryString)
    {
        //Predisposizione di un oggetto Query contente il suggerimento
        PhraseQuery query = new PhraseQuery();

        //Inizializzazione stream dei token considerando la stringa di input
        //e il campo di default su cui basarsi
        TokenStream tstream = analyzer.tokenStream(defaultField, new StringReader(queryString));        
        Token t = new Token();

        try
        {
            Term suggestedTerm = null;
            while ((t = tstream.next(t)) != null)
            {
                //Richiesta suggerimento per ogni token
                suggestedTerm = suggest(t.term());
                if (suggestedTerm != null)
                {
                    //concatenazione suggerimento all'intero della query
                    query.add(suggestedTerm);
                }
            }
        } catch (Exception e)
        {
            Logger.getLogger(this.getClass().getName()).log(Level.INFO, "Token Stream Exception ", e.getMessage());            
        } finally
        {
            if (tstream != null)
            {
                try
                {
                    tstream.close();
                } catch (IOException ex)
                {
                    Logger.getLogger(SuggestAndSeachEngine.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
            return query;
        }
    }

Conclusione

Lucene è una libreria molto potente alla quale è possibile associare feature molto interessanti come quella dello spellChecking