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

Leave a Reply

Your email address will not be published. Required fields are marked *