PostgreSQL Volltextsuche reicht für viele Anwendungen. Nutze eine einfache Entscheidungsregel, eine Starter-Query und eine Index-Checkliste, um zu entscheiden, wann eine Suchmaschine nötig ist.

Die meisten Leute fragen nicht nach „Volltextsuche“. Sie wollen ein Suchfeld, das sich schnell anfühlt und auf der ersten Seite das findet, was sie meinten. Wenn Ergebnisse langsam, laut oder seltsam sortiert sind, ist den Nutzern egal, ob du PostgreSQL Volltextsuche oder eine separate Engine verwendest. Sie verlieren einfach das Vertrauen in die Suche.
Es geht um eine Entscheidung: Suche in Postgres halten oder eine dedizierte Suchmaschine hinzufügen. Das Ziel ist nicht perfekte Relevanz. Es ist eine solide Basis, die sich schnell ausliefern lässt, leicht zu betreiben ist und für die tatsächliche Nutzung deiner App gut genug ist.
Für viele Apps reicht PostgreSQL Volltextsuche lange aus. Wenn du ein paar Textfelder hast (Titel, Beschreibung, Notizen), eine grundlegende Rangfolge und ein oder zwei Filter (Status, Kategorie, Mandant), kann Postgres das ohne zusätzliche Infrastruktur stemmen. Du hast weniger bewegliche Teile, einfachere Backups und weniger Fälle von „warum ist die Suche down, aber die App läuft?“
„Ausreichend“ bedeutet meist, gleichzeitig drei Ziele zu treffen:
Ein konkretes Beispiel: ein SaaS-Dashboard, in dem Nutzer Projekte nach Namen und Notizen durchsuchen. Wenn eine Anfrage wie „onboarding checklist“ das richtige Projekt in den Top-5 zurückgibt, in unter einer Sekunde, und du nicht ständig Analyzer oder Reindex-Jobs anpassen musst, ist das „ausreichend“. Wenn du diese Ziele nur durch zusätzliche Komplexität erreichst, ist das der Punkt, an dem „eingebaute Suche vs. Suchmaschine“ relevant wird.
Teams beschreiben Suche oft als Funktionen, nicht als Ergebnisse. Die nützliche Vorgehensweise ist, jede Funktion in die Kosten zu übersetzen: was kostet es zu bauen, zu tunen und zuverlässig zu halten.
Frühe Anfragen klingen meist nach: Tippfehlertoleranz, Facetten und Filter, Hervorhebungen, „smarter“ Rangfolge und Autocomplete. Für eine erste Version trennt man Muss von Nice-to-have. Ein einfaches Suchfeld muss meist nur relevante Einträge finden, mit gängigen Wortformen (Plural, Zeitform) umgehen, einfache Filter respektieren und schnell bleiben, wenn die Tabelle wächst. Genau hier passt PostgreSQL Volltextsuche oft gut.
Postgres glänzt, wenn dein Inhalt in normalen Textfeldern liegt und du die Suche nahe an den Daten haben willst: Hilfartikel, Blogposts, Support-Tickets, interne Dokumente, Produkttitel und -beschreibungen oder Notizen zu Kundenakten. Das sind meist „finde mir den richtigen Datensatz“-Probleme, keine „baue ein Suchprodukt“-Probleme.
Nice-to-haves sind dort, wo Komplexität hereinbricht. Tippfehlertoleranz und reichhaltiges Autocomplete treiben dich oft zu zusätzlicher Software. Facetten sind in Postgres möglich, aber wenn du viele Facetten, tiefe Analytics und sofortige Counts über riesige Datensätze willst, wird eine dedizierte Engine attraktiver.
Die versteckten Kosten sind selten Lizenzgebühren. Es ist das zweite System. Sobald du eine Suchmaschine hinzufügst, kommen Datensynchronisation und Backfills (und die Bugs, die sie erzeugen), Monitoring und Upgrades, Supportfragen wie „warum zeigt die Suche alte Daten?“ und zwei Sätze von Relevanz-Schaltern dazu.
Wenn du unsicher bist: starte mit Postgres, liefere etwas Einfaches aus und füge eine andere Engine nur hinzu, wenn eine klare Anforderung nicht erfüllbar ist.
Nutze eine Drei-Prüfungen-Regel. Wenn du alle drei bestehst, bleib bei PostgreSQL Volltextsuche. Wenn du eine davon stark verfehlst, ziehe eine dedizierte Suchmaschine in Betracht.
Relevanzbedarf: Reichen „gut genug“-Ergebnisse, oder brauchst du nahezu perfekte Rangfolge über viele Edge-Cases (Tippfehler, Synonyme, „people also searched“, personalisierte Ergebnisse)? Wenn du gelegentliche unperfekte Reihenfolgen tolerieren kannst, funktioniert Postgres meist.
Anfragevolumen und Latenz: Wie viele Suchanfragen pro Sekunde erwartest du am Peak, und wie groß ist dein echtes Latenzbudget? Wenn Suche nur ein kleiner Teil des Traffics ist und du mit passenden Indizes schnelle Abfragen halten kannst, ist Postgres in Ordnung. Wenn die Suche zur Top-Last wird und mit Kern-Lese-/Schreibvorgängen konkurriert, ist das ein Warnsignal.
Komplexität: Suchst du ein oder zwei Textfelder oder kombinierst du viele Signale (Tags, Filter, Zeitverfall, Popularität, Berechtigungen) und mehrere Sprachen? Je komplexer die Logik, desto mehr wirst du in SQL Reibung spüren.
Ein sicherer Startpunkt ist einfach: liefere eine Basis in Postgres, logge langsame Queries und „keine Ergebnisse“-Suchen und entscheide dann. Viele Apps wachsen nie darüber hinaus und du vermeidest, zu früh ein zweites System zu betreiben.
Rote Flaggen, die meist auf eine dedizierte Engine hinweisen:
Grüne Flaggen für das Bleiben in Postgres:
PostgreSQL Volltextsuche ist eine eingebaute Methode, Text in eine durchsuchbare Form zu bringen, ohne jede Zeile zu scannen. Sie funktioniert am besten, wenn dein Inhalt bereits in Postgres liegt und du eine schnelle, brauchbare Suche mit vorhersehbarem Betrieb willst.
Drei Bausteine sind wichtig:
ts_rank (oder ts_rank_cd) nutzen, um relevantere Zeilen voranzustellen.Die Sprachkonfiguration ist wichtig, weil sie beeinflusst, wie Postgres Wörter behandelt. Mit der richtigen Konfiguration können „running“ und „run“ übereinstimmen (Stemming) und Füllwörter ignoriert werden (Stopwörter). Mit der falschen Konfiguration kann die Suche kaputt wirken, weil normale Nutzerformulierungen nicht zum Index passen.
Prefix-Matching ist das Feature, das Leute für „typeahead-ähnliches“ Verhalten nutzen, z. B. „dev“ zu „developer“ matchen. In Postgres FTS wird das typischerweise mit einem Präfix-Operator gemacht (z. B. term:*). Es kann die gefühlte Qualität verbessern, erhöht aber oft die Arbeit pro Query, daher als optionales Upgrade behandeln, nicht als Default.
Was Postgres nicht sein will: eine komplette Suchplattform mit jedem Feature. Wenn du fuzzy spelling correction, fortgeschrittenes Autocomplete, Learning-to-Rank, komplexe Analyzer pro Feld oder verteiltes Indexing über viele Knoten brauchst, liegst du außerhalb der Komfortzone. Für viele Apps liefert PostgreSQL Volltextsuche jedoch das meiste, was Nutzer erwarten, mit deutlich weniger beweglichen Teilen.
Hier eine kleine, realistische Struktur für Inhalte, die du durchsuchen möchtest:
-- Minimal example table
CREATE TABLE articles (
id bigserial PRIMARY KEY,
title text NOT NULL,
body text NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now()
);
Eine gute Basis für PostgreSQL FTS ist: baue eine Query aus dem, was der Nutzer eingegeben hat, filtere Zeilen zuerst (wenn möglich), und ranke dann die verbleibenden Treffer.
-- $1 = user search text, $2 = limit, $3 = offset
WITH q AS (
SELECT websearch_to_tsquery('english', $1) AS query
)
SELECT
a.id,
a.title,
a.updated_at,
ts_rank_cd(
setweight(to_tsvector('english', coalesce(a.title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(a.body, '')), 'B'),
q.query
) AS rank
FROM articles a
CROSS JOIN q
WHERE
a.updated_at >= now() - interval '2 years' -- example safe filter
AND (
setweight(to_tsvector('english', coalesce(a.title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(a.body, '')), 'B')
) @@ q.query
ORDER BY rank DESC, a.updated_at DESC, a.id DESC
LIMIT $2 OFFSET $3;
Ein paar Details, die später Zeit sparen:
WHERE, bevor du rankst (Status, tenant_id, Datumsbereiche). Du rankst weniger Zeilen, also bleibt es schnell.ORDER BY hinzu (wie updated_at, dann id). Das hält die Paginierung stabil, wenn viele Ergebnisse denselben Rank haben.websearch_to_tsquery für Nutzereingaben. Es behandelt Anführungszeichen und einfache Operatoren so, wie Nutzer es erwarten.Wenn diese Basis funktioniert, verschiebe den to_tsvector(...)-Ausdruck in eine gespeicherte Spalte. Das vermeidet Neu-Berechnungen bei jeder Query und macht das Indexieren unkomplizierter.
Die meisten „PostgreSQL Volltextsuche ist langsam“-Geschichten lassen sich auf eine Sache zurückführen: die DB baut das Suchdokument bei jeder Abfrage neu. Behebe das zuerst, indem du ein vorgebautes tsvector speicherst und indexierst.
tsvector speichern: generierte Spalte oder Trigger?Eine generierte Spalte ist die einfachste Option, wenn dein Suchdokument aus Spalten derselben Zeile besteht. Sie bleibt automatisch korrekt und ist schwer zu vergessen bei Updates.
Nutze einen trigger-gepflegten tsvector, wenn das Dokument von verwandten Tabellen abhängt (z. B. eine Produktzeile plus Kategoriename) oder wenn du Logik willst, die sich nicht als einzelne generierte Expression ausdrücken lässt. Trigger fügen bewegliche Teile hinzu, also halte sie klein und teste sie gut.
Erzeuge einen GIN-Index auf der tsvector-Spalte. Das ist die Basis, die PostgreSQL Volltextsuche für typische App-Suchen instant wirken lässt.
Ein Setup, das für viele Apps funktioniert:
tsvector in derselben Tabelle wie die oft gesuchten Zeilen.tsvector.@@ gegen das gespeicherte tsvector verwendet, nicht to_tsvector(...) on-the-fly.VACUUM (ANALYZE) in Betracht, damit der Planner den neuen Index versteht.Das Vector in derselben Tabelle zu halten ist meist schneller und einfacher. Eine separate Such-Tabelle kann Sinn machen, wenn die Basistabelle sehr schreibintensiv ist oder du ein kombiniertes Dokument über viele Tabellen indexierst und es nach eigenem Zeitplan updaten willst.
Partielle Indizes helfen, wenn du nur eine Teilmenge der Zeilen durchsuchst, z. B. status = 'active', ein einzelner Mandant in einer Multi-Tenant-App oder eine bestimmte Sprache. Sie reduzieren die Indexgröße und können Suchen beschleunigen, aber nur wenn deine Queries immer denselben Filter enthalten.
Mit einfachen, vorhersehbaren Regeln lässt sich überraschend gute Relevanz erzielen.
Der leichteste Gewinn ist Feldgewichtung: Treffer im Titel sollten mehr zählen als Treffer tief im Body. Baue ein kombiniertes tsvector, bei dem der Titel höher gewichtet ist als die Beschreibung, und ranke mit ts_rank oder ts_rank_cd.
Wenn du möchtest, dass „frische“ oder „populäre“ Items nach oben schwimmen, mach das vorsichtig. Ein kleiner Bonus ist okay, aber lass ihn nicht die Textrelevanz übersteuern. Ein praktisches Muster: zuerst nach Text ranken, dann bei Gleichstand mit Freshness entscheiden, oder einen gedeckten Bonus hinzufügen, damit ein irrelevantes neues Item keinen älteren perfekten Treffer schlägt.
Synonyme und Phrasenmatching sind Bereiche, in denen Erwartungen oft auseinandergehen. Synonyme sind nicht automatisch vorhanden; du bekommst sie nur, wenn du ein Thesaurus- oder benutzerdefiniertes Wörterbuch hinzufügst oder die Query selbst erweiterst (z. B. „auth“ als „authentication“ behandeln). Phrasenmatching ist ebenfalls nicht Standard: einfache Queries matchen Wörter überall, nicht „genau diese Phrase“. Wenn Nutzer in Anführungszeichen suchen oder lange Fragen tippen, ziehe phraseto_tsquery oder websearch_to_tsquery in Betracht.
Gemischte Sprachen brauchen eine Entscheidung. Wenn du die Sprache pro Dokument kennst, speichere sie und generiere das tsvector mit der passenden Konfiguration (English, Russian, etc.). Wenn nicht, ist ein sicherer Fallback das simple-Konfigurations-Indexieren (kein Stemming) oder zwei Vektoren: einer sprachspezifisch, wenn bekannt, und einer simple für alles.
Um Relevanz zu validieren, halte es klein und konkret:
Das reicht meist für Postgres FTS in Suchfeldern wie „Templates“, „Docs“ oder „Projects".
Die meisten Probleme mit „PostgreSQL FTS ist langsam oder irrelevant“ stammen aus ein paar vermeidbaren Fehlern. Sie zu beheben ist meist einfacher als eine neue Suchlösung einzuführen.
Eine Falle ist, tsvector wie einen berechneten Wert zu behandeln, der von selbst korrekt bleibt. Wenn du tsvector speicherst, es aber nicht bei jedem Insert/Update aktualisierst, wirken die Ergebnisse zufällig, weil der Index nicht mehr zum Text passt. Wenn du to_tsvector(...) on-the-fly in der Query berechnest, sind die Ergebnisse zwar korrekt, aber langsamer, und du nutzt nicht den Vorteil eines dedizierten Index.
Ein weiterer Weg, Performance zu verschlechtern, ist Ranking durchzuführen, bevor du die Kandidatenmenge eingrenzt. ts_rank ist nützlich, sollte aber normalerweise erst laufen, nachdem Postgres den Index genutzt hat, um passende Zeilen zu finden. Wenn du Ranking über einen großen Teil der Tabelle laufen lässt (oder zuerst aufwendige Joins machst), verwandelst du eine schnelle Suche in einen Tablescan.
Viele erwarten, dass „contains“-Suche wie LIKE '%term%' funktioniert. Leading-Wildcards passen nicht gut zu FTS, weil FTS auf Wörtern (Lexemen) basiert, nicht auf beliebigen Substrings. Wenn du Substring-Suche für Produktcodes oder partielle IDs brauchst, nutze dafür ein anderes Werkzeug (z. B. Trigram-Indizes).
Performance-Probleme entstehen oft in der Ergebnisverarbeitung, nicht im Matching. Zwei Muster sind kritisch:
OFFSET-Paginierung, die Postgres dazu zwingt, immer mehr Zeilen zu überspringen.Betriebliche Aspekte zählen auch. Index-Bloat kann nach vielen Updates entstehen, und Reindexing kann teuer werden, wenn du wartest, bis es schmerzhaft ist. Messe echte Query-Zeiten (und prüfe EXPLAIN ANALYZE) vor und nach Änderungen. Ohne Zahlen ist es leicht, PostgreSQL FTS an einer Stelle „zu reparieren“ und es an einer anderen schlechter zu machen.
Bevor du PostgreSQL FTS beschuldigst, führe diese Checks aus. Die meisten Probleme kommen von fehlenden Basics, nicht vom Feature selbst.
Baue ein echtes tsvector: speichere es in einer generierten oder gepflegten Spalte (nicht bei jeder Query neu berechnen), nutze die richtige Sprachkonfiguration (english, simple, etc.) und wende Gewichtungen an, wenn du Felder mischst (title > subtitle > body).
Normalisiere, was du indexierst: halte laute Felder (IDs, Boilerplate, Navigations-Text) aus dem tsvector fern und kürze große Blobs, wenn Nutzer sie nie durchsuchen.
Erzeuge den richtigen Index: füge einen GIN-Index auf der tsvector-Spalte hinzu und bestätige mit EXPLAIN, dass er genutzt wird. Wenn nur ein Subset durchsuchbar ist (z. B. status = 'published'), kann ein partieller Index Größe und Lesezeiten reduzieren.
Halte Tabellen gesund: Dead-Tuples können Index-Scans verlangsamen. Regelmäßiges Vacuuming ist wichtig, besonders bei häufig aktualisierten Inhalten.
Habe einen Reindex-Plan: große Migrationen oder aufgeblähte Indizes brauchen manchmal ein kontrolliertes Reindex-Fenster.
Sobald Daten und Index stimmen, konzentriere dich auf die Query-Form. PostgreSQL FTS ist schnell, wenn es die Kandidatenmenge früh einschränken kann.
Zuerst filtern, dann ranken: wende strikte Filter (Tenant, Sprache, veröffentlicht, Kategorie) vor dem Ranking an. Ranking von Tausenden Zeilen, die du später verwerfen wirst, ist verschwendete Arbeit.
Nutze stabile Sortierung: order by rank und dann einen Tie-Breaker wie updated_at oder id, damit Ergebnisse nicht bei jedem Refresh springen.
Vermeide „die Query macht alles“: wenn du fuzzy matching oder Tippfehlertoleranz brauchst, mach es bewusst (und messe). Erzwinge keine sequentiellen Scans accidental.
Teste reale Queries: sammle die Top-20-Suchen, prüfe Relevanz von Hand und halte eine kleine Liste erwarteter Ergebnisse, um Regressionen zu erkennen.
Überwache langsame Pfade: logge langsame Queries, prüfe EXPLAIN (ANALYZE, BUFFERS) und beobachte Indexgröße und Cache-Hitrate, damit du siehst, wann Wachstum das Verhalten ändert.
Ein SaaS-Helpcenter ist ein guter Startpunkt, weil das Ziel einfach ist: den einen Artikel finden, der die Frage beantwortet. Du hast ein paar tausend Artikel mit Titel, kurzer Zusammenfassung und Body. Die meisten Besucher tippen 2–5 Wörter wie „reset password“ oder „billing invoice".
Mit PostgreSQL FTS kann das überraschend schnell fertig sein. Du speicherst ein tsvector für die kombinierten Felder, fügst einen GIN-Index hinzu und rankst nach Relevanz. Erfolg sieht so aus: Ergebnisse erscheinen in unter 100 ms, die Top-3 sind meist korrekt und du musst das System nicht ständig betreuen.
Dann wächst das Produkt. Support will nach Produktbereich, Plattform (web, iOS, Android) und Plan (free, pro, business) filtern. Autoren wollen Synonyme, „Meintest du“ und bessere Tippfehlerbehandlung. Marketing will Analytics wie „Top-Suchen mit null Ergebnissen“. Traffic steigt und Suche wird zu einem der meistgenutzten Endpunkte.
Das sind Signale, dass eine dedizierte Suchmaschine die Kosten wert sein könnte:
Ein praktikabler Migrationspfad ist, Postgres als Quelle der Wahrheit zu behalten, auch nachdem du eine Suchengine hinzufügst. Fang damit an, Suchanfragen und No-Result-Fälle zu loggen, und betreibe einen asynchronen Sync-Job, der nur die durchsuchbaren Felder in den neuen Index kopiert. Lauf beide Systeme eine Weile parallel und wechsle schrittweise, statt am ersten Tag alles zu riskieren.
Wenn deine Suche hauptsächlich „Dokumente finden, die diese Worte enthalten“ ist und dein Datensatz nicht massiv, reicht PostgreSQL FTS meist aus. Starte dort, bring es ans Laufen und füge eine dedizierte Engine nur hinzu, wenn du ein klares Defizit oder Skalierungsproblem benennen kannst.
Ein Recap zum Merken:
tsvector speichern, einen GIN-Index hinzufügen und einfache Ranking-Anforderungen hast.Ein praktischer nächster Schritt: implementiere die Starter-Query und das Index-Setup aus den vorigen Abschnitten und logge ein paar einfache Metriken für eine Woche. Tracke p95-Query-Zeit, langsame Queries und ein grobes Erfolgssignal wie „Suche -> Klick -> kein sofortiges Bounce“ (selbst ein einfacher Event-Counter hilft). Du wirst schnell sehen, ob du bessere Relevanz oder nur bessere UX (Filter, Hervorhebungen, bessere Snippets) brauchst.
Wenn du schnell auf der App-Seite vorankommen willst, kann Koder.ai (koder.ai) praktisch sein, um die Such-UI und API per Chat zu prototypen und dann sicher mit Snapshots und Rollback zu iterieren, während du misst, wie Nutzer tatsächlich handeln.
PostgreSQL Volltextsuche ist „ausreichend“, wenn du drei Dinge gleichzeitig erreichst:
Wenn du das mit einem gespeicherten tsvector + einem GIN-Index erreichst, bist du meist in einer guten Lage.
Starte standardmäßig mit PostgreSQL Volltextsuche. Sie ist schneller ausgeliefert, hält Daten und Suche am selben Ort und vermeidet das Aufbauen und Betreiben einer separaten Index-Pipeline.
Wechsle zu einer dedizierten Suchmaschine, wenn du eine klare Anforderung hast, die Postgres nicht gut erfüllt (hochwertige Fehlertoleranz bei Tippfehlern, reichhaltige Autocomplete-Funktionen, umfangreiche Facetten oder Suchlast, die mit der primären DB konkurriert).
Eine einfache Regel: bleib bei Postgres, wenn du diese drei Checks bestehst:
Wenn du einen dieser Punkte stark verfehlst (insbesondere Tippfehler/Autocomplete oder hohe Suchlast), ziehe eine dedizierte Engine in Betracht.
Nutze Postgres FTS, wenn die Suche hauptsächlich „den richtigen Datensatz finden“ ist über ein paar Felder wie Titel/Body/Notizen mit einfachen Filtern (Tenant, Status, Kategorie).
Gut passend für Helpcenter, interne Docs, Tickets, Blog-/Artikel-Suche und SaaS-Dashboards, wo nach Projektnamen und Notizen gesucht wird.
Eine gute Basissuche:
websearch_to_tsquery.Speichere ein vorgebautes tsvector und füge einen GIN-Index hinzu. So vermeidest du das ständige Neuberechnen von to_tsvector(...) bei jeder Anfrage.
Praktische Einrichtung:
Verwende eine generierte Spalte, wenn das Suchdokument aus Spalten derselben Zeile entsteht (einfach und zuverlässig).
Nutze eine Trigger-gepflegte Spalte, wenn der Text von verknüpften Tabellen abhängt oder du komplexe Logik brauchst.
Standard: zuerst generierte Spalte, Trigger nur wenn wirklich nötig.
Beginne mit vorhersehbarer Relevanz:
Validiere mit einer kleinen Liste realer Nutzeranfragen und erwarteter Top-Ergebnisse.
FTS arbeitet wortbasiert, nicht substring-basiert. Es verhält sich daher nicht wie LIKE '%term%' für beliebige Teilstrings.
Wenn du Substring-Suche (IDs, Codes, Fragment-Suche) brauchst, nutze dafür andere Werkzeuge (z. B. Trigram-Index), statt FTS dafür zu missbrauchen.
Signale, dass Postgres FTS nicht mehr ausreicht:
Praktischer Weg: Postgres als Quelle der Wahrheit behalten und asynchron in die neue Engine indexieren, wenn die Anforderung klar wird.
@@ gegen einen gespeicherten tsvector.ts_rank/ts_rank_cd plus einem stabilen Tie-Breaker wie updated_at, id.Das hält Ergebnisse relevant, schnell und stabil für die Paginierung.
tsvector in der gleichen Tabelle wie die abgefragten Zeilen ab.tsvector_spalte @@ tsquery verwendet.Das ist die häufigste Korrektur, wenn Suche langsam wirkt.