Le
funzioni di trattamento di dati testuali sono una parte
importante in un numero di applicazioni sempre più
alto; dal CMS al più semplice software gestionale
è infatti necessario permettere all'utente di
interagire con delle risorse testuali. Da alcuni anni
esiste un componente open-source che permette di realizzare
soluzioni estremamente avanzate, Apache Lucene.
DBMS
vs Full-Text
Possiamo evidenziare con grande efficacia le possibilità
offerte da una soluzione full-text partendo da un confronto
con le soluzioni presenti nei sistemi maggiormente utilizzati
dagli sviluppatori, le basi di dati relazionali.
Di solito siamo abituati a lavorare con i DBMS basati
sul modello relazionale che, come è ben noto,
prevede una rappresentazione dei dati in tabelle, le
cui righe rappresentano gli elementi inseriti e le cui
colonne sono dei campi cui viene assegnato un valore;
all'interno di queste tabelle possiamo effettuare operazioni
di selezione ed ordinamento, basandoci sui valori di
alcune colonne, che contengono valori numerici (o pseudonumerici).
Pensiamo, ad esempio, ad un sito Web di carattere giornalistico:
il sistema di presentazione molto spesso si avvarrà
di una base di dati, partendo dalla quale selezionerà
gli articoli da presentare ai visitatori sulla base
dell'identificativo con cui sono salvati nel database,
per selezionare gli ultimi elementi pubblicati, oppure
sulla base di colonne che specificano la categoria della
notizia (per fornire più pagine, ad esempio una
dedicata all'economia ed una allo sport).
Come è possibile notare dalle considerazioni
appena fatte il database relazionale serve perlopiù
a garantire la persistenza dei dati testuali, mentre
poco può fare per quanto riguarda la loro elaborazione.
Volendo eseguire invece una ricerca all'interno dei
testi, per trovare quelli che contengono un certo termini,
possiamo utilizzare l'operatore SQL 'LIKE'; questa soluzione
presenta però notevoli limiti, in termini di
efficienza e di prestazioni.
Per fornire all'utente un sistema più avanzato
di ricerca quasi tutti i DBMS forniscono dei moduli
o delle estensioni full-text: attivando queste funzionalità
è possibile accedere a servizi avanzati di ricerca
testuale. Ad esempio in MySql, una volta definita l'indicizzazione
full-text è possibile eseguire interrogazioni
mediante le parole chiave MATCH() ed AGAINST().
Esistono però altre soluzioni che consentono
di estendere le proprie applicazioni per fornire servizi
avanzati di analisi testuale: tra questi prodotti software
Apache Lucene è uno dei più apprezzati
e potenti.
Ma quali sono i motivi concreti per cui può essere
opportuno appoggiarsi a prodotti specifici per il trattamento
di testi?
Le ragioni sono molteplici, ma preferisco soffermarmi
sulle due che ritengo più importanti. In primo
luogo molto spesso ci si può trovare a confrontarsi
con banche dati testuali che contengono fino a diverse
decine di migliaia di documenti: in questa situazione
le prestazioni ottenute tramite un motore ad hoc sono
nettamente superiori rispetto a quelle raggiungibili
con le espansioni full-text dei DBMS commerciali. Come
esperienza personale posso dire di aver visto situazioni
in cui uno dei più diffusi DBMS enterprise ha
mostrato evidenti limiti a livello di prestazioni, soprattutto
se la base documentale si caratterizza per frequenti
aggiunte e cancellazioni di documenti (comportando problemi
di organizzazione ed ottimizzazione alla base dati).
L'altro motivo, come è prevedibile, risiede nelle
possibilità offerte da una soluzione full-text
pura come Lucene: ricerche complesse con operatori booleani,
ricerche all'interno di una specifica frase (i termini
della query devono apparire vicini o ad una distanza
massima tra loro), ricerche con wildcard e così
via.
Da aggiungere a questi vantaggi è sicuramente
anche la licenza open source: non solo un risparmio
economico, ma soprattutto la possibilità di accedere
ad una comunità di utenti cui appartengono anche
gli stessi sviluppatori del progetto. In questa maniera
è possibile ottenere, semplicemente e senza dispendio,
un aiuto (quasi una consulenza) sulle problematiche
che si manifestano durante lo sviluppo di applicazione.
Una
panoramica su Lucene
Una base di dati testuale Lucene viene chiamata, con
un termine che può essere fuorviante, indice;
riprendendo il confronto con i database relazionali
possiamo dire che ogni indice contiene una sola tabella
le cui righe altro non sono che i documenti da noi inseriti.
L'indice è normalmente contenuto in una cartella
del filesystem, ma esistono altre possibilità,
quali la creazione di un indice in RAM, utilizzato soprattutto
per aumentare le prestazioni (attenzione però
al limite imposto dalle dimensioni della memoria!).
All'interno dell'indice vengono inseriti documenti (istanze
di org.apache.lucene.document.Document), a loro volta
divisi in campi o colonne (Fields); come nei database
esistono vari tipi di campi, anche se in questo caso
il tipo di dati è lo stesso per ogni colonna
(o quasi, in realtà esistono alcune piccole differenze).
I campi testuali veri e propri (come il titolo ed il
corpo di un documento) verranno indicizzati , mentre
altri campi, ad esempio l'identificatore del documento
all'interno di un database, saranno solo salvati, per
essere utilizzati come riferimento.
In pratica non sarà possibile eseguire ricerche
in questi campi, ma solo accedere al loro valore, nei
documenti ritornati da una query eseguita su un altro
campo. Lo scenario di utilizzo prevede infatti nella
quasi totalità dei casi l'affiancamento della
base di dati full-text ad una relazionale.
Così facendo utilizzeremo Lucene per fornire
un sistema di ricerca (e per presentarne i risultati),
mentre quando l'utente desiderà visualizzare
un documento (ad esempio cliccando sul titolo) accederemo
ad una pagina popolata dal database relazionale; sfrutteremo
Lucene per l'efficienza delle ricerche ed il DBMS per
la maggiore velocità nella selezione di un singolo
elemento (aiutato in questo magari da un opportuno meccanismo
di caching, attivo e passivo).
Inserire
un documento nell'indice
L'inserimento di un documento all'interno dell'indice
comporta una scansione del testo, per individuare le
parole presenti; tale procedimento prende il nome di
analisi ed ovviamente è dipendente dalla lingua
del testo. Lucene fornisce un analizzatore standard,
mirato sulle necessità delle lingue occidentali
più diffuse; in questo articolo verrà
utilizzato, ma per noi italiani è necessaria
una piccola precisazione: in questa maniera le parole
apostrofate non verranno riconosciute correttamente
(ad esempio un'ancora non verrà diviso in un
ed ancora, ma resterà un unico termine).
Una volta che il documento (o meglio, i suoi campi)
sono stati indicizzati questi sono disponibili per l'esecuzione
di ricerche. Il
Listato 1 mostra come sia possibile aggiungere un documento
all'indice, prendendo come scenario un ipotetico sistema
CMS Web.
/*
Listato1 */
import org.apache.lucene.index;
protected
void addDocument(String aPath, String theSubject, String
theBody, int anId){
//Apro l'oggetto attraverso il quale accedo all'indice
IndexWriter writer = new IndexWriter(aPath, new StandardAnalyzer(),
true);
//Creo un nuovo documento
org.apache.lucene.document.Document aDoc=new org.apache.lucene.document.Document
();
//Aggiungo i campi opportuni
aDoc.add(Field.Text("subject", theSubject));
aDoc.add(Field.Text("body", theBody));
aDoc.add(Field.UnIndexed("id", anId));
//Salvo il documento nell'indice
writer.addDocument(aDoc);
//eseguo un'ottimizzazione dell'indice
writer.optimize();
//Infine chiudo l'accesso in scrittura all'indice
writer.close();
}
Le
operazioni avvengono attraverso un'istanza di org.apache.lucene.index.IndexWriter.
Il documento contiene tre campi, il titolo, il corpo
del testo e l'identificativo numerico del documento
all'interno della base di dati relazionale.
Come abbiamo già detto, i campi possono essere
trattati in maniera diversa, a seconda della funzione
che rivestono. I campi testuali sono normalmente analizzati
(scomposti in una lista di termini) e quindi indicizzati,
mentre il campo identificatore (che contiene solamente
un riferimento al corrispondente elemento della base
di dati relazionale) deve essere solamente salvato all'interno
dell'indice, per essere usato come riferimento tra l'indice
full-text e una base di dati relazionale.
Nel listato1, come possiamo vedere utilizziamo i metodi
statici della classe Field per creare i campi adatti
alle nostre esigenze (Field.Text(), Field.UnIndexed()).
Il
listato 2, mostra uno scenario differente, in cui l'indicizzazione
avviene in una directory del filesystem.
/**
Listato 2 **/
import java.io;
import org.apache.lucene.index;
public
static void index(String aPath, String indexPath) throws
IOException {
File file=new File(aPath);
if (!file.exists() || !file.isDirectory()) {
throw new IOException("Verificare l'esistenza della
directory " + file + "!");
}
IndexWriter writer = new IndexWriter(indexPath, new
StandardAnalyzer(), true);
indexDirectory(writer, file);
writer.close();
}
private
static void indexDirectory(IndexWriter writer, File
dir) throws IOException {
File[] files = dir.listFiles();
for (int i=0; i < files.length; i++) {
File f = files[i];
if (f.isDirectory()) {
//Esplora la sottodirectory
indexDirectory(writer, f);
}
else if (f.getName().endsWith(".txt")) {
indexFile(writer, f);
}
}
}
Un'interessante
osservazione, relativa sia al primo che al secondo listato,
ci consente di notare che in entrambi è presente
la seguente istruzione:
writer.optimize();
L'indicizzazione
di documenti è ottimizzata da Lucene, in maniera
tale da aumentare le prestazioni senza però interferire
con eventuali operazioni contemporanee di ricerca. Il
metodo IndexWriter.optimize() serve appunto a riorganizzare
l'indice, per aumentare le prestazioni delle future
ricerche (e scritture). Nel primo caso, avendo ipotizzato
di trovarci in un CMS, possiamo supporre che gli inserimenti
di nuovi documenti siano relativamente sporadici, mentre
nel secondo ci troviamo di fronte ad una sorta di operazione
batch, pertanto effettuiamo l'ottimizzazione solamente
al termine di tutti gli inserimenti (o di una tranche
di questi).
La gestione dell'accesso concorrente (in scrittura e
lettura, quindi indicizzazione e ricerca) ad un indice
Lucene è piuttosto articolata, anche se di facile
comprensione: è possibile un solo accesso in
scrittura, ma che può essere condiviso da vari
processi logici (o thread) di indicizzazione, mentre
non sono poste limitazioni alle ricerche contemporanee.
La
ricerca
E' possibile eseguire ricerche sull'indice in due modalità
distinte: utilizzando direttamente le API di ricerca
oppure utilizzando una sorta di linguaggio di interrogazione.
Il primo metodo prevede l'utilizzo delle classi che
derivano da Query e che si trovano nel package org.apache.lucene.search,
per brevità aggiungerò solo alcune note
su queste classi più avanti, per informazioni
più approfondite si può leggere la documentazione.
Nel listato 3 possiamo trovare un esempio di come sia
possibile utilizzare la classe QueryParser per eseguire
una semplice ricerca. La sintassi della stringa di ricerca
da passare al metodo QueryParser.parse() è disponibile
in questa pagina.
/**
Listato 3 **/
public void search(String aDirectory, String q) throws
Exception{
IndexSearcher searcher = new IndexSearcher(aDirectory);
Query query = QueryParser.parse(q, "body",
new StandardAnalyzer());
//questo metodo esegue la ricerca nell'indice
Hits hits = searcher.search(query);
System.out.println("La ricerca ha restituito "+
hits.length() +" elementi.");
int i, c;
c= hits.length();
//ciclo sui risultati della ricerca
for (i = 0; i < c;i++) {
Document doc = hits.doc(i);
System.out.println(doc.get("subject") + "
- Score: " + hits.score(i));
}
//chiudo l'accesso in lettura all'indice
searcher.close();
}
L'accesso
in lettura all'indice avviene attraverso la classe IndexSearcher.
Il metodo QueryParser.parse() ci permette di ottenere
direttamente un oggetto Query; è necessario passare
a questo metodo la stringa di ricerca (che naturalmente
può essere composta da più parole), un
campo in cui eseguire la ricerca ed un analizzatore
(usato per trattare la query string, che sostanzialmente
è elaborata come se fosse un documento da inserire
nell'indice).
Il metodo IndexSearcher.search() restituisce un oggetto
di tipo Hits, che contiene i risultati. Il codice mostra
come accedere al titolo dei documenti restituiti ed
anche come stampare un punteggio che rappresenta in
forma numerica l'attinenza del documento alla query.
Questo punteggio è calcolato sulla base di un'analisi
semantica della query, cercando cioè di interpretare
l'importanza che hanno i termini nel fornire il significato
del testo.
Per evitare di incappare in errori abbastanza fastidiosi
e che potrebbero rivelarsi di difficile interpretazione
è bene avere presente che i documenti all'interno
della collezione Hits non sono più accessibili
quando si è chiamato il metodo IndexSearcher.close(),
pertanto è necessario estrarre i dati necessari
alla presentazione dei risultati prima di procedere
a questa chiamata.
Naturalmente utilizzando le API è possibile eseguire
ricerche più complesse, ad esempio per termini
vicini tra loro(PhraseQuery), oppure utilizzando wildcards.
Per un'introduzione a queste funzionalità rimando
ai documenti presenti nella bibliografia in fondo all'articolo.
Conclusioni
La necessità di trattare in maniera opportuna
dati testuali appartiene ormai alla maggior parte dei
progetti software, soprattutto grazie all'enorme sviluppo
di Internet. In questo articolo è stato presentato
un componente estremamente avanzato, che permette di
realizzare con facilità soluzioni anche complesse.
Per testimoniare la potenza di Lucene posso aggiungere
che su questa base è stato costruito un progetto,
Nutch, che ha creato un'infrastruttura tecnologica assolutamente
in grado di competere con quelle dei più diffusi
motori di ricerca commerciali.
Per rimanere nel campo delle soluzioni che lo sviluppatore
si trova a dover realizzare più frequentemente
segnalo, ad esempio, la possibilità di estrarre
da un indice documenti in base alla similitudine: in
questa maniera un sistema di commercio elettronico potrà
mostrare prodotti simili a quello correntemente visualizzato
dall'utente (sulla base delle descrizione), oppure consultando
una banca dati sarà possibile estrarre articoli
simili per significato ad un testo scelto dall'utente.
Inoltre è possibile realizzare sistemi di correzione
ortografica, simili a quelli presenti negli elaboratori
di testi.
Naturalmente lo scopo di questo articolo non è
quello di mostrare le soluzioni più avanzate,
ma avvicinare all'utilizzo di tale tecnologia. Credo
che a questo punto per il lettore non sarà difficile
realizzare le prime applicazioni basate su Lucene, magari
avvalendosi dei documenti riportati nella bibliografia.
Tra questi mi sento di raccomandare la lettura e la
frequentazione della mailing list general@lucene.apache.org,
attraverso la quale è possibile accedere ad esempi
o best practices.
Bibliografia
Lucene, http://lucene.apache.org
Lucene Wiki, http://wiki.apache.org/jakarta-lucene
E. Hatcher, O. Gospodnetic, Lucene in Action, Manning,
2004
Mailing List general@lucene.apache.org (archivi su http://mail-archives.apache.org/mod_mbox/lucene-general/)
Lorenzo
Viscanti è un ingegnere informatico e si
occupa soprattutto di problematiche relative al trattamento
di testi, quali realizzazioni di motori di ricerca,
text clustering e text mining.
|