Wir bereiten eine Volltextsuche in Postgres vor. Teil 2

Im letzten Artikel haben wir die Suche in PostgreSQL mit Standardtools optimiert. In diesem Artikel werden wir die Verwendung des RUM-Index weiter optimieren und seine Vor- und Nachteile im Vergleich zum GIN analysieren.


Einführung


RUM ist eine Erweiterung für Postgres, einen neuen Index für die Volltextsuche. Sie können Ergebnisse zurückgeben, die nach Relevanz sortiert sind, wenn Sie den Index durchlaufen. Ich werde mich nicht auf die Installation konzentrieren - sie ist in der README-Datei im Repository beschrieben.


Wir verwenden einen Index


Ein Index wird ähnlich wie der GIN-Index erstellt, jedoch mit einigen Parametern. Die gesamte Liste der Parameter finden Sie in der Dokumentation.


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

Suchanfrage für RUM:


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

Anfrage für GIN
 SELECT document_id, ts_rank("text", plainto_tsquery('')) AS rank FROM documents_documentvector WHERE "text" @@ plainto_tsquery('') ORDER BY rank DESC; 

Der Unterschied zum GIN besteht darin, dass die Relevanz nicht mithilfe der Funktion ts_rank ermittelt wird, sondern mithilfe einer Abfrage mit dem Operator <=> : "text" <=> plainto_tsquery('') . Eine solche Abfrage gibt einen gewissen Abstand zwischen dem Suchvektor und der Suchabfrage zurück. Je kleiner es ist, desto besser stimmt die Abfrage mit dem Vektor überein.


Vergleich mit GIN


Hier werden wir auf Testbasis mit ~ 500.000 Dokumenten vergleichen, um Unterschiede in den Suchergebnissen festzustellen.


Geschwindigkeit anfordern


Mal sehen, was EXPLAIN for GIN auf dieser Basis produzieren wird:


 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 

Und für 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 

Was ist das Was nützt dieses gepriesene RUM, wenn es dreimal langsamer läuft als das GIN? Und wo ist die berüchtigte Sortierung im Index?


Ruhe: Versuchen wir, der Anfrage ein LIMIT 1000 hinzuzufügen.


EXPLAIN für RUM
  Limit (tatsächliche Zeit = 115.568..137.313 Zeilen = 1000 Schleifen = 1)
    -> Index-Scan mit idx_rum_document auf documents_documentvector (tatsächliche Zeit = 115.567..137.239 Zeilen = 1000 Schleifen = 1)
          Index Cond: (text @@ plago_tsquery ('query' :: text))
          Bestellen nach: (text <=> plago_tsquery ('query' :: text))
  Planungszeit: 0,481 ms
  Ausführungszeit: 137.678 ms 

EXPLAIN für GIN
  Limit (tatsächliche Zeit = 579.905..585.650 Zeilen = 1000 Schleifen = 1)
    -> Zusammenführung sammeln (tatsächliche Zeit = 579.904..585.604 Zeilen = 1000 Schleifen = 1)
          Geplante Arbeitnehmer: 2
          Gestartete Arbeiter: 2
          -> Sortieren (tatsächliche Zeit = 574.061..574.171 Zeilen = 992 Schleifen = 3)
                Sortierschlüssel: (ts_rank (text, plago_tsquery ('query' :: text))) DESC
                Sortiermethode: Externe Zusammenführung Datenträger: 1224 KB
                -> Paralleler Bitmap-Heap-Scan auf documents_documentvector (tatsächliche Zeit = 8.920..555.571 Zeilen = 39851 Schleifen = 3)
                      Überprüfen Sie erneut Cond: (text @@ plago_tsquery ('query' :: text))
                      Heap-Blöcke: genau = 5422
                      -> Bitmap-Index-Scan auf idx_gin_document (tatsächliche Zeit = 8.945..8.945 Zeilen = 119553 Schleifen = 1)
                            Index Cond: (text @@ plago_tsquery ('query' :: text))
  Planungszeit: 0,223 ms
  Ausführungszeit: 585.948 ms 

~ 150 ms vs ~ 600 ms! Schon nicht für GIN, oder? Und die Sortierung hat sich innerhalb des Index verschoben!


Und wenn Sie nach dem LIMIT 100 suchen?


EXPLAIN für RUM
  Limit (tatsächliche Zeit = 105.863..108.530 Zeilen = 100 Schleifen = 1)
    -> Index-Scan mit idx_rum_document auf documents_documentvector (tatsächliche Zeit = 105.862..108.517 Zeilen = 100 Schleifen = 1)
          Index Cond: (text @@ plago_tsquery ('query' :: text))
          Bestellen nach: (text <=> plago_tsquery ('query' :: text))
  Planungszeit: 0,199 ms
  Ausführungszeit: 108,958 ms 

EXPLAIN für GIN
  Limit (tatsächliche Zeit = 582.924..588.351 Zeilen = 100 Schleifen = 1)
    -> Zusammenführung sammeln (tatsächliche Zeit = 582.923..588.344 Zeilen = 100 Schleifen = 1)
          Geplante Arbeitnehmer: 2
          Gestartete Arbeiter: 2
          -> Sortieren (tatsächliche Zeit = 573.809..573.889 Zeilen = 806 Schleifen = 3)
                Sortierschlüssel: (ts_rank (text, plago_tsquery ('query' :: text))) DESC
                Sortiermethode: Externe Zusammenführung Datenträger: 1224 KB
                -> Paralleler Bitmap-Heap-Scan auf documents_documentvector (tatsächliche Zeit = 18.038..552.827 Zeilen = 39851 Schleifen = 3)
                      Überprüfen Sie erneut Cond: (text @@ plago_tsquery ('query' :: text))
                      Heap-Blöcke: genau = 5275
                      -> Bitmap-Index-Scan auf idx_gin_document (tatsächliche Zeit = 16.541..16.541 Zeilen = 119553 Schleifen = 1)
                            Index Cond: (text @@ plago_tsquery ('query' :: text))
  Planungszeit: 0,487 ms
  Ausführungszeit: 588.583 ms 

Der Unterschied ist noch deutlicher.


Die Sache ist, dass die GIN nicht genau spielt, wie viele Zeilen Sie am Ende erhalten - sie muss alle Zeilen durchlaufen, für die die Anforderung erfolgreich war, und sie bewerten. RUM macht das nur für die Zeilen, die wir wirklich brauchen. Wenn wir viele Zeilen brauchen, gewinnt GIN. Sein ts_rank führt Berechnungen effizienter durch als der Operator <=> . Bei kleinen Anfragen ist der Vorteil von RUM jedoch nicht zu leugnen.


In den meisten Fällen muss der Benutzer nicht alle 50.000 Dokumente gleichzeitig aus der Datenbank entladen. Er benötigt nur 10 Beiträge auf der ersten, zweiten, dritten Seite usw. Und genau in solchen Fällen wird dieser Index geschärft, und die Suchleistung wird auf einer großen Basis deutlich gesteigert.


Treten Sie der Toleranz bei


Was ist, wenn Sie bei einer Suche einer anderen oder mehreren Tabellen beitreten müssen? Um beispielsweise in den Ergebnissen den Dokumenttyp anzuzeigen, dessen Eigentümer? Oder wie in meinem Fall nach den Namen verwandter Entitäten filtern?


Vergleichen Sie:


Anfrage mit zwei Beiträgen für 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; 

Ergebnis:


 Limit (tatsächliche Zeit = 1637.902..1643.483 Zeilen = 10 Schleifen = 1)
    -> Zusammenführung sammeln (tatsächliche Zeit = 1637.901..1643.479 Zeilen = 10 Schleifen = 1)
          Geplante Arbeitnehmer: 2
          Gestartete Arbeiter: 2
          -> Sortieren (tatsächliche Zeit = 1070.614..1070.687 Zeilen = 652 Schleifen = 3)
                Sortierschlüssel: (ts_rank (documents_documentvector.text, plago_tsquery ('query' :: text))) DESC
                Sortiermethode: Externe Zusammenführung Datenträger: 2968 KB
                -> Hash Left Join (tatsächliche Zeit = 323.386..1049.092 Zeilen = 39851 Schleifen = 3)
                      Hash Cond: (documents_document.case_id = documents_case.id)
                      -> Hash Join (tatsächliche Zeit = 239.312..324.797 Zeilen = 39851 Schleifen = 3)
                            Hash Cond: (documents_documentvector.document_id = documents_document.id)
                            -> Paralleler Bitmap-Heap-Scan auf documents_documentvector (tatsächliche Zeit = 11.022..37.073 Zeilen = 39851 Schleifen = 3)
                                  Überprüfen Sie erneut Cond: (text @@ plago_tsquery ('query' :: text))
                                  Heap-Blöcke: genau = 9362
                                  -> Bitmap-Index-Scan auf idx_gin_document (tatsächliche Zeit = 12.094..12.094 Zeilen = 119553 Schleifen = 1)
                                        Index Cond: (text @@ plago_tsquery ('query' :: text))
                            -> Hash (tatsächliche Zeit = 227.856..227.856 Zeilen = 472089 Schleifen = 3)
                                  Buckets: 65536 Batches: 16 Speichernutzung: 2264kB
                                  -> Seq Scan on documents_document (tatsächliche Zeit = 0,009..147,104 Zeilen = 472089 Schleifen = 3)
                      -> Hash (tatsächliche Zeit = 83.338..83.338 Zeilen = 273695 Schleifen = 3)
                            Buckets: 65536 Batches: 8 Speichernutzung: 2602kB
                            -> Seq Scan on documents_case (tatsächliche Zeit = 0,009..39.082 Zeilen = 273695 Schleifen = 3)
 Planungszeit: 0,857 ms
 Ausführungszeit: 1644.028 ms

Bei drei Joins und mehr erreicht die Anforderungszeit 2-3 Sekunden und wächst mit der Anzahl der Joins.


Aber was ist mit RUM? Lassen Sie die Anfrage sofort mit fünf Beiträgen sein.


Fünf Beitrittsanfragen für 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; 

Ergebnis:


  Limit (tatsächliche Zeit = 70.524..72.292 Zeilen = 10 Schleifen = 1)
   -> Left Join der verschachtelten Schleife (tatsächliche Zeit = 70.521..72.279 Zeilen = 10 Schleifen = 1)
         -> Linker Join für verschachtelte Schleife (tatsächliche Zeit = 70.104..70.406 Zeilen = 10 Schleifen = 1)
               -> Left Join der verschachtelten Schleife (tatsächliche Zeit = 70.089..70.351 Zeilen = 10 Schleifen = 1)
                     -> Left Join der verschachtelten Schleife (tatsächliche Zeit = 70.073..70.302 Zeilen = 10 Schleifen = 1)
                           -> Verschachtelte Schleife (tatsächliche Zeit = 70.052..70.201 Zeilen = 10 Schleifen = 1)
                                 -> Index-Scan mit document_vector_rum_index für documents_documentvector (tatsächliche Zeit = 70.001..70.035 Zeilen = 10 Schleifen = 1)
                                       Index Cond: (text @@ plago_tsquery ('query' :: text))
                                       Bestellen nach: (text <=> plago_tsquery ('query' :: text))
                                 -> Index-Scan mit documents_document_pkey für documents_document (tatsächliche Zeit = 0.013..0.013 Zeilen = 1 Schleifen = 10)
                                       Index Cond: (id = documents_documentvector.document_id)
                                       Filter: (is_active IS TRUE)
                           -> Index-Scan mit documents_case_pkey für documents_case (tatsächliche Zeit = 0,009..0,009 Zeilen = 1 Schleifen = 10)
                                 Index Cond: (documents_document.case_id = id)
                     -> Index-Scan mit classifier_procedure_pkey für classifier_procedure (tatsächliche Zeit = 0,003..0,003 Zeilen = 1 Schleifen = 10)
                           Index Cond: (documents_case.procedure_id = id)
               -> Index-Scan mit classifier_division_pkey für classifier_division (tatsächliche Zeit = 0,004..0,004 Zeilen = 1 Schleifen = 10)
                     Index Cond: (documents_case.division_id = id)
         -> Index-Scan mit classifier_category_pkey für classifier_category (tatsächliche Zeit = 0,003..0,003 Zeilen = 1 Schleifen = 10)
               Index Cond: (documents_document.category_id = id)
 Planungszeit: 2.861 ms
 Ausführungszeit: 72,865 ms

Wenn Sie bei der Suche nicht auf Join verzichten können, ist RUM eindeutig für Sie geeignet.


Speicherplatz


Auf einer Testbasis von ~ 500.000 Dokumenten und 3,6 GB-Indizes belegten sehr unterschiedliche Volumes.


  idx_rum_document |  1950 MB
  idx_gin_document |  418 MB

Ja, das Laufwerk ist billig. Aber 2 GB statt 400 MB können nicht gefallen. Die halbe Größe der Basis ist ein bisschen viel für den Index. Hier gewinnt GIN bedingungslos.


Schlussfolgerungen


Sie benötigen eine RUM, wenn:


  • Sie haben viele Dokumente, geben aber Seite für Seite Suchergebnisse an
  • Sie benötigen eine ausgefeilte Filterung der Suchergebnisse
  • Sie haben nichts gegen den Speicherplatz

Sie werden mit dem GIN vollkommen zufrieden sein, wenn:


  • Du hast eine kleine Basis
  • Sie haben eine große Basis, aber Sie müssen sofort Ergebnisse erzielen, und das war's
  • Sie müssen nicht mit Joins filtern
  • Interessieren Sie sich für die Mindestindexgröße auf der Festplatte?

Ich hoffe, dieser Artikel entfernt viel WTF?! Das tritt auf, wenn Sie in Postgres arbeiten und die Suche einrichten. Ich freue mich über Ratschläge von denen, die wissen, wie man alles noch besser konfiguriert!)


In den nächsten Abschnitten möchte ich mehr über RUM in meinem Projekt erzählen: über die Verwendung zusätzlicher RUM-Optionen, die im Django + PostgreSQL-Bundle arbeiten.

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


All Articles