Engineering Notes

Note 03 / Architektur · Suche

pgvector mit Django: Filter und Recall prüfen

Ein Tenant-Filter begrenzt sichtbare Daten. Ob die ANN-Suche genug Treffer findet, entscheidet der Ausführungsplan.

Sichtbarkeit und Suchqualität sind zwei Fragen

PostgreSQL kann Dokumentmetadaten, Berechtigungen und Embeddings in derselben Datenbank halten. Das vereinfacht die Zuordnung, ersetzt aber weder Autorisierung noch Suchtests. Ein WHERE auf tenant_id begrenzt die zurückgegebenen Daten; es garantiert nicht, dass ein approximativer Index genug Kandidaten aus diesem Mandanten findet.

Insbesondere erzwingt die Reihenfolge von filter() und order_by() im Django-Quelltext keine Reihenfolge der physischen Ausführung. In diesem Beispiel erzeugen beide Verkettungen dieselbe SQL-Abfrage. PostgreSQL wählt den Plan. Bei ANN-Indizes wird der Filter nach dem Indexscan angewandt; selektive Filter können deshalb weniger Treffer als erwartet übrig lassen.

Eine begrenzte ORM-Abfrage

Das Beispiel nutzt synthetische Vektoren mit drei Dimensionen. Der übergebene Mandant muss bereits autorisiert sein. Für HNSW aktiviert es iterative Scans lokal in einer Transaktion; dafür ist pgvector ab 0.8.0 erforderlich. Vor der Modellmigration muss die Extension vector angelegt sein. Die Liste wird innerhalb der Transaktion ausgewertet, damit die Einstellung tatsächlich gilt.

Python · Modell und tenantbezogene Vektorsuche
from django.db import models, transaction, connection
from pgvector.django import VectorField, HnswIndex, CosineDistance

class VectorDocument(models.Model):
    tenant_id = models.PositiveIntegerField(db_index=True)
    embedding = VectorField(dimensions=3)

    class Meta:
        app_label = "notes_demo"
        indexes = [HnswIndex(
            name="demo_vector_hnsw", fields=["embedding"],
            opclasses=["vector_cosine_ops"])]


def nearest(authorized_tenant_id, query_vector, limit=5):
    # Kleine synthetische Vektoren für den Funktionstest.
    if len(query_vector) != 3 or not any(query_vector):
        raise ValueError("non-zero three-dimensional vector required")
    if limit < 1:
        raise ValueError("positive limit required")
    with transaction.atomic():
        # Gilt nur in dieser Transaktion; pgvector ab 0.8.0.
        with connection.cursor() as cursor:
            cursor.execute("SET LOCAL hnsw.iterative_scan = strict_order")
        return list(VectorDocument.objects.filter(
            tenant_id=authorized_tenant_id
        ).order_by(CosineDistance("embedding", query_vector))[:limit])

Was der lokale Test nachweist

Der Test läuft gegen einen isolierten PostgreSQL-16-Container mit pgvector 0.8.6, Django 5.2 und synthetischen Daten. Er legt Extension, Tabelle und HNSW-Index an, prüft den tenantbezogenen Treffer und vergleicht die SQL-Ausgabe der beiden ORM-Verkettungen. Ein zusätzlicher Plancheck erzwingt nur für die Fixture einen HNSW-Indexscan und prüft die Ausgabe der iterativen Suche. Das ist ein Funktionstest, kein Recall- oder Lastbenchmark.

Plan prüfen, dann die Architektur wählen

Vergleichen Sie approximative Treffer mit einer exakten Referenzsuche auf einem repräsentativen Testbestand. Iterative Scans bleiben durch Such- und Speichergrenzen begrenzt; sie garantieren keinen vollständigen Recall. Ein B-Tree auf der Filterspalte, ein partieller Index oder Partitionierung können je nach Datenverteilung helfen. Viele partielle Indizes erhöhen jedoch den Betriebsaufwand.

Eine separate Vektordatenbank ist eine Architekturentscheidung mit zusätzlicher Synchronisation, Berechtigungslogik und Wiederherstellung. Entscheiden Sie anhand Ihrer Daten und Messungen, nicht anhand einer pauschalen Dokumentanzahl. Embedding-Modell und Dimensionen müssen bei Änderungen versioniert werden; das Beispiel erzeugt keine Embeddings automatisch beim Speichern.