Suche ohne Zweitsystem

Vektorsuche in PostgreSQL, ohne zweiten Datenspeicher

pgvector liefert Spaltentyp, Distanzoperatoren und zwei Indexarten. Die eigentliche Arbeit beginnt danach bei der Frage, wie genau die Treffer sein müssen und was diese Genauigkeit kostet.

KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Seit 1997 am Markt Kleine Gruppen Präsenz und Live-Online Zertifizierte Trainer
Worum es geht

Ein zweiter Datenspeicher für die Suche kostet mehr, als er zunächst spart

Sobald in einem Projekt semantische Suche auftaucht, steht schnell die Frage im Raum, welche Vektordatenbank es werden soll. Damit entsteht ein zweiter Speicherort für Daten, die schon in PostgreSQL liegen: mit eigener Sicherung, eigener Rechteverwaltung und einem Abgleich, der bei jedem Löschvorgang und jeder Änderung wieder stimmen muss.

Teuer wird das an der Stelle, an der gefiltert werden soll. Ein Treffer darf nur erscheinen, wenn das Dokument zum Mandanten der anfragenden Person gehört, nicht archiviert ist und in ihrer Sprache vorliegt. Diese Merkmale stehen in der relationalen Datenbank. Wer die Ähnlichkeitssuche außerhalb betreibt, holt sich erst eine Kandidatenliste fester Größe und filtert sie danach in der Anwendung, und je mehr Dokumente für die anfragende Person gesperrt sind, desto kürzer wird die Liste, die am Ende übrig bleibt.

In PostgreSQL bleiben WHERE, JOIN, Transaktion und Berechtigungskonzept, wie sie sind. Der Preis dafür ist, dass Speicherbedarf und Antwortzeit der Vektorspalte auf dasselbe System drücken wie das Tagesgeschäft, und dass ein Index über Vektoren anderen Regeln folgt als ein B-Baum.

KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Der Aufbau im Detail

Der Aufbau im Detail

Der Aufbau

CREATE INDEX ON dokumente USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);
  1. 01 Die Indexart USING hnsw

    HNSW legt einen mehrschichtigen Graphen über die Vektoren und sucht darin absteigend nach Nachbarn. Der Bau dauert deutlich länger als bei IVFFlat und braucht mehr Arbeitsspeicher, dafür funktioniert der Index auch auf einer Tabelle, die erst noch gefüllt wird, und liefert bei gleicher Laufzeit meist die besseren Treffer.

  2. 02 Die Vektorspalte embedding

    Die Spalte wird als vector mit fester Dimensionszahl angelegt, etwa vector(1536). Der Wert belegt vier Byte je Dimension plus acht Byte Kopf, eine Million Zeilen mit 1.536 Dimensionen sind also gut sechs Gigabyte, bevor überhaupt ein Index existiert.

  3. 03 Die Operatorklasse vector_cosine_ops

    Sie legt fest, für welchen Distanzoperator der Index gilt. vector_cosine_ops gehört zu <=>, vector_l2_ops zu <->, vector_ip_ops zu <#>. Sortiert die Abfrage nach einem anderen Operator, bleibt der Index unbenutzt, und PostgreSQL rechnet die Distanz für jede Zeile aus, ohne dass eine Fehlermeldung darauf hinweist.

  4. 04 Verbindungen je Knoten m = 16

    Wie viele Nachbarn ein Knoten im Graphen behält. Ein höherer Wert verbessert die Trefferquote, vergrößert den Index und verlängert den Bau. 16 ist die Voreinstellung und für die meisten Bestände ein brauchbarer Anfang.

  5. 05 Kandidatenliste beim Bau ef_construction = 64

    Wie viele Kandidaten beim Einfügen eines Knotens betrachtet werden. Der Wert muss mindestens dem Doppelten von m entsprechen. Höher gesetzt entsteht ein besserer Graph, der Bau dauert dafür spürbar länger, und beides lässt sich nachträglich nur durch einen neuen Index ändern.

Wenn es nicht funktioniert

Das siehst du

EXPLAIN zeigt einen sequenziellen Scan, obwohl der Index existiert.

Warum

Die Sortierung nutzt einen anderen Operator als die Operatorklasse des Index, oder in der Abfrage fehlt das LIMIT. Ohne Begrenzung braucht PostgreSQL ohnehin alle Zeilen und geht den kürzeren Weg über die Tabelle.

Was hilft

Denselben Operator wie im Index verwenden, ORDER BY direkt auf die Spalte setzen und ein LIMIT ergänzen. Rechnest du in der Sortierung mit einem Ausdruck, braucht dieser Ausdruck einen eigenen Index.

Das siehst du

Die Abfrage liefert drei Zeilen, obwohl LIMIT 10 gefordert ist.

Warum

Der Index liefert eine feste Zahl von Kandidaten, und der Filter im WHERE streicht die meisten davon wieder. Nachgeliefert wird ohne weitere Einstellung nichts.

Was hilft

hnsw.iterative_scan auf relaxed_order setzen, damit der Index weiter durchsucht wird, bis genug gefilterte Zeilen zusammenkommen. Für einen Filter, der immer gleich lautet, ist ein Teilindex mit WHERE-Bedingung die sauberere Lösung.

Das siehst du

CREATE INDEX läuft über Stunden und die Platte arbeitet durchgehend.

Warum

Der Graph passt nicht mehr in maintenance_work_mem, deshalb baut pgvector ihn in mehreren Durchgängen über die Platte. Der Serverprotokolleintrag beim Bau nennt genau diesen Punkt.

Was hilft

maintenance_work_mem für die Sitzung hochsetzen, max_parallel_maintenance_workers erhöhen und den Fortschritt über pg_stat_progress_create_index verfolgen.

Das siehst du

Der Indexbau bricht wegen zu vieler Dimensionen ab.

Warum

Ein HNSW-Index über den Typ vector verkraftet höchstens 2.000 Dimensionen. Modelle mit mehr Dimensionen lassen sich zwar speichern, aber nicht so indizieren.

Was hilft

Die Spalte auf halfvec umstellen, das reicht bis 4.000 Dimensionen und halbiert den Speicherbedarf, oder die Dimensionszahl schon beim Erzeugen der Embeddings reduzieren.

Das siehst du

Nach dem Massenimport liefert der IVFFlat-Index deutlich schlechtere Treffer als vorher.

Warum

IVFFlat teilt den Raum beim Bau anhand der vorhandenen Zeilen in Listen auf. Wächst der Bestand danach stark oder verschiebt er sich inhaltlich, passen die Listen nicht mehr zu den Daten.

Was hilft

Den Index nach dem Import neu bauen und die Zahl der Listen an der Zeilenzahl ausrichten, als Anhaltspunkt Zeilen geteilt durch 1.000 bis zu einer Million Zeilen.

Sechs Festlegungen auf dem Weg zur Vektorspalte

  1. 01 Die Erweiterung pgvector muss auf dem Server bereitstehen und aktiviert sein.
  2. 02 Die Spalte bekommt eine feste Dimensionszahl, die zum Einbettungsmodell passt.
  3. 03 Die Distanzfunktion richtet sich danach, wie das Modell trainiert wurde.
  4. 04 Der Index braucht dieselbe Operatorklasse wie die Sortierung der Abfrage.
  5. 05 Die Suchtiefe stellst du je Sitzung ein, nicht dauerhaft im ganzen Server.
  6. 06 Filter im WHERE brauchen iterative Scans, sonst bleiben Trefferlisten kurz.
Was du mitnimmst

Danach richtest du eine Vektorspalte ein, ohne zu raten

Vier Entscheidungen bestimmen, ob die Suche brauchbare Treffer in vertretbarer Zeit liefert: der Datentyp mitsamt Dimensionszahl, die Distanzfunktion, die Indexart und die Suchtiefe zur Laufzeit. Alle vier hängen zusammen, und drei davon lassen sich später nur mit einem Neuaufbau des Index ändern.

Die Distanzfunktion zum Modell wählen

Der Operator <=> rechnet Kosinusabstand, <-> den euklidischen Abstand, <#> das negative Skalarprodukt. Sind die Vektoren auf Länge eins normalisiert, liefern Kosinus und Skalarprodukt dieselbe Reihenfolge, und du kannst nach Laufzeit entscheiden.

HNSW oder IVFFlat begründen

HNSW baut einen Graphen, braucht dafür Zeit und Arbeitsspeicher und liefert ohne Vorwissen über die Daten gute Treffer. IVFFlat ist schnell gebaut und klein, setzt aber voraus, dass die Zeilen beim Bau schon vorhanden sind.

Suchtiefe je Sitzung stellen

hnsw.ef_search steht standardmäßig auf 40, ivfflat.probes auf 1. Beide Werte kaufst du mit Laufzeit gegen Trefferquote ein, und beide setzt du sinnvoll pro Verbindung oder Transaktion statt global im Server.

Filter ohne leere Trefferlisten

Kombinierst du die Vektorsuche mit WHERE, hilft hnsw.iterative_scan aus pgvector 0.8. Der Index wird dann so lange weiter durchsucht, bis genug Zeilen den Filter überstehen, begrenzt durch hnsw.max_scan_tuples. Auf älteren Fassungen bleibt nur der Teilindex oder ein deutlich höheres hnsw.ef_search.

Dimensionen und Speicherbedarf rechnen

Der Typ vector belegt vier Byte je Dimension plus acht Byte. Über halfvec sind es zwei Byte, und ein HNSW-Index verkraftet damit bis zu 4.000 statt 2.000 Dimensionen. Das entscheidet sich vor dem ersten Import.

Erkennen, wann PostgreSQL nicht mehr passt

Wenn der Index nicht mehr in den Arbeitsspeicher passt und jede Suche von der Platte liest, hilft keine Einstellung mehr. Dann ist eine eigene Instanz für die Suche der nächste Schritt, nicht sofort ein anderes Produkt.

KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt

Was pgvector mitbringt und was du selbst beisteuern musst

Die Erweiterung liefert seit Fassung 0.7 vier Datentypen. vector speichert Fließkommazahlen mit vier Byte je Dimension und fasst bis zu 16.000 Dimensionen, halfvec halbiert das auf zwei Byte, bit speichert Bitfolgen bis 64.000 Stellen, sparsevec ist für Vektoren gedacht, bei denen fast alle Stellen null sind. Dazu kommen sechs Operatoren: <-> für euklidischen Abstand, <=> für Kosinusabstand, <#> für das negative Skalarprodukt, <+> für die Taxifahrer-Distanz sowie <~> und <%> für Hamming- und Jaccard-Abstand auf Bitfolgen. Welche Fassung installiert ist, beantwortet SELECT extversion FROM pg_extension WHERE extname = 'vector'.

Was pgvector nicht macht: Es erzeugt keine Embeddings. Der Vektor entsteht außerhalb der Datenbank, bei einem Modell im eigenen Netz oder bei einem Anbieter, und wird als Zahlenliste eingefügt. Damit ist auch klar, was passiert, wenn das Modell gewechselt wird. Vektoren zweier Modelle sind nicht vergleichbar, ein Modellwechsel bedeutet also, den gesamten Bestand neu einzubetten und den Index neu zu bauen.

Ohne Index rechnet PostgreSQL die Distanz für jede Zeile aus und liefert damit die exakt richtige Reihenfolge. Bis in den unteren sechsstelligen Zeilenbereich ist das für Hintergrundauswertungen oft schnell genug, und es ist der einzige verlässliche Maßstab dafür, wie gut ein Index die Treffer trifft. Vergleich die zehn Treffer aus dem Index mit den zehn Treffern ohne Index, dann hast du eine Zahl für die Trefferquote statt eines Gefühls.

HNSW oder IVFFlat, und warum die Antwort meist HNSW lautet

IVFFlat sortiert die Vektoren beim Bau in eine feste Zahl von Listen ein und durchsucht zur Laufzeit nur die nächstgelegenen davon. Der Index ist klein und schnell gebaut, hat aber zwei unangenehme Eigenschaften: Er braucht repräsentative Daten zum Zeitpunkt des Baus, und die Voreinstellung ivfflat.probes = 1 durchsucht genau eine Liste, was bei Vektoren nahe an einer Listengrenze zu offensichtlich falschen Treffern führt.

HNSW baut stattdessen einen Graphen aus Nachbarschaftsbeziehungen. Der Bau ist teuer, weil jeder eingefügte Vektor seinen Platz im Graphen suchen muss, dafür ist der Index gegenüber wachsenden Beständen robust und liefert bei gleicher Antwortzeit in der Regel die besseren Treffer. Für einen Bestand, der laufend wächst, ist das der pragmatische Standard.

Beim Bau selbst entscheidet der Arbeitsspeicher über die Dauer. Passt der entstehende Graph in maintenance_work_mem, läuft der Bau im Speicher, sonst in mehreren Durchgängen über die Platte, und das kostet leicht das Zehnfache an Zeit. Setz maintenance_work_mem deshalb für die Sitzung hoch, in der du den Index baust, und erhöhe max_parallel_maintenance_workers, denn pgvector baut HNSW-Indizes parallel. Wie weit der Bau ist, zeigt pg_stat_progress_create_index.

Der Filter ist der Punkt, an dem die meisten Umsetzungen kippen

Eine Vektorsuche mit Bedingung sieht harmlos aus: die ähnlichsten zehn Absätze, aber nur aus Dokumenten des eigenen Mandanten. Der Index kennt den Mandanten nicht. Er liefert eine begrenzte Kandidatenmenge, die sich an hnsw.ef_search orientiert, und erst danach greift der Filter. Sind neunzig Prozent des Bestands für diese Person gesperrt, bleiben von vierzig Kandidaten vier Treffer übrig, und die Anwendung zeigt eine kurze Liste, ohne dass irgendwo ein Fehler auftaucht.

Dagegen gibt es drei Mittel. Erstens iterative Scans: Mit hnsw.iterative_scan auf strict_order oder relaxed_order durchsucht pgvector den Index weiter, bis genug gefilterte Zeilen zusammengekommen sind, begrenzt durch hnsw.max_scan_tuples. strict_order behält die exakte Reihenfolge bei, relaxed_order erlaubt kleine Abweichungen und ist dafür schneller. Zweitens Teilindizes: Wenn der Filter nur wenige feste Ausprägungen kennt, etwa aktive gegenüber archivierten Dokumenten, ist ein eigener Index je Fall der direkte Weg. Drittens Partitionierung nach Mandant, wenn die Trennung ohnehin die Struktur der Daten bestimmt.

Prüf in jedem Fall mit EXPLAIN (ANALYZE, BUFFERS), was tatsächlich passiert. Interessant sind zwei Dinge: ob überhaupt ein Index Scan auftaucht und wie viele Zeilen der Filter wegwirft. Wenn dort Rows Removed by Filter in der Größenordnung der Kandidatenzahl steht, hast du den Fall vor dir.

Speicher sparen, bevor du ein anderes System suchst

Der Speicherbedarf ist die Grenze, die zuerst erreicht wird. Zehn Millionen Absätze mit 1.536 Dimensionen sind rund 60 Gigabyte allein an Vektordaten, dazu kommt der Index. Solange beides in den Arbeitsspeicher passt, antwortet die Suche in Millisekunden, sobald sie von der Platte liest, wird sie um Größenordnungen langsamer.

Zwei Hebel greifen, bevor ein anderes Produkt nötig wird. Der erste ist halfvec: halbe Genauigkeit je Zahl, halber Speicher, und die Trefferquote sinkt in der Regel kaum messbar. Der zweite ist ein zweistufiges Vorgehen mit binary_quantize: Du legst einen Index über die auf ein Bit je Dimension reduzierte Fassung, holst darüber ein paar hundert Kandidaten mit dem Hamming-Abstand und sortierst diese Kandidaten anschließend exakt über die vollständigen Vektoren nach. Das kostet eine zweite Stufe in der Abfrage und spart den Großteil des Indexspeichers.

Erst wenn beides nicht reicht, ist eine eigene PostgreSQL-Instanz nur für die Suche der nächste Schritt, mit eigenem Arbeitsspeicher und eigenem Wartungsfenster. Der Sprung zu einem spezialisierten Suchsystem lohnt sich, wenn die Vektoren häufiger gelesen als geschrieben werden, die Filter einfach bleiben und die Zeilenzahl in die Hunderte Millionen geht. Bei den Größen, mit denen die meisten Fachanwendungen arbeiten, ist er nicht nötig.

Dazu passende Kurse

Wer Erweiterungen, Operatorklassen und Indexarten einmal im Zusammenhang gesehen hat, entscheidet solche Fragen schneller, und genau darauf zielen die PostgreSQL-Trainings mit Vektorsuche und Erweiterungen .

Ob die Suche überhaupt in die relationale Datenbank gehört, ist eine Architekturfrage, und dazu hilft die Übersicht über alle Datenbankthemen bei cmt bei der Einordnung.

Wissen prüfen

Wie sicher bist du beim Thema wirklich?

Lesen fühlt sich schnell nach Können an. Ein kurzer Test zeigt dir, was davon schon sitzt und wo sich ein Kurs lohnt. Kostenlos, ohne Anmeldung, mit einer Erklärung zu jeder Antwort.

Super Schulung, in der der Trainer auch auf eigene Fragestellungen eingegangen ist.
PostgreSQL Datenbankentwicklung Aufbaukurs
Super Grundlagenschulung mit allen wichtigen Punkten und hilfreichen Tipps!
PostgreSQL Einführung und SQL Grundlagenkurs
Schön, sich 5 Tage ungestört mit Postgres beschäftigen zu können.
PostgreSQL für Administratoren Grundkurs

Häufige Fragen

Brauche ich eine eigene Vektordatenbank, wenn ich schon PostgreSQL einsetze?
Für die meisten Anwendungen nicht. Solange Vektoren und Metadaten zusammen abgefragt werden, spricht viel dafür, sie in einem System zu halten, weil Filter, Berechtigungen und Transaktionen dann von selbst stimmen. Der Wechsel lohnt sich, wenn der Index nicht mehr in den Arbeitsspeicher passt, die Suchlast das Tagesgeschäft stört oder du Funktionen brauchst, die pgvector nicht kennt.
Welche Distanzfunktion ist die richtige?
Die, mit der das Modell trainiert wurde. Die meisten Einbettungsmodelle sind auf Kosinusähnlichkeit ausgelegt, dann ist <=> die richtige Wahl. Sind die Vektoren auf Länge eins normalisiert, liefern Kosinus und Skalarprodukt exakt dieselbe Reihenfolge, und du kannst nach Laufzeit entscheiden. Der euklidische Abstand über <-> passt, wenn die Länge der Vektoren eine Bedeutung trägt.
Wie messe ich, ob mein Index gute Treffer liefert?
Über einen Vergleich mit der exakten Suche. Nimm ein paar hundert echte Anfragen, hol dir für jede die zehn Treffer ohne Index und die zehn Treffer mit Index und zähl die Überschneidung. Der Anteil ist deine Trefferquote. Erst mit dieser Zahl kannst du hnsw.ef_search sinnvoll einstellen, denn jeder höhere Wert kostet Laufzeit und bringt irgendwann nichts mehr.
Kann ich pgvector bei einem verwalteten Datenbankdienst nutzen?
In der Regel ja, die großen Anbieter führen pgvector in ihrer Liste zugelassener Erweiterungen. Zwei Dinge solltest du vorher prüfen: welche Version dort bereitsteht, weil iterative Scans und die neueren Datentypen erst ab bestimmten Versionen verfügbar sind, und ob du maintenance_work_mem und die Zahl paralleler Wartungsprozesse selbst setzen darfst, denn davon hängt die Dauer des Indexbaus ab.

Passt thematisch dazu

Die Vektoren selbst entstehen außerhalb der Datenbank, und was beim Zerlegen der Texte und beim Wechsel des Modells schiefgeht, steht in die Erklärung, wie aus Text ein Zahlenvektor wird .

Persönlich für dich da

Deine Ansprechpartner

Du bist dir nicht sicher, welcher Kurs oder welches Level zu dir passt? Wir beraten dich persönlich und kostenlos.

Yves Hoppe

Yves Hoppe

Weiterbildung & Beratung

Hilft dir, aus dem Datenbanken-Programm den passenden Kurs für deinen Stand zu finden.

Norbert Jansen

Norbert Jansen

Beratung & Inhouse

Plant mit dir Inhouse-Trainings, die auf eure Abläufe und euren Datenbestand zugeschnitten sind.

Vektorsuche im eigenen Bestand statt im Beispielprojekt

Wie ein Vektorindex im laufenden Betrieb gebaut, gemessen und wieder verworfen wird, arbeitest du bei cmt an einer eigenen Datenbank durch, samt der Frage, was das für Speicher und Wartungsfenster bedeutet.