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:
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
bom comeco
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
Postar um comentário
<< Home