Volltextsuche: Warum LIKE und tsvector nicht ausreichen
Jede Anwendung braucht früher oder später eine Suche. Zuerst reicht WHERE name ILIKE '%query%' — bei tausend Einträgen. Dann kommt PostgreSQL tsvector — und das funktioniert bei einer Million. Aber wenn man sofortige Typeahead-Vorschläge, Tippfehler-Toleranz, Facetten-Filterung, Synonyme und Relevanz-Ranking braucht — reicht PostgreSQL nicht mehr aus.
Drei Engines dominieren 2026: Elasticsearch — der Veteran mit riesigem Ökosystem, Meilisearch — minimalistisch und schnell out of the box, Typesense — positioniert als „selbst gehostetes Algolia." Jede löst die Aufgabe der Volltextsuche, aber Ansatz, Komplexität und Leistung unterscheiden sich radikal.
Ich habe mit Elasticsearch auf zwei E-Commerce-Projekten gearbeitet (Produktkatalog, 2M+ Dokumente), Meilisearch an ein Rails-Portfolio für Artikelsuche angebunden und Typesense auf einem HR-System für Lebenslauf-Suche getestet. Hier ist ein ehrlicher Vergleich.
Architektur und Philosophie
Elasticsearch — Das Schweizer Taschenmesser
Elasticsearch basiert auf Apache Lucene. Es ist nicht nur eine Suchmaschine — es ist ein verteiltes System für Speicherung, Suche und Analytics. Cluster, Shards, Replicas, Aggregationen, Ingest-Pipelines — alles eingebaut.
Vorteil: skaliert horizontal auf Petabytes. Nachteil: ein minimaler Production-Cluster braucht 3 Knoten, jeder mit 4-8 GB RAM. Für die Suche in 10K Dokumenten ist das wie einen Muldenkipper zum Rucksacktransport zu mieten.
Meilisearch — Einfachheit zuerst
Meilisearch ist eine einzelne Rust-Binary. Herunterladen, starten, fertig. Keine Cluster, keine Shards, keine Lucene-Konfiguration.
curl -L https://install.meilisearch.com | sh
./meilisearch --master-key="your-secret-key"
# Fertig. Lauscht auf :7700
Philosophie: 95% der Anwendungen brauchen keine Petabytes und 50-Knoten-Cluster. Sie brauchen schnelle Suche mit Tippfehler-Toleranz, die sofort funktioniert.
Typesense — Balance zwischen Einfachheit und Leistung
Typesense ist in C++ geschrieben, optimiert für Geschwindigkeit. Unterstützt Clustering (Raft Consensus), kann aber als einzelner Prozess laufen. Positioniert als selbst gehostete Algolia-Alternative.
Architektur-Vergleich
| Aspekt | Elasticsearch | Meilisearch | Typesense |
|---|---|---|---|
| Sprache | Java (Lucene) | Rust | C++ |
| Clustering | Ja (eingebaut) | Nein (Single Node) | Ja (Raft) |
| Minimum RAM | 4 GB | 256 MB | 256 MB |
| Speicherung | Festplatte (Lucene) | Festplatte (LMDB) | Speicher + Festplatte |
| Lizenz | SSPL / Elastic License | MIT | GPL-3.0 |
Daten indexieren
Elasticsearch: Mappings und Analyzer
curl -X PUT "localhost:9200/products" -H 'Content-Type: application/json' -d'
{
"settings": {
"number_of_shards": 1,
"analysis": {
"analyzer": {
"product_analyzer": {
"type": "custom",
"tokenizer": "standard",
"filter": ["lowercase", "german_snowball", "synonym_filter"]
}
},
"filter": {
"german_snowball": { "type": "snowball", "language": "German" },
"synonym_filter": {
"type": "synonym",
"synonyms": ["handy, smartphone, telefon", "laptop, notebook"]
}
}
}
},
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "product_analyzer",
"fields": { "keyword": { "type": "keyword" } }
},
"category": { "type": "keyword" },
"brand": { "type": "keyword" },
"price": { "type": "float" },
"in_stock": { "type": "boolean" },
"rating": { "type": "float" }
}
}
}'
Elasticsearch erfordert explizites Mapping für die Produktion. Analyzer-Konfiguration ist eine eigene Wissenschaft: Tokenizer, Filter, Stemming pro Sprache.
Meilisearch: Zero-Config-Indexierung
curl -X POST "localhost:7700/indexes/products/documents" \
-H "Authorization: Bearer your-master-key" \
-H "Content-Type: application/json" \
-d '[
{"id": 1, "name": "iPhone 16 Pro Max", "category": "smartphones", "brand": "Apple", "price": 1199.99, "in_stock": true, "rating": 4.8},
{"id": 2, "name": "Samsung Galaxy S26 Ultra", "category": "smartphones", "brand": "Samsung", "price": 1099.99, "in_stock": true, "rating": 4.7}
]'
Kein Mapping nötig. Meilisearch erkennt Feldtypen automatisch. Für 90% der Fälle ausreichend.
Typesense: Typisiertes Schema
curl -X POST "localhost:8108/collections" \
-H "X-TYPESENSE-API-KEY: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"name": "products",
"fields": [
{"name": "name", "type": "string"},
{"name": "description", "type": "string"},
{"name": "category", "type": "string", "facet": true},
{"name": "brand", "type": "string", "facet": true},
{"name": "price", "type": "float"},
{"name": "in_stock", "type": "bool", "facet": true},
{"name": "rating", "type": "float"}
],
"default_sorting_field": "rating"
}'
Suche: Syntax und Möglichkeiten
Elasticsearch: Mächtiger Query DSL
{
"query": {
"bool": {
"must": [{
"multi_match": {
"query": "smartphone apple",
"fields": ["name^3", "description", "brand^2"],
"fuzziness": "AUTO"
}
}],
"filter": [
{ "term": { "in_stock": true } },
{ "range": { "price": { "gte": 500, "lte": 2000 } } }
]
}
},
"aggs": {
"categories": { "terms": { "field": "category" } },
"brands": { "terms": { "field": "brand" } }
},
"highlight": { "fields": { "name": {}, "description": {} } },
"size": 20
}
Der mächtigste der drei. Bool Queries, Nested Queries, Function Score, Aggregationen auf Analytics-Datenbank-Niveau. Aber auch der wortreichste.
Meilisearch: Minimalistisches API
{
"q": "smartphone apple",
"filter": "in_stock = true AND price >= 500 AND price <= 2000",
"facets": ["category", "brand"],
"sort": ["rating:desc"],
"limit": 20,
"attributesToHighlight": ["name", "description"]
}
Antwort enthält processingTimeMs: 2 — zwei Millisekunden. Tippfehler werden automatisch behandelt.
Typesense: Typisierte Suche
q=smartphone apple
query_by=name,description,brand
filter_by=in_stock:true && price:=[500..2000]
facet_by=category,brand
sort_by=_text_match:desc,rating:desc
Tippfehler-Toleranz
| Abfrage | Elasticsearch | Meilisearch | Typesense |
|---|---|---|---|
| "iphone" (exakt) | Ja | Ja | Ja |
| "iphon" (fehlender Buchstabe) | Ja | Ja | Ja |
| "iphonee" (zusätzlicher Buchstabe) | Ja | Ja | Ja |
| "iphnoe" (Vertauschung) | Nein* | Ja | Ja |
| "smarphone" (Tippfehler) | Teilweise | Ja | Ja |
Meilisearch und Typesense sind bei Tippfehlern out of the box deutlich besser.
Integration mit Rails
Elasticsearch: searchkick Gem
gem 'searchkick'
class Product < ApplicationRecord
searchkick language: "german",
word_start: [:name],
callbacks: :async
def search_data
{ name: name, description: description, category: category.name,
brand: brand.name, price: price.to_f, in_stock: in_stock?, rating: rating.to_f }
end
end
results = Product.search("smartphone apple",
where: { in_stock: true, price: { gte: 500, lte: 2000 } },
aggs: [:category, :brand],
order: { rating: :desc },
highlight: true)
Meilisearch: meilisearch-rails Gem
gem 'meilisearch-rails'
class Product < ApplicationRecord
include MeiliSearch::Rails
meilisearch do
attribute :name, :description, :category_name, :brand_name, :price, :rating
searchable_attributes [:name, :description, :brand_name]
filterable_attributes [:category_name, :price, :in_stock]
sortable_attributes [:price, :rating]
end
end
results = Product.search("smartphone apple",
filter: "in_stock = true AND price >= 500",
sort: ["rating:desc"],
facets: ["category_name", "brand_name"])
Performance-Benchmarks
Tests: 1M Dokumente, 4 Kerne, 16 GB RAM, SSD, Single-Node Docker, 100 gleichzeitige Clients.
Indexierungsgeschwindigkeit
| Operation | Elasticsearch | Meilisearch | Typesense |
|---|---|---|---|
| 1M Dokumente indexieren | 45s | 28s | 22s |
| 1 Dokument indexieren | 12ms | 8ms | 3ms |
Suchlatenz (p50 / p99)
| Abfrage | Elasticsearch | Meilisearch | Typesense |
|---|---|---|---|
| Einfach (1 Wort) | 5ms / 25ms | 2ms / 8ms | 1ms / 5ms |
| Phrase (3 Wörter) | 12ms / 45ms | 3ms / 12ms | 2ms / 8ms |
| Suche + Filter + Facetten | 18ms / 65ms | 5ms / 18ms | 3ms / 12ms |
| Suche mit Tippfehler | 15ms / 55ms | 3ms / 10ms | 2ms / 7ms |
| Typeahead (Präfix) | 8ms / 30ms | 1ms / 5ms | 1ms / 4ms |
Ressourcenverbrauch
| Metrik | Elasticsearch | Meilisearch | Typesense |
|---|---|---|---|
| RAM (1M Docs) | 2,8 GB | 450 MB | 800 MB |
| Festplatte (1M Docs) | 1,2 GB | 380 MB | 520 MB |
| CPU (100 qps) | 25% | 8% | 6% |
Wann was wählen
Elasticsearch
- Mehr als 10M Dokumente und horizontale Skalierung nötig
- Komplexe Aggregationen: Histogramme, Pipeline Aggs, Statistiken
- Bereits ELK-Stack vorhanden
- Team hat Elasticsearch-Expertise
- Budget: mindestens 3 Knoten × 4 GB RAM
Meilisearch
- Produktkatalog, Artikel, Dokumentation — bis 10M Dokumente
- Ergebnis in 2 Tagen, nicht 2 Wochen
- Begrenztes Budget (VPS mit 1-2 GB RAM)
- Tippfehler-Toleranz kritisch (E-Commerce)
- MVPs und Startups
Typesense
- Selbst gehostetes Algolia: sofortiges Typeahead, Facetten
- Hochverfügbarkeit: eingebautes Raft-Clustering
- Geo-Suche mit Entfernungssortierung
- Budget zwischen Meilisearch und Elasticsearch
Keiner der drei
- Unter 100K Einträge — PostgreSQL
tsvector+pg_trgmreicht - Nur exakte Feldsuche — normales
WHERE+ Indexe - Log-Suche — ClickHouse oder Loki
- Echtzeit-Analytics — ClickHouse
Abschließende Checkliste
Engine-Auswahl
- [ ] Volumen bestimmen: < 100K → PostgreSQL, < 10M → Meilisearch/Typesense, > 10M → Elasticsearch
- [ ] Anforderungen bestimmen: Tippfehler, Facetten, Geo, Aggregationen
- [ ] Budget bewerten: Elasticsearch = 12+ GB RAM, Meilisearch/Typesense = 1-2 GB
Integration
- [ ] Gem wählen: searchkick (ES), meilisearch-rails (Meili), typesense-ruby (Typesense)
- [ ] Async-Indexierung konfigurieren
- [ ] Searchable Attributes definieren
- [ ] Filterable Attributes definieren
Produktion
- [ ] Backups: Snapshots (ES), Data Dump (Meili), Export (Typesense)
- [ ] Monitoring: Query-Latenz, Indexierungs-Lag, Speicherverbrauch
- [ ] Rate Limiting: Schutz vor Such-Missbrauch
- [ ] Relevanz-Tests: prüfen dass Suche erwartete Ergebnisse liefert
- [ ] Fallback: bei Ausfall der Suchmaschine → PostgreSQL ILIKE
Kommentare (0)