Apache Lucene: ricerca libera per il vostro sito web
Se venisse chiesto di pensare a un motore di ricerca, la quasi totalità delle persone penserebbe a Google. Anche i gestori di siti web utilizzano Google sotto forma di Custom Search Engine (CSE) per fornire agli utenti una funzione di ricerca dei propri contenuti rapida e di semplice utilizzo. Ma Google non è l’unico servizio a disposizione – per molti gestori di siti web certamente anche non il migliore – per fornire ai propri visitatori una ricerca completa dei testi. Lucene, progetto open source e quindi gratuito di Apache, rappresenta in questo caso un’ottima alternativa.
Numerose sono le aziende che, online o offline, hanno integrato Apache Lucene. Wikipedia lo ha utilizzato fino a pochi anni fa come funzione di ricerca, mentre ora ha deciso di affidarsi a Solr, altro software basato su Lucene; anche la ricerca su Twitter è completamente targata Lucene. Dal progetto avviato da Doug Cutting a fine degli anni 90 come hobby è stato sviluppato un software utilizzato giornalmente da milioni di persone.
Che cos’è Lucene?
Lucene è una libreria di programmi pubblicata da Apache Software Foundation. È open source e gratuita e può essere utilizzata e modificata dagli utenti. All’inizio Lucene era scritto interamente in Java, ma ora ci sono anche porte in altri linguaggi di programmazione. Apache Solr ed Elasticsearch sono potenti estensioni che ampliano la funzione di ricerca con ancora più possibilità.
Lucene è una ricerca full-text: ciò significa in parole povere che un programma ricerca una serie di documenti testuali per uno o più termini definiti dall’utente. È perciò subito evidente che Lucene non è utilizzato esclusivamente nel contesto del World Wide Web, anche se le funzioni di ricerca sono qui onnipresenti. Lucene può infatti essere utilizzato anche per archivi, librerie o anche per PC desktop domestici. Lucene cerca non solo documenti HTML, ma funziona per esempio anche con e-mail o file PDF.
Decisivo per la ricerca è un indice, cuore del software Apache: qui sono salvati tutti i termini di tutti i documenti. Chiamato Inverted Index, fondamentalmente è soltanto una tabella in cui per ogni termine viene memorizzata la posizione corrispondente. Per costruire un simile indice occorre innanzitutto un’estrazione: ciò significa che tutti i termini devono essere estratti da tutti i termini e salvati nell’indice. Lucene offre agli utenti la possibilità di configurare questa estrazione individualmente. Gli sviluppatori decidono quali campi vogliono includere nell’indice. Ma per capire meglio occorre fare un passo indietro.
Gli oggetti con cui lavora Lucene sono documenti in qualsiasi forma. Tuttavia i documenti stessi contengono, dal punto di vista di Lucene, dei campi. Essi sono ad esempio il nome dell’autore, il titolo del documento o il nome del file stesso. Ogni campo ha come nome univoco un valore. Ad esempio, il campo title potrebbe avere il valore “Istruzioni per l’uso di Apache Lucene”. Quando si crea l’indice è quindi possibile decidere quali metadati si desidera registrare.
Quando si indicizzano i documenti avviene anche una cosiddetta tokenizzazione. Dal punto di vista di una macchina un documento è inizialmente una raccolta di informazioni. Anche se vi allontanate dal livello dei bit e vi rivolgete al contenuto leggibile dagli esseri umani, un documento consiste in una stringa di caratteri: lettere, punteggiatura, spazi.
Da questo set di dati è possibile utilizzare la tokenizzazione per creare segmenti, i termini (principalmente parole singole) che è possibile cercare. Il modo più semplice per eseguire tale tokenizzazione funziona con il metodo “white space”: un termine finisce quando c’è uno spazio bianco. Tuttavia tale metodo non porta allo scopo desiderato se i termini fissi consistono di diverse parole, per esempio “Notte santa”. Per questo vengono utilizzati dizionari che possono anche essere implementati nel codice Lucene.
Quando analizza i dati di cui la tokenizzazione è una parte, Lucene esegue anche una normalizzazione. Ciò significa che i termini sono portati in una forma standardizzata, nella quale ad esempio tutte le lettere maiuscole vengono scritte comunque in minuscolo. Inoltre Lucene crea uno smistamento che funziona tramite algoritmi, ad esempio utilizzando il metodo TF-IDF. In qualità di utente, probabilmente desiderate ottenere per primi i risultati più rilevanti o più recenti, come rendono possibile gli algoritmi del motore di ricerca.
Per fare in modo che gli utenti possano trovare qualcosa devono inserire un termine di ricerca in una riga di testo. Nel contesto di Lucene il termine o i termini sono chiamati “query”, che in inglese significa “richiesta”. Essa indica che l’input non deve consistere solo di una o più parole, ma può anche contenere modificatori come AND, OR, + e -, nonché metacaratteri. QueryParser, una classe all’interno della libreria del programma, traduce l’input in una richiesta di ricerca specifica per il motore di ricerca. Inoltre gli sviluppatori di QueryParser hanno la possibilità di modificare le opzioni di configurazione, così il parser può essere configurato per soddisfare le esigenze dell’utente.
Ciò che ha reso Lucene completamente nuova alla sua comparsa è stata l’indicizzazione incrementale: prima di Lucene era possibile soltanto un batch indexing con il quale si implementavano solo indici completi, mentre con l’indicizzazione incrementale ora si può aggiornare l’indice. Le singole voci possono essere aggiunte o rimosse.
Lucene vs Google & Co?
La domanda sorge spontanea: perché costruire un proprio motore di ricerca quando ci sono Google, Bing e altri search engine? Naturalmente non è facile rispondere a questa domanda, perché dopotutto si tratta sempre di considerare le esigenze individuali di chi utilizza queste applicazioni.
Di sicuro quando parliamo di Lucene come motore di ricerca si tratta di una descrizione semplificata. Di fatto si tratta di una Information Retrieval Library, ovvero un sistema in cui è possibile trovare informazioni. È una caratteristica comune anche a Google e altri motori di ricerca, tuttavia questi sono limitati alle informazioni che provengono dal World Wide Web, mentre Lucene può invece essere applicato a qualsiasi scenario e configurato conformemente ai vostri scopi. Per esempio potete utilizzare Lucene anche per altre applicazioni.
Apache Lucene è, a differenza dei web search engine, un software non finito: per beneficiare delle possibilità del sistema è necessario prima programmarlo. Vi mostreremo i primi passi per farlo nel nostro tutorial di Lucene.
Lucene, Solr, Elasticsearch: quali sono le differenze?
Soprattutto i principianti si chiedono dove stia la differenza tra Apache Lucene da una parte e Apache Solr ed Elasticsearch dall’altra. Gli ultimi due sono basati su Lucene: il vecchio prodotto è un motore di ricerca puro, mentre Solr ed Elasticsearch sono server di ricerca completi che estendono ulteriormente le capacità di Lucene.
Se avete solo bisogno di una ricerca per il vostro sito web, probabilmente vi troverete meglio utilizzando Solr o Elasticsearch, due sistemi progettati specificamente per l’uso sul web.
Apache Lucene: tutorial
Lucene è basato su Java nella versione originale, il che consente di utilizzarlo per diverse piattaforme sia online che offline, se si sa come farlo. Spiegheremo passo per passo come costruire il vostro motore di ricerca con Apache Lucene.
In questo tutorial parleremo di Lucene basato su Java. Il codice è stato testato con Lucene versione 7.3.1 e JDK versione 8. Lavoriamo con Eclipse su Ubuntu. I singoli passaggi possono essere diversi se si utilizzano altri sistemi operativi e ambienti di sviluppo.
Installazione
Per poter lavorare con Apache Lucene è necessario aver installato Java. Come Lucene, potete scaricare gratuitamente anche il Java Development Kit (JDK) sul sito web ufficiale di Oracle. Dovreste anche installare un ambiente di sviluppo che vi permetta di scrivere il codice per Lucene. Molti sviluppatori si affidano a Eclipse, ma ci sono molte altre offerte open source. Infine è possibile scaricare Lucene dalla pagina del progetto. Selezionate la core version del programma.
Non è necessario installare Lucene: basta decomprimere il download in una posizione desiderata. Quindi si crea un nuovo progetto in Eclipse o in un altro ambiente di sviluppo e si aggiunge Lucene come library. Per questo esempio utilizzeremo tre librerie che sono tutte incluse nel pacchetto di installazione:
- …/lucene-7.3.1/core/lucene-core-7.3.1.jar
- …/lucene-7.3.1/queryparser/lucene-queryparser-7.3.1.jar
- …/lucene-7.3.1/analysis/common/lucene-analyzers-common-7.3.1.jar
Se utilizzate una versione diversa o se avete modificato la struttura della cartella, dovete modificare di conseguenza queste indicazioni.
Per comprendere i seguenti passaggi sono necessarie delle conoscenze di base di Java e di programmazione in generale. Se avete delle conoscenze di base di questo linguaggio di programmazione, lavorare con Lucene è un ottimo modo per sviluppare le vostre competenze.
Indicizzazione
Il nucleo di un motore di ricerca basato su Lucene è l’index, senza il quale non si possono dare funzioni di ricerca. Pertanto il primo passo è proprio quello di creare una classe Java per l’indicizzazione.
Ma prima di costruire il meccanismo di indicizzazione creeremo due classi che vi aiuteranno per i passi successivi. Sia la classe indice che la classe di ricerca si basano su queste due.
package tutorial;
public class LuceneConstants {
public static final String CONTENTS = "contents";
public static final String FILE_NAME = "filename";
public static final String FILE_PATH = "filepath";
public static final int MAX_SEARCH = 10;
}
Queste informazioni si riveleranno importanti più avanti, quando si tratterà di stabilire i campi in modo preciso.
package tutorial;
import java.io.File;
import java.io.FileFilter;
public class TextFileFilter implements FileFilter {
@Override
public boolean accept(File pathname) {
return pathname.getName().toLowerCase().endsWith(".txt");
}
}
In questo modo implementiamo un filtro che legge correttamente nei nostri documenti. A questo punto vi rendete già conto che il nostro motore di ricerca funzionerà solo per i file txt. Questo semplice esempio ignora tutti gli altri formati.
All’inizio di una classe per prima cosa si importano altre classi che possono essere già parte della vostra installazione Java, altrimenti sono disponibili attraverso l’integrazione di librerie esterne.
Ora create la classe appropriata per l’indicizzazione.
package tutorial;
import java.io.File;
import java.io.FileFilter;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Paths;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
public class Indexer {
private IndexWriter writer;
public Indexer(String indexDirectoryPath) throws IOException {
Directory indexDirectory =
FSDirectory.open(Paths.get(indexDirectoryPath));
StandardAnalyzer analyzer = new StandardAnalyzer();
IndexWriterConfig iwc = new IndexWriterConfig(analyzer);
writer = new IndexWriter(indexDirectory, iwc);
}
public void close() throws CorruptIndexException, IOException {
writer.close();
}
private Document getDocument(File file) throws IOException {
Document document = new Document();
TextField contentField = new TextField(LuceneConstants.CONTENTS, new FileReader(file));
TextField fileNameField = new TextField(LuceneConstants.FILE_NAME,
file.getName(),TextField.Store.YES);
TextField filePathField = new TextField(LuceneConstants.FILE_PATH,
file.getCanonicalPath(),TextField.Store.YES);
document.add(contentField);
document.add(fileNameField);
document.add(filePathField);
return document;
}
private void indexFile(File file) throws IOException {
System.out.println("Indexing "+file.getCanonicalPath());
Document document = getDocument(file);
writer.addDocument(document);
}
public int createIndex(String dataDirPath, FileFilter filter)
throws IOException {
File[] files = new File(dataDirPath).listFiles();
for (File file : files) {
if(!file.isDirectory()
&& !file.isHidden()
&& file.exists()
&& file.canRead()
&& filter.accept(file)
){
indexFile(file);
}
}
return writer.numDocs();
}
}
Nel corso del codice vengono effettuati diversi passaggi: avete impostato IndexWriter utilizzando StandardAnalyzer. Lucene offre diverse classi di analisi che possono essere trovate nella libreria corrispondente.
Nella documentazione ufficiale di Apache Lucene trovate tutte le classi contenute nel download.
Inoltre il programma si addentra nei file e imposta campi per l’indicizzazione. Alla fine del codice vengono creati i file index.
Funzione di ricerca
Naturalmente l’indice da solo non vi porta a nulla. Dovete infatti ancora stabilire una funzione di ricerca.
package tutorial;
import java.io.IOException;
import java.nio.file.Paths;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.classic.QueryParser;
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 org.apache.lucene.store.FSDirectory;
public class Searcher {
IndexSearcher indexSearcher;
QueryParser queryParser;
Query query;
public Searcher(String indexDirectoryPath)
throws IOException {
Directory indexDirectory =
FSDirectory.open(Paths.get(indexDirectoryPath));
IndexReader reader = DirectoryReader.open(indexDirectory);
indexSearcher = new IndexSearcher(reader);
queryParser = new QueryParser(LuceneConstants.CONTENTS,
new StandardAnalyzer());
}
public TopDocs search( String searchQuery)
throws IOException, ParseException {
query = queryParser.parse(searchQuery);
return indexSearcher.search(query, LuceneConstants.MAX_SEARCH);
}
public Document getDocument(ScoreDoc scoreDoc)
throws CorruptIndexException, IOException {
return indexSearcher.doc(scoreDoc.doc);
}
}
Due delle classi importate da Lucene sono particolarmente importanti all’interno del codice: IndexSearcher e QueryParser. Mentre il primo cerca nell’indice creato, QueryParser è responsabile del trasferimento della query di ricerca per elaborare informazioni comprensibili.
Ora avete sia una classe per l’indicizzazione sia una per cercare all’interno dell’indice, ma non potete ancora fare una ricerca concreta con queste due: per farlo avete bisogno di una quinta classe.
package tutorial;
import java.io.IOException;
import org.apache.lucene.document.Document;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
public class LuceneTester {
String indexDir = "/home/Index/";
String dataDir = "/home/Data/";
Indexer indexer;
Searcher searcher;
public static void main(String[] args) {
LuceneTester tester;
try {
tester = new LuceneTester();
tester.createIndex();
tester.search("YourSearchTerm");
} catch (IOException e) {
e.printStackTrace();
} catch (ParseException e) {
e.printStackTrace();
}
}
private void createIndex() throws IOException {
indexer = new Indexer(indexDir);
int numIndexed;
long startTime = System.currentTimeMillis();
numIndexed = indexer.createIndex(dataDir, new TextFileFilter());
long endTime = System.currentTimeMillis();
indexer.close();
System.out.println(numIndexed+" File indexed, time taken: "
+(endTime-startTime)+" ms");
}
private void search(String searchQuery) throws IOException, ParseException {
searcher = new Searcher(indexDir);
long startTime = System.currentTimeMillis();
TopDocs hits = searcher.search(searchQuery);
long endTime = System.currentTimeMillis();
System.out.println(hits.totalHits +
" documents found. Time :" + (endTime - startTime));
for(ScoreDoc scoreDoc : hits.scoreDocs) {
Document doc = searcher.getDocument(scoreDoc);
System.out.println("File: "
+ doc.get(LuceneConstants.FILE_PATH));
}
}
}
È necessario adattare almeno tre voci in queste classi finali, poiché qui si specificano i percorsi dei documenti originali e dei file di indice nonché il termine di ricerca.
- String indexDir: qui si inserisce il percorso della cartella in cui si desidera salvare il file di indice.
- String dataDir: qui il codice sorgente si aspetta il percorso per la cartella in cui sono archiviati i documenti da cercare.
- tester.search: qui inserite il termine di ricerca.
Poiché si tratta di stringhe in tutti e tre i casi, è necessario racchiudere le espressioni tra virgolette. Anche in Windows si usano le barre normali invece dei backslash per i percorsi.
Per testare il programma copiate alcuni file di testo normale nella directory specificata come dataDir. Assicuratevi che le estensioni dei file siano “.txt”. Ora potete avviare il programma: in Eclipse, ad esempio, per farlo vi basterà cliccare sul pulsante con la freccia verde nella barra dei menu.
Il codice di programma presentato è solo un progetto dimostrativo per chiarire come funziona Lucene. Ad esempio, in questo programma manca un’interfaccia utente grafica: è necessario inserire il termine di ricerca direttamente nel codice sorgente e il risultato è disponibile solo tramite la console.
Lucene Query Syntax
I motori di ricerca, anche quelli che vi sono noti dal web, in genere non consentono soltanto di cercare un termine singolo. Alcuni metodi infatti permettono di mettere insieme le parole, cercare frasi o escludere singole parole. Naturalmente anche Apache Lucene offre queste possibilità: con Lucene Query Syntax si cercano espressioni complesse, anche in diversi campi.
- Single Term: consiste nell’immettere un termine semplice così com’è. A differenza di Google e Co., Lucene presuppone che sappiate come si scrive il termine. Se commettete errori nella digitazione, i risultati saranno negativi. Esempio: auto.
- Phrase: per frasi si intendono successioni stabilite di parole. Non sono decisivi solo i termini individuali all’interno della frase, ma anche l’ordine in cui si trovano. Esempio: "La mia auto è rossa".
- Wildcard Searches: consiste nell’utilizzare metacaratteri per sostituire uno o più caratteri nella query di ricerca. I metacaratteri possono essere utilizzati alla fine o alla metà di un termine, ma non all’inizio.
- ?: il punto interrogativo è appunto un tale metacarattere. Esempio: Au?o
- *: l’asterisco può sostituire da zero a infiniti caratteri. In questo modo può consentire la ricerca ad esempio di altre forme di un termine. Esempio: Auto* per “automobile”
- Regular Expression Searches: le espressioni regolari cercano più termini contemporaneamente, alcuni dei quali presentano somiglianze e talvolta differiscono. Contrariamente ai metacaratteri, essi definiscono esattamente quali variazioni si prendono in considerazione. Per farlo si possono usare le barre e le parentesi quadre. Esempio: /[MS]on/
- Fuzzy Searches: si utilizzano ad esempio quando desiderate una certa tolleranza di errore. Utilizzando la distanza di Damerau-Levenshtein, una formula che valuta le somiglianza, si imposta quanto ampia possa essere la deviazione. Per questo si usa il simbolo della tilde. Sono consentite distanze da 0 a 2. Esempio: Auto~1
- Proximity Searches: anche se si desidera che ci sia un’approssimazione delle frasi, utilizzate la tilde. Ad esempio potete specificare due termini di ricerca che vanno ricercati anche quadndo tra di loro sono frapposte altre 5 parole. Esempio: "Auto rossa"~5
- Range Searches: in questa forma di richiesta si cerca in una particolare area tra due termini. Sebbene tale ricerca abbia poco senso per il contenuto generale di un documento, può essere molto utile per trattare specifici campi come autori o titoli. Lo smistamento funziona secondo un ordine lessicografico. Nel chiarificare un’area inclusiva con parentesi quadre utilizzate le parentesi graffe per escludere dall’interrogazione l’area specificata dai due termini di ricerca. I due termini si delimitano con TO. Esempio: [Allende TO Borges] o {Byron TO Shelley}
- Boosting: Lucene vi dà la possibilità di fornire termini e frasi di ricerca più pertinenti di altri. Questo influenza l’ordine dei risultati. Il boosting si imposta con il circonflesso seguito da un valore. Esempio: Auto^2 rot
- Boolean Operators: potete utilizzare gli operatori logici per creare connessioni tra termini all’interno di una query di ricerca. Gli operatori devono sempre essere scritti in lettere minuscole in modo che Lucene non li valuti come normali termini di ricerca.
- AND: per un’operazione AND devono essere presenti entrambi i termini nel documento affinché appaiano come risultato. Invece dell’espressione in lettere potete anche utilizzare due e commerciali (“&”) consecutive. Esempio: Auto && rosso
- OR: l’operatore OR è l’opzione predefinita (quindi sottintesa) quando semplicemente si inseriscono due parole una dopo l’altra. Uno dei termini deve essere obbligatorio, ma possono anche essere presenti insieme nel documento. La combinazione OR si crea o con OR, “||” o non inserendo alcun operatore. Esempio: Auto rossa
- +: con il segno “+” create un caso specifico dell’operazione OR. Posizionando il segno direttamente davanti alla parola di una ricerca composta da due (o più) termini, significa che essa deve essere presente mentre l’altra è facoltativa. Esempio: +Auto rossa
- NOT: l’operatore NOT esclude determinati termini o frasi della ricerca. È possibile sostituire l’operatore con un punto esclamativo o mettere un segno negativo immediatamente prima del termine da escludere. Non è possibile utilizzare l’operatore NOT con un solo termine o una sola frase. Esempio: Auto rossa -blu
- Grouping: con le parentesi si possono raggruppare termini nelle query di ricerca. In questo modo create input più complessi con i quali ad esempio accoppiate in modo necessario un termine con un altro dei due termini. Esempio: Auto AND (rossa OR blu)
- Escaping Special Characters: per utilizzare caratteri che sono utilizzabili per Lucene Query Syntax, combinateli con un backslash. In questo modo potete inserire un punto interrogativo in una ricerca senza che l’analizzatore lo interpreti come un metacarattere. Esempio: "Dov’è la mia auto\?"
Apache Lucene: vantaggi e svantaggi
Lucene è un potente strumento per stabilire una funzione di ricerca sul web, negli archivi o nelle applicazioni. I fan di Lucene apprezzano il fatto di poter costruire, attraverso l’indicizzazione, un motore di ricerca molto veloce che può essere adattato in modo molto dettagliato alle proprie esigenze. Essendo un progetto open source, infatti, Lucene non solo è disponibile gratuitamente, ma è anche sviluppato costantemente da una grande community.
Quindi ora potrete usarlo oltre che in Java anche in PHP, Python e altri linguaggi di programmazione. E qui si arriva all’unico lato negativo: sono assolutamente necessarie competenze di programmazione. La ricerca full-text non è perciò una soluzione adatta a tutti. Se avete soltanto bisogno di una funzione di ricerca per il vostro sito web, è sicuramente meglio optare per altre soluzioni.
Vantaggi | Svantaggi |
---|---|
Disponibile per diversi linguaggi di programmazione | Necessita conoscenze di programmazione |
Open source | |
Veloce e snello | |
Ranked searching | |
Possibili query di ricerca complesse | |
Molto flessibile |