BLEIBEN SIE INFORMIERT

Melden Sie sich für unsere Newsletter an und erhalten Sie exklusive Updates zu IT-Trends und Neuigkeiten der ORIDX AG.

BLEIBEN SIE INFORMIERT

Melden Sie sich für unsere Newsletter an und erhalten Sie exklusive Updates zu IT-Trends und Neuigkeiten der ORIDX AG.

PostgreSQL als KI-Enabler: Teil 1 – Wie pgvector a...
7 Minuten Lesezeit (1480 Worte)

PostgreSQL als KI-Enabler: Teil 2 – Semantische Suche und RAG in der Praxis

Im ersten Teil dieser Serie wurde erläutert, was pgvector ist, wie Embeddings entstehen und welche Distanzoperatoren für welche Anwendungsfälle geeignet sind. Dieser Beitrag schließt dort an und zeigt den vollständigen Weg von rohen Texten über die semantische Suche bis zur Antwort eines lokalen Sprachmodells.

Das Beispiel basiert auf einem Reiseangebotskatalog mit 20 Einträgen. Das Ziel: Eine Suchanfrage in natürlicher Sprache findet semantisch passende Reiseangebote, auch wenn kein einziges Wort der Anfrage in den Dokumenten vorkommt. Die Ergebnisse werden anschließend als Kontext an ein lokales LLM übergeben, das daraus eine strukturierte Antwort formuliert.

Die gesamte Pipeline läuft lokal. Es werden keine externen APIs benötigt, keine Daten verlassen das System.

Voraussetzungen und Einrichtung

Abhängigkeiten installieren

pip install psycopg2-binary pgvector langchain-ollama numpy ollama

Embedding-Modell und LLM herunterladen

ollama pull mxbai-embed-large

ollama pull llama3

mxbai-embed-large erzeugt Vektoren mit 1024 Dimensionen und liefert für deutschsprachige Texte zuverlässige semantische Repräsentationen. llama3 übernimmt die Antwortgenerierung auf Basis des bereitgestellten Kontexts.

Datenbankstruktur anlegen

CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE vacation (
id SERIAL PRIMARY KEY,
content TEXT, 
embedding vector(1024)
); 

Die Tabelle ist bewusst schlank gehalten. In einer Produktionsumgebung würden weitere Metadatenspalten ergänzt, etwa Kategorie, Sprache oder Datum, die später als WHERE-Filter in der Vektorsuche eingesetzt werden können. 

Die Indexierung

Die Indexierungsphase läuft einmalig beim initialen Befüllen der Datenbank. Neue Dokumente können später einzeln nachträglich eingebettet und eingefügt werden. 

Die Datenbasis

texts = [ 
"Städtetrip Paris: Eiffelturm, Louvre und Croissants am Morgen.",
"Roadtrip durch die USA: Route 66 von Chicago bis Los Angeles.",
"Strandurlaub auf Bali: Reisfelder, Tempel und türkisblaues Wasser.",
"Wandern in Neuseeland: Fjorde, Gletscher und endlose Weite.",
"Kreuzfahrt durch die Karibik: Inseln, Rum und weißer Sand.",
"Tokio im Frühling: Kirschblüten, Sushi-Bars und Neonlichter in Shinjuku.",
"Safari in Kenia: Löwen, Elefanten und Sonnenuntergang in der Savanne.",
"Winterurlaub in Lappland: Nordlichter, Rentiere und Schneehütten.",
"Backpacking durch Südostasien: Thailand, Vietnam und Kambodscha.",
"Kulturreise nach Marokko: Marrakesch, Gewürze und die Sahara.",
"Skiurlaub in den Schweizer Alpen: Powder, Fondue und Bergpanoramen.",
"Island Rundreise: Geysire, Wasserfälle und heiße Quellen.",
"Kyoto entdecken: Geishas, Bambushaine und jahrhundertealte Zen-Gärten.",
"Australien Ostküste: Sydney Opera House, Great Barrier Reef und Koalas."
"Städtetrip nach New York: Times Square, Central Park und Broadway.",
"Urlaub in Griechenland: Santorini, blaue Kuppeln und Meeresfrüchte.",
"Trekking im Himalaya: Nepal, Everest Base Camp und Sherpa-Kultur.",
"Rondreis durch Portugal: Lissabon, Porto und die Algarve-Küste.",
"Osaka Streetfood Tour: Takoyaki, Ramen und lebhafte Märkte.",
"Kreuzfahrt durch Norwegen: Fjorde, Wasserfälle und Mitternachtssonne."
 ] 

Jeder Eintrag beschreibt ein Reiseangebot in einem kurzen, prägnanten Satz. Das Wort „Japan” taucht in keinem Eintrag auf, obwohl Tokio, Kyoto und Osaka eindeutig Japan-Reisen beschreiben. Genau diese semantische Lücke wird die Vektorsuche später schließen. 

import psycopg2
import numpy as np
from langchain_ollama import OllamaEmbeddings

# Embedding-Modell initialisieren
embeddings = OllamaEmbeddings(model="mxbai-embed-large")

# Jeden Text in einen Vektor mit 1024 Dimensionen umwandeln 
embeddings_list = []
for text in texts:
embeddings_list.append(embeddings.embed_query(text))

# Verbindung zur Datenbank aufbauen
conn = psycopg2.connect(
dbname="vector",
user="postgres",
password="postgres",
host="localhost",
port="5432" )
cur = conn.cursor()
# Originaltext und Vektor gemeinsam in die Tabelle schreiben
for i in range(len(embeddings_list)):
cur.execute(
"INSERT INTO vacation (content, embedding) VALUES (%s, %s)",
 (texts[i], embeddings_list[i])
) 
conn.commit()
cur.close()
conn.close() 

Jeder der 20 Texte wird einzeln durch mxbai-embed-large verarbeitet. Das Modell gibt für jeden Text eine Liste von 1024 Dezimalzahlen zurück, die die semantische Bedeutung des Textes im Vektorraum repräsentieren. Anschließend werden Originaltext und Vektor gemeinsam in dieselbe Tabellenzeile geschrieben. Nach dem commit() enthält die Tabelle 20 Einträge, jeder mit Text und zugehörigem Embedding.

Entscheidend ist, dass für die Indexierung und die spätere Suche dasselbe Embedding-Modell verwendet wird. Nur wenn beide Seiten, die Dokumente und die Anfragen, im selben Vektorraum liegen, sind die berechneten Distanzen inhaltlich aussagekräftig. 

Die semantische Suche

Die Suchanfrage vorbereiten:

from pgvector.psycopg2 import register
vector query = "Ich möchte Japan besuchen, Tokio erkunden und alte Tempel in Kyoto sehen."
query_embedding = embeddings.embed_query(query)
query_embedding = np.array(query_embedding, dtype=np.float32) 

Die Suchanfrage wird durch dasselbe Modell eingebettet wie die gespeicherten Dokumente. Das Ergebnis ist ein einzelner Vektor mit 1024 Dimensionen, der die semantische Bedeutung der Anfrage repräsentiert.

np.array(..., dtype=np.float32) konvertiert den Python-Vektor in das Format, das pgvector erwartet. register_vector(conn) registriert den pgvector-Datentyp bei psycopg2, damit der Python-Treiber und PostgreSQL den Typ korrekt austauschen.

conn = psycopg2.connect(
    dbname="vector",
    user="postgres",
    password="postgres",
    host="localhost",
    port="5432" )
register_vector(conn)
cur = conn.cursor()
cur.execute("""
    SELECT id, content,
    1 - (embedding <=> %s) AS cosine_similarity
    FROM vacation
    ORDER BY embedding <=> %s
    LIMIT 5;
    """, (query_embedding, query_embedding))
    results = cur.fetchall()
    for row in results:
        print(f"{row[2]:.2f} {row[1]}")
cur.close()
conn.close() 

Der Query-Vektor wird zweimal übergeben: einmal für die Berechnung des Ähnlichkeitswerts (1 - (embedding <=> %s) und einmal für die Sortierung (ORDER BY embedding <=> %s). PostgreSQL berechnet für jede Zeile in der Tabelle die Cosine Distance zum Query-Vektor und gibt die fünf ähnlichsten Einträge zurück. 

Das Ergebnis

Das Wort „Japan” kommt in keinem der gefundenen Dokumente vor. Die Suchanfrage enthält umgekehrt weder „Tokio” noch „Kyoto” als exakten Treffer, dennoch landen alle drei Japan-Einträge unter den Top 3. Das Embedding-Modell hat gelernt, dass Tokio, Kyoto und Osaka zu Japan gehören, und bildet diese Beziehung im Vektorraum ab. Eine klassische SQL-Suche mit LIKE oder Full-Text-Search hätte hier keine Treffer geliefert. 

0.91 Tokio im Frühling: Kirschblüten, Sushi-Bars und Neonlichter in Shinjuku.
0.88 Kyoto entdecken: Geishas, Bambushaine und jahrhundertealte Zen-Gärten.
0.84 Osaka Streetfood Tour: Takoyaki, Ramen und lebhafte Märkte.
0.61 Backpacking durch Südostasien: Thailand, Vietnam und Kambodscha.
0.54 Wandern in Neuseeland: Fjorde, Gletscher und endlose Weite. 

Die RAG-Architektur

Die Vektorsuche liefert die semantisch relevantesten Textabschnitte. Im nächsten Schritt werden diese Abschnitte als Kontext in einen Prompt eingebettet und an ein lokales LLM übergeben. Das Ergebnis ist eine strukturierte, auf den gefundenen Dokumenten basierende Antwort.

RAG steht für Retrieval Augmented Generation und beschreibt ein Architekturmuster, das drei Schritte kombiniert. Im Retrieval-Schritt wird die Anfrage in einen Vektor umgewandelt und die semantisch ähnlichsten Dokumente werden aus der Datenbank abgerufen. Im Augmentation-Schritt werden die gefundenen Dokumente in einen Prompt eingebettet, der dem LLM als Kontext zur Verfügung gestellt wird. Im Generationsschritt erzeugt das LLM eine Antwort, die ausschließlich auf dem bereitgestellten Kontext basiert, nicht auf seinem Trainingswissen. Eine ausführliche Einführung in das RAG-Konzept, seine Vorteile und typische Einsatzgebiete bietet der Blogbeitrag „Wenn KI den Spickzettel zückt”.

Der entscheidende Mechanismus ist die explizite Einschränkung im Prompt: Das Modell wird angewiesen, ausschließlich auf Basis der bereitgestellten Informationen zu antworten. Das verhindert, dass das Modell Inhalte erfindet, die nicht in den Dokumenten vorhanden sind.

import ollama
# Alle Ergebnisse aus der Vektorsuche als Kontext verwenden
context = "\n".join([f"- {row[1]}" for row in results])
    # Prompt zusammenbauen
prompt = f"""

Du bist ein Reiseberater. Beantworte die folgende Anfrage ausschließlich auf Basis der unten aufgeführten Reiseangebote. Sollte die Anfrage anhand der verfügbaren Angebote nicht beantwortet werden können, weise ausdrücklich darauf hin.
Anfrage: {query}
Verfügbare Reiseangebote: {context}
Antwort:""" 
response = ollama.generate(
model="llama3",
prompt=prompt
)
print(response["response"]) 

Das LLM antwortet auf Basis der übergebenen Chunks: 

Basierend auf den verfügbaren Reiseangeboten empfehle ich folgende Optionen für Ihre Japan-Reise:
1. Tokio im Frühling Ideal für die Kirschblüten-Saison. Shinjuku bietet ein eindrucksvolles Nachtleben mit Neonlichtern, und zahlreiche Sushi-Bars machen den kulinarischen Einstieg in Japan besonders authentisch.
2. Kyoto Die kulturelle Hauptstadt Japans. Jahrhundertealte Zen-Gärten, Bambushaine und die Möglichkeit, Geishas zu begegnen, machen Kyoto zur ersten Adresse für alle, die das traditionelle Japan erleben möchten.
3. Osaka Bekannt für seine lebhafte Streetfood-Kultur mit Takoyaki und Ramen sowie seine belebten Märkte. Ideal als Ergänzung zu Tokio und Kyoto im Rahmen einer Rundreise. 

Das LLM hat ausschließlich die übergebenen Textabschnitte als Grundlage verwendet. Es hat keine eigenständigen Informationen über Japan ergänzt, sondern die vorhandenen Daten strukturiert und in eine lesbare Antwort überführt. 

Erweiterungsmöglichkeiten

Das hier gezeigte Beispiel ist eine funktionierende Grundlage. In einer produktionsreifen Implementierung würden folgende Aspekte ergänzt. Die Vektorsuche kann durch WHERE-Bedingungen eingeschränkt werden, etwa auf bestimmte Kategorien, Zeiträume oder Zugriffsrechte, da PostgreSQL die Kombination von Vektorsuche und relationalen Filtern in einer einzigen Abfrage erlaubt. Längere Dokumente sollten vor der Indexierung in kleinere Abschnitte aufgeteilt werden, da zu große Chunks die semantische Aussagekraft des Vektors verwässern und zu kleine Chunks den Kontext verlieren. Überlappende Chunks mit einem definierten Stride-Wert sind hier eine bewährte Methode. In einer produktiven Umgebung sollten Datenbankverbindungen über einen Connection Pool verwaltet, alle Embedding-Aufrufe mit Timeouts versehen und fehlerhafte Anfragen protokolliert werden. Der Wechsel von mxbai-embed-large zu einem anderen Embedding-Modell erfordert eine vollständige Neuindexierung aller Dokumente, da sich der Vektorraum ändert. Eine Versionierung der Embeddings in der Datenbank erleichtert spätere Migrationen erheblich. 

Fazit

Die drei Phasen, Indexierung, semantische Suche und RAG, bilden zusammen eine vollständige KI-Pipeline auf Basis von PostgreSQL. Das Embedding-Modell übersetzt Bedeutung in Zahlen. pgvector macht diese Zahlen durchsuchbar. Der Prompt-Mechanismus stellt sicher, dass das LLM ausschließlich auf Basis der gefundenen Dokumente antwortet und keine Informationen erfindet, die nicht in den Daten vorhanden sind.

Was dieses Beispiel zeigt, ist kein akademisches Konstrukt, sondern ein direkt übertragbares Muster. Der Austausch des Embedding-Modells oder des LLMs erfordert keine strukturellen Änderungen an der Architektur, solange die Konsistenz des Vektorraums gewahrt bleibt. Das Ergebnis ist eine lokal betreibbare RAG-Architektur ohne externe APIs und ohne zusätzliche Infrastruktur, die über eine bestehende PostgreSQL-Instanz hinausgeht. 

Seminarempfehlungen

Ähnliche Beiträge

 

Kommentare

Derzeit gibt es keine Kommentare. Schreibe den ersten Kommentar!
Donnerstag, 09. April 2026

Sicherheitscode (Captcha)