SSD Nodes Learn 🎉 VPS ab $5.50/Monat
Anleitungen Matt ConnorVon Matt Connor

Vektordatenbank auf dem VPS: pgvector oder Qdrant?

Anwendung und Index auf demselben VPS: Vergleichen Sie pgvector, Qdrant, Chroma und Brute Force. So berechnen Sie Indexgröße und RAM-Bedarf korrekt.

Was eine Vektordatenbank auf einem VPS tatsächlich kostet

Der Betrieb einer Vektordatenbank auf einem VPS (Virtual Private Server) beseitigt das Problem, für das jeder Managed-Anbieter eine Lösung verkauft. Ihre Anwendung und Ihr Index befinden sich auf derselben Maschine. Eine Suchanfrage läuft daher über einen Loopback-Socket und nicht über das Netzwerk. Übrig bleiben die Kosten, die schon immer die tatsächlichen Kosten waren: Text in Vektoren umzuwandeln. Dazu kommen zwei weitere Kostenfaktoren: die Zeit zum Erstellen des Index und der Arbeitsspeicher, den der Index während des Betriebs belegt.

Dadurch verschiebt sich die Bedeutung einzelner Entscheidungen. Region und Round-Trip-Zeit zum Endpunkt sind nicht mehr Ihr Problem. Entscheidend sind die Anzahl der Vektoren, ihre Dimensionen und vier Bytes pro Dimension. Daraus ergibt sich, ob der Index in den Arbeitsspeicher passt, den Sie jeden Monat mieten.

Wo die Millisekunden auf einem einzelnen System tatsächlich anfallen

Verfolgen Sie eine Ähnlichkeitsabfrage durch einen selbst gehosteten Stack.

  1. Der Abfragetext wird von einem Embedding-Modell in einen Vektor umgewandelt. Auf einer CPU dauert das bei einer kurzen Zeichenfolge mehrere bis mehrere hundert Millisekunden. Auf einer GPU sind es einstellige Millisekundenwerte.
  2. Der Vektor wird an den Store gesendet. Über eine Loopback-TCP-Verbindung oder einen Unix-Domain-Socket dauert das nur einen Bruchteil einer Millisekunde.
  3. Der Store durchsucht seinen Index und gibt die nächstgelegenen Zeilen zurück.
  4. Ihr Code liest den passenden Text und erstellt daraus einen Prompt.

Schritt 1 ist in dieser Aufstellung normalerweise der größte Zeitanteil. Auf diesen Schritt konzentrieren sich Anbieter gehosteter Dienste im Wettbewerb. Auf einem einzelnen System fällt dieser Anteil jedoch kaum an. Schätzen Sie die Aufteilung nicht. Messen Sie beide Seiten auf Ihrem eigenen Server.

curl http://127.0.0.1:11434/api/embed -s -o /dev/null \
  -w 'embed: %{time_total}s\n' \
  -d '{"model": "nomic-embed-text", "input": "how do I rotate the api key"}'

Führen Sie anschließend \timing on in psql vor der Suchabfrage aus. Gibt der erste Befehl embed: 0.184312s aus und beantwortet psql die Abfrage mit Time: 4.201 ms, ist die Optimierung des Index die falsche Aufgabe: Ihre Latenz wird durch das Embedding-Modell verursacht. Das Embedding-Modell lokal mit Ollama ausführen legt Schritt 1 auf dieselbe CPU wie die Schritte 2 und 3. Dadurch konkurrieren beide Hälften um dieselben CPU-Kerne und denselben Arbeitsspeicher. Die von diesem Store umschlossene Schleife für Ingest und Retrieval wird in der Anleitung zur selbst gehosteten RAG-Pipeline beschrieben. RAG steht für Retrieval-Augmented Generation: Sie durchsuchen Ihre eigenen Dokumente und fügen die besten Treffer in einen Prompt ein.

Unter etwa hunderttausend Vektoren: alle durchsuchen

Ein vollständiger Scan vergleicht die Abfrage mit jedem gespeicherten Vektor. Der Recall ist per Definition vollständig. Dafür wird kein Index und kein Build-Schritt benötigt. Außerdem kann der Index nicht hinter dem Datenbestand zurückbleiben.

Die Berechnung zeigt, ab wann dieser Ansatz nicht mehr ausreicht. Ein Scan liest n * d * 4 Byte pro Abfrage, wobei n die Anzahl der Vektoren und d die Dimension ist. Bei 100,000 Vektoren mit jeweils 768 Dimensionen sind das 307 MB pro Abfrage. Eine moderne CPU verarbeitet diese Daten sequenziell in wenigen Dutzend Millisekunden. Bei 5 Millionen Vektoren sind es 15 GB pro Abfrage. Dann handelt es sich nicht mehr um eine praktikable Abfrage.

Speichern Sie die Vektoren daher in SQLite und führen Sie den Vergleich in NumPy aus.

import sqlite3, numpy as np

db = sqlite3.connect("docs.db")
db.execute("CREATE TABLE IF NOT EXISTS docs (id INTEGER PRIMARY KEY, body TEXT, vec BLOB)")

def add(body, vec):
    v = np.asarray(vec, dtype=np.float32)
    v /= np.linalg.norm(v)
    db.execute("INSERT INTO docs (body, vec) VALUES (?, ?)", (body, v.tobytes()))
    db.commit()

def search(query_vec, k=5):
    rows = db.execute("SELECT id, body, vec FROM docs").fetchall()
    mat = np.frombuffer(b"".join(r[2] for r in rows), dtype=np.float32).reshape(len(rows), -1)
    q = np.asarray(query_vec, dtype=np.float32)
    q /= np.linalg.norm(q)
    scores = mat @ q
    return [(rows[i][0], rows[i][1], float(scores[i])) for i in np.argsort(-scores)[:k]]

Beide Seiten werden auf die Einheitslänge skaliert. Dadurch ist das Skalarprodukt die Kosinusähnlichkeit, und ein höherer Wert entspricht einer größeren Übereinstimmung. Laden Sie mat einmal beim Start statt einmal pro Abfrage. Dadurch verlässt der SQLite-Lesezugriff vollständig den zeitkritischen Pfad.

Messen Sie die Laufzeit auf Ihrem eigenen System, bevor Sie diesen Ansatz verwerfen.

import time
t = time.perf_counter()
search(q)
print(f"{(time.perf_counter() - t) * 1000:.1f} ms")

Die Grenzen dieses Ansatzes sind eindeutig: Ein Prozess hält die gesamte Matrix im RAM. Außerdem gibt es weder eine Filterung nach Metadaten noch ein Konzept für gleichzeitige Schreibzugriffe. Wenn einer dieser Punkte das Problem verursacht, sollten Sie auf eine andere Lösung wechseln. SQLite selbst ist ein leistungsfähiger serverseitiger Datenspeicher. Das wird im Leitfaden zu SQLite im Produktivbetrieb ausführlich behandelt. Wenn Ihre tatsächliche Arbeitslast statt der Bereitstellung von Zeilen das Durchsuchen von Spalten umfasst, ist der Vergleich von DuckDB und SQLite die nützlichere Lektüre.

pgvector, wenn Sie bereits Postgres verwenden

Wenn Ihre Anwendung bereits über eine Postgres-Datenbank verfügt, fügt pgvector die geringste zusätzliche Komplexität hinzu. Es ist eine Erweiterung und kein Dienst. Die Vektoren liegen in einer normalen Tabelle neben der von ihnen beschriebenen Zeile. Eine gefilterte Suche ist dadurch eine WHERE-Klausel und kein zweites System, das synchron gehalten werden muss.

Ubuntu 24.04 enthält das Paket in der Komponente universe.

sudo apt update
sudo apt install -y postgresql-16-pgvector
sudo -u postgres psql -d yourdb -c 'CREATE EXTENSION vector;'

Dieses Paket ist Stand August 2026 pgvector 0.6.0 und damit deutlich älter als die aktuelle Upstream-Version. Insbesondere iterative Indexscans benötigen Version 0.8. Installieren Sie diese daher aus dem Repository des PostgreSQL-Projekts.

sudo apt install -y postgresql-common
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh
sudo apt install -y postgresql-17-pgvector

Ersetzen Sie 17 durch die Hauptversion Ihres Servers. sudo -u postgres psql -tAc 'SHOW server_version' gibt diese aus. Wenn Sie das Erweiterungspaket für die falsche Hauptversion installieren, schlägt CREATE EXTENSION fehl, weil Postgres nur im share-Verzeichnis der laufenden Version sucht:

ERROR:  could not open extension control file "/usr/share/postgresql/16/extension/vector.control": No such file or directory

Das Schema besteht aus gewöhnlichem SQL mit einem zusätzlichen Datentyp.

CREATE TABLE chunks (
  id        bigserial PRIMARY KEY,
  doc_id    bigint NOT NULL,
  body      text   NOT NULL,
  embedding vector(768)
);

SELECT id, body FROM chunks
ORDER BY embedding <=> '[0.013, -0.021, 0.004]'
LIMIT 5;

<=> ist die Cosinusdistanz, <-> ist die L2-Distanz (euklidische Distanz), und <#> ist das negative innere Produkt. Verwenden Sie die Distanzfunktion, für die Ihr Embedding-Modell trainiert wurde. Bei der falschen Auswahl tritt kein Fehler auf. Die Ergebnisse sind lediglich unbemerkt schlechter.

Ohne Index durchsucht diese Abfrage jede Zeile exakt. Das ist die Postgres-Variante der oben beschriebenen Brute-Force-Suche und erzielt denselben vollständigen Recall. Mit max_parallel_workers_per_gather weisen Sie der Suche mehr CPU-Kerne zu. Führen Sie diesen Schritt zuerst aus und erstellen Sie den Index erst danach. So haben Sie eine Recall-Baseline, mit der Sie den Index vergleichen können.

Qdrant, wenn der Index die Datenbank überfordert

Qdrant ist ein dedizierter Vektorspeicher, der in Rust geschrieben ist. Qdrant ist sinnvoll, wenn der Index so groß ist, dass sein Aufbau nicht mit dem Postgres Ihrer Anwendung um Ressourcen konkurrieren soll, oder wenn Sie Payload-Filterung und Quantisierung benötigen, die pgvector nicht bietet.

docker run -d --name qdrant \
  -p 127.0.0.1:6333:6333 -p 127.0.0.1:6334:6334 \
  -e QDRANT__SERVICE__API_KEY="$(openssl rand -hex 32)" \
  -v "$(pwd)/qdrant_storage:/qdrant/storage:z" \
  qdrant/qdrant

Port 6333 stellt die REST-API (Representational State Transfer) und ein Dashboard unter /dashboard bereit. Port 6334 stellt gRPC bereit. Auf einem öffentlich erreichbaren VPS sind zwei Details wichtig. Laut der Qdrant-Dokumentation läuft der Dienst standardmäßig „ohne Verschlüsselung oder Authentifizierung“. Außerdem bindet der -p 6333:6333 aus der Kurzanleitung an alle Schnittstellen. Docker veröffentlicht den Port dadurch trotz einer ufw-Regel, weil Docker eigene Weiterleitungsregeln schreibt. Binden Sie an 127.0.0.1 und setzen Sie einen API-Schlüssel. Eine Qdrant-Instanz, die über eine öffentliche IP-Adresse ohne Schlüssel erreichbar ist, macht Ihre Dokumente öffentlich zugänglich.

curl -s http://127.0.0.1:6333/collections -H "api-key: $QDRANT_API_KEY"

Eine erfolgreiche Antwort sieht wie {"result":{"collections":[]},"status":"ok","time":0.00002} aus. Wenn {"status":{"error":"Unauthorized"}} zurückgegeben wird, ist der Headername oder der Schlüssel falsch. Wenn überhaupt keine Antwort zurückkommt, läuft der Container nicht oder ist an eine andere Adresse gebunden. Ob dieser Container für Ihren Server die richtige Wahl ist, ist die übliche Frage bei zustandsbehafteten Diensten. Daher gilt der Vergleich zwischen Docker- und Host-Datenbank hier unverändert.

Chroma und sein Zweck

Chroma ist der kürzeste Weg von null zu einer funktionierenden Retrieval-Demo.

pip install chromadb
chroma run --path /srv/chroma

Der Dienst lauscht auf Port 8000, und chromadb.HttpClient(host="localhost", port=8000) stellt eine Verbindung zu ihm her. Chroma wird mit einer standardmäßigen Embedding-Funktion ausgeliefert. Für einen ersten Prototyp ist daher kein separater Modellserver erforderlich.

Die Nachteile sollten klar sein. Chroma ist angenehm, weil es die Entscheidungen verbirgt, um die es in diesem Leitfaden geht: die Wahl der Distanzmetrik und die benötigte RAM-Größe. Für einen Prototyp ist das richtig. Für ein System, für dessen Störungen Sie Bereitschaftsdienst leisten, ist es falsch. Wenn Ihre Daten bereits in Postgres liegen, führt die Migration zu Chroma einen zusätzlichen Prozess und ein Synchronisationsproblem ein. Damit würde ein Problem gelöst, das pgvector nicht hat.

Wie viel RAM benötigt der Index

Beginnen Sie mit den Rohvektoren. Sie bilden die Untergrenze, die sich durch keine Optimierung verändert.

bytes = number_of_vectors * dimensions * 4

Vier Bytes entsprechen einem 32-Bit-Gleitkommawert pro Dimension. Die Kapazitätsplanung von Qdrant verwendet zusätzlich einen Faktor von 1.5 für Metadaten und temporäre Segmente, die während der Optimierung entstehen:

memory_size = number_of_vectors * vector_dimension * 4 bytes * 1.5

Hier ist diese Formel auf eine Million Vektoren angewendet, bei den Dimensionen, die reale Embedding-Modelle erzeugen.

ChartRAM for 1 million vectors, by embedding dimension
The data behind this chart
[
  {
    "label": "384 dims",
    "raw_gib": 1.43,
    "with_overhead_gib": 2.15
  },
  {
    "label": "768 dims",
    "raw_gib": 2.86,
    "with_overhead_gib": 4.29
  },
  {
    "label": "1024 dims",
    "raw_gib": 3.81,
    "with_overhead_gib": 5.72
  },
  {
    "label": "1536 dims",
    "raw_gib": 5.72,
    "with_overhead_gib": 8.58
  },
  {
    "label": "3072 dims",
    "raw_gib": 11.44,
    "with_overhead_gib": 17.17
  }
]

Das sind die Ergebnisse der Formel in Gibibytes (GiB), keine Messwerte. Betrachten Sie sie als die Größe des Speicherbereichs, den Sie im RAM freihalten müssen. Ein Modell mit 768 Dimensionen wie nomic-embed-text benötigt für eine Million Chunks etwa 4.29 GiB. Das passt in einen 8-GB-Tarif, sodass noch Speicher für Postgres verfügbar bleibt. Derselbe Korpus mit 3072 Dimensionen benötigt 17.17 GiB und passt nicht.

Der entscheidende Stellhebel ist die erste Spalte dieser Tabelle, nicht die letzte. Wenn Sie die Anzahl der Dimensionen halbieren, halbiert sich dauerhaft jedes nachgelagerte Byte. Ein Modell mit 768 Dimensionen, das auf einer öffentlichen Bestenliste etwas schlechter abschneidet, ist auf einem VPS häufig die bessere technische Wahl. Der pgvector-Typ halfvec speichert anschließend 16-Bit-Gleitkommawerte. Dadurch halbiert sich der Speicherbedarf erneut. Die Indizierung erfolgt über einen Ausdruck:

CREATE INDEX ON chunks USING hnsw ((embedding::halfvec(768)) halfvec_cosine_ops);

Beachten Sie vor der Auswahl eines Modells eine Einschränkung. Der pgvector-Typ vector akzeptiert bis zu 16,000 Dimensionen. Die HNSW- und IVFFlat-Indizes von pgvector unterstützen jedoch nur 2,000 Dimensionen. Oberhalb dieses Werts indizieren Sie einen halfvec-Cast, der bis zu 4,000 Dimensionen erreicht, oder Sie verzichten vollständig auf einen Index.

Kosten von m und ef_construction beim Erstellen

HNSW (hierarchical navigable small world) ist der Index, auf den sowohl pgvector als auch Qdrant zurückgreifen. Er besteht aus einem geschichteten Graphen. Jeder Vektor ist ein Knoten mit Verbindungen zu nahe gelegenen Knoten. Eine Suche bewegt sich über diese Verbindungen schrittweise in Richtung der Abfrage, statt alle Vektoren zu lesen.

m gibt an, wie viele Verbindungen jeder Knoten behält. Die Faiss-Dokumentation veranschlagt für HNSW (d * 4 + m * 2 * 4) Byte pro Vektor und empfiehlt, m zwischen 4 und 64 zu halten. Führen Sie das bei 768 Dimensionen über eine Million Vektoren aus.

ChartCost of raising m at 768 dimensions, 1 million vectors
The data behind this chart
[
  {
    "label": "m = 8",
    "link_bytes_per_vector": 64,
    "total_gib": 2.92
  },
  {
    "label": "m = 16 (default)",
    "link_bytes_per_vector": 128,
    "total_gib": 2.98
  },
  {
    "label": "m = 32",
    "link_bytes_per_vector": 256,
    "total_gib": 3.1
  },
  {
    "label": "m = 64",
    "link_bytes_per_vector": 512,
    "total_gib": 3.34
  }
]

Der Größenunterschied fällt deutlich aus. Eine Erhöhung vom Standardwert m = 16 auf m = 64 fügt pro Vektor 512 Byte für Verbindungen hinzu. Dem stehen 3072 Byte für die Vektordaten gegenüber. Die Gesamtgröße steigt dadurch von 2.98 GiB auf 3.34 GiB. Das entspricht ungefähr 12 Prozent. Bei diesen Dimensionen ist m nicht der entscheidende Speicherverbrauch. Die Vektoren benötigen den Speicher.

m verursacht vor allem beim Erstellen und Einfügen Kosten. Beim Einfügen eines Knotens müssen so viele Nachbarn gefunden und verknüpft werden. ef_construction gibt die Größe der Kandidatenliste an, die der Builder beim Einfügen jedes Knotens berücksichtigt. Ein höherer Wert erzeugt einen besseren Graphen und verlängert den Erstellungsprozess. Die Größe des fertigen Index ändert sich dadurch nicht.

SET maintenance_work_mem = '4GB';
SET max_parallel_maintenance_workers = 7;
CREATE INDEX ON chunks USING hnsw (embedding vector_cosine_ops)
  WITH (m = 16, ef_construction = 64);

maintenance_work_mem legt fest, ob die Erstellung Minuten oder Stunden dauert. pgvector erstellt den Graphen im Arbeitsspeicher, sofern er dort hineinpasst. Wenn das nicht mehr der Fall ist, meldet pgvector dies:

NOTICE:  hnsw graph no longer fits into maintenance_work_mem after 100000 tuples
DETAIL:  Building will take significantly more time.
HINT:  Increase maintenance_work_mem to speed up builds.

Diese Meldung ist die wichtigste Zeile, die pgvector ausgibt. Sie bedeutet, dass die Erstellung auf einen deutlich langsameren Pfad gewechselt hat. Brechen Sie den Vorgang ab, erhöhen Sie die Einstellung über den zuvor berechneten RAM-Wert und starten Sie die Erstellung erneut. Überwachen Sie den Fortschritt in einer zweiten Sitzung:

SELECT phase, round(100.0 * blocks_done / nullif(blocks_total, 0), 1) AS pct
FROM pg_stat_progress_create_index;

HNSW meldet zunächst initializing und anschließend loading tuples. Wenn die Erstellung auf einem nicht ausgelasteten Datenträger lange bei einem niedrigen Prozentsatz bleibt, liegt das Problem bei maintenance_work_mem und nicht bei einer blockierten Abfrage.

Für die Planung sind zwei Fakten wichtig. In der README von pgvector steht, dass HNSW im Vergleich zu IVFFlat „langsamer erstellt wird und mehr Speicher verwendet“. Dafür bietet HNSW einen besseren Kompromiss zwischen Geschwindigkeit und Recall. Außerdem kann HNSW auf einer leeren Tabelle erstellt werden. IVFFlat muss dagegen zuerst k-means über repräsentative Daten ausführen. Auf einer leeren Tabelle liefert IVFFlat daher einen schlechten Recall. Bei einem neuen Schema können Sie HNSW vorab erstellen.

ef_search: der Regler nach dem Build

m und ef_construction sind im Index unveränderlich. ef_search nicht. Der Wert legt fest, wie viele Kandidaten die Suche während des Durchlaufs durch den Graphen behält. Sie können ihn pro Sitzung oder pro Abfrage ändern.

SET hnsw.ef_search = 100;
SELECT id, body FROM chunks ORDER BY embedding <=> $1 LIMIT 5;

Der Standardwert ist 40. Wenn Sie ihn erhöhen, steigt der Recall, ebenso die Latenz. Wenn Sie ihn verringern, sinken beide Werte. Dies ist die einzige Recall-Einstellung, die Sie ohne einen erneuten Build ändern können. Stimmen Sie sie daher auf einen festen Satz von Abfragen ab, deren korrekte Antworten Sie bereits kennen, und beenden Sie die Anpassung, sobald sich der Recall nicht mehr verbessert.

Es gibt eine Falle. ef_search funktioniert schlecht mit einer selektiven WHERE-Klausel, weil der Index eine feste Anzahl von Kandidaten zurückgibt und der Filter anschließend angewendet wird. Ein Filter, der die meisten Zeilen verwirft, kann dazu führen, dass Sie weniger als LIMIT Ergebnisse erhalten, obwohl passende Zeilen in der Tabelle vorhanden sind. pgvector 0.8 löst dieses Problem mit iterativen Scans:

SET hnsw.iterative_scan = relaxed_order;

Der Index wird anschließend erneut nach weiteren Kandidaten durchsucht, bis das Limit erfüllt ist, höchstens jedoch bis hnsw.max_scan_tuples. Der Standardwert dafür ist 20000. strict_order behält die exakte Sortierung nach der Distanz bei und verursacht höhere Kosten. Diese Funktion ist im Ubuntu-0.6.0-Paket nicht enthalten. Fehlende Zeilen bei einem Filter zeigen Ihnen, dass sie fehlt.

Warum der Index in den RAM passen muss

Eine HNSW-Suche ist ein Durchlauf über einen Graphen. Jeder Sprung liest einen Knoten, der an einer vom vorherigen Knoten unabhängigen Stelle gespeichert ist. Das Zugriffsmuster ist daher nahezu zufällig, und Read-Ahead hilft nicht. Solange sich der Graph im RAM befindet, ist jeder Sprung ein Speicherzugriff. Sobald das nicht mehr der Fall ist, kann ein Sprung zu einem Festplattenzugriff werden. Eine Suche, die einige hundert Knoten berührt, führt dann zu einigen hundert Lesezugriffen.

Die Dokumentation von Qdrant formuliert dafür eine klare Größenordnung: „Wenn Sie halb so viele Vektoren im RAM speichern, wird sich die Suchlatenz ungefähr verdoppeln.“ Planen Sie auf dieser Grundlage.

Wenn der Index tatsächlich nicht in den RAM passt, ist jede Option ein Kompromiss, den Sie bewusst wählen sollten.

  • Ordnen Sie die Vektoren per Memory-Mapping zu, damit das Betriebssystem häufig verwendete Seiten im Cache hält und selten verwendete Seiten auf der Festplatte belässt. Darunter ist schneller NVMe-Speicher erforderlich, damit die Leistung akzeptabel bleibt.
  • Quantisieren Sie die Vektoren, indem Sie jede Dimension mit einem Byte statt mit vier Bytes speichern. Dadurch reduziert sich der Speicherbedarf für die Vektoren auf ein Viertel. Der messbare Verlust beim Recall bleibt gering.
  • Konvertieren Sie in pgvector in halfvec. Dadurch halbiert sich der Speicherbedarf, bei einem geringeren Recall-Verlust als bei einer Quantisierung auf ein Byte.
  • Verwenden Sie ein kleineres Modell für die Embeddings. Das ist die kostengünstigste Lösung. Sie wird jedoch häufig übersprungen, weil dafür der gesamte Korpus erneut eingebettet werden muss.

Fehlerbilder und die angezeigten Meldungen

could not open extension control file. Das pgvector-Paket für die verwendete Postgres-Hauptversion ist nicht installiert. Geben Sie die Version mit sudo -u postgres psql -tAc 'SHOW server_version' aus und installieren Sie das passende postgresql-NN-pgvector.

ERROR: expected 768 dimensions, not 1536. Der Spaltentyp und das Modell stimmen nicht überein. Sie haben die Embedding-Modelle geändert, aber keine neuen Embeddings erzeugt. Eine teilweise Behebung ist hier nicht möglich, weil Vektoren aus zwei verschiedenen Modellen überhaupt nicht vergleichbar sind. Daher müssen alle Zeilen neu erzeugt werden.

Die Abfrage ist langsam und EXPLAIN zeigt einen sequenziellen Scan. Die Operator-Klasse des Index und der Operator der Abfrage stimmen nicht überein. vector_cosine_ops unterstützt nur <=>. Führen Sie EXPLAIN ANALYZE für die Abfrage aus und suchen Sie nach Index Scan using ... on chunks. Wenn stattdessen Seq Scan on chunks angezeigt wird, erstellen Sie den Index mit der Operator-Klasse neu, die zum tatsächlich verwendeten Abfrageoperator passt.

Weniger Zeilen als LIMIT und eine WHERE-Klausel ist vorhanden. Das ist die oben beschriebene Falle bei der Filterung. Erhöhen Sie hnsw.ef_search oder wechseln Sie zu pgvector 0.8 und setzen Sie hnsw.iterative_scan.

Der Indexaufbau endet, weil der Prozess beendet wurde, und in psql ist kein Fehler zu sehen. Wenn maintenance_work_mem auf den größten Teil des Arbeitsspeichers des Systems gesetzt ist, während shared_buffers und Ihre Anwendung ebenfalls Speicher benötigen, beendet der Kernel den Prozess durch den Out-of-Memory-Killer. sudo dmesg -T | grep -i 'killed process' zeigt die Zeile mit der Bezeichnung postgres. Verringern Sie den Wert oder erstellen Sie den Index auf einem größeren Plan und spielen Sie den Dump anschließend wieder ein.

Auswahl

Wenn Sie bereits Postgres einsetzen und weniger als einige Millionen Vektoren haben, verwenden Sie pgvector. Der Index liegt direkt neben den Daten, die Filterung erfolgt als WHERE-Klausel, und Ihre vorhandene Sicherung deckt ihn bereits ab. Wenn der Index groß genug ist, dass Sie eine eigene Speicherobergrenze benötigen, oder wenn Sie umfangreiche Payload-Filterung brauchen, betreiben Sie Qdrant daneben und nehmen einen zusätzlichen zu verwaltenden Dienst in Kauf.

Bei weniger als ungefähr hunderttausend Vektoren sollten Sie zunächst die Brute-Force-Suche messen, bevor Sie etwas installieren. Eine vollständige Suche mit perfektem Recall und ohne Erstellungsschritt ist bei dieser Größe kein Kompromiss. Sie ist die richtige Lösung. Ein approximativer Index würde stattdessen zusätzliche Abstimmung und RAM-Belastung verursachen, obwohl Sie die dadurch eingesparten Millisekunden bisher gar nicht benötigt haben.

FAQ

Benötige ich eine dedizierte Vektordatenbank, oder reicht Postgres aus?

Wenn Ihre Daten bereits in Postgres liegen, reicht pgvector deutlich länger aus, als die meisten Vergleiche nahelegen. Die Vektoren werden in einer gewöhnlichen Spalte gespeichert. Eine gefilterte Suche ist daher eine WHERE-Klausel, und Ihr vorhandenes Backup umfasst auch den Index. Wechseln Sie zu einem dedizierten Speicher wie Qdrant, wenn die Vektor-Workload eine eigene Speicherobergrenze benötigt oder wenn Sie Payload-Filterung und Quantisierung brauchen, die pgvector nicht bietet.

Wie viele Vektoren kann ein einzelner VPS speichern?

Berechnen Sie den Wert, statt ihn zu schätzen, und verwenden Sie dazu number_of_vectors * dimensions * 4 bytes * 1.5. Eine Million Vektoren mit 768 Dimensionen benötigen ungefähr 4.3 GiB. Damit kann ein Tarif mit 8 GB neben Postgres noch ausreichend Speicher bieten. Eine Million Vektoren mit 3072 Dimensionen benötigen ungefähr 17 GiB und erfordern einen deutlich größeren Tarif. Die Dimensionszahl Ihres Embedding-Modells beeinflusst diesen Wert am stärksten. Wählen Sie das Modell daher unter Berücksichtigung des Speicherbedarfs aus.

Warum ist meine Vektorsuche langsam, wenn der Index auf derselben Maschine liegt?

Bei einer einzelnen Maschine ist das Netzwerk nicht die Ursache. Prüfen Sie daher die beiden relevanten Punkte. Messen Sie erstens den Embedding-Aufruf separat. Das Erzeugen des Abfragevektors auf der CPU dauert häufig deutlich länger als die Suche selbst. Prüfen Sie zweitens, ob der Index im RAM liegt. Eine HNSW-Suche springt zufällig durch einen Graphen. Sobald der Graph auf die Festplatte ausgelagert wird, kann jeder Sprung einen Festplattenzugriff auslösen. Die eigene Empfehlung von Qdrant lautet, dass sich die Suchlatenz ungefähr verdoppelt, wenn nur halb so viele Vektoren im RAM gehalten werden.

Sollte ich überhaupt einen HNSW-Index erstellen?

Bei weniger als ungefähr hunderttausend Vektoren ist das nicht erforderlich. Ein vollständiger Scan liest pro Abfrage n * d * 4 Byte. Bei 100,000 Vektoren mit 768 Dimensionen sind das 307 MB. Eine moderne CPU verarbeitet diese Daten bei perfektem Recall und ohne Erstellungsphase in einigen zehn Millisekunden. Messen Sie den Scan zunächst auf Ihrer eigenen Hardware. Erstellen Sie den Index, wenn die gemessene Scan-Zeit tatsächlich zu langsam ist, nicht nur, weil ein Benchmark-Artikel dies empfiehlt.

Welche tatsächlichen Kosten entstehen durch die Erhöhung von m?

Vor allem bei Erstellungs- und Einfügezeiten, deutlich weniger beim Speicherbedarf. Bei 768 Dimensionen erhöht die Änderung vom Standardwert m = 16 auf m = 64 die Anzahl der Graphverknüpfungen um 512 Byte pro Vektor. Die Vektordaten benötigen 3072 Byte pro Vektor. Dadurch steigt der Gesamtspeicherbedarf um ungefähr 12 Prozent. Jeder Einfügevorgang muss jedoch viermal so viele Nachbarn finden und verknüpfen. Passen Sie zuerst ef_search an. Dieser Wert lässt sich ohne zusätzliche Kosten ändern und erfordert keinen Neuaufbau.

#vector-database#rag#pgvector#qdrant#self-hosting