PostgreSQL: Motor de Busca Textual do PostgreSQL
Última atualização: 2026-08-26
1. O Que Você Vai Aprender
- Entender os conceitos principais da busca textual (documento, lexema, dicionário, configuração)
- Usar os tipos de dados
tsvectoretsquery - Usar
to_tsvector/to_tsquery/plainto_tsquery/phraseto_tsquery/websearch_to_tsquerypara conversão - Usar o operador
@@para realizar busca textual - Usar
ts_rankpara ranqueamento ets_headlinepara destaque - Criar índices GIN para acelerar a busca textual
- Configurar busca textual multilíngue (english/simple/zhpinyin)
- Usar estatísticas
ts_state consultas de catálogo do sistema
2. A História
A plataforma de e-commerce do Charlie tem 500.000 registros de produtos e os usuários precisam buscar produtos. Ele começou com LIKE '%red shoes%', mas buscar "red shoes" só retornava produtos cuja descrição continha exatamente a string consecutiva "red shoes" e a consulta levava 3 segundos. Após migrar para a busca textual do PostgreSQL:
- "red shoes" encontra produtos contendo tanto "red" quanto "shoes" (em qualquer ordem)
- Stop words são ignoradas automaticamente (the/a/an)
- Stemming automático (shoes → shoe)
- Acelerada por um índice GIN, a consulta leva apenas 30 milissegundos — 100x mais rápida
3. Conceito: Conceitos Principais da Busca Textual
(1) Quatro Conceitos Principais
| Conceito | Descrição | Equivalente no PostgreSQL |
|---|---|---|
| Documento | O conteúdo de texto a ser buscado | tsvector |
| Consulta | Os termos de busca do usuário | tsquery |
| Dicionário | Define como as palavras são processadas (stop words, stemming) | catálogo do sistema pg_ts_dict |
| Configuração | Combina um analisador e dicionários | catálogo do sistema pg_ts_config |
(2) Pipeline de Processamento da Busca Textual
flowchart LR
A[Texto Bruto] --> B[Analisador<br/>Tokenizar]
B --> C[Dicionário<br/>Stemming + Stop words]
C --> D[tsvector<br/>Lexemas ordenados]
D --> E[Índice GIN<br/>Busca rápida]
F[Consulta do Usuário] --> G[to_tsquery<br/>Analisar e normalizar]
G --> H[tsquery<br/>Lexemas + operadores]
E --> I["Operador @@<br/>Correspondência"]
H --> I
I --> J[ts_rank<br/>Pontuação]
J --> K[ts_headline<br/>Destaque]
K --> L[Resultados]
▶ Exemplo: Veja Como o Texto É Processado
SELECT * FROM ts_debug('english', 'The red shoes are beautiful');
alias | descriptor | token | dictionaries | dictionary | lexemes
-------+------------+--------+----------------+------------+---------
ascii | word | The | {english_stem} | english_stem| {}
ascii | word | red | {english_stem} | english_stem| {red}
ascii | word | shoes | {english_stem} | english_stem| {shoe}
ascii | word | are | {english_stem} | english_stem| {}
ascii | word | beautiful | {english_stem}| english_stem| {beauti}
4. Conceito: tsvector e tsquery
(1) tsvector — O Resultado Pré-processado de um Documento
tsvector é uma lista ordenada e sem duplicatas de lexemas, cada um com informação posicional.
▶ Exemplo: Converter Texto para tsvector
SELECT to_tsvector('english', 'The red shoes are beautiful shoes');
'beauti':5 'red':2 'shoe':3,6
| Etapa | Entrada | Saída |
|---|---|---|
| Tokenizar | The red shoes are beautiful shoes | [The, red, shoes, are, beautiful, shoes] |
| Remover stop words | [The, red, shoes, are, beautiful, shoes] | [red, shoes, beautiful, shoes] |
| Stemming | [red, shoes, beautiful, shoes] | [red, shoe, beauti, shoe] |
| Remover duplicatas + posição | [red, shoe, beauti, shoe] | 'beauti':5 'red':2 'shoe':3,6 |
(2) tsquery — O Resultado Pré-processado de uma Consulta
tsquery é uma combinação de lexemas e operadores booleanos.
| Operador | Significado | Exemplo |
|---|---|---|
& |
AND | red & shoe |
| |
OR | red | blue |
! |
NOT | !broken |
<-> |
SEGUIDO POR (adjacente) | red <-> shoe |
`<N>` |
Distância N | red <2> shoe |
▶ Exemplo: Construir um tsquery
SELECT to_tsquery('english', 'red & shoe');
-- 'red' & 'shoe'
SELECT to_tsquery('english', 'red | blue');
-- 'red' | 'blue'
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
5. Conceito: Funções de Consulta de Busca Textual
(1) Cinco Funções de Construção de Consulta
| Função | Versão PG | Formato de entrada | Descrição |
|---|---|---|---|
to_tsquery(config, text) |
todas | red & shoe |
Você escreve operadores manualmente |
plainto_tsquery(config, text) |
todas | red shoes |
Texto simples, unido automaticamente com & |
phraseto_tsquery(config, text) |
9.6+ | red shoes |
Texto simples, unido automaticamente com <-> |
websearch_to_tsquery(config, text) |
11+ | "red shoes" -broken |
Sintaxe de motor de busca web |
tsvector @@ tsquery |
todas | — | Operador de correspondência |
▶ Exemplo: plainto_tsquery — Texto Simples Auto-AND
SELECT plainto_tsquery('english', 'red shoes');
-- 'red' & 'shoe' (encontra docs com AMBOS os termos)
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ Exemplo: phraseto_tsquery — Busca por Frase
SELECT phraseto_tsquery('english', 'red shoes');
-- 'red' <-> 'shoe' (encontra docs onde 'red' está imediatamente antes de 'shoe')
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ Exemplo: websearch_to_tsquery — Sintaxe de Motor de Busca
SELECT websearch_to_tsquery('english', '"red shoes" -broken OR new');
-- 'red' <-> 'shoe' & !'broken' | 'new'
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
| Sintaxe websearch | Significado |
|---|---|
"frase exata" |
Correspondência de frase (<->) |
-palavra |
Excluir (!) |
palavra1 OR palavra2 |
OR (|) |
palavra1 palavra2 |
AND (&) |
▶ Exemplo: Buscar com o Operador @@
CREATE TABLE catalog (
product_id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
search_vector tsvector
);
INSERT INTO catalog (name, description, search_vector) VALUES
('Red Running Shoes', 'Lightweight red shoes for running',
to_tsvector('english', 'Lightweight red shoes for running')),
('Blue Casual Shoes', 'Comfortable blue shoes for daily wear',
to_tsvector('english', 'Comfortable blue shoes for daily wear')),
('Red Dress', 'Elegant red dress for special occasions',
to_tsvector('english', 'Elegant red dress for special occasions'));
-- Buscar por "red shoes"
SELECT name, description
FROM catalog
WHERE search_vector @@ plainto_tsquery('english', 'red shoes');
Output:
INSERT 0 1
6. Conceito: Ranqueamento e Destaque
(1) ts_rank Ranqueamento
ts_rank calcula a relevância de uma consulta para um documento, retornando um float de 0 a 1 — quanto maior o valor, mais relevante.
▶ Exemplo: Ordenar por Relevância
SELECT name,
ts_rank(search_vector, plainto_tsquery('english', 'red shoes')) AS rank
FROM catalog
WHERE search_vector @@ plainto_tsquery('english', 'red shoes')
ORDER BY rank DESC;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
| Função de ranqueamento | Descrição | Caso de uso |
|---|---|---|
ts_rank(vector, query) |
Ranqueamento baseado em frequência | Ranqueamento geral |
ts_rank_cd(vector, query) |
Ranqueamento de densidade de cobertura | Ranqueamento mais preciso para docs curtos |
ts_rank(vector, query, weights) |
Ranqueamento ponderado | Título com peso maior que corpo |
▶ Exemplo: Ranqueamento Ponderado (título acima da descrição)
ALTER TABLE catalog ADD COLUMN title_vector tsvector;
UPDATE catalog SET title_vector = to_tsvector('english', name);
-- Definir pesos: title=D(padrão), body=A(mais alto)
SELECT name,
ts_rank(
setweight(title_vector, 'A') || setweight(search_vector, 'B'),
plainto_tsquery('english', 'red shoes')
) AS rank
FROM catalog
WHERE setweight(title_vector, 'A') || setweight(search_vector, 'B')
@@ plainto_tsquery('english', 'red shoes')
ORDER BY rank DESC;
Output:
UPDATE 3
| Letra de peso | Peso padrão | Uso típico |
|---|---|---|
| A | 1.0 | Título |
| B | 0.4 | Corpo |
| C | 0.2 | Resumo |
| D | 0.1 | Informação suplementar |
(2) ts_headline Destaque
ts_headline marca lexemas correspondentes no texto original.
▶ Exemplo: Destacar Resultados da Busca
SELECT name,
ts_headline('english', description, plainto_tsquery('english', 'red shoes')) AS highlighted
FROM catalog
WHERE search_vector @@ plainto_tsquery('english', 'red shoes');
name | highlighted
-------------------+-------------------------------------------------
Red Running Shoes | Lightweight <b>red</b> <b>shoes</b> for running
▶ Exemplo: Tags de Destaque Personalizadas
SELECT ts_headline(
'english',
description,
plainto_tsquery('english', 'red shoes'),
'StartSel=<em>,StopSel=</em>,MaxWords=10,MinWords=5'
) AS highlighted
FROM catalog
WHERE search_vector @@ plainto_tsquery('english', 'red shoes');
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
| Parâmetro de destaque | Descrição | Padrão |
|---|---|---|
StartSel |
Tag de início da correspondência | <b> |
StopSel |
Tag de fim da correspondência | </b> |
MaxWords |
Máximo de palavras no fragmento de destaque | 35 |
MinWords |
Mínimo de palavras no fragmento de destaque | 15 |
ShortWord |
Comprimento mínimo de palavra a ignorar | 3 |
7. Conceito: Acelerando com Índices GIN
(1) Três Abordagens de Índice GIN
| Abordagem | Sintaxe | Sobrecarga de atualização | Melhor para |
|---|---|---|---|
| Baseada em coluna | gin(to_tsvector(config, column)) |
Reconstruído na atualização | Melhor quando valores de coluna não mudam |
| Baseada em coluna armazenada | gin(search_vector) |
Menor sobrecarga de atualização | Atualizações de alta frequência, precisa de trigger sync |
| FASTUPDATE | Ativado por padrão | Mesclagem adiada | Cenários com muita escrita e leitura |
▶ Exemplo: Índice GIN Baseado em Função
CREATE INDEX idx_catalog_desc_fts
ON catalog USING gin (to_tsvector('english', description));
-- A consulta deve usar a mesma config
SELECT name FROM catalog
WHERE to_tsvector('english', description) @@ plainto_tsquery('english', 'red shoes');
Output:
CREATE TABLE
▶ Exemplo: Índice GIN em Coluna tsvector Armazenada
CREATE INDEX idx_catalog_search_fts ON catalog USING gin (search_vector);
-- A consulta usa coluna armazenada
SELECT name FROM catalog
WHERE search_vector @@ plainto_tsquery('english', 'red shoes');
Output:
CREATE TABLE
▶ Exemplo: Trigger para Sincronizar Automaticamente a Coluna tsvector
CREATE FUNCTION catalog_search_vector_update() RETURNS trigger AS $$
BEGIN
NEW.search_vector :=
setweight(to_tsvector('english', COALESCE(NEW.name, '')), 'A') ||
setweight(to_tsvector('english', COALESCE(NEW.description, '')), 'B');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_catalog_search
BEFORE INSERT OR UPDATE OF name, description ON catalog
FOR EACH ROW EXECUTE FUNCTION catalog_search_vector_update();
Output:
INSERT 0 1
| Abordagem | Estilo de consulta | Índice usado | Custo de manutenção |
|---|---|---|---|
| Índice de função | to_tsvector('english', col) @@ query |
Sim | Zero (automático) |
| Coluna armazenada + índice | stored_col @@ query |
Sim | Precisa de trigger sync |
| Sem índice | to_tsvector('english', col) @@ query |
Varredura completa da tabela | Zero |
8. Conceito: Configuração Multilíngue
(1) Configurações Integradas do PostgreSQL
| Config | Idioma | Descrição |
|---|---|---|
english |
Inglês | Padrão; suporta stemming e stop words |
simple |
Sem processamento de idioma | Apenas tokenizar; sem stop words, sem stemming |
zhpinyin |
Pinyin chinês | Extensão PG; busca chinês-para-pinyin |
▶ Exemplo: Config simple Não Filtra Stop Words
SELECT to_tsvector('simple', 'The red shoes');
-- 'red':2 'shoes':3 'the':1 (mantém 'the', sem stemming)
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ Exemplo: Comparar Comportamento de Busca entre Configurações
| Config | to_tsvector('the red shoes are nice') | Característica |
|---|---|---|
| english | 'nice':5 'red':2 'shoe':3 | Remove stop words + stemming |
| simple | 'are':4 'nice':5 'red':2 'shoes':3 'the':1 | Apenas minúsculas + tokenizar |
| zhpinyin | Depende da implementação da extensão | Chinês para pinyin |
(2) Opções de Busca Textual em Chinês
O PostgreSQL não vem com um tokenizador nativo para chinês. Abordagens comuns:
| Abordagem | Extensão | Tokenização | Complexidade de instalação |
|---|---|---|---|
| zhparser | Extensão PostgreSQL | Baseada em SimpleChineseSeg | Média |
| pg_jieba | Extensão PostgreSQL | Baseada no tokenizador jieba | Média |
| zhpinyin | Extensão PostgreSQL | Busca por pinyin | Baixa |
| Tokenização na camada de aplicação | Nenhuma | App gera tsvector | Baixa |
▶ Exemplo: Tokenização na Camada de Aplicação (sem necessidade de extensão)
-- Pré-segmentar texto chinês com espaços na camada de aplicação
INSERT INTO catalog (name, description, search_vector)
VALUES (
'Tênis de Corrida Vermelho',
'tênis de corrida vermelho leve para corrida',
to_tsvector('simple', 'tênis de corrida vermelho leve para corrida')
);
-- Buscar com consulta segmentada
SELECT name FROM catalog
WHERE search_vector @@ to_tsquery('simple', 'tênis & corrida');
Output:
INSERT 0 1
▶ Exemplo: Busca Mista Chinês-Inglês
CREATE TABLE articles (
article_id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
content TEXT,
fts_en tsvector,
fts_zh tsvector
);
-- Config english para conteúdo em inglês, simple para chinês
UPDATE articles SET
fts_en = to_tsvector('english', COALESCE(title, '') || ' ' || COALESCE(content, '')),
fts_zh = to_tsvector('simple', COALESCE(title, '') || ' ' || COALESCE(content, ''));
-- Buscar em inglês
SELECT title FROM articles
WHERE fts_en @@ plainto_tsquery('english', 'database optimization');
-- Buscar em chinês (segmentado pela aplicação)
SELECT title FROM articles
WHERE fts_zh @@ to_tsquery('simple', 'data & optimization');
Output:
UPDATE 3
9. Conceito: Estatísticas e Catálogos do Sistema
(1) ts_stat para Frequência de Palavras
ts_stat ajuda a analisar a distribuição de lexemas em um tsvector — comumente usado para ajustar a configuração de busca.
▶ Exemplo: Visualizar os Lexemas Mais Comuns
SELECT word, ndoc, nentry
FROM ts_stat(
'SELECT search_vector FROM catalog'
)
ORDER BY nentry DESC
LIMIT 10;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
| Coluna | Descrição |
|---|---|
word |
Lexema |
ndoc |
Em quantos documentos aparece |
nentry |
Número total de ocorrências |
(2) Catálogos do Sistema
▶ Exemplo: Visualizar Configurações Disponíveis
SELECT cfgname FROM pg_ts_config;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ Exemplo: Visualizar Dicionários Disponíveis
SELECT dictname, dictnamespace FROM pg_ts_dict;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ Exemplo: Comparação de Desempenho LIKE vs Busca Textual
-- Criar dados de teste
INSERT INTO catalog (name, description, search_vector)
SELECT
'Product ' || i,
'High quality product number ' || i || ' with great features and amazing design',
to_tsvector('english', 'High quality product number ' || i || ' with great features and amazing design')
FROM generate_series(1, 100000) AS i;
-- Consulta LIKE (lenta, sem suporte de índice para curinga inicial)
EXPLAIN ANALYZE
SELECT name FROM catalog WHERE description LIKE '%great features%';
-- Busca textual com índice GIN (rápida)
CREATE INDEX idx_catalog_fts ON catalog USING gin (search_vector);
EXPLAIN ANALYZE
SELECT name FROM catalog
WHERE search_vector @@ plainto_tsquery('english', 'great features');
Output:
INSERT 0 1
| Método de consulta | Tempo | Suporte de índice | Capacidade |
|---|---|---|---|
LIKE '%keyword%' |
~3000ms | Nenhum | Correspondência exata de substring |
LIKE 'keyword%' |
~5ms | B-tree | Correspondência de prefixo |
@@ busca textual + GIN |
~30ms | GIN | Correspondência de lexema + stemming + ranqueamento |
10. Prática: Motor de Busca de Produtos de E-commerce
Charlie precisa implementar uma busca completa de produtos para a plataforma de e-commerce, incluindo busca em múltiplos campos, ranqueamento, destaque e paginação.
-- Etapa 1: Criar tabela de produtos com tsvector pré-computado
CREATE TABLE shop_products (
product_id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
category TEXT NOT NULL,
description TEXT,
price NUMERIC(10,2) NOT NULL,
search_doc tsvector GENERATED ALWAYS AS (
setweight(to_tsvector('english', COALESCE(name, '')), 'A') ||
setweight(to_tsvector('english', COALESCE(category, '')), 'B') ||
setweight(to_tsvector('english', COALESCE(description, '')), 'C')
) STORED
);
-- Etapa 2: Inserir dados de exemplo
INSERT INTO shop_products (name, category, description, price) VALUES
('Red Running Shoes', 'Shoes', 'Lightweight red running shoes with cushioned sole', 89.99),
('Blue Casual Shoes', 'Shoes', 'Comfortable blue shoes for everyday casual wear', 59.99),
('Red Leather Dress Shoes', 'Shoes', 'Premium red leather dress shoes for formal events', 149.99),
('Red Silk Dress', 'Dresses', 'Elegant red silk dress for special occasions', 199.99),
('Running Watch', 'Accessories', 'GPS running watch with heart rate monitor', 249.99),
('Running Shorts', 'Clothing', 'Breathable running shorts with pockets', 39.99);
-- Etapa 3: Criar índice GIN na coluna gerada
CREATE INDEX idx_shop_products_search ON shop_products USING gin (search_doc);
-- Etapa 4: Função de busca com ranqueamento, destaque e paginação
CREATE FUNCTION search_products(
p_query TEXT,
p_limit INT DEFAULT 20,
p_offset INT DEFAULT 0
) RETURNS TABLE (
product_id INT,
name TEXT,
category TEXT,
price NUMERIC,
rank REAL,
headline TEXT
) AS $$
BEGIN
RETURN QUERY
SELECT
s.product_id,
s.name,
s.category,
s.price,
ts_rank(s.search_doc, websearch_to_tsquery('english', p_query)) AS rank,
ts_headline(
'english',
COALESCE(s.name, '') || '. ' || COALESCE(s.description, ''),
websearch_to_tsquery('english', p_query),
'StartSel=<mark>,StopSel=</mark>,MaxWords=20,MinWords=5'
) AS headline
FROM shop_products s
WHERE s.search_doc @@ websearch_to_tsquery('english', p_query)
ORDER BY rank DESC
LIMIT p_limit
OFFSET p_offset;
END;
$$ LANGUAGE plpgsql;
-- Etapa 5: Testar busca
SELECT * FROM search_products('red shoes');
SELECT * FROM search_products('"running shoes"');
SELECT * FROM search_products('running -shoes');
-- Etapa 6: Buscar com filtro de categoria
SELECT name, price,
ts_rank(search_doc, plainto_tsquery('english', 'red')) AS rank
FROM shop_products
WHERE search_doc @@ plainto_tsquery('english', 'red')
AND category = 'Shoes'
ORDER BY rank DESC;
❓ Perguntas Frequentes
P: Qual é a diferença essencial entre busca textual e LIKE? R: LIKE é uma correspondência exata de substring — não entende semântica. A busca textual tokeniza, remove stop words e aplica stemming, correspondendo por lexema com combinações booleanas e ranqueamento. A busca textual tem suporte de índice GIN; LIKE com curinga inicial não pode usar índice.
P: Qual devo usar, to_tsquery ou plainto_tsquery? R: Se os usuários digitam termos de busca diretamente, use plainto_tsquery ou websearch_to_tsquery (PG 11+), que convertem automaticamente texto simples em consulta. to_tsquery precisa que você escreva operadores & | ! manualmente — melhor para consultas geradas programaticamente.
P: Por que devo usar a mesma config para o índice e a consulta? R: Configs diferentes tokenizam e aplicam stemming de forma diferente. Se o índice usa config english mas a consulta usa simple, os lexemas não corresponderão e a busca não retorna nada. Eles devem permanecer consistentes.
P: O que é melhor, colunas GENERATED ALWAYS AS ... STORED ou triggers? R: Colunas GENERATED (PG 12+) são computadas automaticamente — sem necessidade de trigger — e são recomendadas. Se você está no PG abaixo da 12 ou precisa combinar tsvectors entre tabelas, use um trigger.
P: Para que serve a informação posicional no tsvector? R: A informação posicional é usada para busca por frase (phraseto_tsquery usa o operador
<->para corresponder lexemas adjacentes) e busca por proximidade (operador de distância`<N>`). Se você não precisa de busca por frase, pode descartar posições no to_tsvector.
P: Um índice GIN afeta o desempenho de escrita? R: Sim. Atualizações de índice GIN são mais lentas que B-tree porque listas invertidas devem ser mescladas. O PostgreSQL ativa FASTUPDATE por padrão, armazenando atualizações em buffer e depois mesclando em lotes — trocando um pouco de latência de consulta em tempo real por melhor desempenho de escrita.
P: Como faço busca textual em chinês? R: O PostgreSQL não suporta nativamente tokenização em chinês. Recomendado: instalar a extensão zhparser/pg_jieba (tokenizador nativo) ou pré-segmentar texto com um tokenizador na camada de aplicação e armazená-lo com config simple em um tsvector. A abordagem de camada de aplicação é a mais simples — sem necessidade de instalar extensão.
📖 Resumo
- A busca textual converte texto em tsvector via tokenização, remoção de stop words e stemming; consultas se tornam tsquery
plainto_tsqueryserve para entrada do usuário;websearch_to_tsquery(PG 11+) suporta sintaxe de motor de busca- O operador
@@realiza correspondência;ts_rankranqueia;ts_headlinedestaca - Índices GIN são a chave para o desempenho da busca textual; índices de função e de coluna armazenada têm seus usos
- Pesos (setweight A/B/C/D) fazem correspondências no título ranquearem acima do corpo
- Multilíngue usa diferentes configs; chinês precisa da extensão zhparser/pg_jieba ou tokenização na camada de aplicação
ts_statanalisa frequência de palavras; os catálogos do sistemapg_ts_config/pg_ts_dictlistam configurações
📝 Exercícios
-
⭐ Crie uma tabela
blog_postscomtitle(TEXT) ebody(TEXT), insira 3 linhas e useto_tsvector+plainto_tsquerypara buscar artigos contendo "banco de dados". -
⭐⭐ Adicione uma coluna
search_vector(tsvector) ablog_posts, crie um índice GIN e escreva um trigger que atualiza automaticamente search_vector em INSERT/UPDATE (título peso A, corpo peso B). Depois busque "performance optimization" comts_rank+ts_headlinee ordene por ranqueamento. -
⭐⭐⭐ Projete uma tabela
knowledge_articlessuportando busca mista chinês-inglês: conteúdo em inglês com config english, conteúdo em chinês com config simple (pré-segmentação na camada de aplicação), com duas colunas tsvector e índices GIN correspondentes. Escreva uma função de busca que escolhe a coluna certa com base no idioma da consulta de entrada, destaca resultados comts_headline, ordena ports_ranke suporta paginação.