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