Lucene: programar um motor de busca

O que é o Lucene

O Lucene talvez dever-se-ia tratar por “a” Lucene, uma vez que se trata de uma biblioteca de recuperação de informação textual (do inglês “information retrieval”) de código aberto e criada por Doug Cutting. Originalmente, foi escrita em Java, mas foi rapidamente adaptada a outras linguagens de programação, nomeadamente Python (Pylucene), Perl (Plucene), C# (Lucene.net), C++ (CLucene), e Ruby (Ferret). Contudo, estas adaptações estão normalmente ligeiramente atrasadas no que toca à versão original em Java, actualmente mantida e alojada pela Apache Software Foundation.

Simples de aprender a usar, mas poderosa nas mãos de um programador experiente, esta biblioteca suporta desde índices estáticos com um campo, até múltiplos índices em paralelo, com centenas de campos e milhares de acessos simultâneos. É ideal para todo o tipo de projectos, desde o simples website com a “search box” até um grande motor de busca sobre, por exemplo, a colecção de PDFs que se tem no disco rígido.

Onde é usado o Lucene

Provavelmente desconhecido para o leitor, o certo é que já passou os dedos por esta útil ferramenta. Ao fim de contas, quem nunca pesquisou no site Sourceforge, ou na CNET, ou mesmo até na Wikipedia? Para os utilizadores de Ubuntu, quem nunca tirou partido do software de indexação recentemente incorporado na distribuição: o Beagle. Este, não é mais que suportado pela Lucene.Net, a implementação em C# desta biblioteca. Assim, a utilização desta biblioteca por estas entidades (re)afirma por si só a qualidade e a utilidade da mesma.

Como funciona o Lucene

O modo de funcionamento do Lucene é trivial de se perceber, mas mais complexo de se manusear na perfeição. Para começar, como já se indicou, há uma palavra chave em todo o processo: índice. O índice não passa de uma estrutura altamente ordenada de texto, ou de pedaços de texto (tokens), que torna a pesquisa muito mais eficiente. Porém, falha na capacidade de compressão de dados. Um índice de 6 milhões de abstracts científicos atinge facilmente os 1.7 GB, no caso de armazenar apenas tokens, ou os 6.6 GB caso guarde o texto. Esta diferença de tamanho vai, obviamente, afectar a performance. Daí, uma vantagem para a pesquisa, é ter o índice fragmentado em vários “sub-índices”, de modo a cada um ter menos volume de informação.

O modo como o texto é tratado pelo Lucene, também é alvo de atenção. Ao lermos texto para o Lucene, ele será primeiro submetido a um processo chamado analisador (analyzer), que pode ter várias formas: Standard, Simple, Stop, regional, etc. Sem entrar em grandes explicações, cada analisador trata o texto de maneira diferente. Uns ignoram a pontuação, outros são específicos para certas línguas (como a nossa com o ~ e os acentos), outros ignoram palavras comuns, baseando-se numa lista de “stop-words”, etc.

De seguida, tem que se criar um índice, que pode ser, na maior parte das vezes, ou baseado no disco rígido (acesso mais lento), ou na memória RAM (temporário). Uma estratégia para aumentar a velocidade de construção do índice passa por criar o índice na RAM e no fim, “despejá-lo” para o disco rígido. Na pesquisa, o mesmo índice pode voltar a ser lido para a RAM, potenciando (muito significativamente) a performance. Tendo o índice criado, há que criar um escritor de índices, de modo a populá-lo com os nossos textos. Das várias opções a passar ao escritor, estão o analisador em causa e o índice previamente criado.

A inserção de documentos no índice faz-se por último, adicionando documento a documento, criando os campos que forem precisos (por exemplo, a data de criação de um ficheiro, e o seu conteúdo, ou o título de um livro, a sua editora, autor, ano de lançamento, e preço). Quantos mais campos, mais lento ficará o índice, mas mais específica poderá ser a nossa pesquisa. Tomando o exemplo da livraria, podemos pesquisar apenas por editora, ou por ano de lançamento, ou mesmo por preço. A maneira de guardar a informação também varia. Podemos ter a necessidade de guardar alguns campos para mais tarde devolver ao utilizador que pesquise, mas outros campos podem ser fúteis e são apenas guardados “tokens” dos mesmos. Estes tokens são fragmentos únicos do texto e não estão disponíveis para futuras recuperações. Exemplificando, um resumo de um artigo científico está dividido em título e resumo. Podemos pesquisar sobre os resumos por uma determinada palavra, sendo devolvidos os títulos que correspondem à nossa pesquisa.

No fim do índice criado, pode-se, opcionalmente, mas sempre recomendado, passar por um processo de optimização que vai aumentar a performance da pesquisa no índice recém criado. Este processo vai reduzir os “segmentos” do índice a apenas um, facilitando assim a tarefa de abrir o índice, em vez de múltiplos pequenos ficheiros. Explicações sobre a formação destes segmentos estão para além do âmbito deste artigo, bastando apenas dizer que são resultado da construção do índice, podendo ser imaginados como sub-índices que degradam a performance devido ao seu diminuto tamanho e conteúdo.

Após se ter um índice, resta programar o pesquisador. O pesquisador vai usar um pesquisador de índices (IndexSearcher), que vai precisar da directoria do índice (tal como o IndexWriter), e de um parser da pesquisa (QueryParser), que vai depender do nome do campo onde pesquisar e de um analisador (igual ao usado na construção do índice). Recuperam-se depois os resultados, que estão num iterador, e depois vai-se retirar de cada objecto-resultado o campo pretendido (voltando ao exemplo do artigo científico, o título do artigo). Os resultados estão ordenados por ordem de relevância. O próprio Lucene tem um mecanismo de atribuição de relevância, baseado no TF-IDF, que dá mais peso a documentos com maior frequência do termo, e a termos pouco comuns no geral.

Desta forma, é simples construir um motor de busca, desde que se tenha maneira de ler os ficheiros (parser de PDFs para PDFs por exemplo). A documentação oficial do projecto é bastante completa e simples de seguir, fornecendo um caminho perfeito para tirar as dúvidas que possam surgir. Para além disso, há bastantes mailing-lists que ajudam os novatos. A cereja no topo do bolo, é a inclusão de vários scripts exemplo na distribuição do Lucene, e do PyLucene por exemplo. Estes exemplos são um óptimo ponto de partida para o Lucene.

PyLucene: usando o Lucene em Python

A biblioteca PyLucene está disponível desde, pelo menos, 2004. Criada e mantida por uma única pessoa, Andi Vadja, veio não só trazer aos utilizadores de Python o poder do Lucene, como também o fez de uma forma extremamente simplista e fiel à biblioteca original. De início, havia duas implementações: GCJ e JCC. Porém, desde a versão 2.3.0 do Lucene, a implementação em GCJ foi deixada de parte. As principais diferenças entre as implementações traduziam-se na velocidade de pesquisa, onde a GCJ liderava por larga margem, velocidade de indexação, onde era a JCC a levar a melhor, e noutras características como o tamanho dos índices suportados (maior na JCC) e maior estabilidade (JCC). Contudo, o processo de instalação tornou-se mais complicado, daí que apesar das fontes serem cedidas para compilação pelo utilizador, há quem disponibilize versões binárias para os mais novatos.

Exemplos de código

Tal como qualquer biblioteca em Python, chama-se o Lucene da seguinte forma:

import lucene

Porém, isto não chega. Com a implementação JCC, trabalha-se dentro de uma máquina virtual de Java, sendo portanto necessário criá-la, podendo definir-se os seus parâmetros (maxheap – limite máximo de memória RAM a ser usado, initialheap – memória RAM a ser usada inicialmente, etc):

lucene.initVM(lucene.CLASSPATH)
lucene.initVM(lucene.CLASSPATH, maxheap=800m)
lucene.initVM(lucene.CLASSPATH, maxheap=2g)
lucene.initVM(lucene.CLASSPATH, initialheap=800m)
lucene.initVM(lucene.CLASSPATH, initialheap=200m, maxheap=800m)

O resultado que se deve obter, numa consola de Python, é semelhante a:

>>> import lucene
>>> lucene.initVM(lucene.CLASSPATH)
<jcc.JCCEnv object at 0x8192240>
>>> 

Visto como iniciar a máquina virtual, pode-se agora tirar partido de todo o potencial da biblioteca.

Vamos começar por criar um indexador, ou seja, um script que crie um índice a partir de uma pasta contendo ficheiros de texto, no formato TXT, por uma questão de simplicidade. É de ter em atenção que, ao contrário dos ficheiros TXT, os ficheiros como por exemplo os PDF precisam de um parser para os ler, uma vez que ao Lucene é alimentado o conteúdo do ficheiro, e não o ficheiro em si. A lógica por detrás do funcionamento do Lucene já foi explicada anteriormente.

import lucene, os
 
lucene.initVM(lucene.CLASSPATH) # Iniciar a máquina virtual
 
indexPath = "Texto/Index" # indicar o caminho da pasta com os ficheiros
 
analyzer = lucene.StandardAnalyzer() # criar o analisador
 
indexDir = lucene.FSDirectory.getDirectory(indexPath) # criar o índice
 
indexWriter = lucene.IndexWriter(indexDir, analyzer) # criar o escritor do índice
 
for root, dirs, files in os.walk('Texto'): # percorremos os ficheiros da pasta
 for eachfile in files:
 fileinfo = eachfile.split('.')
 filename = ' '.join(fileinfo[:-1])
 if fileinfo[-1] == 'txt': # Para assegurar que so lemos ficheiros de texto simples
 
 document = lucene.Document() # Criamos um documento pronto a ser inserido no indice
 
 document.add(lucene.Field(("filename"), filename, lucene.Field.Store.YES, lucene.Field.Index.UN_TOKENIZED)) # Adicionamos o campo do nome do ficheiro, que vai ser posteriormente devolvido em pesquisas ao utilizador
 
 contents = open(os.path.join(root, eachfile), 'r').read()
 
 document.add(lucene.Field(("contents"), contents, lucene.Field.Store.NO, lucene.Field.Index.TOKENIZED)) # Campo com o conteudo do ficheiro, tokenizado e nao armazenado.
 
 indexWriter.addDocument(document) # Adiciona o documento ao indice
 
indexWriter.close() # Fechamos o escritor

De seguida, podemos escrever em meia dúzia de linhas um pesquisador para comprovarmos que o nosso índice foi bem construido. No meu exemplo, indexei apenas 2 ficheiros, cada um com a seguinte frase:

hello.txt – “Hello World?” goodbye.txt – “Python is a wonderful language!”

import lucene
 
lucene.initVM(lucene.CLASSPATH)
 
directory = lucene.FSDirectory.getDirectory('Texto/Index') # apontar para a directoria do indice 
 
searcher = lucene.IndexSearcher(directory) # criar o pesquisador
analyzer = lucene.StandardAnalyzer() # criar o analisador
 
term = raw_input('Escreva o termo a pesquisar: ')
 
query = lucene.QueryParser("contents", analyzer).parse(term) # Passamos o termo a procurar para o QueryParser, de modo a ser analisado, apontando tambem que campo do indice deve ser procurado.
 
hits = searcher.search(query) # Recuperamos os resultados
 
if len(hits)>0:
 results = [hits[x] for x in range(len(hits))] # Criamos uma lista com os documentos dos resultados
 filenames = [doc.get("filename") for doc in results] # recuperamos o campo filename de cada resultado
 scores = [hits.score(x) for x in range(len(hits))] # recuperamos a pontuação que cada documento teve na pesquisa
 
 for i in range(len(filenames)):
 print "%s, %s" %(filenames[i], scores[i]) # Devolvemos os resultados
else:
 print "Sem resultados!"

O resultado deste script será:

joao@jUbuntu:~/Desktop$ python pesquisador.py
Escreva o termo a pesquisar: python
goodbye, 0.5
 
joao@jUbuntu:~/Desktop$ python pesquisador.py
Escreva o termo a pesquisar: hello
hello, 0.625
 
joao@jUbuntu:~/Desktop$ python pesquisador.py
Escreva o termo a pesquisar: portugal
Sem resultados!

Conclusão

A biblioteca Lucene permite ao programador criar rapidamente um motor de busca que pode ser usado como aplicação por si só, ou para ser incorporado noutra, ou num website. Apesar de drasticamente simples, pode tomar contornos bastante complexos à medida que se vagueia pelas suas funcionalidades, permitindo uma personalização e uma costumização fenomenal, ideal para perfeccionistas que querem lidar com todos os pormenores, ou para aqueles que precisam de um sistema com um determinado conjunto de características. Para além disso, é grátis, de código livre, de maneira a que para ser usada não precisamos de despender nenhum tostão e podemos sempre dar uma olhadela ao código por detrás do seu funcionamento para ganhar um maior conhecimento.

Publicado na edição 15 (PDF) da Revista PROGRAMAR.