Eu tenho uma tabela que contém dados extraídos de documentos de texto. Os dados são armazenados em uma coluna chamada "CONTENT"
para a qual eu criei esse índice usando o GIN:
CREATE INDEX "File_contentIndex"
ON "File"
USING gin
(setweight(to_tsvector('english'::regconfig
, COALESCE("CONTENT", ''::character varying)::text), 'C'::"char"));
Eu uso a seguinte consulta para executar uma pesquisa de texto completo na tabela:
SELECT "ITEMID",
ts_rank(setweight(to_tsvector('english', coalesce("CONTENT",'')), 'C') ,
plainto_tsquery('english', 'searchTerm')) AS "RANK"
FROM "File"
WHERE setweight(to_tsvector('english', coalesce("CONTENT",'')), 'C')
@@ plainto_tsquery('english', 'searchTerm')
ORDER BY "RANK" DESC
LIMIT 5;
A tabela Arquivo contém 250 000 linhas e cada "CONTENT"
entrada consiste em uma palavra aleatória e uma sequência de texto que é igual para todas as linhas.
Agora, quando procuro uma palavra aleatória (1 acerto na tabela inteira), a consulta é executada muito rapidamente (<100 ms). No entanto, quando procuro uma palavra que esteja presente em todas as linhas, a consulta é extremamente lenta (10 minutos ou mais).
EXPLAIN ANALYZE
mostra que, para a pesquisa de um clique, é realizada uma verificação de índice de bitmap seguida de uma verificação de heap de bitmap . Para a pesquisa lenta, é realizada uma Seq Scan , que é o que está demorando tanto.
Concedido, não é realista ter os mesmos dados em todas as linhas. Mas como não consigo controlar os documentos de texto enviados pelos usuários, nem as pesquisas que eles realizam, é possível que ocorra um cenário semelhante (pesquise termos com ocorrência muito alta no DB). Como posso aumentar o desempenho da minha consulta de pesquisa para esse cenário?
Executando o PostgreSQL 9.3.4
Planos de consulta de EXPLAIN ANALYZE
:
Pesquisa rápida (1 hit no DB)
"Limit (cost=2802.89..2802.90 rows=5 width=26) (actual time=0.037..0.037 rows=1 loops=1)"
" -> Sort (cost=2802.89..2806.15 rows=1305 width=26) (actual time=0.037..0.037 rows=1 loops=1)"
" Sort Key: (ts_rank(setweight(to_tsvector('english'::regconfig, (COALESCE("CONTENT", ''::character varying))::text), 'C'::"char"), '''wfecg'''::tsquery))"
" Sort Method: quicksort Memory: 25kB"
" -> Bitmap Heap Scan on "File" (cost=38.12..2781.21 rows=1305 width=26) (actual time=0.030..0.031 rows=1 loops=1)"
" Recheck Cond: (setweight(to_tsvector('english'::regconfig, (COALESCE("CONTENT", ''::character varying))::text), 'C'::"char") @@ '''wfecg'''::tsquery)"
" -> Bitmap Index Scan on "File_contentIndex" (cost=0.00..37.79 rows=1305 width=0) (actual time=0.012..0.012 rows=1 loops=1)"
" Index Cond: (setweight(to_tsvector('english'::regconfig, (COALESCE("CONTENT", ''::character varying))::text), 'C'::"char") @@ '''wfecg'''::tsquery)"
"Total runtime: 0.069 ms"
Pesquisa lenta (250k ocorrências no banco de dados)
"Limit (cost=14876.82..14876.84 rows=5 width=26) (actual time=519667.404..519667.405 rows=5 loops=1)"
" -> Sort (cost=14876.82..15529.37 rows=261017 width=26) (actual time=519667.402..519667.402 rows=5 loops=1)"
" Sort Key: (ts_rank(setweight(to_tsvector('english'::regconfig, (COALESCE("CONTENT", ''::character varying))::text), 'C'::"char"), '''cyberspace'''::tsquery))"
" Sort Method: top-N heapsort Memory: 25kB"
" -> Seq Scan on "File" (cost=0.00..10541.43 rows=261017 width=26) (actual time=2.097..519465.953 rows=261011 loops=1)"
" Filter: (setweight(to_tsvector('english'::regconfig, (COALESCE("CONTENT", ''::character varying))::text), 'C'::"char") @@ '''cyberspace'''::tsquery)"
" Rows Removed by Filter: 6"
"Total runtime: 519667.429 ms"
ORDER BY "RANK" DESC
. Eu investigariapg_trgm
com o índice GiST e os operadores de similaridade / distância como alternativa. Considere: dba.stackexchange.com/questions/56224/… . Pode até produzir resultados "melhores" (além de serem mais rápidos).explain (analyze, buffers)
, de preferência com track_io_timing definido comoON
? Não há como levar 520 segundos para verificar seq essa tabela, a menos que você a armazene em um RAID de disquetes. Algo é definitivamente patológico lá. Além disso, qual é a sua configuraçãorandom_page_cost
e os outros parâmetros de custo?Respostas:
Caso de uso questionável
Uma sequência de texto que é a mesma para todas as linhas é apenas frete morto. Remova-o e concatene-o em uma exibição, se precisar mostrá-lo.
Obviamente, você está ciente disso:
Atualize sua versão do Postgres
Enquanto ainda estiver no Postgres 9.3, você deve pelo menos atualizar para a versão mais recente do ponto (atualmente 9.3.9). A recomendação oficial do projeto:
Melhor ainda, atualize para a 9.4, que recebeu grandes melhorias nos índices GIN .
Problema principal 1: estimativas de custo
O custo de algumas funções de pesquisa de texto foi seriamente subestimado até e incluindo a versão 9.4. Esse custo é aumentado pelo fator 100 na próxima versão 9.5, como @jjanes descreve em sua resposta recente:
Aqui estão os respectivos tópicos em que isso foi discutido e a mensagem de confirmação de Tom Lane.
Como você pode ver na mensagem de confirmação,
to_tsvector()
está entre essas funções. Você pode aplicar a alteração imediatamente (como superusuário):o que deve tornar muito mais provável que seu índice funcional seja usado.
Grande problema 2: KNN
O principal problema é que o Postgres precisa calcular uma classificação com
ts_rank()
260k linhas (rows=261011
) antes de poder solicitar e selecionar as cinco principais. Isso vai ser caro , mesmo depois que você tiver corrigido outros problemas, conforme discutido. É um problema K-vizinho mais próximo (KNN) por natureza e existem soluções para casos relacionados. Mas não consigo pensar em uma solução geral para o seu caso, pois o próprio cálculo de classificação depende da entrada do usuário. Eu tentaria eliminar antecipadamente a maior parte das correspondências de baixa classificação, para que o cálculo completo só seja feito para poucos bons candidatos.Uma maneira que eu posso pensar é combinar a sua pesquisa de texto completo com trigrama pesquisa por similaridade - que oferece uma implementação de trabalho para o problema KNN. Dessa forma, você pode pré-selecionar as "melhores" correspondências com
LIKE
predicado como candidatos (em uma subconsulta com,LIMIT 50
por exemplo) e, em seguida, escolher as 5 linhas de classificação superior de acordo com o cálculo de classificação na consulta principal.Ou aplique os dois predicados na mesma consulta e escolha as correspondências mais próximas de acordo com a semelhança do trigrama (que produziria resultados diferentes), como nesta resposta relacionada:
Eu fiz mais algumas pesquisas e você não é o primeiro a encontrar esse problema. Posts relacionados no pgsql-general:
Estão em andamento trabalhos para eventualmente implementar um
tsvector <-> tsquery
operador.Oleg Bartunov e Alexander Korotkov chegaram a apresentar um protótipo funcional (usando
><
como operador em vez de<->
na época), mas é muito complexo integrar-se ao Postgres, toda a infraestrutura para os índices GIN precisa ser reformulada (a maioria já é feita agora).Grande problema 3: pesos e índice
E identifiquei mais um fator a ser adicionado à lentidão da consulta. Por documentação:
Negrito ênfase minha. Assim que o peso é envolvido, cada linha precisa ser buscada da pilha (não apenas uma verificação de visibilidade barata) e valores longos devem ser retirados da torrada, o que aumenta o custo. Mas parece haver uma solução para isso:
Definição de índice
Olhando para o seu índice novamente, não parece fazer sentido, para começar. Você atribui um peso a uma única coluna, que não faz sentido , desde que não concatenar outras colunas com um peso diferente .
COALESCE()
também não faz sentido, desde que você não concatene mais colunas.Simplifique seu índice:
E sua consulta:
Ainda caro para um termo de pesquisa que corresponda a todas as linhas, mas provavelmente muito menos.
Apartes
Todos esses problemas combinados, o custo insano de 520 segundos para sua segunda consulta está começando a fazer sentido. Mas ainda pode haver mais problemas. Você configurou seu servidor?
Todos os conselhos usuais para otimização de desempenho se aplicam.
Facilita sua vida se você não trabalha com identificadores de caso CaMeL com aspas duplas:
fonte
USING gin (to_tsvector('english', "CONTENT")
Eu tive um problema semelhante. Eu cuidei disso pré-computando o ts_rank de cada termo popular de consulta de texto em um campo: tupla de tabela e armazenando-o em uma tabela de pesquisa. Isso me salvou muito tempo (fator 40X) durante a pesquisa de palavras populares no corpus pesado do texto.
Consulta: consulte esta tabela e obtenha os IDs de documentos classificados por sua respectiva classificação. se não estiver lá, faça da maneira antiga.
fonte