PostgreSQL-Volltextsuche vs. Elasticsearch – wann die Datenbank reicht
Suchfunktionen in PostgreSQL mit pg_trgm, tsvector und Fuzzystrmatch – und wann Elasticsearch oder OpenSearch die bessere Wahl ist. Mit Code-Beispielen und Entscheidungsmatrix.
PostgreSQL-Volltextsuche vs. Elasticsearch – wann die Datenbank reicht
Suchfunktionen sind aus modernen Webanwendungen nicht wegzudenken. Die Frage ist nur: Zieht man eine dedizierte Suchmaschine wie Elasticsearch oder OpenSearch, oder reichen die Bordmittel der bereits verwendeten PostgreSQL-Datenbank?
Die Antwort hängt stark von den Anforderungen ab. Dieser Artikel zeigt, was PostgreSQL mit tsvector, pg_trgm, fuzzystrmatch und pgvector kann, wo die Grenzen liegen und wann ein dedizierter Suchserver die richtige Wahl ist.
PostgreSQL-Bordmittel für die Suche
PostgreSQL bringt drei leistungsstarke Erweiterungen für verschiedene Suchszenarien mit, die in der Standardinstallation verfügbar sind – kein zusätzlicher Dienst, keine Betriebskomplexität.
1. Volltextsuche mit tsvector / tsquery
Die Volltextsuche arbeitet auf Wortebene: Sie zerlegt Text in Lexeme (Wortstämme) und kann nach Wortformen suchen.
CREATE TABLE artikel (
id SERIAL PRIMARY KEY,
titel TEXT,
inhalt TEXT,
suchvektor TSVECTOR
GENERATED ALWAYS AS (
to_tsvector('german', titel || ' ' || inhalt)
) STORED
);
CREATE INDEX idx_suche ON artikel USING GIN (suchvektor);
INSERT INTO artikel (titel, inhalt) VALUES
('Die Datenbank und ihre Performance',
'Die Performance einer Datenbank hängt von vielen Faktoren ab.'),
('Auto fahren auf der Autobahn',
'Auf der Autobahn mit dem Auto zu fahren macht Spaß.'),
('Hunde laufen im Park',
'Die Hunde laufen jeden Morgen durch den Park.');
-- Volltextsuche: findet Artikel 1 (enthält "Datenbank" UND "Performance")
SELECT titel FROM artikel
WHERE suchvektor @@ to_tsquery('german', 'Datenbank & Performance');
-- to_tsquery('german', 'Datenbank & Performance')
-- → sucht nach Lexemen: 'datenbak' & 'perform'
-- "Datenbank" → Lexem "datenbak" ✓
-- "Performance" → Lexem "perform" ✓
Der tsvector normalisiert Wörter: Aus "läuft", "lief", "gelaufen" wird der gemeinsame Stamm "lauf". Die to_tsquery-Funktion unterstützt & (UND), | (ODER) und ! (NICHT). Der GIN-Index macht die Suche auch auf Millionen Datensätzen schnell.
-- ODER-Suche: findet Artikel mit "Auto" ODER "Hund"
SELECT titel FROM artikel
WHERE suchvektor @@ to_tsquery('german', 'Auto | Hund');
-- NICHT-Suche: findet Artikel mit "Auto", aber NICHT "Autobahn"
SELECT titel FROM artikel
WHERE suchvektor @@ to_tsquery('german', 'Auto & !Autobahn');
Grenzen: Keine Tippfehlertoleranz („Datenbak" findet „Datenbank" nicht), keine Teilwortsuche („auto" findet nicht „Autobahn"), keine phonetische Ähnlichkeit („Meier" ≠ „Meyer").
2. Trigramm-Suche mit pg_trgm
pg_trgm zerlegt Text in Dreierbuchstaben-Gruppen (Trigrams) und ermittelt die Ähnlichkeit über die Überlappung dieser Trigrams.
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX idx_trgm_titel ON artikel USING GIN (titel gin_trgm_ops);
CREATE INDEX idx_trgm_inhalt ON artikel USING GIN (inhalt gin_trgm_ops);
-- Tippfehlertoleranz: "Autobahn" vs "Autoban"
-- similarity('Autobahn', 'Autoban') → 0.78 (78 % Übereinstimmung)
SELECT titel, similarity(titel, 'Autoban') AS aehnlichkeit
FROM artikel
WHERE titel % 'Autoban'
ORDER BY aehnlichkeit DESC;
-- Expliziter Schwellwert (statt default 0,3)
SELECT titel, similarity(titel, 'Autoban') AS aehnlichkeit
FROM artikel
WHERE similarity(titel, 'Autoban') > 0.5
ORDER BY aehnlichkeit DESC;
-- show() zeigt alle Trigrams eines Strings an
SELECT show_trgm('Autobahn');
-- → {" a"," au",aut,bah,hn ,oba,oto,tob,uto}
-- show_limit() und set_limit() steuern den Schwellwert
SELECT set_limit(0.4);
-- Teilwortsuche: ILIKE nutzt den pg_trgm-Index
-- Findet "Auto" auch in "Autobahn" oder "Automatik"
SELECT titel FROM artikel WHERE titel ILIKE '%auto%';
-- Gewichtete Sortierung: pg_trgm + tsvector kombinieren
SELECT titel,
similarity(titel, 'Autoban') AS trigramm_aehnlichkeit,
ts_rank(suchvektor,
to_tsquery('german', 'Auto')) AS volltext_relevanz
FROM artikel
WHERE titel % 'Autoban'
OR suchvektor @@ to_tsquery('german', 'Auto')
ORDER BY trigramm_aehnlichkeit DESC;
Der %-Operator und die similarity()-Funktion arbeiten mit einem Schwellwert (standardmäßig 0,3). pg_trgm liefert echte Tippfehlertoleranz und ermöglicht mit ILIKE auch Teilworttreffer – ohne dass der Text vorher tokenisiert werden muss.
3. Phonetische Suche mit fuzzystrmatch
fuzzystrmatch bietet Algorithmen für die klangbasierte Ähnlichkeitssuche – nützlich für Namenssuche, bei der Schreibvarianten häufig sind.
CREATE EXTENSION IF NOT EXISTS fuzzystrmatch;
-- === Soundex ===
-- Kodiert Wörter nach ihrem Klang (Buchstabe + 3 Ziffern)
SELECT soundex('Meyer'); -- M600
SELECT soundex('Meier'); -- M600
SELECT soundex('Maier'); -- M600
-- Alle drei klingen gleich → gleicher Soundex-Code
SELECT soundex('Meyer') = soundex('Meier'); -- true
SELECT soundex('Schmidt') = soundex('Schmitt'); -- true
-- === Levenshtein ===
-- Minimale Anzahl an Edit-Operationen (Einfügen, Löschen, Ersetzen)
SELECT levenshtein('Hund', 'Hund'); -- 0 (identisch)
SELECT levenshtein('Hund', 'Hunt'); -- 1 (ein Buchstabe vertauscht)
SELECT levenshtein('Hund', 'Hunde'); -- 1 (ein Buchstabe fehlt)
SELECT levenshtein('Schmidt', 'Schmitt'); -- 2 (2 Buchstaben differenz)
-- === Metaphone (deutscher Metaphon) ===
-- Moderner und sprachsensitiver als Soundex
SELECT metaphon('Schmidt'); -- SMT
SELECT metaphon('Schmitt'); -- SMT
SELECT metaphon('Schmidt') = metaphon('Schmitt'); -- true
-- === Praktische Namenssuche ===
CREATE TABLE kunden (
id SERIAL PRIMARY KEY,
vorname TEXT,
nachname TEXT
);
INSERT INTO kunden (vorname, nachname) VALUES
('Thomas', 'Schmidt'),
('Tomas', 'Schmitt'),
('Tom', 'Smith');
-- Finde "Thomas Schmidt" trotz Schreibvarianten
SELECT vorname, nachname,
levenshtein(vorname, 'Thomas') AS vorname_distanz,
levenshtein(nachname, 'Schmidt') AS nachname_distanz,
metaphon(nachname) = metaphon('Schmidt') AS klang_gleich
FROM kunden
WHERE metaphon(nachname) = metaphon('Schmidt')
OR levenshtein(nachname, 'Schmidt') <= 2;
4. Alles kombinieren: Universelle Suchfunktion
Die wahre Stärke von PostgreSQL liegt darin, alle Verfahren in einer Abfrage zu vereinen:
-- Universelle Suchfunktion
CREATE OR REPLACE FUNCTION suche_artikel(suchbegriff TEXT)
RETURNS TABLE(titel TEXT, score FLOAT8) AS $$
BEGIN
RETURN QUERY
SELECT a.titel,
(
-- Volltext-Treffer (höchste Gewichtung)
CASE WHEN a.suchvektor @@ to_tsquery('german',
replace(suchbegriff, ' ', ' & '))
THEN 10 ELSE 0 END
+
-- Trigramm-Ähnlichkeit
COALESCE(similarity(a.titel, suchbegriff) * 5, 0)
+
-- Phonetik (Bonus für Namensähnlichkeit)
CASE WHEN metaphon(a.titel) = metaphon(suchbegriff)
THEN 3 ELSE 0 END
+
-- Kurze Editier-Distanz (max 2 Änderungen)
CASE WHEN levenshtein(a.titel, suchbegriff) <= 2
THEN 2 ELSE 0 END
)::FLOAT8 AS score
FROM artikel a
WHERE
a.suchvektor @@ to_tsquery('german',
replace(suchbegriff, ' ', ' & '))
OR a.titel % suchbegriff
OR metaphon(a.titel) = metaphon(suchbegriff)
OR levenshtein(a.titel, suchbegriff) <= 2
ORDER BY score DESC
LIMIT 10;
END;
$$ LANGUAGE plpgsql;
-- Aufruf mit Tippfehler: findet trotzdem "Autobahn"
SELECT * FROM suche_artikel('Autoban');
-- Aufruf mit phonetischer Variante: findet "Schmidt"
SELECT * FROM suche_artikel('Schmitt');
Eine solche Abfrage deckt Volltext-Treffer, Tippfehler, phonetische Varianten und entfernte Ähnlichkeiten gleichzeitig ab – ohne einen externen Dienst. Der Score-Wert lässt sich beliebig anpassen.
5. pgvector – Semantische Vektor-Suche
pgvector ist eine PostgreSQL-Extension, die Vektor-Ähnlichkeitssuche direkt in der Datenbank ermöglicht. Text, Bilder oder andere Daten werden über Machine-Learning-Modelle (z. B. OpenAI Embeddings, Sentence-Transformers) in numerische Vektoren (Embeddings) umgewandelt. pgvector speichert diese Vektoren als eigenen Datentyp (vector) und kann per ORDER BY über Distanzmetriken wie L2 distance, cosine distance oder inner product die ähnlichsten Einträge finden – unterstützt durch IVFFlat- oder HNSW-Indizes für performante ANN-Suche (Approximate Nearest Neighbor).
Im Gegensatz zu tsvector sucht pgvector nicht nach exakten Wörtern oder Lexemen, sondern nach semantischer Ähnlichkeit: „König" und „Monarch" liegen im Vektorraum nah beieinander, auch wenn sie keine gemeinsamen Wortstämme teilen. Das Verfahren eignet sich für Empfehlungssysteme, semantische Duplikatserkennung, Ähnlichkeitssuche in Produktkatalogen und Retrieval-Augmented Generation (RAG) in KI-Anwendungen.
Bewertung: pgvector macht PostgreSQL zu einem ernsthaften Kandidaten für semantische Suche, die früher nur dedizierten Vektor-Datenbanken vorbehalten war. Die Integration in die bestehende SQL-Umgebung und der Verzicht auf einen zusätzlichen Dienst sind große Vorteile. Allerdings ist die Performance bei sehr großen Vektorkollektionen (>10 Mio. Vektoren) und die Feature-Tiefe (kein hybrides Scoring, kein eingebautes Highlighting) nicht mit Elasticsearch 8.x oder OpenSearch 2.x vergleichbar, die Vektor-Suche nativ mit textuellen Queries kombinieren.
Elasticsearch / OpenSearch
Dedizierte Suchserver wie Elasticsearch oder die OpenSearch-Variante sind als eigenständige Cluster konzipiert.
Der Funktionsumfang geht weit über das hinaus, was PostgreSQL bietet:
// Index-Definition mit deutschem Analyzer
PUT /artikel
{
"settings": {
"analysis": {
"analyzer": {
"german_advanced": {
"type": "german",
"stopwords": "_german_"
}
}
}
},
"mappings": {
"properties": {
"titel": {
"type": "text",
"analyzer": "german_advanced",
"fields": {
"keyword": { "type": "keyword" },
"trigram": {
"type": "text",
"analyzer": "trigram"
}
}
},
"inhalt": { "type": "text", "analyzer": "german_advanced" },
"status": { "type": "keyword" },
"preis": { "type": "float" },
"tags": { "type": "keyword" },
"erstellt": { "type": "date" }
}
},
"settings": {
"analysis": {
"analyzer": {
"trigram": {
"tokenizer": "ngram",
"ngram": { "min_gram": 3, "max_gram": 3 }
}
}
}
}
}
// Komplexe Suche mit Highlighting, Filtern, Boosting und Aggregation
GET /artikel/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"titel": {
"query": "Autobahn",
"fuzziness": "AUTO",
"operator": "and",
"boost": 3.0
}
}
}
],
"filter": [
{ "term": { "status": "published" } },
{ "range": { "preis": { "lte": 100 } } }
],
"should": [
{ "match": { "tags": "neu" } }
]
}
},
"aggregations": {
"preis_gruppen": {
"range": {
"field": "preis",
"ranges": [
{ "to": 10 },
{ "from": 10, "to": 50 },
{ "from": 50 }
]
}
},
"tags": {
"terms": { "field": "tags", "size": 20 }
}
},
"highlight": {
"fields": {
"titel": { "number_of_fragments": 0 },
"inhalt": { "fragment_size": 150 }
}
},
"suggest": {
"did_you_mean": {
"text": "Autoban",
"term": { "field": "titel" }
}
}
}
// Antwort (Auszug)
{
"hits": {
"total": 42,
"hits": [
{
"_score": 12.34,
"_source": { "titel": "Auto fahren auf der Autobahn" },
"highlight": {
"titel": ["Auto fahren auf der <em>Autobahn</em>"]
}
}
]
},
"aggregations": {
"preis_gruppen": {
"buckets": [
{ "key": "*-10.0", "doc_count": 12 },
{ "key": "10.0-50.0","doc_count": 24 },
{ "key": "50.0-*", "doc_count": 6 }
]
}
},
"suggest": {
"did_you_mean": [
{ "text": "Autobahn", "score": 0.95 }
]
}
}
Elasticsearch liefert mit einem JSON-Request Highlighting, Fuzziness, Filter, Boosting, Aggregationen und "Did you mean?" aus einer Hand. Die Antwort ist direkt konsumierbar – kein manuelles Ranking, kein Post-Processing im Anwendungscode.
Entscheidungsmatrix
| Kriterium | PostgreSQL | Elasticsearch / OpenSearch |
|---|---|---|
| Tippfehlertoleranz | ✅ pg_trgm, fuzzystrmatch | ✅ Fuzziness, Suggest |
| Volltextsuche | ✅ tsvector/tsquery | ✅ Multi-Field, Analyzer |
| Phonetische Suche | ✅ fuzzystrmatch | ✅ (via Plugin) |
| Teilwortsuche | ✅ pg_trgm (ILIKE) | ✅ N-Gram-Tokenizer |
| Skalierung (>50 Mio. Docs) | ⚠️ Vertikal | ✅ Horizontal (Sharding) |
| Facettierung / Aggregation | ❌ Umständlich mit GROUP BY | ✅ Nativ |
| Vektor-Suche (semantisch) | ✅ pgvector (IVFFlat/HNSW-Indizes) | ✅ Native kNN + hybrid Scoring (ab ES 8.x) |
| Highlighting | ❌ Self-made | ✅ Integriert |
| "Did you mean?" | ❌ Self-made | ✅ Term Suggest |
| Betriebskomplexität | ✅ Kein zusätzlicher Dienst | ❌ Cluster-Operationen |
| Latenz (Single-Request) | ✅ 2–10 ms (Index-Lookup) | ⚠️ 10–50 ms (Netzwerk) |
| Kosten | ✅ In PostgreSQL enthalten | ❌ Eigenes Hosting oder Cloud |
Wann reicht PostgreSQL?
PostgreSQL mit tsvector, pg_trgm, fuzzystrmatch und optional pgvector ist die richtige Wahl, wenn:
- Daten und Suche auf demselben Server liegen (kein Netzwerk-Overhead)
- Bis zu mehreren Millionen Dokumenten durchsucht werden
- Keine Facettierung oder Aggregationen benötigt werden
- Das Team PostgreSQL bereits beherrscht und kein Elasticsearch-Wissen aufbauen möchte
- Die Infrastruktur einfach bleiben soll
Ideal für: CMS-Suche, Blog-Suche, einfache Produktsuche, interne Verwaltungsoberflächen, API-Endpunkte mit "Suche"-Funktion.
Wann braucht es Elasticsearch?
Ein dedizierter Suchserver wird interessant, wenn:
- Sehr große Datenmengen (Milliarden Dokumente) über mehrere Maschinen verteilt werden müssen
- Komplexe Suchanalysen mit Aggregationen, Facetten und Echtzeit-Statistiken nötig sind
- Semantische Suche mit hybridem Scoring (Vektor + Text) benötigt wird, das Elasticsearch nativ kann, pgvector aber nur mit Zusatzaufwand
- Hochverfügbarkeit und Ausfallsicherheit für die Suchfunktion vertraglich zugesichert werden müssen
- "Did you mean?"-Vorschläge und Autovervollständigung aus dem Suchverlauf abgeleitet werden
Ideal für: E-Commerce-Suche, Log-Analyse (ELK-Stack), DMS-Suche, Wissensdatenbanken.
Fazit
PostgreSQL ist für 80 % der Suchanwendungen vollkommen ausreichend – besonders in Webanwendungen mit überschaubaren Datenmengen. Die Kombination aus tsvector (Volltext), pg_trgm (Tippfehler, Teilwörter), fuzzystrmatch (Phonetik) und optional pgvector (semantische Vektor-Suche) deckt ein breites Spektrum ab, ohne dass ein zweites System betrieben werden muss.
Erst wenn horizontale Skalierung, komplexe Facetten, hybrides Vektor-Text-Scoring (kombinierte Gewichtung aus Embedding-Distanz und textuellem Match) oder "Did you mean?" ins Spiel kommen, wird Elasticsearch / OpenSearch zum unverzichtbaren Werkzeug. Für reine Vektor-Ähnlichkeitssuche ohne diese Zusatzanforderungen ist pgvector dagegen eine vollwertige Alternative.
Der beste Ansatz: Mit PostgreSQL starten, den Suchindex beobachten und erst migrieren, wenn die Anforderungen es wirklich erfordern. Die Umstellung von PostgreSQL auf Elasticsearch ist gut dokumentiert – der umgekehrte Weg (unnötige Komplexität wieder abbauen) ist deutlich schwieriger.
Hinweis: Die PostgreSQL-Beispiele wurden mit Version 16 getestet. pg_trgm und fuzzystrmatch sind in der contrib-Extension enthalten und müssen ggf. mit CREATE EXTENSION aktiviert werden. pgvector muss separat installiert werden (nicht in contrib). Elasticsearch und OpenSearch beziehen sich auf die Versionen 8.x bzw. 2.x.