Die wichtigsten Erkenntnisse
- Embeddings sind Vektoren – Listen von Zahlen die semantische Bedeutung kodieren. Ähnliche Texte haben ähnliche Vektoren
- Cosine Similarity misst den Winkel zwischen Vektoren – nicht die Distanz. Werte nahe 1.0 = sehr ähnlich
- Für Prototypen: Chroma (lokal, kostenlos). Für Produktion: Pinecone oder pgvector je nach Stack
- Embedding-Modell und Vektordatenbank sind unabhängig – du kannst sie frei kombinieren
- Metadaten-Filter sind oft wichtiger als die Vektorsuche selbst – immer mitplanen
Was sind Embeddings?
Ein Embedding ist eine numerische Darstellung von Text (oder Bildern, Audio, etc.) als Vektor – eine Liste von Zahlen. Das Besondere: Texte mit ähnlicher Bedeutung haben ähnliche Vektoren, auch wenn sie völlig unterschiedliche Wörter verwenden.
"Wie trainiere ich ein ML-Modell?"
→ [0.023, -0.187, 0.445, 0.012, -0.334, ...] # 1536 Zahlen
"Welche Schritte brauche ich um Machine Learning anzuwenden?"
→ [0.019, -0.201, 0.438, 0.008, -0.341, ...] # sehr ähnliche Zahlen!
"Das Wetter ist heute schön."
→ [0.412, 0.089, -0.234, 0.567, 0.123, ...] # völlig andere Zahlen
Diese Eigenschaft macht Embeddings zur Grundlage für semantische Suche, RAG und viele andere KI-Anwendungen.
Wie Ähnlichkeit gemessen wird
Cosine Similarity
Die häufigste Metrik ist Cosine Similarity – sie misst den Winkel zwischen zwei Vektoren, nicht ihre Distanz.
Cosine Similarity = 1.0 → identische Bedeutung
Cosine Similarity = 0.9 → sehr ähnlich
Cosine Similarity = 0.7 → verwandt
Cosine Similarity = 0.0 → keine Beziehung
Cosine Similarity = -1.0 → gegensätzlich
import numpy as np
def cosine_similarity(a: list[float], b: list[float]) -> float:
a, b = np.array(a), np.array(b)
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
# Beispiel
vec_frage = [0.023, -0.187, 0.445]
vec_antwort = [0.019, -0.201, 0.438]
vec_unrelated = [0.412, 0.089, -0.234]
print(cosine_similarity(vec_frage, vec_antwort)) # ~0.999 – sehr ähnlich
print(cosine_similarity(vec_frage, vec_unrelated)) # ~0.1 – nicht verwandt
Andere Metriken
- Euclidean Distance (L2): Misst den Abstand im Raum – funktioniert, aber Cosine ist bei normalisierten Vektoren meist besser
- Dot Product: Schneller als Cosine, aber abhängig von der Vektorgröße
- Manhattan Distance: Selten bei Embeddings genutzt
Embeddings generieren
OpenAI Embeddings API
from openai import OpenAI
client = OpenAI()
def get_embedding(text: str, model: str = "text-embedding-3-small") -> list[float]:
text = text.replace("\n", " ")
response = client.embeddings.create(input=[text], model=model)
return response.data[0].embedding
# Einzelner Text
embedding = get_embedding("Was ist Machine Learning?")
print(f"Dimensionen: {len(embedding)}") # 1536
# Batch (günstiger als einzeln)
texts = ["Text 1", "Text 2", "Text 3"]
response = client.embeddings.create(input=texts, model="text-embedding-3-small")
embeddings = [item.embedding for item in response.data]
Kosten: text-embedding-3-small kostet $0.02 pro Million Tokens – für die meisten Projekte vernachlässigbar.
Lokale Embeddings (kostenlos)
from sentence_transformers import SentenceTransformer
# Mehrsprachiges Modell – gut für Deutsch
model = SentenceTransformer("intfloat/multilingual-e5-large")
texts = ["Was ist Machine Learning?", "Wie funktioniert KI?"]
embeddings = model.encode(texts, normalize_embeddings=True)
print(embeddings.shape) # (2, 1024)
Vektordatenbanken im Vergleich
| Datenbank | Typ | Stärken | Wann nutzen? |
|---|---|---|---|
| Chroma | Lokal / Cloud | Einfach, Python-nativ, kostenlos | Prototypen, kleine Projekte |
| Pinecone | Cloud (managed) | Skalierbar, einfache API, Metadaten-Filter | Produktion, wenn kein eigener Server |
| Weaviate | Self-hosted / Cloud | Hybrid Search, GraphQL, sehr flexibel | Komplexe Anforderungen |
| pgvector | PostgreSQL-Extension | Kein neues System, SQL-Queries | Wenn PostgreSQL schon im Stack |
| Qdrant | Self-hosted / Cloud | Schnell, Rust-basiert, gute Filter | Performance-kritische Anwendungen |
| FAISS | Library (Meta) | Extrem schnell, kein Server | Wenn du alles selbst kontrollieren willst |
Chroma – für den Einstieg
import chromadb
from openai import OpenAI
client = OpenAI()
chroma = chromadb.Client()
collection = chroma.create_collection("meine-dokumente")
# Dokumente hinzufügen
dokumente = [
"Machine Learning ist ein Teilbereich der KI.",
"RAG kombiniert Retrieval mit Sprachmodellen.",
"Embeddings kodieren semantische Bedeutung als Vektoren.",
]
# Embeddings generieren und speichern
embeddings = [
client.embeddings.create(input=[doc], model="text-embedding-3-small").data[0].embedding
for doc in dokumente
]
collection.add(
documents=dokumente,
embeddings=embeddings,
ids=[f"doc_{i}" for i in range(len(dokumente))]
)
# Suche
results = collection.query(
query_embeddings=[
client.embeddings.create(
input=["Was sind Vektoren in der KI?"],
model="text-embedding-3-small"
).data[0].embedding
],
n_results=2
)
print(results["documents"])
pgvector – wenn PostgreSQL schon im Stack
-- Extension aktivieren
CREATE EXTENSION vector;
-- Tabelle mit Vektor-Spalte
CREATE TABLE dokumente (
id SERIAL PRIMARY KEY,
inhalt TEXT,
embedding vector(1536),
kategorie TEXT,
erstellt_am TIMESTAMP DEFAULT NOW()
);
-- Index für schnelle Suche
CREATE INDEX ON dokumente USING ivfflat (embedding vector_cosine_ops);
-- Ähnlichkeitssuche mit Metadaten-Filter
SELECT inhalt, 1 - (embedding <=> '[0.1, 0.2, ...]'::vector) AS similarity
FROM dokumente
WHERE kategorie = 'technik'
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector
LIMIT 5;
import psycopg2
from openai import OpenAI
client = OpenAI()
conn = psycopg2.connect("postgresql://user:pass@localhost/db")
def semantic_search(query: str, kategorie: str = None, top_k: int = 5):
query_embedding = client.embeddings.create(
input=[query], model="text-embedding-3-small"
).data[0].embedding
with conn.cursor() as cur:
if kategorie:
cur.execute("""
SELECT inhalt, 1 - (embedding <=> %s::vector) AS score
FROM dokumente WHERE kategorie = %s
ORDER BY embedding <=> %s::vector LIMIT %s
""", (query_embedding, kategorie, query_embedding, top_k))
else:
cur.execute("""
SELECT inhalt, 1 - (embedding <=> %s::vector) AS score
FROM dokumente ORDER BY embedding <=> %s::vector LIMIT %s
""", (query_embedding, query_embedding, top_k))
return cur.fetchall()
Metadaten-Filter: oft wichtiger als die Vektorsuche
Reine Vektorsuche findet semantisch ähnliche Dokumente – aber oft willst du zusätzlich filtern:
# Nur Dokumente aus 2024, Kategorie "Technik", Sprache "de"
results = collection.query(
query_embeddings=[query_embedding],
n_results=5,
where={
"$and": [
{"jahr": {"$eq": 2024}},
{"kategorie": {"$eq": "technik"}},
{"sprache": {"$eq": "de"}}
]
}
)
Praxistipp: Plane Metadaten von Anfang an mit. Nachträglich hinzufügen bedeutet alle Dokumente neu zu indexieren.
Hybrid Search: Vektor + Keyword
Reine Vektorsuche hat Schwächen bei exakten Begriffen (Produktnummern, Namen, Abkürzungen). Hybrid Search kombiniert Vektorsuche mit klassischer BM25-Keyword-Suche:
from rank_bm25 import BM25Okapi
# BM25 für Keyword-Suche
tokenized_docs = [doc.split() for doc in dokumente]
bm25 = BM25Okapi(tokenized_docs)
def hybrid_search(query: str, alpha: float = 0.5, top_k: int = 5):
"""alpha=1.0: nur Vektor, alpha=0.0: nur BM25"""
# Vektor-Scores
vector_scores = get_vector_scores(query) # normalisiert 0-1
# BM25-Scores
bm25_scores = bm25.get_scores(query.split())
bm25_scores = bm25_scores / bm25_scores.max() # normalisieren
# Kombinieren
combined = alpha * vector_scores + (1 - alpha) * bm25_scores
return sorted(enumerate(combined), key=lambda x: x[1], reverse=True)[:top_k]
Faustregel: alpha=0.6 (60% Vektor, 40% BM25) ist ein guter Startpunkt für die meisten Anwendungen.
Häufige Probleme
Problem: Suche findet irrelevante Ergebnisse
Ursache: Embedding-Modell passt nicht zum Domänenvokabular, oder Chunks zu groß. Lösung: Kleinere Chunks (200–400 Token), domänenspezifisches Embedding-Modell, Hybrid Search.
Problem: Suche ist zu langsam
Ursache: Kein Index, oder Index-Typ falsch gewählt.
Lösung: ivfflat für Geschwindigkeit, hnsw für bessere Recall-Rate. Bei pgvector: SET ivfflat.probes = 10.
Problem: Unterschiedliche Ergebnisse bei gleicher Anfrage
Ursache: Approximate Nearest Neighbor (ANN) Algorithmen sind nicht deterministisch.
Lösung: nprobe/ef_search erhöhen für mehr Genauigkeit (auf Kosten der Geschwindigkeit).
Problem: Embedding-Modell gewechselt, alte Daten passen nicht mehr
Ursache: Embeddings verschiedener Modelle sind nicht kompatibel. Lösung: Alle Dokumente neu einbetten wenn das Modell wechselt. Modellversion in Metadaten speichern.