DanLevy.net

Leitfaden zur Postgres-Textsuche 2026

Die Suchwerkzeuge, die bereits in deiner Datenbank stecken – und wann sich jedes einzelne lohnt.

Die meisten Teams nutzen nur ein einziges Postgres-Suchwerkzeug. Teams, die alle drei kennen, liefern bessere Suchergebnisse mit weniger Komplexität – und vermeiden den kostspieligen Umweg zu einem dedizierten Suchdienst, den sie noch gar nicht brauchten.

Dieser Leitfaden behandelt die gesamte Palette Postgres-nativer Optionen: was jede tut, wann sie die richtige Wahl ist und wie man sie kombiniert.


Die drei Werkzeuge

Volltextsuche (tsvector / GIN-Index) ist lexikalisch. Sie zerlegt Text in Lexeme, reduziert sie auf ihren Wortstamm und gleicht Abfragen mit dem Index ab. »Running« und »runs« fallen auf dasselbe Lexem. Ebenso »dog« und »dogs«. Die Ranking-Funktion (ts_rank) belohnt Dokumente, in denen Suchbegriffe häufig oder prominent vorkommen.

Trigramme (pg_trgm) zerlegen Zeichenketten in überlappende 3-Zeichen-Abschnitte und messen, wie viele Abschnitte zwei Zeichenketten gemeinsam haben. »Dan« → " da", "dan", "an ". »Micheal« und »Michael« teilen die meisten ihrer Trigramme, die Ähnlichkeit ist also hoch. Das macht pg_trgm exzellent für unscharfe Namenssuche, Tippfehlertoleranz und Autovervollständigung – den Bereich, in dem die Volltextsuche schwächelt.

Exakt-Match-Indizes (B-Baum, Hash) behandeln Primärschlüssel, E-Mail-Adressen, IDs, Artikelnummern und alles, bei dem die Antwort binär ist: Es passt oder es passt nicht. Sie fühlen sich nicht wie »Suche« an, gehören aber in diese Betrachtung, weil das schlechteste Muster darin besteht, unscharfe oder semantische Suche für Probleme einzusetzen, die richtige Antworten haben.

Die Wahl geht nicht um ausgefeiltere Technik. Es geht darum, das Werkzeug an die Form der Abfrage anzupassen.

Postgres-Suchwerkzeug-MatrixEin Vergleich von pg_trgm, Volltextsuche, pgvector und hybrider Suche nach Eingabeform und Abfrageintention.Such-Primitive nach Eingabeform wählenDieselbe Postgres-Tabelle kann alle vier unterstützen. Der Trick ist, die Abfrage auf den Text abzustimmen.Exakte Wörter zählenBedeutung zähltKurzer / strukturierter TextLange Prosa / Textblöckeunscharfpg_trgmNamen, Adressen, Titel, Tippfehler,Autovervollständigung, Teilzeichenfolgen.Orthografische Ähnlichkeit: Schreibabstand.ähnlichpgvectorVerwandte Einträge, doppelte Tickets,Empfehlungen aus Kurzbeschreibungen.Embedding-Ähnlichkeit: Bedeutungsabstand.lexikalischVolltextsucheArtikel, Doku, Logs, Support-Inhalte,bei denen Suchwörter vorkommen sollen.Lexeme, Wortstämme, Ranking, boolesche Filter.hybridFTS + pgvectorTechnische Doku und RAG, bei denen Nutzerkonzeptionelle Fragen plus exakte Symbole stellen.Beide ausführen, Ränge mit RRF fusionieren.Beginne mit der Abfrageintention, prüfe dann die Textform
Die vier Postgres-Such-Primitive, aufgetragen nach Abfrageintention (exakt vs. semantisch) und Textform (strukturiert vs. Prosa). Dieselbe Tabelle kann alle vier Indizes tragen – die Wahl erfolgt pro Abfrage, nicht pro Tabelle.

Wann die Volltextsuche gewinnt

Suche nach Schlüsselwörtern in Prosa. Blogbeiträge, Dokumentation, Produktbeschreibungen, Support-Tickets, Rechtsdokumente. Die Volltextsuche wurde für diese Art von Inhalt entwickelt: indizierte, bewertete Suche über natürlichsprachlichen Text.

Schlüsselwortbasierte Nutzerabfragen. Nutzer geben einen Suchbegriff ein, filtern nach Schlagwort oder stöbern nach Kategorie. Die Volltextsuche bewältigt diese Intention nativ ohne Embedding-Infrastruktur.

Bewertete Ergebnisse ohne externe Abhängigkeiten. Volltext-Indizes sind schnell, deterministisch und benötigen keine API-Aufrufe. Das Relevanzsignal stammt aus der Termhäufigkeit, gewichtet nach Feldposition.

Boolesche Filterung parallel zur Suche. Die Volltextsuche fügt sich natürlich in bestehende Abfragelogik ein:

SELECT * FROM posts
WHERE search_vector @@ to_tsquery('english', 'postgres & performance')
AND category = 'tutorial'
AND published_at > NOW() - INTERVAL '6 months';

Volltextsuche einrichten

-- Die generierte Spalte hält den Index automatisch aktuell
ALTER TABLE posts ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(body, '')), 'B')
) STORED;
CREATE INDEX posts_search_idx ON posts USING GIN (search_vector);
-- Abfrage
SELECT title, ts_rank(search_vector, query) AS rank
FROM posts, to_tsquery('english', 'postgres & performance') query
WHERE search_vector @@ query
ORDER BY rank DESC
LIMIT 10;

setweight weist Gewichtungen zu: A (Titel) rangiert vor B (Textkörper). Das ist das gesamte Relevanzmodell für die meisten Content-Such-Anwendungsfälle.

Was die Volltextsuche nicht gut kann


Wann Trigramme gewinnen (pg_trgm)

pg_trgm deckt die unangenehme Mitte ab, die der Volltextsuche konsequent entgleitet.

Die Volltextsuche zerlegt Text in Lexeme und leitet Wortstämme ab. Bei Prosa ist das richtig. Bei Namen und kurzen Identifikatoren oft nicht:

pg_trgm ist zudem sprachunabhängig, was für Namen aus verschiedenen Sprachräumen wichtig ist. Die Volltextsuche benötigt eine Wörterbuchkonfiguration pro Sprache.

Unscharfe Namenssuche

CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX users_name_trgm_idx ON users USING GIN (name gin_trgm_ops);
-- Findet "Micheal Jordan" bei der Suche nach "Michael Jordan"
SELECT id, name, similarity(name, $1) AS score
FROM users
WHERE name % $1 -- %-Operator = Ähnlichkeitsschwelle (Standard 0.3)
ORDER BY score DESC
LIMIT 10;

Der %-Operator verwendet pg_trgm.similarity_threshold (Standard 0.3, Bereich 0–1). Für die Namenssuche fängt 0.3–0.4 Tippfehler ab, während das Rauschen niedrig bleibt.

Autovervollständigung, Präfix- und Enthaltensein-Suche

-- Präfix-Matching für Autovervollständigung. Ein Trigramm-GIN-Index kann helfen,
-- aber ein B-Baum-Pattern-Index ist für rein linksverankerte Präfixe oft besser.
SELECT name FROM users
WHERE name ILIKE $1 || '%'
ORDER BY name
LIMIT 10;
-- word_similarity für Teiltreffer innerhalb längerer Zeichenketten
-- ("Johnson" in "Andrew Johnson III")
SELECT id, name, word_similarity($1, name) AS score
FROM users
WHERE $1 <% name
ORDER BY score DESC
LIMIT 10;

Der Trigramm-GIN-Index ist besonders nützlich für ILIKE '%muster%'-Enthaltensein-Abfragen und tippfehlertolerantes Matching – Muster, die ohne Trigramm-Index üblicherweise Volltabellenscans erfordern.

Wann pg_trgm statt Volltextsuche

SzenarioEinsatz
Personen-/Firmennamensuche mit Tippfehlernpg_trgm
Autovervollständigung / Präfixsuchepg_trgm (oder Volltextsuche mit Präfixabfragen)
Kurze Zeichenketten, Identifikatoren, Codespg_trgm
Prosa-Artikel, Dokumentation, TicketsVolltextsuche
Logmeldungen nach SchlüsselwörternVolltextsuche
Mehrsprachige Namenssuchepg_trgm (sprachunabhängig)

Wann exakte SQL-Suche gewinnt

Manche »Such«-Probleme sind gar keine Suche.

»Finde den Benutzer mit der E-Mail dan@example.com« ist ein Gleichheitscheck. »Finde Bestellung ORD-12345« ist ein Primärschlüssel-Zugriff. »Liste Beiträge in der Kategorie tutorial sortiert nach Datum« ist eine gefilterte Abfrage. Diese gehören auf B-Baum- oder Hash-Indizes.

Die Volltextsuche oder Trigramme hier einzusetzen, erhöht die Komplexität, ohne die Korrektheit zu verbessern – und bei exakten Identifikatoren ist eine Beinahe-Übereinstimmung schlimmer als keine.

CREATE INDEX users_email_idx ON users (email);
-- Exakte Suche: schnell und eindeutig
SELECT id, name FROM users WHERE email = $1;

Die grundsätzliche Lektion: Unscharfe Suche für Probleme mit richtigen Antworten ist ein Kategorienfehler. Sie liefert irgendetwas – das möglicherweise mit falscher Sicherheit daherkommt.


Diese Werkzeuge kombinieren

Diese Werkzeuge lassen sich sauber kombinieren. Man muss sich nicht für genau eines entscheiden.

Volltextsuche + pg_trgm für ein Suchfeld, das Tippfehler in Schlüsselwörtern toleriert:

-- Trigramm-Ähnlichkeit im Titel fängt Tippfehler ab; ts_rank behandelt die Textkörper-Relevanz
SELECT id, title,
ts_rank(search_vector, to_tsquery('simple', $1)) AS fts_rank,
similarity(title, $1) AS trgm_score
FROM posts
WHERE search_vector @@ to_tsquery('simple', $1)
OR title % $1
ORDER BY (ts_rank(search_vector, to_tsquery('simple', $1)) + similarity(title, $1)) DESC
LIMIT 10;

Volltextsuche + unaccent für internationale Inhalte:

-- Entfernt diakritische Zeichen, sodass "José" zu "Jose" passt
CREATE EXTENSION IF NOT EXISTS unaccent;
CREATE TEXT SEARCH CONFIGURATION public.simple_unaccent (COPY = pg_catalog.simple);
ALTER TEXT SEARCH CONFIGURATION public.simple_unaccent
ALTER MAPPING FOR hword, hword_part, word
WITH unaccent, simple;
ALTER TABLE posts ADD COLUMN search_vector tsvector;
CREATE TRIGGER posts_search_vector_refresh
BEFORE INSERT OR UPDATE OF title, body ON posts
FOR EACH ROW EXECUTE FUNCTION
tsvector_update_trigger(search_vector, 'public.simple_unaccent', title, body);

unaccent + pg_trgm für internationale Namenssuche:

ALTER TABLE users ADD COLUMN name_search text;
CREATE FUNCTION users_name_search_refresh()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
NEW.name_search := unaccent(coalesce(NEW.name, ''));
RETURN NEW;
END;
$$;
CREATE TRIGGER users_name_search_refresh
BEFORE INSERT OR UPDATE OF name ON users
FOR EACH ROW EXECUTE FUNCTION users_name_search_refresh();
CREATE INDEX users_name_search_trgm_idx
ON users USING GIN (name_search gin_trgm_ops);
SELECT id, name
FROM users
WHERE name_search % unaccent($1)
ORDER BY similarity(name_search, unaccent($1)) DESC
LIMIT 10;

Die Trigger-Beispiele vermeiden die Verwendung von unaccent() in Generated-Column- oder Index-Ausdrücken, wo die Unveränderlichkeitsregeln von PostgreSQL eine Rolle spielen. Wenn du unaccent() in eine eigene unveränderliche Funktion kapselst, dokumentiere, dass du Upgrade-/Konfigurationsrisiken in Kauf nimmst.


Bemerkenswerte Erweiterungen

pg_trgm ist bei den meisten Postgres-Distributionen enthalten, muss aber explizit aktiviert werden. Die Grundlage für unscharfen Zeichenkettenabgleich in Postgres.

unaccent entfernt diakritische Zeichen vor der Indizierung und Abfrage. Passt gut zu pg_trgm und der Volltextsuche für europäischsprachige Inhalte. Bei Postgres enthalten.

pg_bigm erweitert den Trigramm-Ansatz auf Bigramme (2-Zeichen-Abschnitte), was die Ergebnisse für CJK-Sprachen (Chinesisch, Japanisch, Koreanisch) deutlich verbessert, bei denen pg_trgm schwächelt. Muss separat installiert werden; nicht enthalten.

pg_search (von ParadeDB) ersetzt den standardmäßigen GIN/tsvector-Stack durch einen Tantivy-basierten BM25-Index. Das liefert BM25-Bewertung (oft besser als ts_rank), unscharfes Matching innerhalb von Volltextabfragen, Facettensuche und deutlich schnellere Indizierung bei großen Tabellen. Es ist ein nahtloser Upgrade-Pfad, wenn die Standard-Volltextsuche an Ranking- oder Leistungsgrenzen stößt.

-- pg_search: BM25-Volltextsuche mit unscharfem Matching
CREATE INDEX posts_bm25_idx ON posts
USING bm25 (id, title, body)
WITH (key_field = 'id', text_fields = '{"title": {}, "body": {}}');
-- Abfrage mit BM25-Bewertung + unscharfem Matching (findet "javascipt")
SELECT id, title, paradedb.score(id) AS rank
FROM posts
WHERE posts @@@ paradedb.fuzzy_phrase(field => 'title', value => 'postgres performnce')
ORDER BY rank DESC
LIMIT 10;

pgvector ergänzt dichte Vektorspeicherung und Ähnlichkeitssuche. Es ist das richtige Werkzeug, wenn Nutzer beschreiben, was sie wollen, statt es zu benennen – semantische Suche, RAG, Empfehlungen für verwandte Inhalte, mehrsprachige Abfragen. Ausführlich behandelt in Semantische Vektorsuche und Hybridstrategien.


Entscheidungstabelle

Was du suchstEmpfohlen
Prosa-Artikel, Doku, TicketsVolltextsuche
Personen-/Firmennamen mit Tippfehlernpg_trgm
Autovervollständigung, Präfixsuchepg_trgm
Kurze Codes, Identifikatorenpg_trgm
Logmeldungen nach SchlüsselwörternVolltextsuche
Internationale Namenpg_trgm + unaccent
Große Inhalte, besseres Rankingpg_search (ParadeDB BM25)
Primärschlüssel, exakte E-Mails, IDsB-Baum-Index
Datumsangaben, Bereiche, sortierte ListenB-Baum-Index
Berechtigungen, Kategorien, FilterGewöhnliche WHERE-Klausel
Fragen, Paraphrasen, Konzeptepgvector (siehe nächster Artikel)

Im Zweifel: Kurze Zeichenketten mit Schreibabweichungen → Trigramme. Lange Prosa für Schlüsselwortabfragen → Volltextsuche. Strukturierte Identifikatoren → normale Indizes. Konzeptionelle oder natürlichsprachliche Abfragen → pgvector.


Hybride Suche: Zwei Signale, ein Rang

Wenn eine Abfrage wie "withRetry timeout errors" in ein Suchfeld eingegeben wird, trägt sie zwei Arten von Intention: exakte Symbolnamen, die der Nutzer kennt (withRetry), und eine konzeptionelle Beschreibung (timeout errors). Kein einzelnes Primitive deckt beides ab. Die parallele Ausführung von Volltextsuche und Vektorsuche – und anschließende Zusammenführung ihrer Ranglisten mit Reciprocal Rank Fusion – schon.

RRF bewertet jedes Ergebnis mit 1 / (60 + Rang) in jeder Liste und summiert über die Listen hinweg. Die Konstante 60 dämpft den Vorteil der vorderen Ränge, sodass ein Ergebnis, das in beiden Listen Zweiter wird, ein Ergebnis schlagen kann, das eine Liste gewinnt und die andere komplett verfehlt. Entscheidend: RRF mittelt niemals Rohwerte über Methoden hinweg – Volltext-Rang und Cosinus-Distanz sind unterschiedliche Währungen und können nicht arithmetisch kombiniert werden.

Hybride Suche mit Reciprocal Rank FusionEine Abfrage wird an Volltextsuche und Vektorsuche verteilt, beide erzeugen Ranglisten, und Reciprocal Rank Fusion kombiniert sie zu einer Ergebnisliste.Hybride Suche: zwei ehrliche Signale, dann ein fusionierter RangMittle niemals Rohwerte. Volltext-Rang und Cosinus-Distanz sind unterschiedliche Währungen.Nutzerabfrage”withRetrytimeout errors”FTS / BM25Exakte Symbole und Wörter1. API-Referenz2. Retry-LeitfadenpgvectorKonzeptionelle Nachbarn1. Netzwerkausfälle2. Retry-LeitfadenRRF-FusionGib jedem Ergebnis Anerkennungfür seinen Rang in jeder Liste.1 / (60 + Rang)EndergebnisseDer Top-Treffer ist, wo exakte Begriffeund semantische Bedeutung übereinstimmen.
Eine Abfrage wird parallel an Volltextsuche und pgvector verteilt. Jede erzeugt ihre eigene Rangliste. RRF bewertet jedes Dokument nach seiner Position in jeder Liste und summiert die Werte – das Ergebnis zeigt Dokumente, auf die sich beide Signale einigen.
-- Hybride Suche: Volltextsuche + pgvector fusioniert mit RRF
WITH fts AS (
SELECT id, ts_rank(search_vector, query) AS score,
ROW_NUMBER() OVER (ORDER BY ts_rank(search_vector, query) DESC) AS rank
FROM docs, to_tsquery('english', 'withRetry & timeout') query
WHERE search_vector @@ query
LIMIT 60
),
vec AS (
SELECT id,
ROW_NUMBER() OVER (ORDER BY embedding <=> $embedding) AS rank
FROM docs
ORDER BY embedding <=> $embedding
LIMIT 60
)
SELECT COALESCE(fts.id, vec.id) AS id,
(COALESCE(1.0 / (60 + fts.rank), 0) +
COALESCE(1.0 / (60 + vec.rank), 0)) AS rrf_score
FROM fts FULL JOIN vec ON fts.id = vec.id
ORDER BY rrf_score DESC
LIMIT 10;

Der 60-Dokumente-Kandidatenpool pro Zweig (LIMIT 60) ist ein üblicher Ausgangspunkt. Erweitere ihn bei geringer Trefferquote; verkleinere ihn für Geschwindigkeit.


Wie es weitergeht

Die Postgres-Textsuche deckt viel ab, aber sie hat eine Obergrenze. Wenn Nutzer beschreiben, was sie wollen, statt es zu benennen – »etwas, das mir auf einem Flug beim Schlafen hilft«, »Artikel über Debugging-Selbstvertrauen als Junior-Entwickler« – versagen sowohl die lexikalische als auch die Trigramm-Suche.

Das ist das Terrain von Vektor-Embeddings, semantischer Suche und hybriden Architekturen. Behandelt in Semantische Vektorsuche und Hybridstrategien.