CallMarx.dev

Busca em texto otimizada com a Gem pg_search - Parte I

18-01-2021 5 minutos de leitura.
Magnifying glass on a text

Naufragando pela internet me deparei com a Gem PgSearch que tira proveito das funcionalidades de Full Text Searching do PostgreSQL. O conceito Full Text Searching refere a técnicas e estrateǵias computacionais para otimizar a pesquisa de palavras, e até frases, em longos e múltiplos textos armazenados em bancos de dados. Trata-se de uma ampla área de estudo e pesquisa dentro da Ciências da Computação, desde de 1990.

Mas confesso: tenho preconceitos com NoSQL.

A proposta do banco de dados relacional é bem intuitiva, como diz na wikipédia brazuca:

é um banco de dados que modela os dados de uma forma que eles sejam percebidos pelo usuário como tabelas, ou mais formalmente relações

Na democracia das planilhas de excel gigantes, abstrair o conceito de tabelas é bem complicado. Quando traduzimos a lógica de negócio a uma aplicação, atender as demandas de uma empresa, por exemplo, entidades como usuário, produto, conta, perfil etc são intuitivamente aplicadas como classes dentro de uma programação orientada à objetos e, mais comumente armazenadas como tabelas. Afinal, quando falamos de PostgreSQL, MySQL ou SejaláoqueforSQL, estamos tratando de um sistema gerenciador de banco de dados objeto-relacional (SGBD). Objeto-relacional, né?! Mas vamos ao que interessa, apesar de encher meu coração tecer meu ódio à modinha meus receios à NoSQL. Meu foco aqui será mais sobre o uso e vantagens dessa Gem.

PostgreSQL, seu lindo!

PostgreSQL, seu lindo!

Fanboy que sou desse SGBD, qualquer desculpa é válida para estudar e aproveitar ainda mais essa poderosa ferramenta. Vamos então explorar as funcionalidades que ele nos oferece para Full Text Searching.

Os códigos a seguir foram executados em um Docker de PostgreSQL, para fazer o mesmo utilize os seguintes comandos:

$ docker run --rm --name testing-textsearch -p 5432:5432 \
-e POSTGRES_USER=test -e POSTGRES_PASSWORD=test \
-e POSTGRES_DB=textsearchdb \
-d postgres:latest

$ docker exec -it testing-textsearch psql -d textsearchdb -U test

Você deve obter o terminal psql com algo como:

psql (13.1 (Debian 13.1-1.pgdg100+1))
Type "help" for help.

textsearchdb=#

Vamos começar com a função ts_vector

textsearchdb=# SELECT to_tsvector('english', 'Life is like riding a bicycle.
To keep your balance, you must keep moving.');
                                    to_tsvector
-----------------------------------------------------------------------------------
 'balanc':10 'bicycl':6 'keep':8,13 'life':1 'like':3 'move':14 'must':12 'ride':4
(1 row)

Temos o texto “Life is like riding a bicycle. To keep your balance, you must keep moving”, normalmente referido como document dentro da área de Full Text Searching. Esse documento é analisado e convertido em um tipo de dados especial chamado de tsvector, que é basicamente lexemas, palavras-chave normalizadas contidas no documento. No caso, usamos o dicionário do idioma inglês para normalizar as palavras, minimizando-as em sua raiz. Por isso a palavra “balance” foi reduzida à “balanc”, “riding” à “ride”, “moving” à “move” etc. A numeração ao lado de cada lexema é a sua posição no texto (documento), termos e palavras muito comuns como ‘is’, ‘to’, ‘you’ etc, são removidos para reduzir o tamanho dos dados e a chance de falsos positivos.

Para verificar se uma ou mais palavras existem dentro de um documento utilizamos a função to_tsquery. Da mesma forma que to_tsvector, ela também normaliza o que será buscando antes de fazer a busca.

textsearchdb=# SELECT to_tsquery('english', 'keep & moves');
   to_tsquery
-----------------
 'keep' & 'move'
(1 row)

Com o operador @@ podemos verificar se um tsquery esta presente em um tsvector, ou melhor dizendo, se uma palavra (ou mais com operadores booleanos | e &) normalizada encontra-se em um documento.

textsearchdb=# SELECT
  to_tsvector('english', 'Life is like riding a bicycle.
    To keep your balance, you must keep moving.') @@
  to_tsquery('english', 'keep & moves');
 ?column?
----------
 t
(1 row)

textsearchdb=# SELECT
  to_tsvector('english', 'Life is like riding a bicycle.
    To keep your balance, you must keep moving.') @@
  to_tsquery('english', 'unpresent');
 ?column?
----------
 f
(1 row)

Obs: As letras ‘t’ e ‘f’ aqui significam, respectivamente, à true e false.

Com a função ts_rank podemos classificar os resultados da busca, podendo ordenar da mais alta para a mais baixa quando buscamos um termo em múltiplos documentos.

textsearchdb=# SELECT
  ts_rank(
    to_tsvector('english', '
      Life is like riding a bicycle. To keep your balance, you must
      keep moving.
    '),
    to_tsquery('english', 'keep')
   );
   ts_rank
-------------
 0.075990885
(1 row)

textsearchdb=# SELECT
  ts_rank(
    to_tsvector('english', '
      Making mistakes is a natural part of the language learning process.
      The key is to learn from these mistakes. Do not be afraid to try out
      new things in English but always remember to reflect on them and decide
      what was successful and what you need to keep working on.
    '),
    to_tsquery('english', 'keep')
);
  ts_rank
------------
 0.06079271
(1 row)

textsearchdb=# SELECT
  ts_rank(
    to_tsvector('english', '
      If you really want to eat, keep climbing. The fruits are on the top of
      the tree. Stretch your hands and keep stretching them. Success is on
      the top, keep going.
    '),
    to_tsquery('english', 'keep')
   );
   ts_rank
-------------
 0.082745634
(1 row)

Há uma série de fatores que influenciam essa pontuação, como a posição do termo no documento, a quantidade de palavras que o documento tem, quantas vezes o termo aparece e mais outras tantas. Vale ressaltar que existem inúmeras estratégias de ranqueamento que dependem do contexto e objetivo, como colocar na conta o tempo de modificação e/ou criação do texto, partes mais importantes que outras como o título em relação ao conteúdo de um post, por exemplo.

Bem, isso foi apenas uma introdução das funcionalidades de Full Text Searching do PostgreSQL, na próxima parte vou mostrar como aplicá-las com a Gem PgSearch em projetinho Ruby on Rails.

Link para parte II