sexta-feira, 29 de julho de 2011

Full Text Search em português no PostgreSQL



Full Text Searching (ou simplesmente busca textual ou FTS) é uma poderosa ferramenta disponível em bancos de dados que visa aumentar a probabilidade de sucesso nas consultas efetuadas pelos usuários.



Operadores de busca textual existem nos SGBDs há anos. O PostgreSQL oferece, além do LIKE, o ILIKE (versão que desconsidera a capitalização) e operadores baseados em expressão regular (i.e., ~ e ~*) para tipos de dados textuais. Entretanto, tais operadores falham ao não prover muitas propriedades essenciais requeridas por sistemas de informações modernos, tais como suporte linguístico, ranking dos resultados e indexação [1].

Na Universidade de Moscou foi criado o projeto Tsearch2 [2], no qual os russos Oleg Bartunov e Teodor Sigaev desenvolveram um motor do tipo full text inteiramente integrado ao SGBD PostgreSQL. Além da referida solução de busca textual, foram criados dois poderosos mecanismos de indexação: GiST e GIN.



O Tsearch2 residiu no diretório contrib do PostgreSQL até a sua versão 8.1, sendo finalmente incorporado ao core do SGBD na versão 8.3, tornando-se a opção default para FTS nele.



Para ilustrar, tomemos como exemplo o clássico cadastro de municípios brasileiros. A missão é efetuar buscas textuais (neste caso pelo nome dos municípios) usando FTS.

Eis uma tabela no PostgreSQL populada com as cidades:

        nome         | uf 
---------------------+----
 Abadia de Goiás     | GO
 Abadia dos Dourados | MG
 Abadiânia           | GO
 Abaeté              | MG
 Abaetetuba          | PA
 Abaiara             | CE
 Abaíra              | BA
 Abaré               | BA
 Abatiá              | PR
 Abdon Batista       | SC
 ...


A ideia é fazer com que buscas aproximadas possam ser efetuadas pelo usuário, ou seja, algo mais "poderoso" que um falho operador LIKE.

A primeira modificação será criar uma coluna adicional, chamada de "busca", a fim de armazenar os vetores já normalizados dos termos para ser usada posteriormente nas consultas.

ALTER TABLE municipios ADD busca tsvector;


Eis a estrutura da tabela após a execução do comando:

         Table "public.municipios"
 Column |         Type          | Modifiers 
--------+-----------------------+-----------
 nome   | character varying(50) | 
 uf     | character(2)          | 
 busca  | tsvector              | 


Como o foco deste estudo é usar o idioma português, e considerando que o servidor PostgreSQL esteja configurado com a codificação UTF-8 (i.e., Unicode), precisaremos criar algumas funções adicionais. Baseando-se em [3] e [4], criei essas funções:

CREATE FUNCTION to_ascii(bytea, name) RETURNS text AS
 'to_ascii_encname'
 LANGUAGE internal STRICT;

CREATE FUNCTION simples(texto varchar) RETURNS varchar AS
 'select lower(to_ascii(convert_to($1, ''latin1''), ''latin1''))'
 LANGUAGE sql IMMUTABLE STRICT;


Veja o que essa nova função simples() é capaz de fazer:

brasil=# select simples('Pinhão com Açaí');
    simples     
----------------
 pinhao com acai
(1 row)


Ou seja, trata-se de uma função de transformação que normaliza o texto recebido como argumento. Ela será essencial para os passos a seguir.

Podemos usar a função simples() em conjunto com to_tsvector() a fim de produzir o vetor de lexemas (termos processados a serem usados nas buscas). Veja:

brasil=# select to_tsvector(simples('Pinhão com Açaí'));
    to_tsvector    
-------------------
 'aca':3 'pinha':1
(1 row)


Voltando à tabela de municípios, execute o comando SQL a seguir para popular a recém-criada coluna "busca" com o vetor transformado em cada uma de suas tuplas.

UPDATE municipios SET busca = to_tsvector(simples(nome));


Pronto! Agora que os textos foram pré-processados, basta executar as consultas SQL usando o operador especial @@ no PostgreSQL. Veja um exemplo de uso no qual o usuário digitou o texto "sao mateus" na aplicação. A consulta SQL fica assim:

brasil=# select nome, uf from municipios
brasil=# where busca @@ plainto_tsquery(simples('sao mateus'));
          nome          | uf 
------------------------+----
 São Mateus             | ES
 São Mateus do Maranhão | MA
 São Mateus do Sul      | PR
(3 rows)


Note que além do vetor (coluna "busca" do tipo tsvector) é preciso especificar um tipo tsquery, fornecido pelas funções to_tsquery() e plainto_tsquery(). A função simples() foi usada novamente para realizar a transformação do texto informado.

Ótimo! É só isso?

Não... Funcionou, mas o desempenho não pode ser garantido. Na realidade a busca com essa abordagem será mais feliz que se usássemos um ardiloso LIKE "%sao mateus%". (Jogue a primeira pedra quem nunca encontrou um monstrinho desses em uma aplicação!).

Vejamos como o SGBD está tratando a consulta SQL acima:

brasil=# explain select nome, uf from municipios
brasil=# where busca @@ plainto_tsquery(simples('sao mateus'));
                         QUERY PLAN                          
-------------------------------------------------------------
 Seq Scan on municipios  (cost=0.00..163.60 rows=6 width=16)
   Filter: (busca @@ plainto_tsquery('sao mateus'::text))
(2 rows)


Ops, uma varredura sequencial (também conhecido por full scan ou sequential scan) está sendo efetuada! Se você não sabe o que é isso, consulte o seu DBA. Se ele for teu amigo, não irá te xingar.

Bom, e como podemos corrigir isso? Criando um índice! :D

Neste caso específico do FTS no PostgreSQL, temos a escolha de usar uma das duas opções de índices, GiST ou GIN (lembra dos russos?). Cada uma tem a sua aplicação. Escolhi o GIN por esta apresentar maior precisão para esta aplicação. Veja como criar o índice:

CREATE INDEX municipios_gidx ON municipios USING gin(busca);


E agora, será que funcionou? Roda o EXPLAIN novamente! Veja:

brasil=# explain select nome, uf from municipios
brasil=# where busca @@ plainto_tsquery(simples('sao mateus'));
                                  QUERY PLAN                                  
------------------------------------------------------------------------------
 Bitmap Heap Scan on municipios  (cost=4.30..23.49 rows=6 width=16)
   Recheck Cond: (busca @@ plainto_tsquery('sao mateus'::text))
   ->  Bitmap Index Scan on municipios_gidx  (cost=0.00..4.30 rows=6 width=0)
         Index Cond: (busca @@ plainto_tsquery('sao mateus'::text))
(4 rows)


Sucesso! Agora a busca textual com FTS está devidamente indexada no PostgreSQL.

Referências


[1] PostgreSQL 8.3 Documentation - Full Text Search

[2] Tsearch2 - full text search extension for PostgreSQL

[3] PostgreSQL: TO_ASCII & UTF8

[4] PostgreSQL 8.3 Documentation - String Functions and Operators