
O Full Text Search do PostgreSQL oferece recursos para pesquisar documentos, ranquear resultados e também destacar os trechos mais relevantes de um texto. Entre essas funcionalidades está a ts_headline, que pode ser usada para gerar um resumo do conteúdo encontrado e destacar os termos que correspondem à busca.
Mas o que acontece quando você precisa de mais do que um único texto de retorno?
Imagine uma tabela de livros em que cada registro possui o conteúdo de um capítulo ou parágrafo. Ao realizar uma busca por uma determinada palavra, pode ser interessante retornar não apenas o registro encontrado, mas cada trecho relevante do conteúdo como uma linha independente.
É aí que surge uma situação interessante: a função ts_headline consegue retornar vários fragmentos, mas o resultado ainda é um único valor do tipo texto.
O papel do ts_headline
No PostgreSQL, a função ts_headline recebe um documento e uma consulta de texto e retorna um trecho do documento no qual os termos encontrados são destacados. Ela também permite configurar como esses trechos serão gerados.
Entre as opções disponíveis estão:
- MaxWords e MinWords: Número máximo e mínimo de palavras em cada trecho exibido
- HighlightAll: Quando True, o resultado será o trecho completo com o texto pesquisado destacado, ignorando os parâmetros acima
- MaxFragments: Máximo de fragmentos de texto a serem exibidos
- FragmentDelimiter: Texto que será utilizado para separar os diferentes fragmentos no resultado (ex.: $)
Por exemplo, podemos configurar a função para retornar vários fragmentos separados pelo caractere $:
SELECT
ts_headline(
content,
to_tsquery('english', 'happy'),
'MaxFragments=3
FragmentDelimiter=$'
)
FROM mytable
WHERE ts_content @@ to_tsquery('english', 'happy');
Entendendo a Consulta:
- content: Coluna do tipo text com o texto original não processado onde a marcação será aplicada
- ts_content: Coluna do tipo tsvector pré-processada (GENERATED ALWAYS AS (to_tsvector(‘english’, content)) STORED), usada na cláusula WHERE. Necessário a criação de um índice GIN sobre ela (CREATE INDEX … USING gin (ts_content)), para garantir melhor desempenho nas buscas
- to_tsquery: Converte a string informada em uma consulta estruturada (tsquery) no idioma correspondente
O resultado pode ser algo semelhante a:
Este é um fragmento com <b>happy</b>$outro fragmento com <b>happy</b>$e mais um fragmento com <b>happy</b>
Para quem precisa apenas exibir o conteúdo, isso pode ser suficiente. Mas, se a aplicação precisa trabalhar com cada fragmento individualmente, ainda existe um problema: todos eles continuam dentro de uma única string.
Transformando o texto em linhas
Uma alternativa é utilizar a função regexp_split_to_table.
Ela recebe um texto e uma expressão regular que define onde o conteúdo deve ser dividido. Diferentemente de funções que retornam um único valor, regexp_split_to_table transforma cada parte resultante da divisão em uma linha.
Como já definimos um delimitador específico para o ts_headline, podemos utilizá-lo para separar os fragmentos:
SELECT
regexp_split_to_table(
ts_headline(
content,
to_tsquery('english', 'happy'),
'MaxFragments=3
FragmentDelimiter=$'
),
'$'
)
FROM mytable WHERE ts_content @@ to_tsquery('english', 'happy');
Nesse caso, o ts_headline continua responsável por encontrar e destacar os trechos relevantes, enquanto o regexp_split_to_table transforma o resultado em múltiplas linhas.
Em vez de receber:
Este é um fragmento com <b>happy</b>$outro fragmento com <b>happy</b>$e mais um fragmento com <b>happy</b>
passamos a ter algo conceitualmente como:
Este é um fragmento com <b>happy</b>
outro fragmento com <b>happy</b>
e mais um fragmento com <b>happy</b>
Onde cada fragmento agora é apresentado em linhas.
Essa combinação é útil justamente porque cada função resolve uma parte diferente do problema: uma trabalha com a busca e a geração dos trechos relevantes, enquanto a outra reorganiza o resultado para que ele possa ser tratado como um conjunto de linhas.
Por que isso é interessante?
Esse tipo de situação aparece quando o resultado de uma função SQL não está no formato mais conveniente para o restante da consulta ou para a aplicação que vai consumir os dados.
No caso do Full Text Search, podemos pensar em três etapas:
- Encontrar os documentos relevantes: O operador @@ verifica se o tsvector corresponde à consulta.
- Gerar os fragmentos: O ts_headline utiliza a consulta para produzir trechos relevantes e destacar os termos encontrados.
- Transformar o resultado em linhas: O regexp_split_to_table utiliza o delimitador definido anteriormente para transformar o texto em registros individuais.
Essa separação de responsabilidades deixa a consulta mais flexível e permite que os fragmentos sejam tratados individualmente em etapas posteriores.
Um cuidado importante com ts_headline
Existe, porém, um ponto importante quando trabalhamos com essa abordagem: o custo da própria função ts_headline.
A documentação do PostgreSQL destaca que ts_headline trabalha sobre o documento original, e não simplesmente sobre o tsvector, o que pode tornar a operação custosa. Por isso, seu uso deve ser pensado com cuidado, especialmente quando existe uma quantidade grande de documentos correspondentes à consulta.
Uma estratégia comum é primeiro encontrar e ordenar os resultados relevantes e só depois aplicar a geração dos trechos aos registros que realmente serão apresentados. Dessa forma, evitamos executar o processamento de destaque em um conjunto muito maior do que o necessário.
Quando o formato do resultado também importa
Esse exemplo mostra uma característica interessante do PostgreSQL: muitas vezes, o desafio não está em encontrar uma função que faça exatamente tudo o que precisamos, mas em combinar funções que resolvem etapas diferentes do problema.
O Full Text Search já oferece os mecanismos para identificar documentos relevantes e gerar destaques. Quando precisamos transformar esse resultado em uma estrutura diferente, outras funções SQL podem entrar em cena para reorganizar os dados.
Nesse caso, ts_headline e regexp_split_to_table trabalham juntos para transformar um texto com vários fragmentos em um conjunto de linhas que pode ser manipulado individualmente.
É um exemplo simples, mas que mostra como explorar as funções nativas do PostgreSQL pode evitar processamento adicional na aplicação e resolver problemas de transformação diretamente na consulta.
Receba mais conteúdos como este!
Assine a nossa newsletter e acompanhe conteúdos técnicos, análises e discussões sobre PostgreSQL, arquitetura e engenharia de dados.