segunda-feira, outubro 23, 2006

Usar o Apache Lucene: parte 1

O meu estágio na faculdade, no grupo XLDB, tem-me levado a investigar com particular atenção e interesse o mundo da Information Retrieval e tem-me levado a experimentar uma série de bibliotecas.
Uma das bibliotecas mais famosas é o Apache Lucene , que é amplamente reputada pelo seu desempenho tanto em indexação como em pesquisas, e pela sua escalabilidade. Só por curiosidade, o Lucene é a base do sistema de pesquisa da Amazon.
Contudo a qualidade dos seus resultados de pesquisa não é das mais brilhantes visto que não parece haver particular esforço na melhoria dos algoritmos de classificação.

Para programadores que queiram implementar funcionalidades de pesquisa nas suas aplicações, o Lucene é uma boa proposta. Fácil de usar e com documentação abundante.

Para que possam ter um bom ponto de partida, de seguida apresento um pedaço de código em Java que implementa um programa que indexa todos os ficheiros .txt de um dada directoria e sub-directorias. Este código foi feito usando o Lucene 2.0.0.

Mas antes de chegarmos ao código propriamente dito, julgo que é importante explicar o que são de facto esses índices. Os índices são ficheiro onde, a cada termo se mantêm uma lista de ficheiros onde estes termos aparecem. Graças a isto, é possível, sem grande esforço, que dado um termo se obtenha todos os ficheiros onde este aparece.

Agora, já vos autorizo ver o código;)
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.Date;

import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.IndexWriter;

public class lucene_indexing {

public static void main(String[] args) throws IOException {
File indexDir = new File("/directoria/onde/ficará/o/index");
File dataDir = new File("/directoria/a/indexar");

long start = new Date().getTime();
int numIndexed = index(indexDir, dataDir);
long end = new Date().getTime();

System.out.println("> "+ numIndexed +" indexados em: "+ (end - start) +"ms");
}

public static int index(File indexDir, File dataDir) throws IOException {
if (!dataDir.exists() || !dataDir.isDirectory()) {
throw new IOException (dataDir + "não existe ou não é directoria");
}

IndexWriter writer = new IndexWriter(indexDir, new StandardAnalyzer(), true);
writer.setUseCompoundFile(false);

indexDirectory(writer, dataDir);

int numIndexed = writer.docCount();
writer.optimize();
writer.close();

return numIndexed;
}

public static void indexDirectory(IndexWriter writer, File dir) throws IOException {
File[] files = dir.listFiles();

for (File file : files) {
if (file.isDirectory()) {
indexDirectory(writer, file);
}
else if (file.getName().endsWith(".txt")) {
indexFile(writer, file);
}
}
}

public static void indexFile(IndexWriter writer, File file) throws IOException {
if (file.isHidden() || !file.exists() || !file.canRead()) {
return;
}

System.out.println("A indexar: \t"+ file.getCanonicalPath());

Document doc = new Document();
doc.add(new Field("content", new FileReader(file)));
doc.add(new Field("filename", file.getPath(), Field.Store.YES, Field.Index.UN_TOKENIZED));
writer.addDocument(doc);
}
}

A função index(File indexDir, File dataDir) inicia o processo de indexação e é-lhe indicada a directoria onde residirá o index e a directoria a indexar.
Nesta parte do código é criado o escritor do índice, componente essencial para este programa.
Se repararmos nesta linha de código: IndexWriter writer = new IndexWriter(indexDir, new StandardAnalyzer(), true);, vemos que é passado ao escritor do índice um analisador. Este analisador é essencial no processo de indexação já que é ele que tem a responsabilidade de processar o texto e de definir quais são os separadores que delimitam os termos, tais como espaços, vírgulas. Em particular, o StandarAnalyzer possui regras complexas que o permite, por exemplo, manter e-mails como um todo invés de os dividir em parcelas.
Outra linha crucial é: writer.optimize();, esta chamada permite unificar vários ficheiros de índice que foram criados durante a indexação, com o intuito de ser possível obter um melhor desempenho. Esta chamada é feita uma única vez, no final do processo de indexação.

A função indexDirectory(IndexWriter writer, File dir) chama a função que indexa, caso encontre um ficheiro .txt, caso encontre uma directoria, chama-se recursivamente.

A função indexFile(IndexWriter writer, File file) é a função que indexa os ficheiros .txt, indexando o seu conteúdo transformando-o em parcelas mas sem o guardar e indexando o nome do ficheiro qualquer tipo de processamento ou interpretação.
Porquê esta diferença na indexação entre o conteúdo e o nome do ficheiro? No caso do conteúdo, nós queremos ser capazes de efectuar pesquisas aos termos que o compõe, como tal é preciso processar as parcelas de texto do ficheiro, tipicamente palavra a palavra, mas não guardamos o conteúdo por motivos de espaço em disco, só guardamos o processamento das parcelas de esse conteúdo. Isto permite diminuir em muito o tamanho dos índices face aos ficheiros que lhes deram origem.
No caso do nome do ficheiro, como vamos querer saber que ficheiros é que contêm determinados termos, vamos guardar os seus nomes para poderem ser apresentados e como não vamos fazer pesquisas sobre os nomes dos ficheiros, não é necessário processar os seus nomes, só guardá-los.

Agora, só é preciso alterar no código a directoria a indexar e a directoria onde vai ficar o índice, e compilar este código sem esquecer de colocar o .jar do Lucene no classpath.
Depois de compilar, é só correr, sem esquecer, novamente, de colocar a referência ao Lucene no classpath.

Num futuro próximo, irei explicar como é que é possível efectuar pesquisas sobre os índices criados.

2 comentários:

Diana disse...

Ihhh minha nossa...
Que preguiça para ler isso! Tambem depois da dose de ontem, né menino??

Apache, não mais! =P


Porta-te **

Unknown disse...

Qdo vai publicar a segunda parte do artigo LUCENE