Tutorial do Lucene: como indexar arquivos

Tutorial do Lucene: como indexar arquivos

Apache Lucene

Tutorial Lucene
Lucene
O Lucene é um framework para busca textual de alta performance escrito totalmente em Java e bastante fácil de usar. Neste tutorial do Lucene vamos ver as funcionalidades de busca e indexação que podemos adicionar em qualquer aplicação, web ou desktop, uma vez que o framework conta com uma API bastante completa e plugável. A biblioteca é bastante famosa, sendo utilizada em muitos portais da internet.

Há uma versão para .Net, o Lucene.Net, e ports para outras linguagens. Uma alternativa bastante interessante é o Solr (pronuncía-se ‘sólar’), um servidor corporativo de buscas já otimizado para aplicações web, que é um subprojeto do Lucene. No Solr, as operações podem ser feitas através de JSON, XML, HTTP, CSV, etc, o que permite a integração com virtualmente todas as plataformas.

Tanto Windows quanto o Mac OS X têm mecanismo de busca nativo, encontrar um arquivo qualquer é muito fácil. No Mac, o Spotlight procura em todo o sistema de arquivo em busca de aplicativos, e-mails, documentos, definição no vocabulário, diretórios, etc.

Spotlight
Spotlight

Nos dois casos (Windows e Mac) estamos limitados ao sistema de arquivos da máquina do usuário. Neste artigo vamos implementar um buscador utilizando Lucene que pode indexar vários formatos de documento, até mesmo através da rede, e disponibilizar essa informação para todos os usuários de uma corporação.

Em artigos posteriores veremos como indexar bancos de dados relacionais e discutiremos as vantagens, desvantagens, possibilidades e limitações deste tipo de solução.

As funcionalidades de um buscador: indexação e busca

A indexação consiste em recuperar o texto contido em um documento e adicioná-lo ao índice, tornando essa informação disponível para o usuário. Imaginando que hoje estamos acostumados a ter qualquer informação imediatamente, o fator velocidade é essencial. Essa operação é lenta, pois envolve muito processamento e gravação em disco. Indexar grandes volumes de dados demora bastante tempo e para isso há ferramentas específicas.

Para o Lucene, cada item indexado é um Document e contém uma coleção de campos. Um campo deve ter nome e valor textual. A busca pode ser feita em qualquer um desses campos.

Google, Bing, Yahoo!, Ask, etc funcionam assim. Além da página de busca, há um webcrawler visitando todos os sites da internet e indexando seu conteúdo. Esse processo é constante, até porque a internet é dinâmica. Sem contar que hoje temos conteúdo multimídia, não é apenas texto ou HTML como foi no começo da internet, há décadas atrás. Hoje a internet tem muito mais que apenas texto. Temos imagem, som, vídeo, portais verticais, Wolpham Alpha, web semântica e mídias sociais. E tudo isso deve estar disponível em tempo real.

A busca consite em recuperar os documentos que contém um termo informado pelo usuário. No Lucene essa operação é extremamente rápida. Mesmo uma consulta complexa, feita em um índice com milhões de documento, dura menos de 1 segundo. Além disso, o resultado da busca pode vir ordenado ou classificado (melhores resultados aparecem primeiro). E são muitas as opções de consulta fornecidas pelo Lucene:
– busca por palavra-chave ou frase
– busca em campos específicos
– busca com wildcard (* e ?)
– busca aproximada, utilizando a Distância de Levenshtein
– busca por proximidade entre palavras
– busca por intervalos de valores (datas, números ou letras)

Vale notar que o Lucene indexa apenas texto. Para indexar documentos binários (MS Office, PDF, RTF, etc) temos que utilizar alguma biblioteca de extração de texto, como o Apache Tika, que consegue recuperar texto em diversos formatos de arquivo.

Tutorial do Lucene

Composto por duas classes (Indexador e Buscador), o projeto proposto indexa um diretório informado pelo programador. Pode ser um diretório local, um compartilhamento Windows ou um diretório NFS.

Indexador

package net.marcoreis.util;

import java.io.*;
import java.text.*;

import org.apache.log4j.*;
import org.apache.lucene.analysis.*;
import org.apache.lucene.analysis.standard.*;
import org.apache.lucene.document.*;
import org.apache.lucene.index.*;
import org.apache.lucene.store.*;
import org.apache.lucene.util.*;
import org.apache.tika.*;

public class Indexador {
private static Logger logger = Logger.getLogger(Indexador.class);
//{1}
private String diretorioDosIndices = System.getProperty("user.home")
+ "/indice-lucene";
//{2}
private String diretorioParaIndexar = System.getProperty("user.home")
+ "/Dropbox/MaterialDeEstudo/big-data";
//{3}
private IndexWriter writer;
//{4}
private Tika tika;

public static void main(String[] args) {
Indexador indexador = new Indexador();
indexador.indexaArquivosDoDiretorio();
}

public void indexaArquivosDoDiretorio() {
try {
File diretorio = new File(diretorioDosIndices);
apagaIndices(diretorio);
//{5}
Directory d = new SimpleFSDirectory(diretorio);
logger.info("Diretório do índice: " + diretorioDosIndices);
//{6}
Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_36);
//{7}
IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_36,
analyzer);
//{8}
writer = new IndexWriter(d, config);
long inicio = System.currentTimeMillis();
indexaArquivosDoDiretorio(new File(diretorioParaIndexar));
//{12}
writer.commit();
writer.close();
long fim = System.currentTimeMillis();
logger.info("Tempo para indexar: " + ((fim - inicio) / 1000) + "s");
} catch (IOException e) {
logger.error(e);
}
}

private void apagaIndices(File diretorio) {
if (diretorio.exists()) {
File arquivos[] = diretorio.listFiles();
for (File arquivo : arquivos) {
arquivo.delete();
}
}
}

public void indexaArquivosDoDiretorio(File raiz) {
FilenameFilter filtro = new FilenameFilter() {
public boolean accept(File arquivo, String nome) {
if (nome.toLowerCase().endsWith(".pdf")
|| nome.toLowerCase().endsWith(".odt")
|| nome.toLowerCase().endsWith(".doc")
|| nome.toLowerCase().endsWith(".docx")
|| nome.toLowerCase().endsWith(".ppt")
|| nome.toLowerCase().endsWith(".pptx")
|| nome.toLowerCase().endsWith(".xls")
|| nome.toLowerCase().endsWith(".txt")
|| nome.toLowerCase().endsWith(".rtf")) {
return true;
}
return false;
}
};
for (File arquivo : raiz.listFiles(filtro)) {
if (arquivo.isFile()) {
StringBuffer msg = new StringBuffer();
msg.append("Indexando o arquivo ");
msg.append(arquivo.getAbsoluteFile());
msg.append(", ");
msg.append(arquivo.length() / 1000);
msg.append("kb");
logger.info(msg);
try {
//{9}
String textoExtraido = getTika().parseToString(arquivo);
indexaArquivo(arquivo, textoExtraido);
} catch (Exception e) {
logger.error(e);
}
} else {
indexaArquivosDoDiretorio(arquivo);
}
}
}

private void indexaArquivo(File arquivo, String textoExtraido) {
SimpleDateFormat formatador = new SimpleDateFormat("yyyyMMdd");
String ultimaModificacao = formatador.format(arquivo.lastModified());
//{10}
Document documento = new Document();
documento.add(new Field("UltimaModificacao", ultimaModificacao,
Field.Store.YES, Field.Index.NOT_ANALYZED));
documento.add(new Field("Caminho", arquivo.getAbsolutePath(),
Field.Store.YES, Field.Index.NOT_ANALYZED));
documento.add(new Field("Texto", textoExtraido, Field.Store.YES,
Field.Index.ANALYZED));
try {
//{11}
getWriter().addDocument(documento);
} catch (IOException e) {
logger.error(e);
}
}

public Tika getTika() {
if (tika == null) {
tika = new Tika();
}
return tika;
}

public IndexWriter getWriter() {
return writer;
}
}

1. Diretório que irá guardar o índice.
2. Diretório que contém os documentos que serão indexados.
3. IndexWriter: cria e mantém o índice.
4. Biblioteca que extrai texto de diversos formatos conhecidos.
5. Directory: representa o diretório do índice.
6. Analyser/StandardAnalyser: fazem o pré-processamento do texto. Existem analisadores inclusive em português.
7. IndexWriterConfig: configurações para criação do índice. No projeto serão utilizados os valores padrão.
8. Inicializa o IndexWriter para gravação.
9. Extrai o conteúdo do arquivo com o Tika.
10. Monta um Document para indexação.
Field.Store.YES: armazena uma cópia do texto no índice, aumentando muito o seu tamanho.
Field.Index.ANALYZED: utilizado quando o campo é de texto livre.
Field.Index.NOT_ANALYZED: utilizado quando o campo é um ID, data ou númerico.
11. Adiciona o Document no índice, mas este só estará disponível para consulta após o commit.

Buscador

package net.marcoreis.util;

import java.io.*;

import javax.swing.*;

import org.apache.log4j.*;
import org.apache.lucene.analysis.*;
import org.apache.lucene.analysis.standard.*;
import org.apache.lucene.document.*;
import org.apache.lucene.index.*;
import org.apache.lucene.queryParser.*;
import org.apache.lucene.search.*;
import org.apache.lucene.store.*;
import org.apache.lucene.util.*;

public class Buscador {
private static Logger logger = Logger.getLogger(Buscador.class);
private String diretorioDoIndice = System.getProperty("user.home")
+ "/indice-lucene";

public void buscaComParser(String parametro) {
try {
Directory diretorio = new SimpleFSDirectory(new File(diretorioDoIndice));
//{1}
IndexReader leitor = IndexReader.open(diretorio);
//{2}
IndexSearcher buscador = new IndexSearcher(leitor);
Analyzer analisador = new StandardAnalyzer(Version.LUCENE_36);
//{3}
QueryParser parser = new QueryParser(Version.LUCENE_36, "Texto",
analisador);
Query consulta = parser.parse(parametro);
long inicio = System.currentTimeMillis();
//{4}
TopDocs resultado = buscador.search(consulta, 100);
long fim = System.currentTimeMillis();
int totalDeOcorrencias = resultado.totalHits;
logger.info("Total de documentos encontrados:" + totalDeOcorrencias);
logger.info("Tempo total para busca: " + (fim - inicio) + "ms");
//{5}
for (ScoreDoc sd : resultado.scoreDocs) {
Document documento = buscador.doc(sd.doc);
logger.info("Caminho:" + documento.get("Caminho"));
logger.info("Última modificação:" + documento.get("UltimaModificacao"));
logger.info("Score:" + sd.score);
logger.info("--------");
}
buscador.close();
} catch (Exception e) {
logger.error(e);
}
}

public static void main(String[] args) {
Buscador b = new Buscador();
String parametro = JOptionPane.showInputDialog("Consulta");
b.buscaComParser(parametro);
}
}

1. IndexReader: classe abstrata responsável por acessar o índice.
2. IndexSearcher: implementa os métodos necessários para realizar buscas em um índice.
3. QueryParser/Query: representa a consulta do usuário. Outros exemplos de query podem ser vistos no Javadoc.
4. Realiza a busca e armazena o resultado em um TopDocs.
5. ScoreDoc: representa cada um dos documentos retornados na busca.

Exemplos de busca

Busca por palavra-chave

Rode o Buscador e digite “java” como termo de consulta. Será mostrado o resultado com vários documentos contendo essa palavra. Em seguida, busque por “java -ejb”, ou seja, documentos que contém o termo “java” e não “ejb”.

Intervalos

Para pesquisar um intervalo utilize a consulta “UltimaModificacao:[20110101 TO 20110606]”. Funciona também para intervalo de letras.

Busca aproximada

Digite “servlet~” ou “servlet~0.7” e compare os resultados. O resultado mostra documentos que contenham algum termo parecido com “servlet”, usando a Distância de Levenshtein. O padrão é 0.5, ou seja, “servlet~” e “servlet~0.5” são iguais para o Lucene. Podemos aumentar a precisão, como é o caso de servlet~0.7

Código

O código-fonte do aplicativo está disponível aqui.

Palavras-chave: tika, lucene, solr, indexação, buscador, java, distância de levenshtein, big data

Deixe uma resposta

O seu endereço de email não será publicado. Campos obrigatórios marcados com *