Como funciona a busca textual do MongoDB

MongoDB

MongoDB

A versão 2.4.0 do MongoDB vai ser lançada em breve e terá suporte a busca textual (Full Text Search – FTS) nativa! Esse recurso estava sendo solicitado desde as primeiras versões do MongoDB, consta no ticket SERVER-380 do JIRA, aberto em 2009.

Esse recurso permite realizar busca textual simples e mais complexas, incluindo uso de stemming, stopwords, negação, busca por frases e suporte a multi idiomas, incluindo o português. Atenção, esse recurso ainda está em desenvolvimento, não use em produção!

Eu fiz um teste, utilizando a versão 2.4.0-rc1 que foi lançada dia 25/02/2013 e pode ser encontrada na página de download do MongoDB.

Como habilitar a busca textual

Se você estiver usando a versão 2.4.0-rc ou superior, poderá habilitar o suporte a busca textual com o seguinte comando:

$ ./mongod --setParameter textSearchEnabled=true

Com o servidor sendo executado com o comando acima, os exemplos a seguir foram executados dentro do shell do próprio MongoDB.

O comando abaixo cria o índice no banco para a língua portuguesa:

> db.teste.ensureIndex( {txt: "text"}, {default_language: "portuguese"} )

Para verificar se o índice foi criado com sucesso, basta rodar o comando:

 > db.teste.getIndices()

A saída deverá ser algo do tipo:

> db.teste.getIndices()
[
    {
        "v" : 1,
        "key" : {
            "_id" : 1
        },
        "ns" : "test.teste",
        "name" : "_id_"
    },
    {
        "v" : 1,
        "key" : {
            "_fts" : "text",
            "_ftsx" : 1
        },
        "ns" : "test.teste",
        "name" : "txt_text",
        "default_language" : "portuguese",
        "weights" : {
            "txt" : 1
        },
        "language_override" : "language",
        "textIndexVersion" : 1
    }
]

Repare que na linha 19, o parâmetro “default_language” confirma que o índice foi criado respeitando os padrões do idioma português.

Agora é hora de indexar alguma coisa:

> db.teste.insert( {txt: "O rato roeu a roupa do rei de Roma"} )

Abaixo, um exemplo da busca, a palavra pesquisada é apenas “roma“:

> db.teste.runCommand( "text", { search : "roma"})
{
    "queryDebugString" : "rom||||||",
    "language" : "portuguese",
    "results" : [
        {
            "score" : 0.6,
            "obj" : {
                "_id" : ObjectId("512e28809d5aac13c2980cc9"),
                "txt" : "O rato roeu a roupa do rei de Roma"
            }
        }
    ],
    "stats" : {
        "nscanned" : 1,
        "nscannedObjects" : 0,
        "n" : 1,
        "nfound" : 1,
        "timeMicros" : 95
    },
    "ok" : 1
}

Repare que o registro com o termo “roma” foi encontrado, junto com seu score.

Podemos fazer uma brincadeira para testar o stemming, vou realizar uma busca com a frase “o rato roeu a roupa da rainha de roma”. Troquei o termo “rei” por “rainha”, vejam:

> db.teste.runCommand( "text", { search : "o rato roeu a roupa da rainha de roma"})
{
    "queryDebugString" : "rainh|rat|roeu|rom|roup||||||",
    "language" : "portuguese",
    "results" : [
        {
            "score" : 5.4,
            "obj" : {
                "_id" : ObjectId("512e28809d5aac13c2980cc9"),
                "txt" : "O rato roeu a roupa do rei de Roma"
            }
        }
    ],
    "stats" : {
        "nscanned" : 4,
        "nscannedObjects" : 0,
        "n" : 1,
        "nfound" : 1,
        "timeMicros" : 221
    },
    "ok" : 1
}

O score dessa busca ficou mais alto que o da primeira. Quanto mais baixo o score, maior é a precisão da busca. Mesmo assim a busca retornou um resultado válido, encontrando o registro que contém esse termo.

Vamos agora adicionar mais algumas frases:

> db.teste.insert( {txt: "Batatinha quando nasce, espalha rama pelo chão"} )
> db.teste.insert( {txt: "Escolhe um trabalho de que gostes, e não terás que trabalhar nem um dia na tua vida."} )
> db.teste.insert( {txt: "Enquanto houver vontade de lutar, haverá esperança de vencer"} )
> db.teste.insert( {txt: "A vida só pode ser compreendida, olhando-se para trás; mas só pode ser vivida, olhando-se para frente"} )

Indexei algumas frases aleatórias encontradas na internet, sendo que duas frases possuem a palavra “vida”.

Se fizermos uma busca com o termo “vida”, teremos o seguinte resultado:

> db.teste.runCommand( "text", { search : "vida"})
{
    "queryDebugString" : "vid||||||",
    "language" : "portuguese",
    "results" : [
        {
            "score" : 0.5625,
            "obj" : {
                "_id" : ObjectId("512e2ad69d5aac13c2980ccb"),
                "txt" : "Escolhe um trabalho de que gostes, e não terás que trabalhar nem um dia na tua vida."
            }
        },
        {
            "score" : 0.5454545454545454,
            "obj" : {
                "_id" : ObjectId("512e2b6c9d5aac13c2980ccd"),
                "txt" : "A vida só pode ser compreendida, olhando-se para trás; mas só pode ser vivida, olhando-se para frente"
            }
        }
    ],
    "stats" : {
        "nscanned" : 2,
        "nscannedObjects" : 0,
        "n" : 2,
        "nfound" : 2,
        "timeMicros" : 113
    },
    "ok" : 1
}

Agora, vejam o stemming em ação, vou fazer uma busca com o termo “vencendo”. Repare que essa palavra não está indexada, mas temos um registro com a palavra “vencer”, que é da mesma raiz da busca em questão.

> db.teste.runCommand( "text", { search : "vencendo"})
{
    "queryDebugString" : "venc||||||",
    "language" : "portuguese",
    "results" : [
        {
            "score" : 0.5714285714285714,
            "obj" : {
                "_id" : ObjectId("512e2b2d9d5aac13c2980ccc"),
                "txt" : "Enquanto houver vontade de lutar, haverá esperança de vencer"
            }
        }
    ],
    "stats" : {
        "nscanned" : 1,
        "nscannedObjects" : 0,
        "n" : 1,
        "nfound" : 1,
        "timeMicros" : 98
    },
    "ok" : 1
}

Outro exemplo, com o verbo “nascer”, que também não existe, mas uma palavra com a mesma raiz foi indexada: “nasce”:

> db.teste.runCommand( "text", { search : "nascer"})
{
    "queryDebugString" : "nasc||||||",
    "language" : "portuguese",
    "results" : [
        {
            "score" : 0.5714285714285714,
            "obj" : {
                "_id" : ObjectId("512e2a929d5aac13c2980cca"),
                "txt" : "Batatinha quando nasce, espalha rama pelo chão"
            }
        }
    ],
    "stats" : {
        "nscanned" : 1,
        "nscannedObjects" : 0,
        "n" : 1,
        "nfound" : 1,
        "timeMicros" : 96
    },
    "ok" : 1
}

Para finalizar, vamos fazer uma busca com negação. Nesse caso quero todos os registros que contenham a palavra “vida” mas que não tenha a palavra “escolhe”, fazemos assim:

> db.teste.runCommand( "text", { search : "vida -escolhe"})
{
    "queryDebugString" : "vid||escolh||||",
    "language" : "portuguese",
    "results" : [
        {
            "score" : 0.5454545454545454,
            "obj" : {
                "_id" : ObjectId("512e2b6c9d5aac13c2980ccd"),
                "txt" : "A vida só pode ser compreendida, olhando-se para trás; mas só pode ser vivida, olhando-se para frente"
            }
        }
    ],
    "stats" : {
        "nscanned" : 2,
        "nscannedObjects" : 0,
        "n" : 1,
        "nfound" : 1,
        "timeMicros" : 192
    },
    "ok" : 1
}

O registro que tinha o termo “escolhe” foi ignorado do resultado, como esperado.

São exemplos simples, mas mostram que a busca textual do MongoDB possui um grande potencial.

Estou fazendo alguns testes de realizar buscas dentro de uma aplicação desenvolvida em Python (utilizando o PyMongo). Assim que tiver material suficiente, vou escrever outro post.


Veja também