RavenDB consegue falar português

RavenDB utiliza Lucene como motor de indexação. Isso significa suporte natural a full-text search que pode ser facilmente habilitado a partir da definição de um índice como o que segue.


public class Products_SearchByName : AbstractIndexCreationTask<Product>
{
    public Products_SearchByName()
    {
        Map = products =>
            from p in products
            select new
            {
                p.Name
            };

        Index(entry => entry.Name, FieldIndexing.Analyzed);
    }
}

Como inidicado no código, o campo Name está marcado como Analyzed.

Analyzed?

Quando um campo é marcado como analyzed, isso indica que seu conteúdo será quebrado em termos. São esses termos que serão considerados no momento de uma busca.


No banco de dados de exemplo Northwind, há um produto cadastrado chamado “Guaraná Fantásica” (um claro representante brasileiro). Por default, RavenDB irá gerar dois termos para essa string: 1) “guaraná” e 2) “fantástica”. Ou seja, cada palavra da string, convertida para minúsculo é considerada como um termo de busca.

Compatibilidade nativa com o inglês

O processo de análise padrão do RavenDB otimiza o processo de geração de termos para atender as regras da língua inglêsa. Palavras vazias (stop words) são ignoradas automaticamente. Além disso, “Mario’s” gera “Mario”, a abreviação “F.Y.I.” gera “FYI”.

No momento em que ocorre uma busca, a string buscada também passa por essa análise. O resultado é que as chances de encontrar o que está sendo buscado cresce (mesmo que o que esteja salvo no banco esteja escrito de forma diferente daquela que está sendo usada no momento da consulta).

Tropicalizando o RavenDB

O processo de geração de termos pode ser muito mais interessante para nós, aqui do Brasil, se este considerar particularidades do nosso idioma. Por exemplo, a geração de termos poderia remover acentos das palavras (para que o usuário possa procurar tanto “guaraná” quanto “guarana”). Além disso, deveria considerar o conjunto de palavras vazias da língua portuguesa (de, da, o, a, …).

A customização do processo de análise do RavenDB ocorre pela escrita de um analisador customizado. Há uma implementação completa no meu Github.

Para escrever esse analisador, parti do código do próprio RavenDB aplicando modificações onde eram convenientes.

Show me the code!

Embora exista um bocado de “Lucene” no código que segue, acho que ele é claro o suficiente para indicar o que estou fazendo.


using System;
using System.IO;
using Lucene.Net.Analysis;
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Analysis.Tokenattributes;

namespace RavenDB.Indexing.BrazilianAnalyzer
{
    public sealed class RavenBrazilianFilter : TokenFilter
    {
        public static string[] BRAZILIAN_STOP_WORDS = {
            "a","ainda","alem","ambas","ambos","antes",
            "ao","aonde","aos","apos","aquele","aqueles",
            "as","assim","com","como","contra","contudo",
            "cuja","cujas","cujo","cujos","da","das","de",
            "dela","dele","deles","demais","depois","desde",
            "desta","deste","dispoe","dispoem","diversa",
            "diversas","diversos","do","dos","durante","e",
            "ela","elas","ele","eles","em","entao","entre",
            "essa","essas","esse","esses","esta","estas",
            "este","estes","ha","isso","isto","logo","mais",
            "mas","mediante","menos","mesma","mesmas","mesmo",
            "mesmos","na","nas","nao","nas","nem","nesse","neste",
            "nos","o","os","ou","outra","outras","outro","outros",
            "pelas","pelas","pelo","pelos","perante","pois","por",
            "porque","portanto","proprio","proprios","quais","qual",
            "qualquer","quando","quanto","que","quem","quer","se",
            "seja","sem","sendo","seu","seus","sob","sobre","sua",
            "suas","tal","tambem","teu","teus","toda","todas","todo",
            "todos","tua","tuas","tudo","um","uma","umas","uns"
        };

        private readonly TokenStream _innerInputStream;
        private readonly ITermAttribute _termAtt;
        private readonly ITypeAttribute _typeAtt;
        
        private const string AcronymType = "<ACRONYM>";
        private readonly CharArraySet _stopWords = new CharArraySet(BRAZILIAN_STOP_WORDS, false);

        public RavenBrazilianFilter(TokenStream input) : base(input)
        {
            _innerInputStream = input;
            _termAtt = AddAttribute<ITermAttribute>();
            _typeAtt = AddAttribute<ITypeAttribute>();
        }

        public override bool IncrementToken()
        {
            if (!input.IncrementToken())
            {
                return false;
            }

            var buffer = _termAtt.TermBuffer();
            var bufferLength = _termAtt.TermLength();
            var type = _typeAtt.Type;

            var bufferUpdated = true;

            if (type == AcronymType)
            {
                // remove dots
                var upto = 0;
                for (int i = 0; i < bufferLength; i++)
                {
                    var c = buffer[i];
                    if (c != '.')
                        buffer[upto++] = CharUtils.ToLower(c);
                }
                _termAtt.SetTermLength(upto);
            }
            else
            {
                do
                {
                    //If we consumed a stop word we need to update the buffer and its length.
                    if (!bufferUpdated)
                    {
                        bufferLength = _termAtt.TermLength();
                        buffer = _termAtt.TermBuffer();
                    }

                    for (var i = 0; i < bufferLength; i++)
                    {
                        buffer[i] = CharUtils.RemoveAccentMark(CharUtils.ToLower(buffer[i]));
                    }

                    if (!_stopWords.Contains(buffer, 0, bufferLength))
                    {
                        return true;
                    }

                    bufferUpdated = false;
                } while (input.IncrementToken());

                return false;
            }
            return true;
        }

        internal bool Reset(TextReader reader)
        {
            var input = _innerInputStream as StandardTokenizer;

            if (input == null) return false;

            input.Reset(reader);
            return true;
        }
    }
}

Adeus acentos!

Como usar o analisador customizado

É importante ter o analisador em uma DLL que possa ser adicionada ao servidor do RavenDB (conforme indicado na documentação). Se você optar por utilizar o analisar que escrevi, basta compilar o projeto para obter um assembly pronto.

Para usar um analisador customizado, basta adicionar a DLL com o analisador em uma pasta chamada “Analyzers” no diretório onde está o servidor do RavenDB e modificar o índice indicando que analisador deverá ser utilizado.


public class Products_SearchByName : AbstractIndexCreationTask<Product>
{
    public Products_SearchByName()
    {
        Map = products =>
            from p in products
            select new
            {
                p.Name
            };

        Index(entry => entry.Name, FieldIndexing.Analyzed);
        Analyze(entry => entry.Name, typeof(RavenBrazilianAnalyzer).AssemblyQualifiedName);
    }
}

Esse novo índice removerá acentos nos termos gerados.

Dessa forma, a seguinte busca retornará o produto sem problemas.


class Program
{
    static void Main(string[] args)
    {
        using (var session = DocumentStoreHolder.Store.OpenSession())
        {
            var results = session
                .Query<Product, Products_SearchByName>()
                .Search(r => r.Name, "guarana")
                .ToList();

            foreach (var result in results)
            {
                Console.WriteLine(result.Name);
            }
        }
    }
}

Afinal a palavra informada está na lista de termos.

Compartilhe este insight:

Uma resposta

  1. curiosamente fiz algo parecido com o mongodb… não sabia que era um approach comum. tinha medo de estar fazendo algo estranho, mas resolvia meu problema 😀

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Elemar Júnior

Sou fundador e CEO da EximiaCo e atuo como tech trusted advisor ajudando diversas empresas a gerar mais resultados através da tecnologia.

Elemar Júnior

Sou fundador e CEO da EximiaCo e atuo como tech trusted advisor ajudando diversas empresas a gerar mais resultados através da tecnologia.

Mais insights para o seu negócio

Veja mais alguns estudos e reflexões que podem gerar alguns insights para o seu negócio:

No meu cotidiano, reconheço que, por mais estranho que pareça, comprometo muito do meu tempo ouvindo música ruim até que,...
I spent the last week talking about RavenDB in San Francisco. By the way, version 4 is out. Also, I...
Na vida, todos temos “momentos traumáticos”. Ou seja, aqueles momentos que, de repente, mudam tudo e, ao mesmo tempo, de...
In the previous post, I mentioned Mario Fusco who wrote a blog post series questioning the way Java developers are...
Neste post, compartilho um exemplo de implementação para um algoritmo extremamente famoso e importante: O algoritmo de Dijkstra. Trata-se de...
Superficialmente, direito e privilégio são conceitos que se aproximam e, eventualmente, se confundem. Mas, com um pouco de cuidado, ficam...