Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Lesedauer 6 Min.

Volltextsuche mit SQLite: FTS5 und Fuzzy Search

Hochperformante Suche ohne externe Suchmaschine? Wie man mit der in SQLite eingebauten Volltextsuch-Engine FTS5 eine effiziente Suche mit Tippfehlertoleranz implementiert – und in welchen Fällen Elasticsearch doch die bessere Wahl ist.
© ChatGPT

In den bisherigen Teilen dieser Serie haben wir SQLite als eingebettete Datenbank für zum Beispiel .NET-Anwendungen kennengelernt: von der Performance über die Integration bis zur Verteilung. Doch all diese Artikel setzten eine Annahme voraus: Die Daten sind strukturiert. Ein Produkt hat einen Namen und einen Preis. Mit WHERE-Klauseln und Indizes funktioniert das gut. Doch die Realität ist messy: Benutzer tippen Suchbegriffe ein, machen Tippfehler und erwarten, dass die Anwendung ihre ungenaue Anfrage versteht. Genau hier betritt FTS5 die Bühne, die beeindruckender Volltext-Search-Engine von SQLite.

Was ist FTS5?

FTS steht für Full Text Search. FTS5 ist nicht einfach nur ein besserer Index für LIKE-Abfragen, sondern ein Indexierungsmodul mit eigener Speicherstruktur, Abfrageschnittstelle und Ranking-Engine. Während reguläre SQLite-Tabellen strukturierte Daten speichern, speichert FTS5 invertierte Indizes, eine Datenstruktur, die von Suchmaschinen seit Jahrzehnten verwendet wird. Das bedeutet konkret: Bei einer Suche nach „Laptop“ schlägt FTS5 den Begriff im Index nach, findet sofort alle Dokumente und gibt sie zurück, nicht durch Durchsuchen jeder Zeile wie LIKE. Das ist um ein Vielfaches schneller.

FTS5-Grundlagen: Virtuelle Tabellen und MATCH

Um FTS5 zu nutzen, erstellen wir eine virtuelle Tabelle:

 

CREATE VIRTUAL TABLE products_fts USING fts5(
  name,
  description,
  content = products,
  content_rowid = id
);

 

Das ist eine externe Content-Tabelle. Die Spalten verweisen auf Spalten in einer bestehenden products-Tabelle. FTS5 verwaltet nur den invertierten Index und referenziert Originalzeilen über content_rowid. Das spart Speicher und vereinfacht Updates durch Trigger. Mit dieser Tabelle führen wir FTS5-Abfragen aus:

 

SELECT p.id, p.name, p.price
FROM products p
JOIN products_fts fts ON p.id = fts.rowid
WHERE fts MATCH 'laptop AND gaming'
ORDER BY rank;

 

Die MATCH-Clause ist das Herzstück von FTS5. Sie erlaubt Boolean-Operatoren (AND, OR, NOT), Phrasen-Suche in Anführungszeichen und Wildcards mit Asterisken. Sehr wichtig: ORDER BY rank nutzt den BM25-Ranking-Algorithmus von FTS5. BM25 liefert bessere Suchergebnisse als einfache Wort-Frequency-Counts. Ein Produkt mit dem Suchbegriff in der Überschrift wird höher bewertet als eines mit dem Begriff nur in der Beschreibung. Seltene Begriffe werden stärker gewichtet als häufige.

Tokenizer und Internationalisierung

Tokenizer zerteilen Text in Wörter und normalisieren ihn. Beim Erstellen einer FTS5-Tabelle lässt sich der Tokenizer angeben. Der Standard unicode61 unterstützt Unicode bis Version 6.1 und behandelt Umlaute wie „Äpfel“ und „Überzeugung“ korrekt. Der Porter Stemmer reduziert Wörter auf ihre Stammform: „laufen“, „läuft“, „gelaufen“ werden alle zu „lauf“. Das funktioniert auch für Deutsch, wenn auch nicht perfekt. Für perfektes Stemming müsste man einen spezifischen Tokenizer schreiben, was aber für die meisten Fälle nicht nötig ist.

Fuzzy Search und Tippfehlertoleranz

Benutzer suchen nach „Laptopp“ (mit doppeltem P) und erwarten trotzdem „Laptop“ in den Ergebnissen. FTS5 allein bietet keine eingebaute Fuzzy Search, aber die spellfix1-Erweiterung implementiert Fehlertoleranz basierend auf Edit Distance. Alternativ kann man seit SQLite 3.34 Trigram-Tokenizer nutzen, 3-Zeichen-Sequenzen für effizientes Approximate Matching. Mit Trigrams findet eine Abfrage „lapt*“ auch „laptop“, selbst wenn ein Buchstabe fehlt. Das ist weniger flexibel als Edit Distance, aber deutlich schneller. Wichtig zu beachten: Das funktioniert alles sehr gut bei typischen Wörtern aus dem Wörterbuch. Bei Namen, Straßennamen, Ortschaften und dergleichen kann das Konzept dagegen schnell auf Grenzen stoßen.

Praktisches Beispiel: Produktsuche in .NET

Wie sieht das in einer echten .NET-Anwendung aus? Angenommen, wir haben 500.000 Produkte und möchten eine performante, fehlertolerante Suche implementieren. Der Code ist straight forward und nutzt Microsoft.Data.Sqlite:

 

public async Task<List<ProductDto>>
    SearchAsync(string query)
{
  using var conn = new
    SqliteConnection(_connString);
  await conn.OpenAsync();
  var sql = @"
    SELECT p.id, p.name, p.price
    FROM products p
    JOIN products_fts fts
      ON p.id = fts.rowid
    WHERE fts MATCH @query
    ORDER BY fts.rank ASC
    LIMIT 50";
  using var cmd =
    new SqliteCommand(sql, conn);
  cmd.Parameters.AddWithValue(
    "@query", query);
  var results = new List<ProductDto>();
  using var reader =
    await cmd.ExecuteReaderAsync();
  while (await reader.ReadAsync())
  {
    results.Add(new ProductDto {
      Id = reader.GetInt32(0),
      Name = reader.GetString(1),
      Price = reader.GetDecimal(2)
    });
  }
  return results;
}

 

Zwei kritische Details: Erstens verwenden wir Parametrisierung mit @query statt String-Konkatenation, das schützt vor Injection und stellt sicher, dass der FTS5-Parser die Abfrage korrekt verarbeitet. Es lassen sich auch mehrere Bedingungen kombinieren: fts MATCH 'gaming AND (laptop OR notebook)' funktioniert perfekt. Zweitens limitieren wir auf 50 Ergebnisse, denn bei 500.000 Produkten gibt „laptop“ möglicherweise 20.000 Matches zurück. Diese über das Netzwerk zu schicken ist teuer. Mit LIMIT erhalten wir sofort die relevantesten 50, und wenn der Benutzer mehr Ergebnisse braucht, gibt es Paging-Mechanismen.

Kolumnen-Filter und Multi-Field-Suche

Häufig möchte man in spezifischen Feldern suchen, nicht in allen: „Suche nach ‚gaming‘ nur in der Beschreibung“. FTS5 erlaubt das über die column-Syntax: WHERE fts MATCH 'description:gaming AND category:Elektronik'. Das ist sehr mächtig. Auch Gewichte für verschiedene Spalten lassen sich vergeben – wenn der Suchbegriff in der Überschrift auftaucht, zählt er stärker als in der Beschreibung. Das geschieht über die bm25(column_weights)-Konfiguration bei der Tabellenerstellung und wird bei Abfragen automatisch angewendet.

Grenzen: Wann man zu Elasticsearch greifen sollte

Hier die ehrliche Antwort: Für die meisten .NET-Anwendungen reicht FTS5 aus. FTS5 glänzt bei kleinen bis mittleren Datenmengen (bis 500K Dokumente), Single-Server-Deployments, einfachen Relevanzanforderungen und keiner Notwendigkeit für separate Infrastruktur. Elasticsearch und OpenSearch bieten verteilte Indizierung über mehrere Server, komplexe Analyzers für verschiedene Sprachen, Faceted Search (Navigationshierarchien), Geo-Queries und Real-Time Analytics. Wir brauchen das, wenn wir mit Millionen von Dokumenten arbeiten, geografische Suche einsetzen oder mehrsprachige Inhalte indexieren müssen.

Praktischer Entscheidungsbaum: Produktkatalog unter 500K Einträge? Single Server? Keine Geo-Suche nötig? Keine Millionen-Dokumente? Nehmt FTS5. Falls ihr später migrieren müsst, schreibt einfach in beide Indizes und switcht um. Das ist nicht elegant, aber es funktioniert. Ein wichtiger Punkt: Bei Millionen von Dokumenten wird auch FTS5 langsamer, aber das ist nicht zwangsläufig ein Deal-Breaker, es hängt von der jeweiligen Latenzanforderung ab.

Updates und Maintenance

Ein häufiges Missverständnis: Beim Einfügen in products wird der FTS5-Index nicht automatisch aktualisiert. Wir müssen Trigger setzen. Das ist Boilerplate, aber notwendig. Viele ORM-Frameworks in .NET tun das nicht automatisch; man muss es selbst implementieren oder einen Custom-Interceptor schreiben.

Fazit

FTS5 ist massiv unterschätzt. Es bringt ohne zusätzliche Infrastruktur Search-Capabilities in .NET-Anwendungen. Für alles unter 500K Dokumenten auf einem Single Server ist es eine exzellente Wahl. Dabei gibt es Grenzen: Geo-Suche, Millionen-Dokumente-Scale, Distributed Search – hier braucht man spezialisierte Lösungen. Aber für die Mehrzahl der Business-Anwendungen ist FTS5 nicht nur ausreichend, sondern überlegen gegenüber klassischem „reguläres SQL + Elasticsearch-Cluster“.

Neueste Beiträge

libSQL und Turso: SQLite für verteilte Systeme - SQLite für .NET-Entwickler, Teil 3
libSQL und Turso lösen die größte Einschränkung von SQLite: die Bindung an eine einzelne Instanz.
6 Minuten
15. Apr 2026
Python in .NET – Integration mit Python.NET - .NET, Python und KI, Teil 1
Python-Code lässt sich in .NET-Anwendungen mit dem Open-Source-Projekt Python.NET einbinden. Wir erklären die Installation und grundlegende Interop-Szenarien. Ein einfaches Beispiel veranschaulicht die Praxis.
6 Minuten
ROI von KI
Wie Entwickler die Lücke bei der Bereitschaft, KI einzusetzen, schließen können.
7 Minuten

Das könnte Dich auch interessieren

SQLite: Wenn weniger mehr ist - SQLite für .NET-Entwickler, Teil 1
Für Entwicklerteams, die jeden Tag mit der Komplexität von Kubernetes, Cloud-Datenbanken und Terraform-Skripten ringen, liegt der eigentliche Gewinn von SQLite in der architektonischen Vereinfachung.
6 Minuten
SQLite in ein .NET-Projekt integrieren - SQLite für .NET-Entwickler, Teil 2
Der eleganteste Aspekt von SQLite in .NET ist die Migration vom Prototyp zur Produktion.
6 Minuten
libSQL und Turso: SQLite für verteilte Systeme - SQLite für .NET-Entwickler, Teil 3
libSQL und Turso lösen die größte Einschränkung von SQLite: die Bindung an eine einzelne Instanz.
6 Minuten
15. Apr 2026
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige