ASP.NET MVC + Lucene.Net ( Parte 2 ) – Implementando Spellchecker, Dicionário de Sinônimos, Tratamentos e Indexações

Standard

Nesta segunda parte da série sobre ASP.NET MVC + Lucene.Net abordaremos diversos recursos interessantes para implementação em nosso motor de busca, tais como:

  • Indexação de diversos campos;
  • Tratamento de preposições e artigos do nosso texto a ser buscado;
  • Dicionário de sinônimos;
  • Corretor ortográfico (mais conhecido como SpellChecker) com sugestão de busca.

OBS: Para o acompanhamento deste artigo, aconselho a leitura do artigo anterior Criando o projeto e o motor de busca. Pois irei dar continuidade no projeto já criado, e implementarei os novos recursos no mesmo.

Não irei seguir a ordem dos itens citados acima, mas sim um fluxo que aborde todos os itens da forma mais fácil e clara para um bom entendimento!

Let’s Go!!!

Dicionário de sinônimos

Para criarmos um dicionário de sinônimos, vamos iniciar criando uma nova tabela no banco de dados com a seguinte estrutura e registros:

create table tbSinonimos
(
   codEspecialidade int not null,
   sinonimo varchar(100) not null
)

Imagem 1

Podemos observar que, não apenas sinônimos são adicionados na tabela, mas tudo que será relevante e referente a especialidade em questão, no caso “Ortopedista”, veremos adiante o por que disto.

Agora, após ter arrastado a tabela para o DataContext, vamos criar uma classe parcial de Consultorio com um método para obter os sinônimos de sua especialidade:

public partial class Consultorio
{
    public string ObtemSinonimos()
    {
        DBDataContext _db = new DBDataContext();

        StringBuilder sb = new StringBuilder();

        foreach (Sinonimo s in _db.Sinonimos.Where(si => si.codEspecialidade == this.codEspecialidade))
            sb.AppendFormat(" {0} ", s.sinonimo);

        return sb.ToString();
    }
}

Observe que é retornado como string todos os sinônimos da especialidade do consultório em questão. Vamos entender abaixo o por que retornar como string estes campos.

 

Indexação de diversos campos

Até o momento criamos apenas dois campos para serem inclusos em nosso índice, o código do consultório e o nome da especialidade, vamos relembrar e entender como está até agora:

Imagem 2

Analisando a imagem acima, vamos relembrar como funciona cada parâmetro do objeto Field:

  • 1º parâmetro: Nome do campo;
  • 2º parâmetro: valor do campo;
  • 3º parâmetro: Definição do Field.Store, ou seja, se o valor será armazenado no índice, podendo ser NO ou YES. No caso de YES, será possível obter o valor do campo posteriormente, e NO não será possível recuperar seu valor;
  • 4º parâmetro: Definição do Field.Index, ou seja, se será possível a busca pelo campo e se o mesmo será analisado, sendo os mais comuns NOT_ANALYZED e ANALYZED.

Desta forma, nossa busca está funcionando levando em conta apenas o campo “especialidade”, isso não devido ao fato de apenas o mesmo estar indexado como ANALYZED, mas sim por sempre que uma busca for realizada, deve-se definir qual o campo que iremos pesquisar, vamos relembrar onde definimos este campo no método RealizaBusca:

Imagem 3

Levando em conta que podemos buscar apenas um campo que é indexado ao índice, vamos criar um campo que possua todas as informações pertinentes para nossa busca. Para isso, primeiramente criaremos outro método dentro da classe parcial do consultório, veja abaixo:

public partial class Consultorio
{
    public string ConcatenaCampos()
    {
        StringBuilder sb = new StringBuilder();

        sb.AppendFormat(" {0} ", this.nome);
        sb.AppendFormat(" {0} ", this.Especialidade.nome);
        sb.AppendFormat(" {0} ", this.logradouro);
        sb.AppendFormat(" {0} ", this.cidade);
        sb.AppendFormat(" {0} ", this.uf);
        sb.AppendFormat(" {0} ", this.ObtemSinonimos());

        return sb.ToString();
    }

    public string ObtemSinonimos()
    {
        ...
    }
}

Observe que é retornado também como string, todos os campos da entidade Consultorio inclusive os sinônimos da especialidade. Agora basta modificarmos a criação do Field para que nossa busca funcione levando em consideração todos os campos da tabela de consultório e todos os sinônimos da especialidade. Vamos ver abaixo como fica a definição do Field especialidade agora:

doc.Add(new Field("especialidade", Suporte.RemoveAcentos(c.ConcatenaCampos()), Field.Store.NO, Field.Index.ANALYZED, Field.TermVector.YES));

NOTA: Atente-se que agora no final da criação do objeto Field, existe um parâmetro Field.TermVector com valor YES. Isso devido ao fato de seu valor possuir várias informações, desta forma iremos mante-lo como uma lista de termos em nosso índice.

 

Tratamento de preposições e artigos

Bom, podemos perceber facilmente que, como nossa busca é livre, o usuário poderá digitar qualquer coisa para ser buscada. Como por exemplo: “Odontologia em São Paulo” ou “Ortopedia em São Paulo” ou “Cardiologista na rua do Bosque“.

Como mencionei no artigo anterior, a biblioteca do Lucene considera todas as palavras do texto a ser buscado. Levando isso em conta, e analisando o exemplo de busca “Ortopedia em São Paulo”, mesmo as palavras “Ortopedia” e “São Paulo” estarem indexadas, a busca não retornará nenhum resultado, pois ainda falta o tratamento da palavra “em”.

Vamos iniciar criando uma tabela de palavras a serem desconsideradas da nossa busca, veja abaixo sua estrutura e alguns registros pertinentes:

create table tbPalavrasEliminadas
(
    codPalavra int,
    palavra varchar(100)
)

Imagem 4

Agora após arrastar a tabela criada anteriormente para o DataContext, vamos criar um método na classe de Suporte para o tratamento e remoção das palavras definidas na tabela:

public static string RemovePalavras(this string texto)
{
    DBDataContext _db = new DBDataContext();

    List<string> palavrasFrase = new List<string>();
    palavrasFrase.AddRange(texto.Trim().Split(' '));

    //Elimina as palavras
    foreach (string palavra in _db.PalavrasEliminadas.Select(p => p.palavra))
    {
        int iPos = palavrasFrase.IndexOf(palavra.Trim());
        if (iPos >= 0)
            palavrasFrase.RemoveAt(iPos);
    }

    //Monta a frase novamente desconsiderando espaços vazios entre as palavras
    string frase = string.Empty;
    foreach (string p in palavrasFrase)
        if (p.Trim() != string.Empty)
            frase += p + " ";

    return frase.Trim();
}

Basta utilizarmos o método criado ao efetuarmos a busca:

//Cria a query que será efetuada a busca
var query = oParser.Parse(Suporte.RemoveAcentos(txt.RemovePalavras()));

Corretor ortográfico (SpellChecker) com sugestão de busca

Para a implementação do corretor ortográfico iremos utilizar uma segunda DLL também do próprio Lucene chamada SpellChecker.Net. Segue abaixo link para download da DLL:

Download SpellChecker.Net

Agora, após adicionar a dll nas referencias do projeto, vamos criar um método para a criação da verificação nas palavras da busca:

private string InformaSugestao(string frase)
{
    Directory directory = FSDirectory.GetDirectory(this.caminhoIndice, false);
    IndexReader indexReader = IndexReader.Open(directory, true);

    Lucene.Net.Store.RAMDirectory rd = new Lucene.Net.Store.RAMDirectory();

    //Cria instância do SpellChecker
    SpellChecker.Net.Search.Spell.SpellChecker spellchecker = new SpellChecker.Net.Search.Spell.SpellChecker(rd);

    //Define qual campo no índice será utilziado para a verificação de palavras similares
    spellchecker.IndexDictionary(new SpellChecker.Net.Search.Spell.LuceneDictionary(indexReader, "consultorio"));

    string fraseSugerida = string.Empty;

    //Para cada palavra na frase da busca
    foreach (string palavra in frase.Split(' '))
    {
	//Recebe um array de string com as palavras similares
        string[] sugestoes = spellchecker.SuggestSimilar(palavra, 1);

        if (sugestoes.Length > 0)
            fraseSugerida += sugestoes[0] + " ";
        else
            fraseSugerida += palavra + " ";
    }

    indexReader.Close();
    directory.Close();

    //Verifica se a frase sugerida é igual a frase buscada
    if (!frase.Equals(fraseSugerida))
        return fraseSugerida.Trim();
    else
        return string.Empty;
}

Atente-se na linha dentro do foreach, onde é chamada a função SuggestSimilar. Esta função recebe dois parâmetros, o primeiro é a palavra a ser verificada, o segundo é a quantidade de sugestões desejada. Na linha em questão será retornado um array de string com apenas uma sugestão.
Por fim, é retornada a frase sugerida no caso de ser diferente da frase buscada, caso as duas frases sejam iguais, retorna um string.Empty.

Agora vamos modificar nosso Model referente a busca, e adicionar uma nova propriedade referente a frase sugerida. Veja abaixo a classe modificada:

public class Busca
{
    public Busca() { this.resultadoBusca = new List<Consultorio>(); }
    public Busca(List<Consultorio> resultados, string sugestao, string texto)
    {
        this.resultadoBusca = resultados;
        this.textoBuscado = texto;
        this.sugestaoBusca = sugestao;
    }

    public List<Consultorio> resultadoBusca { get; set; }
    public string textoBuscado { get; set; }
    public string sugestaoBusca { get; set; }
}

E modificaremos nossa ActionResult Busca para que fique como abaixo:

public ActionResult Busca(string txt)
{
    if (!String.IsNullOrWhiteSpace(txt))
    {
        List<Consultorio> resultados = this.RealizaBusca(txt);

        //Informa uma sugestão apenas se NÃO houver resultados para a busca efetuada
        string sugestao = resultados.Count == 0 ? this.InformaSugestao(txt) : string.Empty;

        return View(new Busca(resultados, sugestao, txt));
    }
    else
        return View(new Busca());
}

NOTA: Observe que é apresentado uma sugestão apenas se não houver resultados encontrados para a busca efetuada.

Agora para que tudo funcione perfeitamente, vamos modificar algumas coisas em nossa View:

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <script type="text/javascript">

        $(document).ready(function () {

            //Evento de click tanto no botão de busca, quanto no link com a sugestão
            $("#btnBuscar, #btnBuscarSugestao").click(function () {

                //Verifica em qual foi clicado e obtem o respectiovo valor
                //Caso no botão de busca, pega o valor de $("#campoBusca").val()
                //Caso no link de sugestão, pega o valor de $("#hSugestao").val()
                var txtBuscado = $(this).attr("id") == "btnBuscar" ? $("#campoBusca").val() : $("#hSugestao").val();

                window.location = "/Busca/Busca/?txt=" + txtBuscado;
            });

        });

    </script>

    <div>
        <center>
            <%= Html.TextBox("campoBusca", Model.textoBuscado, new { @style = "width:500px;" }) %>

            <br />
            <input type="button" id="btnBuscar" value="Buscar" /><br /><br />

            <% if (!String.IsNullOrWhiteSpace(Model.sugestaoBusca))
                { %>
                    <i><b><a href='javascript:void(0);' id='btnBuscarSugestao'>Você quis dizer <%= Model.sugestaoBusca%> ?</a></b></i>
            <%
                } %>
        </center>
    </div>

    <%--Chama UserControl para renderizar os consultórios--%>
    <% Html.RenderPartial("RenderizaResultadosBusca", Model.resultadoBusca); %>

    <%--Hidden com o valor da sugestão--%>
    <%= Html.Hidden("hSugestao", Model.sugestaoBusca) %>

</asp:Content>

 

Testando

Agora para testarmos todas as implementações, irei realizar algumas buscas:

Imagem 5

Buscando “ortopedista em são paulo” – (desconsiderando pronomes e artigos)

 

Imagem 6

Buscando “ortopedia em são paulo” – (Considerando sinônimos e desconsiderando pronomes e artigos)

 

Imagem 7

Buscando “Clinica são paulo” – (Considerando todos os campos da entidade Consultório)

Imagem 8

Buscando “ortopedist” – (Verificando a frase da busca e sugerindo uma palavra similar, levando em consideração os demais campos indexados)

Imagem 9

Buscando ao clicar no link sugerido “ortopedista” – (Realiza a busca com a sugestão apresentada)

Nossa, quanta coisa…Mas felizmente foi possível abordar muitos recursos do Lucene.Net, e ainda implementamos em uma aplicação ASP.NET MVC. 🙂

Sugestões e feedbacks serão sempre bem vindos.

Download do projeto aqui.

 

Abraços.

 

 

 

 

 

 

 

 

 

 

6 thoughts on “ASP.NET MVC + Lucene.Net ( Parte 2 ) – Implementando Spellchecker, Dicionário de Sinônimos, Tratamentos e Indexações

    • rafaelzaccanini

      Obrigado George! Boa ideia quanto ao SOLR, mas para facilitar já exitem algumas “extensions” como SOLRNet e SOLRSharp.

      Abs

  1. Ricardo

    Rafael, parabéns!

    É possível fazer uma pesquisa utlizando dois arquivos texto? Por exemplo: No arquivo A eu tenho duas colunas, que são cpf e nome, e no arquivo B eu também tenho as
    mesmas duas colunas. Nesse caso eu preciso indexar os dois arquivos texto? O objetivo é comparar e localizar as pessoas que tem nos dois arquivos txt.
    Obs: Estou imaginando que cada arquivo texto tenha entre 10 a 15 milhões de registros. Existe alguma alternativa free superior ao Lucene.Net?

Leave a Reply

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