Ricerche Google-like con Apache Lucene
Una delle feature che desidero aggiungere al sistema di shopping online Bazaar è la ricerca in stile google. Attualmente Bazaar usa un database MySQL per la persistenza dei dati. Gli elementi che un tipico utente Bazaar può avere interesse a cercare sono, ovviamente, i prodotti in vendita.
I dati caratteristici del prodotto sono:
id, indice numerico che rappresenta la chiave primaria;
nome, nome del prodotto;
descrizione, descrizione del prodotto.
più altri campi.
L'idea è quella di utilizzare un un search text engine per la ricerca mediante word. Un search text engine, in pratica, memorizza documenti in un suo formato specifico indicizzando attraverso le word. l'engine che abbiamo deciso di utilizzare è Apache Lucene.
In pratica, per ogni prodotto indicizziamo nome e descrizione e come rispettivo valore mettiamo l'id. Quando il cliente inserisce una o più word da ricercare, Apache Lucene riesce in breve tempo a recuperare tutti i prodotti che ombaciano. A quel punto, poichè abbiamo anche gli id di questi prodotti, possiamo andare nel database e caricare i restanti campi.
Qui di seguito riportiamo il codice utilizzato per l'indicizzazione e la query dei prodotti. Per ulteriori dettagli sulle APIs di Lucene consiglio di visitare il sito web.
public abstract class DAOFactory {
private static DAOFactory factory = null;
public static DAOFactory makeFactory() {
if (factory == null) {
factory = new LUCENEDAOFactory();
}
return factory;
}
public abstract DAOProduct getDAOProduct();
}
public interface DAOProduct {
public void create(Product product) throws DAOException;
public void update(Product product) throws DAOException;
public void delete(int nID) throws DAOException;
public void deleteAll() throws DAOException;
public Collection search(String text) throws DAOException;
}
La classe DAOFactory è utile per poter istanziare il DAOProduct implementato attraverso lucene o qualsiasi altro search text engine. DAOProduct, invece, è il DAO che gestisce la persistenza del prodotto nel search text engine. La classe Product è il ValueObject da peristere. Oltre ai metodi CRUD è possibile notare il metodo search che, dato in input il testo inserito dal cliente, restituisce la lista dei prodotti che fanno match con la ricerca.
L'implementazione Lucene di queste due classi è la seguente.
public class LUCENEDAOFactory extends DAOFactory {
private DAOProduct productDAO;
public LUCENEDAOFactory() {
this.productDAO = new LUCENEDAOProduct();
}
public DAOProduct getDAOProduct() {
}
}
public class LUCENEDAOProduct implements DAOProduct {
private static String KEY = "key";
private static String NAME = "name";
private static String DESCRIPTION = "description";
private static String CONTENT = "content";
private File index;
public LUCENEDAOProduct() {
index = new File(site.getPath() + "/lucene-index");
}
public void create(Product product) throws DAOException {
Document doc = new Document();
doc.add(Field.Text(KEY, Integer.toString(product.nID)));
doc.add(Field.Text(CONTENT, product.txtName + " " + product.txtDescription));
doc.add(Field.Text(NAME, product.txtName));
doc.add(Field.Text(DESCRIPTION, product.txtDescription));
if (!index.exists()) {
}
try {
IndexWriter writer = new IndexWriter(index, new StandardAnalyzer(), false);
writer.addDocument(doc);
writer.optimize();
writer.close();
} catch (IOException e) {
throw new DAOException("Unable to index the product in LUCENE: " + e.getMessage());
}
}
Il metodo create aggiunge il prodotto (cioè id, nome e descriprion) nell'indice lucene che è rappresentato dalla directory lucene-index.
public void update(Product product) throws DAOException {
delete(product.nID);
create(product);
}
In Lucene non è possibile effettuare l'update di un indice, per cui si rende necessaria la rimozione/creazione del prodotto.
public void delete(int nID) throws DAOException {
IndexReader reader = null;
try {
reader = IndexReader.open(index);
reader.delete(new Term(KEY, Integer.toString(nID)));
} catch (IOException e) {
throw new DAOException("Error while deleting product data from index:" + e);
} finally {
}
}
Questo metodo, invece, rimuove un prodotto dall'indice.
public void deleteAll() throws DAOException {
IndexReader reader = null;
try {
reader = IndexReader.open(index);
for(TermEnum enum = reader.terms(); enum.next();) {
reader.delete(enum.term());
}
} catch (IOException e) {
throw new DAOException("Error while deleting all products");
} finally {
}
}
Questo metodo, invece, serve alla rimozione di tutti i prodotti dall'indice. Viene utilizzato in Bazaar per la rigenerazione dell'indice dove, si rimuovono tutti i prodotti e si reinseriscono tutti uno ad uno. Questa è un'operazione utile se l'indice si corrompe o la prima volta che lo si crea.
public Collection search(String text) throws DAOException {
if (!index.exists()) {
createIndex();
return new ArrayList();
} else {
Query query;
try {
query = QueryParser.parse(text, CONTENT, new StandardAnalyzer());
} catch (ParseException e) {
throw new DAOException("Unable to make any sense of the query: " + e);
}
ArrayList products = new ArrayList();
IndexReader reader = null;
IndexSearcher searcher = null;
try {
reader = IndexReader.open(index);
searcher = new IndexSearcher(reader);
Hits hits = searcher.search(query);
for (int i = 0; i != hits.length(); ++i) {
Document doc = hits.doc(i);
int key = Integer.parseInt(doc.getField(KEY).stringValue());
products.add(site.products.get(key));
}
} catch (IOException e) {
throw new DAOException("Error while reading product data from index:" + e);
} finally {
close(searcher);
close(reader);
}
return products;
}
}
Questo è il metodo che effettua la ricerca dei prodotti nell'indice. Si controlli il sito web di Apache Lucene per dettagli sulle APIs.
private void close(IndexReader reader) {
try {
if (reader != null) reader.close();
} catch(IOException exc) {
}
}
private void close(IndexSearcher searcher) {
try {
if (searcher != null) searcher.close();
} catch(IOException exc) {
}
}
private void createIndex() {
index.mkdir();
try {
IndexWriter writer = new IndexWriter(index, new StandardAnalyzer(), true);
writer.close();
} catch (IOException e) {
}
}