Nous préparons une recherche plein texte dans Postgres. 2e partie

Dans le dernier article, nous avons optimisé la recherche dans PostgreSQL à l'aide d'outils standard. Dans cet article, nous continuerons d'optimiser l'utilisation de l'indice RUM et d'analyser ses avantages et inconvénients par rapport au GIN.


Présentation


RUM est une extension pour Postgres, un nouvel index pour la recherche plein texte. Il vous permet de renvoyer des résultats triés par pertinence lors du passage dans l'index. Je ne me concentrerai pas sur son installation - elle est décrite dans le README du référentiel.


Nous utilisons un index


Un index est créé similaire à l'index GIN, mais avec certains paramÚtres. La liste complÚte des paramÚtres se trouve dans la documentation.


CREATE INDEX idx_rum_document ON documents_documentvector USING rum ("text" rum_tsvector_ops); 

Recherche de RUM:


 SELECT document_id, "text" <=> plainto_tsquery('') AS rank FROM documents_documentvector WHERE "text" @@ plainto_tsquery('') ORDER BY rank; 

Demande de GIN
 SELECT document_id, ts_rank("text", plainto_tsquery('')) AS rank FROM documents_documentvector WHERE "text" @@ plainto_tsquery('') ORDER BY rank DESC; 

La diffĂ©rence avec le GIN est que la pertinence est obtenue non pas en utilisant la fonction ts_rank, mais en utilisant une requĂȘte avec l'opĂ©rateur <=> : "text" <=> plainto_tsquery('') . Une telle requĂȘte renvoie une certaine distance entre le vecteur de recherche et la requĂȘte de recherche. Plus elle est petite, meilleure est la requĂȘte correspondant au vecteur.


Comparaison avec GIN


Ici, nous comparerons sur une base de test avec ~ 500 000 documents pour remarquer des différences dans les résultats de la recherche.


Vitesse de demande


Voyons ce qu'EXPLAIN for GIN produira sur cette 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 

Et pour 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 

Qu'est-ce que c'est? Quelle est l'utilitĂ© de ce RUM tant vantĂ©, demandez-vous, s'il fonctionne trois fois plus lentement que le GIN? Et oĂč est le tri notoire Ă  l'intĂ©rieur de l'index?


Calme: essayons d'ajouter un LIMIT 1000 Ă  la demande.


EXPLAIN for RUM
  Limite (temps réel = 115,568..137,313 lignes = 1000 boucles = 1)
    -> Index Scan en utilisant idx_rum_document sur documents_documentvector (temps réel = 115,567..137,239 lignes = 1000 boucles = 1)
          Index Cond: (text @@ plainto_tsquery ('query' :: text))
          Trier par: (text <=> plainto_tsquery ('query' :: text))
  Temps de planification: 0,481 ms
  Temps d'exécution: 137,678 ms 

EXPLIQUER pour GIN
  Limite (temps réel = 579.905..585.650 lignes = 1000 boucles = 1)
    -> Gather Merge (temps réel = 579.904..585.604 lignes = 1000 boucles = 1)
          Travailleurs prévus: 2
          Lancement de travailleurs: 2
          -> Trier (temps réel = 574.061..574.171 lignes = 992 boucles = 3)
                Clé de tri: (ts_rank (text, plainto_tsquery ('query' :: text))) DESC
                Méthode de tri: fusion externe Disque: 1224 Ko
                -> Analyse de tas bitmap parallÚle sur documents_documentvector (temps réel = 8,920..555,571 lignes = 39851 boucles = 3)
                      Vérifiez à nouveau Cond: (text @@ plainto_tsquery ('query' :: text))
                      Blocs de tas: exact = 5422
                      -> Bitmap Index Scan sur idx_gin_document (temps réel = 8,945 à 8,945 lignes = 119553 boucles = 1)
                            Index Cond: (text @@ plainto_tsquery ('query' :: text))
  Temps de planification: 0,223 ms
  Temps d'exécution: 585,948 ms 

~ 150 ms vs ~ 600 ms! Déjà pas en faveur de GIN, non? Et le tri s'est déplacé à l'intérieur de l'index!


Et si vous recherchez le LIMIT 100 ?


EXPLAIN for RUM
  Limite (temps réel = 105,863..108,530 lignes = 100 boucles = 1)
    -> Index Scan en utilisant idx_rum_document sur documents_documentvector (temps réel = 105.862..108.517 lignes = 100 boucles = 1)
          Index Cond: (text @@ plainto_tsquery ('query' :: text))
          Trier par: (text <=> plainto_tsquery ('query' :: text))
  Temps de planification: 0,199 ms
  Temps d'exécution: 108,958 ms 

EXPLIQUER pour GIN
  Limite (temps réel = 582.924..588.351 lignes = 100 boucles = 1)
    -> Gather Merge (temps réel = 582.923..588.344 lignes = 100 boucles = 1)
          Travailleurs prévus: 2
          Lancement de travailleurs: 2
          -> Trier (temps réel = 573.809..573.889 lignes = 806 boucles = 3)
                Clé de tri: (ts_rank (text, plainto_tsquery ('query' :: text))) DESC
                Méthode de tri: fusion externe Disque: 1224 Ko
                -> Analyse de tas bitmap parallÚle sur documents_documentvector (temps réel = 18.038..552.827 lignes = 39851 boucles = 3)
                      Vérifiez à nouveau Cond: (text @@ plainto_tsquery ('query' :: text))
                      Blocs de tas: exact = 5275
                      -> Bitmap Index Scan sur idx_gin_document (temps réel = 16,541..16,541 lignes = 119553 boucles = 1)
                            Index Cond: (text @@ plainto_tsquery ('query' :: text))
  Temps de planification: 0,487 ms
  Temps d'exécution: 588,583 ms 

La différence est encore plus notable.


Le fait est que le GIN n'a pas d'importance exactement combien de lignes vous obtenez Ă  la fin - il doit passer par toutes les lignes pour lesquelles la demande a abouti et les classer. RUM ne le fait que pour les lignes dont nous avons vraiment besoin. Si nous avons besoin de beaucoup de lignes, GIN gagne. Son ts_rank effectue des calculs ts_rank efficacement que l'opĂ©rateur <=> . Mais pour les petites requĂȘtes, l'avantage de RUM est indĂ©niable.


Le plus souvent, l'utilisateur n'a pas besoin de décharger les 50 000 documents de la base de données à la fois. Il n'a besoin que de 10 articles sur la premiÚre, la deuxiÚme, la troisiÚme page, etc. Et c'est précisément dans de tels cas que cet indice est affiné, et il donnera une bonne augmentation des performances de recherche sur une large base.


Rejoindre la tolérance


Et si une recherche vous oblige à rejoindre une ou plusieurs tables? Par exemple, pour afficher dans les résultats le type de document, son propriétaire? Ou, comme dans mon cas, filtrer par les noms des entités liées?


Comparez:


Demande avec deux jointures pour 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; 

Résultat:


 Limite (temps réel = 1637.902..1643.483 lignes = 10 boucles = 1)
    -> Gather Merge (temps réel = 1637.901..1643.479 lignes = 10 boucles = 1)
          Travailleurs prévus: 2
          Lancement de travailleurs: 2
          -> Trier (temps réel = 1070.614..1070.687 lignes = 652 boucles = 3)
                Clé de tri: (ts_rank (documents_documentvector.text, plainto_tsquery ('query' :: text))) DESC
                Méthode de tri: fusion externe Disque: 2968 Ko
                -> Hash Left Join (temps réel = 323,386..1049,092 lignes = 39851 boucles = 3)
                      Hash Cond: (documents_document.case_id = documents_case.id)
                      -> Hash Join (temps réel = 239.312..324.797 lignes = 39851 boucles = 3)
                            Hash Cond: (documents_documentvector.document_id = documents_document.id)
                            -> Analyse de tas bitmap parallÚle sur documents_documentvector (temps réel = 11,022..37,073 lignes = 39851 boucles = 3)
                                  Vérifiez à nouveau Cond: (text @@ plainto_tsquery ('query' :: text))
                                  Blocs de tas: exact = 9362
                                  -> Analyse d'index Bitmap sur idx_gin_document (heure réelle = 12.094..12.094 lignes = 119553 boucles = 1)
                                        Index Cond: (text @@ plainto_tsquery ('query' :: text))
                            -> Hash (temps réel = 227.856..227.856 lignes = 472089 boucles = 3)
                                  Godets: 65536 Lots: 16 Utilisation de la mémoire: 2264 Ko
                                  -> Seq Scan sur documents_document (temps réel = 0,009 à 147,104 lignes = 472089 boucles = 3)
                      -> Hash (temps réel = 83,338..83,338 lignes = 273695 boucles = 3)
                            Godets: 65536 Lots: 8 Utilisation de la mémoire: 2602 ko
                            -> Seq Scan sur documents_case (temps réel = 0,009 à 39,082 lignes = 273695 boucles = 3)
 Temps de planification: 0,857 ms
 Temps d'exécution: 1644,028 ms

Sur trois jointures et plus, le temps de requĂȘte atteint 2-3 secondes et augmente avec le nombre de jointures.


Mais qu'en est-il de RUM? Que la demande soit immédiatement avec cinq jointures.


Cinq demandes d'adhésion pour 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; 

Résultat:


  Limite (temps réel = 70,524 à 72,292 lignes = 10 boucles = 1)
   -> Jointure gauche de boucle imbriquée (temps réel = 70,521 à 72,279 lignes = 10 boucles = 1)
         -> Jointure gauche de boucle imbriquée (temps réel = 70.104..70.406 lignes = 10 boucles = 1)
               -> Jointure gauche de boucle imbriquée (temps réel = 70,089 à 70,351 lignes = 10 boucles = 1)
                     -> Jointure gauche de boucle imbriquée (temps réel = 70,073 à 70,302 lignes = 10 boucles = 1)
                           -> Boucle imbriquée (temps réel = 70.052..70.201 lignes = 10 boucles = 1)
                                 -> Index Scan en utilisant document_vector_rum_index sur documents_documentvector (temps réel = 70.001..70.035 lignes = 10 boucles = 1)
                                       Index Cond: (text @@ plainto_tsquery ('query' :: text))
                                       Trier par: (text <=> plainto_tsquery ('query' :: text))
                                 -> Index Scan en utilisant documents_document_pkey sur documents_document (temps réel = 0,013..0,013 lignes = 1 boucles = 10)
                                       Index Cond: (id = documents_documentvector.document_id)
                                       Filtre: (is_active IS TRUE)
                           -> Index Scan utilisant documents_case_pkey sur documents_case (temps réel = 0,009..0,009 lignes = 1 boucles = 10)
                                 Index Cond: (documents_document.case_id = id)
                     -> Index Scan en utilisant classifier_procedure_pkey sur classifier_procedure (temps réel = 0,003..0,003 lignes = 1 boucles = 10)
                           Index Cond: (documents_case.procedure_id = id)
               -> Index Scan en utilisant classifier_division_pkey sur classifier_division (temps réel = 0,004..0,004 lignes = 1 boucles = 10)
                     Index Cond: (documents_case.division_id = id)
         -> Index Scan en utilisant classifier_category_pkey sur classifier_category (temps réel = 0,003..0,003 lignes = 1 boucles = 10)
               Index Cond: (documents_document.category_id = id)
 Temps de planification: 2,861 ms
 Temps d'exécution: 72,865 ms

Si vous ne pouvez pas vous passer de la jointure lors de la recherche, alors RUM vous convient clairement.


Espace disque


Sur une base de test d'environ 500 000 documents et 3,6 Go, les index occupaient des volumes trÚs différents.


  idx_rum_document |  1950 Mo
  idx_gin_document |  418 Mo

Oui, le lecteur est bon marché. Mais 2 Go au lieu de 400 Mo ne peuvent pas plaire. La moitié de la taille de la base est un peu trop pour l'indice. Ici, GIN gagne inconditionnellement.


Conclusions


Vous avez besoin d'un RHUM si:


  • Vous avez beaucoup de documents, mais vous donnez les rĂ©sultats de la recherche page par page
  • Vous avez besoin d'un filtrage sophistiquĂ© des rĂ©sultats de recherche
  • Cela ne vous dĂ©range pas l'espace disque

Vous serez entiĂšrement satisfait du GIN si:


  • Vous avez une petite base
  • Vous avez une grande base, mais vous devez produire des rĂ©sultats immĂ©diatement et c'est tout
  • Vous n'avez pas besoin de filtrer avec join s
  • Êtes-vous intĂ©ressĂ© par la taille minimale de l'index sur le disque

J'espĂšre que cet article supprimera beaucoup de WTF?! Cela se produit lorsque vous travaillez et configurez la recherche dans Postgres. Je serai heureux d'entendre les conseils de ceux qui savent comment tout configurer encore mieux!)


Dans les parties suivantes, je prévois d'en dire plus sur RUM dans mon projet: sur l'utilisation d'options RUM supplémentaires, en travaillant dans le bundle Django + PostgreSQL.

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


All Articles