No último artigo, otimizamos a pesquisa no PostgreSQL usando ferramentas padrão. Neste artigo, continuaremos otimizando o uso do índice RUM e analisando seus prós e contras em comparação ao GIN.
1. Introdução
RUM é uma extensão do Postgres, um novo índice para pesquisa de texto completo. Permite retornar resultados classificados por relevância ao passar pelo índice. Não vou me concentrar em sua instalação - ela está descrita no README no repositório.
Nós usamos um índice
Um índice é criado semelhante ao índice GIN, mas com alguns parâmetros. A lista inteira de parâmetros pode ser encontrada na documentação.
CREATE INDEX idx_rum_document ON documents_documentvector USING rum ("text" rum_tsvector_ops);
Consulta de pesquisa para RUM:
SELECT document_id, "text" <=> plainto_tsquery('') AS rank FROM documents_documentvector WHERE "text" @@ plainto_tsquery('') ORDER BY rank;
Solicitação de GIN SELECT document_id, ts_rank("text", plainto_tsquery('')) AS rank FROM documents_documentvector WHERE "text" @@ plainto_tsquery('') ORDER BY rank DESC;
A diferença do GIN é que a relevância é obtida não com a função ts_rank, mas com uma consulta com o operador <=>
: "text" <=> plainto_tsquery('')
. Essa consulta retorna alguma distância entre o vetor de pesquisa e a consulta de pesquisa. Quanto menor, melhor a consulta corresponde ao vetor.
Comparação com GIN
Aqui, compararemos com ~ 500 mil documentos para observar diferenças nos resultados da pesquisa.
Solicitar velocidade
Vamos ver o que EXPLAIN for GIN produzirá nesta base:
Gather Merge (actual time=563.840..611.844 rows=119553 loops=1) Workers Planned: 2 Workers Launched: 2 -> Sort (actual time=553.427..557.857 rows=39851 loops=3) Sort Key: (ts_rank(text, plainto_tsquery(''::text))) Sort Method: external sort Disk: 1248kB -> Parallel Bitmap Heap Scan on documents_documentvector (actual time=13.402..538.879 rows=39851 loops=3) Recheck Cond: (text @@ plainto_tsquery(''::text)) Heap Blocks: exact=5616 -> Bitmap Index Scan on idx_gin_document (actual time=12.144..12.144 rows=119553 loops=1) Index Cond: (text @@ plainto_tsquery(''::text)) Planning time: 4.573 ms Execution time: 617.534 ms
E para o RUM?
Sort (actual time=1668.573..1676.168 rows=119553 loops=1) Sort Key: ((text <=> plainto_tsquery(''::text))) Sort Method: external merge Disk: 3520kB -> Bitmap Heap Scan on documents_documentvector (actual time=16.706..1605.382 rows=119553 loops=1) Recheck Cond: (text @@ plainto_tsquery(''::text)) Heap Blocks: exact=15599 -> Bitmap Index Scan on idx_rum_document (actual time=14.548..14.548 rows=119553 loops=1) Index Cond: (text @@ plainto_tsquery(''::text)) Planning time: 0.650 ms Execution time: 1679.315 ms
O que é isso? Qual é a utilidade desse RUM vangloriado, você pergunta, se for executado três vezes mais lento que o GIN? E onde está a notória classificação dentro do índice?
Calma: vamos tentar adicionar um LIMIT 1000
à solicitação.
EXPLAIN for RUM Limite (tempo real = 115.568..137.313 linhas = 1000 loops = 1)
-> Varredura de índice usando idx_rum_document no arquivo documents_document (tempo real = 115.567..137.239 linhas = 1000 loops = 1)
Cond do índice: (texto @@ plainto_tsquery ('consulta' :: texto))
Ordenar por: (texto <=> plainto_tsquery ('query' :: texto))
Tempo de planejamento: 0,481 ms
Tempo de execução: 137.678 ms
EXPLAIN for GIN Limite (tempo real = 579.905..585.650 linhas = 1000 loops = 1)
-> Reunir Mesclagem (tempo real = 579.904..585.604 linhas = 1000 loops = 1)
Trabalhadores planejados: 2
Lançamento dos trabalhadores: 2
-> Classificação (tempo real = 574.061..574.171 linhas = 992 loops = 3)
Chave de classificação: (ts_rank (texto, plainto_tsquery ('query' :: texto))) DESC
Método de classificação: fusão externa Disco: 1224kB
-> Varredura de Heap de Bitmap Paralela no documents_documentvector (tempo real = 8.920..555.571 linhas = 39851 loops = 3)
Verifique novamente Cond: (text @@ plainto_tsquery ('query' :: text))
Blocos de heap: exato = 5422
-> Verificação de índice de bitmap no idx_gin_document (tempo real = 8.945..8.945 linhas = 119553 loops = 1)
Cond do índice: (texto @@ plainto_tsquery ('consulta' :: texto))
Tempo de planejamento: 0,223 ms
Tempo de execução: 585.948 ms
~ 150 ms vs ~ 600 ms! Já não é a favor do GIN, certo? E a classificação mudou dentro do índice!
E se você procurar o LIMIT 100
?
EXPLAIN for RUM Limite (tempo real = 105.863..108.530 linhas = 100 loops = 1)
-> Varredura de índice usando idx_rum_document no arquivo documents_document (tempo real = 105.862..108.517 linhas = 100 loops = 1)
Cond do índice: (texto @@ plainto_tsquery ('consulta' :: texto))
Ordenar por: (texto <=> plainto_tsquery ('query' :: texto))
Tempo de planejamento: 0,199 ms
Tempo de execução: 108,958 ms
EXPLAIN for GIN Limite (tempo real = 582.924..588.351 linhas = 100 loops = 1)
-> Reunir Mesclagem (tempo real = 582.923..588.344 linhas = 100 loops = 1)
Trabalhadores planejados: 2
Lançamento dos trabalhadores: 2
-> Classificação (tempo real = 573,809..573,889 linhas = 806 loops = 3)
Chave de classificação: (ts_rank (texto, plainto_tsquery ('query' :: texto))) DESC
Método de classificação: fusão externa Disco: 1224kB
-> Varredura de Heap de Bitmap Paralela no documents_documentvector (tempo real = 18.038..552.827 linhas = 39851 loops = 3)
Verifique novamente Cond: (text @@ plainto_tsquery ('query' :: text))
Blocos de heap: exato = 5275
-> Verificação de índice de bitmap no idx_gin_document (tempo real = 16.541..16.541 linhas = 119553 loops = 1)
Cond do índice: (texto @@ plainto_tsquery ('consulta' :: texto))
Tempo de planejamento: 0,487 ms
Tempo de execução: 588,583 ms
A diferença é ainda mais perceptível.
O problema é que o GIN não importa exatamente quantas linhas você obtém no final - ele deve passar por todas as linhas para as quais a solicitação foi bem-sucedida e classificá-las. O RUM faz isso apenas para as linhas que realmente precisamos. Se precisarmos de muitas linhas, o GIN vence. Seu ts_rank
executa cálculos com ts_rank
eficiência que o operador <=>
. Mas em pequenas consultas, a vantagem do RUM é inegável.
Na maioria das vezes, o usuário não precisa descarregar todos os 50 mil documentos do banco de dados de uma só vez. Ele precisa de apenas 10 posts na primeira, segunda, terceira página etc. E é justamente nesses casos que esse índice é aprimorado e aumenta bastante o desempenho da pesquisa.
Tolerância de junção
E se uma pesquisa exigir que você ingresse em outra ou mais tabelas? Por exemplo, para exibir nos resultados o tipo de documento, seu proprietário? Ou, como no meu caso, filtrar pelos nomes de entidades relacionadas?
Compare:
Solicitação com duas junções para GIN SELECT document_id, ts_rank("text", plainto_tsquery('')) AS rank, case_number FROM documents_documentvector RIGHT JOIN documents_document ON documents_documentvector.document_id = documents_document.id LEFT JOIN documents_case ON documents_document.case_id = documents_case.id WHERE "text" @@ plainto_tsquery('') ORDER BY rank DESC LIMIT 10;
Resultado:
Limite (tempo real = 1637.902..1643.483 linhas = 10 loops = 1)
-> Reunir Mesclagem (tempo real = 1637.901..1643.479 linhas = 10 loops = 1)
Trabalhadores planejados: 2
Lançamento dos trabalhadores: 2
-> Classificação (tempo real = 1070.614..1070.687 linhas = 652 loops = 3)
Chave de classificação: (ts_rank (documents_documentvector.text, plainto_tsquery ('query' :: text))) DESC
Método de classificação: fusão externa Disco: 2968kB
-> Hash Left Join (tempo real = 323.386..1049.092 linhas = 39851 loops = 3)
Condição de hash: (documents_document.case_id = documents_case.id)
-> Hash Join (tempo real = 239.312..324.797 linhas = 39851 loops = 3)
Condição de hash: (documents_documentvector.document_id = documents_document.id)
-> Varredura de Heap de Bitmap Paralela no documents_documentvector (tempo real = 11.022..37.073 linhas = 39851 loops = 3)
Verifique novamente Cond: (text @@ plainto_tsquery ('query' :: text))
Blocos de heap: exato = 9362
-> Verificação de índice de bitmap no idx_gin_document (tempo real = 12.094..12.094 linhas = 119553 loops = 1)
Cond do índice: (texto @@ plainto_tsquery ('consulta' :: texto))
-> Hash (tempo real = 227.856..227.856 linhas = 472089 loops = 3)
Caçambas: 65536 Lotes: 16 Uso de Memória: 2264kB
-> Digitalização Seq em documentos_documento (tempo real = 0,009..147,104 linhas = 472089 loops = 3)
-> Hash (tempo real = 83.338..83.338 linhas = 273695 loops = 3)
Caçambas: 65536 Lotes: 8 Uso de Memória: 2602kB
-> Digitalização Seq em documentos_casa (tempo real = 0,009..39,082 linhas = 273695 loops = 3)
Tempo de planejamento: 0,857 ms
Tempo de execução: 1644.028 ms
Em três junções e mais, o tempo de solicitação chega a 2-3 segundos e aumenta com o número de junções.
Mas e o RUM? Deixe a solicitação ser imediatamente com cinco junções.
Solicitação de cinco junções para RUM SELECT document_id, "text" <=> plainto_tsquery('') AS rank, case_number, classifier_procedure.title, classifier_division.title, classifier_category.title FROM documents_documentvector RIGHT JOIN documents_document ON documents_documentvector.document_id = documents_document.id LEFT JOIN documents_case ON documents_document.case_id = documents_case.id LEFT JOIN classifier_procedure ON documents_case.procedure_id = classifier_procedure.id LEFT JOIN classifier_division ON documents_case.division_id = classifier_division.id LEFT JOIN classifier_category ON documents_document.category_id = classifier_category.id WHERE "text" @@ plainto_tsquery('') AND documents_document.is_active IS TRUE ORDER BY rank LIMIT 10;
Resultado:
Limite (tempo real = 70.524..72.292 linhas = 10 loops = 1)
-> Junção esquerda do loop aninhado (tempo real = 70.521..72.279 linhas = 10 loops = 1)
-> Junção esquerda do loop aninhado (tempo real = 70.104..70.406 linhas = 10 loops = 1)
-> Junção esquerda do loop aninhado (tempo real = 70.089..70.351 linhas = 10 loops = 1)
-> Junção esquerda do loop aninhado (tempo real = 70.073..70.302 linhas = 10 loops = 1)
-> Loop aninhado (tempo real = 70.052..70.201 linhas = 10 loops = 1)
-> Varredura de índice usando document_vector_rum_index em documents_documentvector (tempo real = 70.001..70.035 linhas = 10 loops = 1)
Cond do índice: (texto @@ plainto_tsquery ('consulta' :: texto))
Ordenar por: (texto <=> plainto_tsquery ('query' :: texto))
-> Varredura de índice usando documents_document_pkey em documents_document (tempo real = 0,013..0,013 linhas = 1 loops = 10)
Cond do índice: (id = documents_documentvector.document_id)
Filtro: (is_active IS TRUE)
-> Digitalização de índice usando documents_case_pkey em documents_case (tempo real = 0,009..0,009 linhas = 1 loops = 10)
Cond do índice: (documents_document.case_id = id)
-> Varredura de índice usando classifier_procedure_pkey em classifier_procedure (tempo real = 0,003..0,003 linhas = 1 loops = 10)
Cond do índice: (documents_case.procedure_id = id)
-> Varredura de índice usando classifier_division_pkey em classifier_division (tempo real = 0,004..0,004 linhas = 1 loops = 10)
Cond do índice: (documents_case.division_id = id)
-> Varredura de índice usando classifier_category_pkey em classifier_category (tempo real = 0,003..0,003 linhas = 1 loops = 10)
Cond do índice: (documents_document.category_id = id)
Tempo de planejamento: 2.861 ms
Tempo de execução: 72,865 ms
Se você não pode ficar sem participar da pesquisa, o RUM é claramente adequado para você.
Espaço em disco
Em uma base de testes de ~ 500 mil documentos e índices de 3,6 GB, ocupavam volumes muito diferentes.
idx_rum_document | 1950 MB
idx_gin_document | 418 MB
Sim, a unidade é barata. Mas 2 GB em vez de 400 MB não podem agradar. Metade do tamanho da base é um pouco demais para o índice. Aqui o GIN vence incondicionalmente.
Conclusões
Você precisa de um RUM se:
- Você tem muitos documentos, mas fornece resultados de pesquisa página por página
- Você precisa de uma filtragem sofisticada dos resultados da pesquisa
- Você não se importa com o espaço em disco
Você ficará completamente satisfeito com o GIN se:
- Você tem uma pequena base
- Você tem uma base grande, mas precisa produzir resultados imediatamente e é isso
- Você não precisa filtrar com join s
- Você está interessado no tamanho mínimo do índice em disco
Espero que este artigo remova muito WTF ?! Isso ocorre ao trabalhar e configurar a pesquisa no Postgres. Ficarei feliz em ouvir conselhos daqueles que sabem como configurar tudo ainda melhor!)
Nas próximas partes, planejo contar mais sobre o RUM no meu projeto: sobre o uso de opções adicionais do RUM, trabalhando no pacote Django + PostgreSQL.