Busca universal em PT-BR com RavenDB
Tags: ravendb, filtro, portugues, csharp
Published: 2021-05-21 10:41:07

Na língua portuguesa podemos observar vários tipos de acentos presentes nas palavras, como nas palavras 'Macarrão' e 'Emoção', repare que além do acento, existem caracteres especiais como o 'ç'.

Quando queremos utilizar um filtro em uma busca, normalmente esperamos que estas particularidades da língua sejam ignoradas, assim como não queremos que uma letra maiúscula seja diferenciada de uma minúscula.

Nas buscas de index com RavenDB conseguimos definir um analisador __case insensitive__, mas para os acentos da língua portuguesa precisamos desenvolver um analisar personalizado. Para isto precisamos criar um projeto .NET vazio, no Visual Studio pode-se criar um projeto Console e apagar o 'Program.cs'. Com o projeto limpo, vamos adicionar dois analisadores, um que será usado no __full-text search__ e outro para pesquisa de palavras simples ou composta.

# 1. Criar Analisadores
Com o projeto criado é necessário instalar a dependência de [Lucene](https://www.nuget.org/packages/Lucene.Net). Agora já podemos criar os dois analisadores, segue abaixo o código de cada um deles.
Analisador 1:
```csharp
class AsciiAnalyzer : StandardAnalyzer
{
    public AsciiAnalyzer()
        : base(Version.LUCENE_30)
    { }

    public AsciiAnalyzer(Version matchVersion)
        : base(matchVersion)
    { }

    public AsciiAnalyzer(Version matchVersion, FileInfo stopWords)
        : base(matchVersion, stopWords)
    { }

    public AsciiAnalyzer(Version matchVersion, ISet<string> stopWords)
        : base(matchVersion, stopWords)
    { }

    public AsciiAnalyzer(Version matchVersion, TextReader stopWords)
        : base(matchVersion, stopWords)
    { }

    public override TokenStream TokenStream(string fieldName, TextReader reader)
    {
        //Usar tokenizer do StandardAnalyzer
        var stream = base.TokenStream(fieldName, reader);

        //Filtros
        stream = new LowerCaseFilter(stream);
        stream = new ASCIIFoldingFilter(stream);

        return stream;
    }

}
```
Analisador 2:
```csharp
class AsciiSingleTokenAnalyzer : KeywordAnalyzer
{
    public override TokenStream TokenStream(string fieldName, TextReader reader)
    {
        //Usar tokenizer do KeywordAnalyzer
        var stream = base.TokenStream(fieldName, reader);

        //Filtros
        stream = new LowerCaseFilter(stream);
        stream = new ASCIIFoldingFilter(stream);

        return stream;
    }
}
```


# 2. Compilar e instalar analisadores
O projeto agora precisa ser compilado no modo 'release' para gerar um arquivo '.dll'. Realize a compilação e localize o arquivo '.dll' do seu projeto, em seguida copie apenas este arquivo e coloque na pasta 'Server' do RavenDB.

# 3. Criar Map-Reduce Index
Para utilizar os analisadores, devemos informar isto na criação de um Index. O código abaixo mostra um exemplo de um Map-Redude Index que usa o Analisador criado anteriormente.
```csharp
public class Curriculo_Cargos : AbstractIndexCreationTask<Curriculo, Curriculo_Cargos.Result>
{
    public class Result
    {
        public string Nome { get; set; }
        public string[] Empresas { get; set; }
    }
    
    public Curriculo_Cargos()
    {
        Map = curriculos =>
                from curriculo in curriculos
                where curriculo.ExperienciasProfissionais != null
                from experienciaProfissional in curriculo.ExperienciasProfissionais
                select new Result
                {
                    Nome = experienciaProfissional.DescricaoDoCargo,
                    Empresas = new string[] {experienciaProfissional.NomeDaEmpresa}
                }
            ;

        Reduce = results => from result in results
            group result by result.Nome
            into g
            select new Result
            {
                Nome = g.First().Nome,
                Empresas = g.Select(x => x.Empresas).Select(x => x[0]).Distinct().ToArray()
            };
            
        Analyzers.Add(x => x.Nome, "NomeProjeto.Namespace.AsciiSingleTokenAnalyzer, NomeProjeto");
    }
}
```
Repare que neste caso eu utilizo o AsciiSingleTokenAnalyzer, pois quero analisar 'Nome' como um todo, ao invés de palavra por palavra(full-text search).

# 4. Realizar uma busca
Para realizar uma busca, eu utilizo RAW Query para escrever em linguagem RQL em vez de LINQ, visto que LINQ traz uma certa limitação de operações lógicas envolvendo 'where' e 'search' em uma mesma query. O código abaixo mostra um exemplo de uma query em um Index com o analisador personalizado.
```csharp
var query = _dbSession.Query<Curriculo_Cargos.Result, Curriculo_Cargos>();
if (!String.IsNullOrEmpty(request.IniciaCom))
    query = query.Search(x => x.Nome, $"{request.IniciaCom}*");
if (!String.IsNullOrEmpty(request.Contem))
    query = query.Search(x => x.Nome, $"*{request.Contem}*");
if (!String.IsNullOrEmpty(request.TerminaCom))
    query = query.Search(x => x.Nome, $"*{request.TerminaCom}");
var resposta = await query.ToArrayAsync();
```

Obs.: Utilizo 'where' para busca exata de palavras compostas, pois o 'search', automaticamente separa a palavra composta da busca em um array de palavras.