Estamos preparando uma pesquisa de texto completo no Postgres. Parte 2

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.

Source: https://habr.com/ru/post/pt443368/


All Articles