Note 03 · Architektur · Pattern
pgvector neben dem Django-ORM — Pattern und Benchmark.
Eine separate Vektor-Datenbank wirkt modern. Für die meisten Mittelstands-Use-Cases ist sie schlicht Overhead — eine zweite Datenkopie, zweites Backup, zweite Permission-Logik, zweite Failure-Modes. Wir legen Vektor-Suche direkt in Postgres mit pgvector und greifen über das Django-ORM zu. Diese Note erklärt, warum, wie und wo das Modell an Grenzen stößt.
Für Korpora bis ~5 Millionen Embeddings (768d–1024d) ist pgvector mit HNSW-Index in Postgres 16 schnell genug für Sub-100ms-Recall@10. Wir verwenden einen Django-ORM-Mixin, der Embedding-Generierung, Indexierung und Cosine-Search in drei Zeilen Anwendungscode kapselt. Eine separate Pinecone- oder Weaviate-Instanz haben wir genau einmal empfohlen — bei einem Multi-Tenant-System mit > 50 Mio Embeddings und tenant-scoped Filtering, das pgvector-Indexes überfordert hat.
1 · Warum nicht Pinecone / Weaviate / Qdrant
Die separaten Vektor-DBs sind technisch hervorragend. Sie lösen ein echtes Problem — wenn man es hat. Die meisten Kunden, die uns ein RAG-System geben, haben es nicht:
- Ihr Korpus passt locker auf eine Postgres-Instanz.
- Sie haben bereits Postgres im Stack — für Authentifizierung, Audit-Log, Bestelldaten.
- Sie wollen Volltext-Filter („nur Dokumente vom Mandanten X aus 2024”) gleichzeitig mit Vektor-Suche. Das ist in einer Single-Database trivial, in einer Multi-Database-Architektur eine Joining-Nightmare.
- Sie wollen ein einziges Backup-Regime, eine einzige DSGVO-Auskunft, eine einzige Permission-Logik.
Jede zusätzliche Datenbank ist ein zusätzliches System, das ausfallen, driften und gepflegt werden muss. Wir empfehlen sie nur, wenn die Daten oder Workload-Eigenschaften sie wirklich erfordern.
2 · HNSW vs IVFFlat — was wir nehmen, und wann
pgvector unterstützt zwei Index-Typen für approximate nearest neighbor (ANN). Die Wahl ist nicht akademisch — sie hat reale Auswirkungen auf Latenz, Speicherverbrauch und Insertion-Performance.
| Index | Build-Zeit | Query P95 | Recall@10 |
|---|---|---|---|
| HNSW | ~14 min | 22 ms | 0.978 |
| IVFFlat (lists=100) | ~2 min | 38 ms | 0.943 |
| IVFFlat (lists=1000) | ~3 min | 14 ms ⚠ | 0.881 |
Werte aus einem Test-Corpus mit 1.2 Mio Embeddings (768d, deutsche Geschäftsdokumente). HNSW ist für unsere Workloads fast immer die richtige Wahl — höhere Recall, robuste Latenz, kein Tuning-Wahnsinn. IVFFlat ist nur dann attraktiv, wenn Index-Build-Zeit kritisch ist (häufiges Re-Indexing) oder der Korpus extrem groß und query-volume sehr hoch wird.
HNSW-Tuning, das wir verwenden
CREATE INDEX docs_emb_hnsw ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- Query-Zeit: höher = präziser, langsamer
SET hnsw.ef_search = 40;
m=16 / ef_construction=64 sind die Defaults — wir haben in Benchmarks keine Konfiguration gefunden, die für unsere Korpus-Größen messbar besser wäre. ef_search ist der einzige Knopf, an dem wir pro Use-Case drehen: 20 für niedrig-Latenz-Chat, 40 für Standard-RAG, 80 für „Faithfulness ist alles” (juristische Recherche).
3 · Der ORM-Mixin, den wir wiederverwenden
Wir haben einen kleinen Django-Mixin extrahiert, der in jedem Projekt landet. Er deklariert das Embedding-Feld, kümmert sich um Auto-Generierung beim Save und liefert eine ähnlich-zu-Methode:
from django.db import models
from pgvector.django import VectorField, HnswIndex
from .embeddings import EmbeddedModelMixin
class Document(EmbeddedModelMixin, models.Model):
title = models.CharField(max_length=400)
body = models.TextField()
tenant = models.ForeignKey('tenants.Tenant', on_delete=models.CASCADE)
embedding = VectorField(dimensions=768, null=True, blank=True)
embedding_source = "{title}\n\n{body}" # Mixin liest das
class Meta:
indexes = [
HnswIndex(name="doc_emb_hnsw",
fields=["embedding"],
m=16, ef_construction=64,
opclasses=["vector_cosine_ops"]),
# Wichtig: Index auf tenant für scoped queries
models.Index(fields=["tenant"]),
]
# Verwendung in einem View / Service
query = "Welche Mandanten haben offene Rechnungen aus 2024?"
results = Document.find_similar(
query, tenant=request.user.tenant, top_k=8, ef_search=40
)
Der Mixin macht drei Dinge:
- Im post_save Signal generiert er das Embedding für embedding_source, falls leer.
- Er stellt find_similar() bereit — wendet Tenant- und Datums-Filter VOR der Vektor-Suche an (kritisch für Performance bei Multi-Tenant).
- Er fügt einen Änderungs-Tracker hinzu: ändert sich embedding_source, wird das Embedding bei Save neu generiert (nicht stillschweigend stale).
4 · Tenant-Scoped Filtering — der häufigste Stolperstein
Naiver Code sieht so aus:
# LANGSAM — wendet Filter NACH der Vektor-Suche an
Document.objects.order_by(
L2Distance("embedding", query_emb)
).filter(tenant=current_tenant)[:8]
Das ist ein häufiger Fehler. Postgres macht erst ANN-Suche über alle Tenants, holt Top-N, filtert dann — bei einem System mit 50 Tenants und ungleicher Verteilung ist der Recall katastrophal. Korrekt ist:
# SCHNELL — Filter VOR der Vektor-Suche, mit Partial Index
Document.objects.filter(tenant=current_tenant).order_by(
L2Distance("embedding", query_emb)
)[:8]
# Plus: Partial Index für häufige Tenants
CREATE INDEX docs_emb_tenant_acme ON documents
USING hnsw (embedding vector_cosine_ops)
WHERE tenant_id = 42;
Partial Indexes pro Tenant sind eine Investition, die sich bei großen Tenants schnell auszahlt. Wir generieren sie automatisch ab einer Tenant-Schwelle von 100k Dokumenten.
5 · Wo das Modell bricht
Bei einem Kunden in der Versicherungsbranche haben wir den klaren Fall gesehen, in dem pgvector nicht mehr genügte:
- Korpus > 50 Millionen Embeddings, in Multi-Tenant-Tabelle.
- Query-Volume in Spitzen > 200 req/s.
- Strict-Sub-50ms-Latenz-Anforderung.
- Hochfrequente Updates (Re-Indexing alle 4 Stunden).
Bei dieser Kombination wurde der HNSW-Index zum Insertion-Bottleneck und zur Query-Latency-Quelle. Wir haben dort tatsächlich eine separate Vektor-DB empfohlen (Qdrant in dem Fall) — aber als Read-Cache neben Postgres, nicht als Ersatz. Postgres bleibt source-of-truth, Qdrant wird inkrementell aus dem WAL-Stream aktualisiert.
6 · Die Faustregel
Wenn Sie unter zwei der drei folgenden Werte liegen, bleiben Sie bei pgvector:
- Embeddings ≤ 5 Mio
- Query-Spitzen ≤ 100 req/s
- Latenz-SLA ≥ 100 ms P95
Das deckt nach unserer Erfahrung ungefähr 90% der RAG-Use-Cases im Mittelstand ab. Für die anderen 10% gibt es gute Gründe, die Architektur zu erweitern — aber bitte erst dann, wenn die Messung sie bestätigt.