segunda-feira, maio 23, 2005

Suggest com o Lucene

Um recurso bastante interessante nos mecanismos de busca mais sofisticados é o de sugestões. Funciona assim: você, usuário, digita algo errado e o mecanismo lhe sugere a palavra correta. Ou, em alguns outros casos, sugere outra palavra parecida com a que digitada mas com um numero maior de resultados. Algo como sugerir "lucene" se você digitar "luceni". Criar um mecanismo de sugestão com o Lucene é algo bastante simples. A maior parte da complicação está em definir se a estratégia trará as palavras mais parecidas ou as palavras parecidas com maior numero de resultados. Outro aspecto considerado é o quanto as palavras devem ser parecidas, ou seja, o grau de similaridade entre elas. No Lucene esse grau varia entre no intervalo [0,1] e o valor default é 0.5. Geralmente ambas as decisões são tomadas depois de alguns testes para verificar qual abordagem melhor se adequa ao seu caso, mas, para o grau de similaridade, quanto menor a base de índices, menor deve ser o grau mínimo requerido, já que não há muitas palavras a disposição. Feitas as escolhas, vamos a um exemplo simples. Criamos uma classe Suggest que encapsulará os dados, palavra e quantidade de resultados, de uma sugestão:


public class Suggest implements Comparable<Suggest> {

private String word;
private int occurrences;

/**
* @param word
* @param occurrences
*/
public Suggest ( String word, int occurrences ) {

this.word = word;
this.occurrences = occurrences;
}

/**
* @see java.lang.Object#equals(java.lang.Object)
*/
public boolean equals( Object obj ) {

boolean result = false;

if (obj instanceof Suggest) {

Suggest suggest = (Suggest) obj;

String sWord = suggest.word.toLowerCase();
String thisWord = word.toLowerCase();

result = sWord.equals(thisWord)
∧∧ occurrences == suggest.occurrences;

}

return result;
}

/**
* @see java.lang.Object#hashCode()
*/
public int hashCode() {

return word.toLowerCase().hashCode() * occurrences;

}

/**
* @see java.lang.Object#toString()
*/
public String toString() {

return word + " - " + occurrences;

}

/**
* @param other
* @return
* @see java.lang.Comparable#compareTo(T)
*/
public int compareTo( Suggest other ) {

return other.occurrences - occurrences;

}

/**
* @return Returns the occurrences.
*/
public int getOccurrences() {

return occurrences;
}

/**
* @return Returns the word.
*/
public String getWord() {

return word;
}
}

A classe é Comparable para permitir a ordenação de acordo com o numero de documentos nos quais o termo procurado aparece. E agora a classe que cria as sugestões a partir do seu índice:


import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.Token;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.TermEnum;
import org.apache.lucene.search.FuzzyQuery;
import org.apache.lucene.search.FuzzyTermEnum;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;


/**
* @author Marcos Silva Pereira - marcos.pereira@vicinity.com.br
*
*
* @since 21/05/2005
* @version $Id$
*/
public class Suggestor {

private Directory lucenePath;
private Analyzer analyzer;
private float minSimilarity = FuzzyQuery.defaultMinSimilarity;

/**
* @param path
* @param analyzer
* @param similarity
*/
public Suggestor ( Directory path, Analyzer analyzer, float similarity ) {

lucenePath = path;
minSimilarity = similarity;
this.analyzer = analyzer;
}

/**
* @param word
* @param field
* @return
*/
public List<Suggest> suggestsByFrequency( String word, String field ) {

List<Suggest> suggests = suggestsBySimilarity(word, field);

Collections.sort(suggests);

return suggests;

}

/**
* @param word
* @param field
* @param maxSuggestions
* @return
*/
public List<Suggest> suggestsByFrequency( String word, String field,
int maxSuggestions ) {

List<Suggest> suggests = suggestsByFrequency(word, field);

suggests = chopList(maxSuggestions, suggests);

return suggests;

}

/**
* @param word
* @param field
* @param maxSuggestions
* @return
*/
public List<Suggest> suggestsBySimilarity( String word, String field,
int maxSuggestions ) {

List<Suggest> suggests = suggestsBySimilarity(word, field);

suggests = chopList(maxSuggestions, suggests);

return suggests;

}

/**
* @param word
* @param field
* @return
* @throws IOException
*/
public List<Suggest> suggestsBySimilarity( String word, String field ) {

IndexReader reader = null;

try {

List<Suggest> suggests;

reader = IndexReader.open(lucenePath);
Term term = new Term(field, word);

FuzzyTermEnum termEnum;
termEnum = new FuzzyTermEnum(reader, term, minSimilarity);

suggests = termEnumToList(termEnum);

return suggests;

} catch (IOException ex) {

throw new SuggestException(ex);

} finally {

try {

if (reader != null)
reader.close();

} catch (Exception ex) {

// TODO log this exception...
}

}

}

/**
* @param phrase
* @param field
* @return
*/
public List<Suggest> phrasalSuggest( String phrase, String field ) {

List<Suggest> suggests = new ArrayList<Suggest>();

TokenStream tokenStream = null;

try {

Reader reader = new StringReader(phrase);
tokenStream = analyzer.tokenStream(field, reader);

Token token;
while((token = tokenStream.next()) != null) {

String tokenText = token.termText();
List<Suggest> temp = suggestsByFrequency(tokenText, field, 1);

if(!temp.isEmpty()) {

suggests.addAll(temp);

}

}

} catch (IOException ex) {

throw new SuggestException(ex);

} finally {

try {

if(tokenStream != null) tokenStream.close();

} catch (IOException ex) {

// TODO Logger this exception

}

}

return suggests;

}

private List<Suggest> termEnumToList( TermEnum termEnum )
throws IOException {

List<Suggest> suggests = new ArrayList<Suggest>();

while (termEnum.next()) {

Term term = termEnum.term();

String termValue = term.text();
int frequency = termEnum.docFreq();

suggests.add(new Suggest(termValue, frequency));

}

return suggests;
}

private List<Suggest> chopList( int maxSuggestions, List<Suggest> suggests ) {

if (suggests.size() > maxSuggestions) {

suggests = suggests.subList(0, maxSuggestions);

}

return suggests;
}
}

Os métodos suggestBySimilarity trazem as palavras mais parecidas com a buscada e não se preocupam com o numero de documentos com os quais o termo buscado está relacionado. Já os métodos suggestsByFrequency trazem primeiro as palavras parecidas e com maior numero de documentos. Nesse caso, o numero de documentos é determinante e a similaridade é usada apenas para retornar o conjunto de palavras. Infelizmente o Lucene não provê o grau de similaridade para que se pudesse criar uma media entre esse grau e o numero de documentos. O método phrasalSuggest gera um conjunto de sugestões baseado na frase indicada. Um analyzer é usado para que as palavras da consulta sejam tratadas do mesmo modo que são para a busca. Assim, stop words, palavras descartadas por serem muito comuns, não receberão uma sugestão e daí por diante. Usar a classes Suggestor nas classes clientes é bastante simples. Em uma action do WebWork, por exemplo, bastaria fazer algo como a seguir:


public class ActionSearch extends ActionSupport {

private Suggestor suggestor;
private List<Suggest> suggests;

private List searchResult;
private String query;

/**
* @return Action execute status
*/
public String findByText() {

String result = Action.ERROR;

try {

// abstraia a busca, afinal o foco está em como fazer as sugestões
this.searchResult = Mystical.doSearch(query);

// recuperando conjunto de sugestões.
this.suggests = suggests();

result = Action.SUCCESS;

} catch (Exception e) {

addActionError("Erro ao realizar a busca: " + e.getMessage());

}

return result;

}

private List<Suggest> suggests() {

List<Suggest> suggestions = new ArrayList<Suggest>();

try {

// pode acreditar, a classe Mystical faz um café de primeira
suggestions = suggestor.suggestsByFrequency(query, Mystical.FIELD_TO_SEARCH, 2);

} catch (Exception e) {

// TODO logger this exception

}

return suggestions;

}

// gets and sets

}

O método suggests foi criado para realizar o tratamento de exceções, ou seja, se a recuperação das sugestões lançar alguma exception, a busca é feita, como é de se esperar. As sugestões ficam então disponíveis para serem usadas na pagina que mostra o resultado da busca. Por fim, os códigos acima estão sendo desenvolvidos para o projeto JavaBB e não demora a estarem disponíveis (sem mágicas, ou quase) no CVS do projeto. Até lá!

valeuz...

3 Comentarios:

Anonymous Anônimo disse...

Marcos, meus parabéns pelo excelente exemplo. Realmente vai me ajudar muito para futuras implementações do lucene. Mas gostaria de perguntar pra vc, se existe uma maneira de saber em uma procura, a quantidade de uma palavra existente no documento indexado(PDF) pelo lucene e trazer esta quantidade na tela?

Guilherme - luisbsb@msn.com

1:43 PM  
Anonymous Anônimo disse...

bom comeco

1:17 PM  
Anonymous Anônimo disse...

Marcos,

Eu estava criando o esquema de sugestão do Lucene. Há um problema, quando ele me devolve os termos, ele não leva em consideração o meu analisador. Eu estou usando o BrazilianAnalyzer. Logo, Eu pesquiso pela palavra Sinapse e no índice existe Synapse, ele me sugere Synaps. Ele me devolve os termos da forma como ele foram tokenizados.
No seu funciona certinho?

Marcelo Neves

7:21 PM  

Postar um comentário

<< Home