26 de julho de 2017 - Código

RavenDB consegue falar português

Um analyzer customizado para particularidades do pt-BR

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.

1 pensamento em “RavenDB consegue falar português”

Deixe uma resposta

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