- Die Volltextsuche von PostgreSQL ist ein Ansatz, bei dem sich Suchfunktionen ohne separate Suchmaschine durch die Kombination von
tsvector,tsquery,@@,ts_rankund GIN-Indizes aufbauen lassen - Suchbegriffe und Dokumente werden auf Lexem-Ebene normalisiert, und mit Operatoren wie AND, OR, NOT und FOLLOWED BY lässt sich ein großer Teil üblicher Suchsyntax umsetzen
- GIN-Indizes reduzierten die Suchzeit in der Beispielumgebung von über 200 ms auf etwa 4 ms, aber bei vielen Treffern können die Kosten für Ranking und Sortierung mit
ts_rankzum Flaschenhals werden - Relevanz-Tuning wird umgesetzt, indem Signale wie Titelgewichtung, Zahl der Stimmen, Bewertung, Genre und Aktualität in die Sortierformel aufgenommen oder Spalten per
setweightgewichtet werden - Fehlertoleranz, Facettensuche, Autovervollständigung, exakte Phrasensuche und hybride Suche sind ebenfalls möglich, aber in PostgreSQL müssen die Bausteine direkt zusammengesetzt werden, und bei großen Datensätzen sollten Leistungsgrenzen geprüft werden
Der Ansatz der PostgreSQL-Volltextsuche
- PostgreSQL stellt für die Volltextsuche niedrigstufige Bausteine bereit, die sich zu Suchmaschinenfunktionen kombinieren lassen
- Dieser Ansatz ist flexibel, erfordert aber mehr Implementierungsaufwand als Elasticsearch, Typesense oder Meilisearch, deren Hauptzweck Volltextsuche ist
- Die Beispielabfragen verwenden den Datensatz Wikipedia Movie Plots von Kaggle
- Er enthält 34.000 Filmtitel
- Die Größe im CSV-Format beträgt etwa 81 MB
Zentrale Bausteine
- Die PostgreSQL-Volltextsuche arbeitet im Kern mit den folgenden Elementen
tsvector: speichert durchsuchbaren Text als Liste normalisierter Lexemetsquery: stellt eine normalisierte Suchanfrage dar@@: Match-Operator zur Prüfung, ob einetsqueryzu einemtsvectorpasstts_rank,ts_rank_cd: berechnen den Relevanz-Score von Suchergebnissen- GIN-Indizes: invertierte Indizes, um
tsvectoreffizient abzufragen
tsvector und Suchkonfigurationen
tsvectorspeichert eine sortierte Liste von Lexemen- Ein Lexem ist ähnlich wie ein Token, aber als normalisierte Zeichenkette so aufbereitet, dass verschiedene Formen desselben Wortes auf eine Form abgebildet werden
- In der englischen Konfiguration werden Großbuchstaben in Kleinbuchstaben umgewandelt und Endungen zur Normalisierung entfernt
- Wenn man einen englischen Satz mit
to_tsvectorparst, werden Stoppwörter wie „I“, „to“ und „an“ entfernt- „refuse“ und „Refusing“ werden beide zu
refus - Satzzeichen werden ignoriert
- Wortpositionen und Gewichtungen im Originaltext werden ebenfalls gespeichert
- „refuse“ und „Refusing“ werden beide zu
- Verwendet man statt der Suchkonfiguration
englishdie Konfigurationsimple, werden Wörter in der Form übernommen, in der sie im Text vorkommen- „refuse“ und „refusing“ bleiben unterschiedliche Lexeme
- Die Konfiguration
simpleist besonders nützlich für Spalten mit Labels oder Tags
- PostgreSQL bietet eingebaute Suchkonfigurationen für mehrere Sprachen, aber keine für CJK (Chinesisch, Japanisch, Koreanisch)
- Für nicht unterstützte Sprachen kann die Konfiguration
simplein der Praxis gut funktionieren - Ob das für CJK ausreichend ist, ist allerdings nicht sicher
- Für nicht unterstützte Sprachen kann die Konfiguration
tsquery und Darstellung von Suchanfragen
tsqueryist ein Datentyp zur Darstellung normalisierter Suchanfragen- Suchbegriffe müssen bereits normalisierte Lexeme sein
- Mehrere Suchbegriffe lassen sich mit den Operatoren AND, OR, NOT und FOLLOWED BY kombinieren
to_tsquery,plainto_tsqueryundwebsearch_to_tsqueryhelfen dabei, vom Benutzer eingegebenen Text in eine passendetsqueryzu überführen- Ihre zentrale Aufgabe ist es, die im Eingabetext enthaltenen Wörter zu normalisieren
- Mit
websearch_to_tsquerylassen sich Anfragen erzeugen, die einer gewöhnlichen Suchleiste näherkommendarth vaderwird als logisches AND behandelt, bei dem beide Wörter im Dokument vorkommen müssen- OR-Suche und das Ausschließen von Wörtern sind ebenfalls möglich
- Phrasensuche wird als Folge von Wörtern in der richtigen Reihenfolge ausgedrückt
- In der englischen Konfiguration werden Stoppwörter wie „the“ entfernt, sodass bei manchen Phrasensuchen fast die gesamte Formulierung verschwinden kann
- In solchen Fällen liefert die Konfiguration
simplemöglicherweise die erwarteten Ergebnisse
- In solchen Fällen liefert die Konfiguration
- Der Operator
@@wird verwendet, um zu prüfen, ob einetsqueryzu einemtsvectorpasst
GIN-Indizes und Suchleistung
- GIN steht für Generalized Inverted Index und ist ein Indextyp, der für Abfragen entwickelt wurde, die nach Elementwerten innerhalb zusammengesetzter Werte suchen
- GIN kann nicht nur für Textsuche, sondern auch für JSON-Abfragen verwendet werden
- Es lässt sich eine
tsvector-Spalte anlegen, die mehrere durchsuchbare Spalten zusammenführt, und darauf ein GIN-Index erstellen - In der Beispielumgebung reduzierte ein GIN-Index die Suchzeit von über 200 ms auf etwa 4 ms
Ranking und Relevanzberechnung
- Für eine gute Sucherfahrung sollten Ergebnisse nach Relevanz sortiert werden
- PostgreSQL bietet dafür zwei vordefinierte Ranking-Funktionen:
ts_rankundts_rank_cd- Beide Funktionen berücksichtigen die Häufigkeit des Auftretens von Suchbegriffen
ts_rank_cdbezieht zusätzlich die Nähe zwischen passenden Lexemen ein
- Relevanz hängt stark von der jeweiligen Anwendung ab
- Die Standard-Ranking-Funktionen sind eher ein Ausgangspunkt; bei Bedarf kann man eigene Ranking-Funktionen erstellen oder sie mit anderen Faktoren kombinieren
ts_rankmuss auf diesearch-Spalte jedes Ergebnisses zugreifen- Wenn die
WHERE-Bedingung viele Zeilen trifft, muss PostgreSQL alle diese Zeilen für Ranking-Berechnung und Sortierung besuchen - In der Beispielumgebung wurde eine Anfrage in 5 bis 7 ms zurückgegeben, aber eine Anfrage wie
darth OR vader, bei der mehr als 1.000 Ergebnisse gerankt werden mussten, brauchte etwa 80 ms
- Wenn die
Relevanz-Tuning
- Relevanz auf Basis der Worthäufigkeit ist ein guter Standard, aber in den Daten können wichtigere Signale als reine Häufigkeit stecken
- Im Filmdatensatz lassen sich zum Beispiel folgende Signale in die Relevanz einbeziehen
- Treffer im Titel höher gewichten als Treffer in Beschreibung oder Handlung
- Beliebtere Filme anhand von Bewertung oder Zahl der Stimmen nach oben setzen
- Wenn ein Nutzer Komödien bevorzugt, Komödien höher platzieren
- Neuere Titel als relevanter behandeln als ältere
- Dedizierte Suchmaschinen bieten Funktionen, mit denen festgelegt werden kann, wie verschiedene Spalten oder Felder das Ranking beeinflussen
- Beispiele in der Dokumentation: Elastic, Typesense, Meilisearch
Boosting nach Zahlen, Datum und exakten Werten
- PostgreSQL bietet kein direktes Boosting auf Basis anderer Spalten, aber Ranking ist letztlich eine Sortierformel, daher können eigene Signale ergänzt werden
- Um die Zahl der Stimmen zu berücksichtigen, kann man dem Ranking-Score einen auf der Stimmenzahl basierenden Boost hinzufügen
- Im Beispiel wird ein Logarithmus verwendet, um den Einfluss abzuflachen
- Ein Koeffizient von
0.01bringt den Booster auf eine ähnliche Skala wie den Ranking-Score
- Es lassen sich auch komplexere Funktionen bauen, die eine Bewertung nur dann boosten, wenn die Zahl der Stimmen einen bestimmten Schwellenwert überschreitet
- Wenn ein bestimmtes Genre angehoben werden soll, kann eine Funktion wie
valueBoosterverwendet werden, die nur dann einen Koeffizienten zurückgibt, wenn ein Wert mit einem bestimmten Spaltenwert übereinstimmt
Spaltengewichtungen
- Lexemen in einem
tsvectorkönnen Gewichte zugewiesen werden - PostgreSQL unterstützt vier Gewichtsstufen: A, B, C und D
- A ist die höchste Gewichtung
- D ist die niedrigste und der Standardwert
- Mit der Funktion
setweightlässt sich die Gewichtung beim Erzeugen einertsvector-Spalte steuern - Gibt man der Titelspalte ein höheres Gewicht, erscheinen Filme, deren Suchbegriff im Titel vorkommt, weiter oben in den Ergebnissen und erhalten einen höheren Ranking-Score
- Die Beschränkung auf nur vier Gewichtsklassen ist einschränkend, und die Gewichte müssen bei der Berechnung von
tsvectorangewendet werden
Fehlertoleranz und Fuzzy Search
- PostgreSQL unterstützt bei Verwendung von
tsvectorundtsqueryweder Fuzzy Search noch Fehlertoleranz direkt - Unter der Annahme, dass Tippfehler auf der Anfrageseite liegen, lässt sich dies folgendermaßen umsetzen
- Alle Lexeme aus dem Inhalt in einer separaten Tabelle indexieren
- Für jedes Wort der Anfrage Kandidatenwörter anhand von Ähnlichkeit oder Levenshtein-Distanz suchen
- Die Anfrage so anpassen, dass die gefundenen Wörter eingeschlossen werden
- Die Suche mit der angepassten Anfrage ausführen
- Im Beispiel wird Levenshtein distance verwendet, weil Suchmaschinen dieses Verfahren für Fuzzy Search einsetzen
- Nachdem eine Liste möglicher Kandidatenwörter vorliegt, muss die Anfrage so angepasst werden, dass alle diese Wörter berücksichtigt werden
Facettensuche
- Facettensuche wird vor allem auf E-Commerce-Websites häufig genutzt, damit Nutzer den Suchraum schrittweise eingrenzen können
- In PostgreSQL lässt sie sich umsetzen, indem Kategorien manuell definiert und dann zur
WHERE-Bedingung der Suche hinzugefügt werden - Es ist auch möglich, Kategorien algorithmisch auf Basis vorhandener Daten zu erzeugen
- Im Beispiel wird auf Basis des Filmjahres eine „Decade“-Facette erstellt
- Die Anzahl der Treffer pro Jahrzehnt kann mitberechnet und in Klammern angezeigt werden
- Um mehrere Facetten in einer einzigen Anfrage abzurufen, können CTEs kombiniert werden
- Dieser Ansatz kann bei kleinen bis mittelgroßen Datensätzen gut funktionieren, bei sehr großen Datensätzen aber langsam werden
Umfang und Grenzen einer PostgreSQL-Suchmaschine
- Durch die Kombination der PostgreSQL-Bausteine für Volltextsuche lässt sich eine ziemlich fortgeschrittene Suchmaschine bauen
- Eine auf PostgreSQL basierende Suche unterstützt außerdem Joins und ACID-Transaktionen
- Das sind Funktionen, die andere Suchmaschinen normalerweise nicht bieten
- Weitere fortgeschrittene Suchthemen, die sich behandeln lassen, sind unter anderem
- Vorschläge und Autovervollständigung
- Exaktes Phrasen-Matching
- Hybride Suche in Kombination mit
pg-vector
- Diese Funktionen sind mit PostgreSQL möglich, aber die Bausteine müssen direkt zusammengesetzt werden
- In manchen Fällen kann die Leistung bei sehr großen Datensätzen nachlassen
- Der Folgebeitrag part 2 vergleicht die Implementierung von Suche in PostgreSQL mit dem Hinzufügen von Elasticsearch zur Infrastruktur und der Synchronisierung der Daten
1 Kommentare
Hacker-News-Meinungen
Man sollte nicht versuchen, das auf eine Weise zu machen, die Elasticsearch-Funktionen nachahmt
In den 2000ern habe ich einmal mit MySQL 3.x eine Suchmaschine gebaut, die EXIF-Daten aus Bildern parste und eine dreistufige Taxonomie samt Zählwerten indexierte. Das war etwas, woran mehrere teure Anbieter scheiterten; Autonomy schaffte nicht einmal die oberste Kategorie
Nach 6 Wochen hatte ich es gerade so geschafft, mit fragilem SQL, bei dem schon eine andere Reihenfolge der SELECT-Spalten die Performance änderte. Noch einmal möchte ich das nicht machen. Datenbanken sind im Kern zwar weitgehend ähnlich geblieben, aber Suchmaschinen haben sich deutlich weiterentwickelt
Aus intellektueller Neugier kann man das machen, aber Suche ist nicht einfach Tokenisierung und fertig. Bald kommen Anforderungen wie Facettennavigation, Mehrsprachigkeit, automatische Synonyme, Rechtschreibvorschläge à la „Did you mean“ und Performance in großem Maßstab hinzu, und man manövriert sich selbst in eine Sackgasse. Der eigenen geistigen Gesundheit zuliebe ist es besser, eine Suchmaschine zu nutzen; für die Synchronisierung von PG und ES gibt es auch Tools wie ZomboDB oder PGSync
Wenn dagegen Datenanalysten oder Entwickler Textspalten in einer großen Datenbank flexibler durchsuchen möchten als mit
LIKE/ILIKE, kann es einfacher und schneller sein, innerhalb derselben Datenbank einen Full-Text-Search-Index/eine Full-Text-Search-Tabelle anzulegen und damit 90 % des Weges zu schaffenIch bin gespannt auf Teil 2 mit dem Vergleich Postgres vs. Elasticsearch. Eine Anwendung in unserem Unternehmen nutzt PG für Objekt-CRUD und Elastic für die Suche; wir haben den Aufwand, zwei Datenspeicher synchron zu halten, völlig unterschätzt und prüfen tatsächlich, Elasticsearch zu entfernen
Alle 5 Minuten sucht ein Cronjob in der DB nach zu indexierenden Objekten mit
last_modified_at > last_indexing_started_timestamp, indexiert sie in Elasticsearch und setzt danachlast_indexing_started_timestampauf den Startzeitpunkt der Synchronisierung. Objekte, die zwischen Start und Ende des Laufs geändert wurden, werden dann beim nächsten Lauf erfasstWenn Elasticsearch neu aufgebaut werden muss, leert man den letzten Indexierungszeitpunkt und synchronisiert von Anfang an neu; dadurch wird das System selbstheilend und die Synchronisierung gerät nicht aus dem Takt
Wie bei asynchronen Jobs immer braucht man Monitoring und Retries, aber ES war stabil und schnell, daher gab es selten Probleme. Allerdings waren die Konsistenzanforderungen locker: Es reichte, wenn der aktuelle Zustand aus PG innerhalb einer vernünftigen Zeit in ES ankam. Bei anderen Anforderungen kann das anders aussehen
Nachdem wir auf Postgres-Full-Text-Search mit Index- und Query-Gewichtungen umgestellt hatten, konnten wir alles Nötige innerhalb von Postgres erledigen – nur mit Update-Triggern und sehr schnellen Suchabfragen
Insgesamt war es ein Erfolg, und der Betriebsaufwand sank stark genug, dass sich die investierte Engineering-Zeit locker amortisierte, aber es ist nichts, womit man leichtfertig anfangen sollte
Je nach Bedarf können materialisierte Views, normale Views oder Trigger besser sein. Die eingebaute Textsuche passt möglicherweise nicht zum Anwendungsfall, und eine Alternative zu bauen muss nicht zwangsläufig schwierig sein
Ohne p50/p99-Abfragezeiten unter realistischer Last ist das schwer als belegt anzusehen. Eine Suchmaschine, die Ergebnisse in 1 Minute zurückgibt, ist nicht „fortgeschritten“, und mit einer relationalen Datenbank wie Postgres ist das auf dem Papier natürlich auch möglich
Im Großen und Ganzen stimme ich zu; in großem Maßstab, etwa ab ein paar Millionen Datensätzen, würde ich diesen Ansatz wahrscheinlich nicht verwenden. Mein Hauptinteresse war, wie viel Funktionalität man nachbauen kann
Bei kleinen Suchanwendungsfällen hat man Vorteile wie weniger zu wartende Infrastruktur, starke Konsistenz und Joins. Bei Xata denken wir auch über einen sanften Übergang nach: erst Postgres in kleinem Maßstab nutzen und dann mit möglichst wenigen Breaking Changes zu Elasticsearch wechseln
Aus dieser Perspektive sind schnelle Suchergebnisse an sich nicht besonders überraschend. Man kann die gecachten Ergebnisse im Hintergrund fortlaufend aktualisieren und sie bei einer Anfrage einfach ausliefern. Dieses Caching und die Antwortzeit scheinen unabhängig davon zu sein, wie schnell die eigentliche Berechnung der Suchergebnisse ist
Manche Nutzer haben langsame Verbindungen, und bestimmte Suchbegriffe wie Erdbeben können nur in bestimmten Regionen stark ansteigen
Sind die Suchbegriffe zu zufällig, gibt es keine Ergebnisse im Cache und es sieht schlechter aus als in Wirklichkeit; sind sie nicht zufällig genug, sieht es besser aus als in Wirklichkeit
Daher liegen die Vorteile von ES und ähnlichen Systemen in der horizontalen Skalierung über mehrere Nodes hinweg oder in zusätzlichen Funktionen, die auf dem Hauptindex aufsetzen
Nur
ts_rankzu verwenden ist perfekt, aber normalerweise möchte man das Ranking mit anderen Relevanzmetriken korrigieren. Wenn man löst, dass diese Metrik nicht als primäres Ranking-Kriterium dienen kann, sind die Ergebnisse so schnell wie eine Abfrage auf einer normalen DB-Tabelle mit IndexAls Teenager habe ich einmal von Grund auf eine Suchmaschine und eine Datenbank gebaut, obwohl ich von beidem nicht viel verstand. Ich wollte sehen, wie weit man kommt und wie schnell man einigermaßen relevante Suchergebnisse zurückgeben kann.
Eine einfache Datenbank und eine einfache Suchmaschine schnell zu bauen, ist auch für Amateurprogrammierer ziemlich leicht. Wenn man grundlegende Informatik-Algorithmen versteht und weiß, wie man Betriebssystem und Hardware nutzt, kann man das in ein bis zwei Monaten schaffen.
Selbst in einer High-Level-Sprache war die Geschwindigkeit nicht schlecht; auf einem Laptop von 2003 waren es etwa 250.000 QPS. Mit Sharding ist auch die Skalierbarkeit kein großes Problem. Indexierung, Sperren und Konsistenz sind komplexer als Speichern und Abfragen.
Das wirklich große Problem ist die Subjektivität der Suche. Wenn es darum geht, was man wirklich finden möchte, wie man etwas findet, wenn man selbst nicht weiß, wonach man sucht, wie man Leute aufhält, die das System missbrauchen wollen, und wie man komplexe Queries und Datensätze handhabt, steigt der Schwierigkeitsgrad um mehrere Größenordnungen.
Mich würde interessieren, ob du wirklich eine Allzweck-Datenbank verwendet hast und ob diese Zahl stimmt. Ich wüsste gern, welche Techniken du eingesetzt hast, um diese Engines zu übertreffen.
Wird sie größer, braucht man kreative Lösungen, und ab dann tauchen verschiedene Trade-offs auf.
Eines der Probleme bei der Suche innerhalb von Postgres ist, dass Suche eine CPU-intensive Aufgabe ist und sich zunehmend auch in Richtung GPU-Nutzung bewegt. Idealerweise möchte man die CPU der Datenbank für transaktionale Updates des Kern-Datenmodells freihalten.
Ich habe oft gesehen, dass ES- und Solr-Cluster während des Reindexierens auf mehr als 10 Nodes mit 100 % laufen oder selbst im Normalbetrieb auf mehr als 10 Nodes 30–50 % CPU nutzen. Die entsprechende Datenbank kann zum Beispiel auf einer AWS-L/XL-Instanz mit 50–100 GB Daten und etwa 30 % CPU-Auslastung laufen.
Wenn man die gesamte Such-CPU in die Hauptdatenbank verlagert, muss man die DB anschließend sharden. Für Side Projects sind PG-Erweiterungen für Suche, rekursive Joins, Vektoren usw. aber trotzdem angenehm, spannend und einfach.
Ich habe das früher tatsächlich gemacht und es ziemlich schnell hinbekommen.
https://austingwalters.com/fast-full-text-search-in-postgres...
Die Website ist aktuell https://askhn.ai.
[1]: https://en.wikipedia.org/wiki/Kerning
Kombiniert man solche Techniken mit pgvector, kann man über Embeddings auch relevante Inhalte finden. Das fühlte sich ziemlich magisch an.
Nebenbei: Es heißt nicht „Dark“ Vader, sondern Darth Vader. Als Kind dachte ich auch, es sei „Dark“.
Ein sehr guter und klarer Artikel. Auch SQLite bietet als Standard-Plugin fortgeschrittene Indexfunktionen und Stemming.
Für Englisch funktioniert SQLite ebenfalls gut.
https://github.com/daitangio/knowledge
Es lohnt sich, das auszuprobieren. Es ist ziemlich leistungsfähig.
Guter Artikel, aber die Aussage, PostgreSQL unterstütze keine Fuzzy Search, ist teilweise falsch. Die Erweiterung
pg_trgmund ein GIN-Trigramm-Index unterstützen Fuzzy-Search-Anwendungsfälle wie die Beispiele in diesem Artikel.https://www.postgresonline.com/article_pfriendly/169.html
Queries können dadurch deutlich schneller werden, allerdings um den Preis eines höheren Speicherverbrauchs und längerer Arbeitszeit bei Updates.