Ein Vector Store speichert Vektoren. Doch woher kommen diese Vektoren? Sie entstehen nicht von selbst. Sie werden erzeugt – durch Embedding-Modelle, die Text in numerische Repräsentationen übersetzen. Diese Transformation ist der Dreh- und Angelpunkt jeder semantischen Sucharchitektur. Ohne qualitativ hochwertige Embeddings bleibt der Vector Store ein leeres Versprechen.
Embeddings sind numerische Fingerabdrücke von Bedeutung. Sie komprimieren einen Text – sei es ein Tweet, ein Paragraf oder ein ganzes Kapitel – in einen Vektor fester Länge. Typischerweise 384, 768 oder 1536 Zahlen, abhängig vom Modell. Diese Zahlen sind nicht zufällig gewählt. Sie sind das Ergebnis eines trainierten neuronalen Netzwerks, das gelernt hat, semantische Ähnlichkeiten in geometrische Nähe zu übersetzen.
Der Clou: Zwei Texte mit ähnlicher Bedeutung erhalten ähnliche Vektoren. Nicht identisch, aber nahe beieinander im hochdimensionalen Raum. Ein Satz über “autonomes Fahren” und einer über “selbstfahrende Autos” landen dicht beieinander, obwohl die Worte verschieden sind. Ein Satz über “Kuchenbacken” wäre weit entfernt. Das ist semantisches Verstehen, kodiert in Mathematik.
Wie genau entsteht so ein Embedding? Die Antwort liegt in der Transformer-Architektur, die seit 2017 die Landschaft des Natural Language Processing dominiert. Ein Embedding-Modell ist im Kern ein Encoder – ein neuronales Netz, das Tokens (Wortfragmente) einliest und durch mehrere Schichten von Attention-Mechanismen schickt. Am Ende steht eine verdichtete Repräsentation: der Embedding-Vektor.
BERT war 2018 der Durchbruch. Bidirectional Encoder Representations from Transformers – ein Modell, das Text bidirektional verarbeitet, also sowohl von links nach rechts als auch von rechts nach links. Das erlaubte ein tieferes Verständnis des Kontexts. Ein Wort wie “Bank” kann eine Sitzgelegenheit oder ein Finanzinstitut bedeuten. BERT erkennt aus dem umgebenden Text, welche Bedeutung gemeint ist.
Doch BERT hatte eine Schwäche: Es war nicht für das Erzeugen von Satz-Embeddings optimiert. Man konnte zwar Embeddings extrahieren, aber sie waren nicht direkt vergleichbar. Hier kam SBERT (Sentence-BERT) ins Spiel. SBERT adaptierte die BERT-Architektur mit einem Siamese Network-Ansatz, der gezielt Satz-Embeddings erzeugt, die via Kosinus-Ähnlichkeit verglichen werden können. Das reduzierte den Rechenaufwand für Ähnlichkeitssuchen drastisch – von Stunden auf Sekunden bei großen Korpora.
Heute ist das Feld explodiert.
Es gibt Dutzende von Embedding-Modellen verschiedener Anbieter. OpenAI bietet seine text-embedding-Modelle, Cohere hat seine eigenen, Google stellt Embeddings via Vertex AI bereit, und es gibt zahlreiche Open-Source-Alternativen wie die Modelle von sentence-transformers. Wie wählt man da aus? Eine Orientierungshilfe bietet der MTEB (Massive Text Embedding Benchmark) – ein umfassendes Leaderboard, das Modelle über verschiedene Tasks hinweg vergleicht: Klassifikation, Clustering, Retrieval, semantische Ähnlichkeit und mehr.
Die Wahl des Embedding-Modells beeinflusst alles. Die Qualität der Suchergebnisse. Die Latenz. Die Kosten. Die Sprach-Unterstützung. Es gibt kein universell bestes Modell, nur das beste Modell für einen spezifischen Anwendungsfall.
Dimensionalität spielt eine Rolle. Ein 384-dimensionaler Vektor ist kompakter und schneller zu verarbeiten als ein 1536-dimensionaler. Aber er transportiert weniger Information. Für einfache Anwendungsfälle reicht niedrige Dimensionalität. Für komplexe Domänen mit feinen semantischen Nuancen lohnt höhere Dimensionalität.
Sprache ist ein weiterer Faktor. Viele Modelle sind primär auf Englisch trainiert. Für mehrsprachige Anwendungen braucht man Modelle, die explizit auf mehreren Sprachen trainiert wurden. Die sentence-transformers-Familie bietet hier robuste Optionen.
Latenz und Kosten schließlich bestimmen die Praktikabilität in der Produktion. Ein API-basiertes Modell wie OpenAI Embeddings ist einfach zu integrieren, verursacht aber laufende Kosten und Latenz durch Netzwerk-Roundtrips. Ein lokal gehostetes Open-Source-Modell erfordert eigene Infrastruktur, bietet aber Kosteneffizienz bei Scale und niedrigere Latenz.
LangChain abstrahiert die Vielfalt der Embedding-Anbieter mit einem
einheitlichen Interface. Zwei zentrale Methoden bilden das Fundament:
embed_documents für das Einbetten mehrerer Texte und
embed_query für das Einbetten einer einzelnen
Suchanfrage.
Warum diese Unterscheidung? Manche Anbieter verwenden unterschiedliche Strategien für Dokumente und Queries. Ein Dokument soll durchsuchbar gemacht werden – es wird indexiert. Eine Query ist der Suchbegriff – sie wird gegen den Index geprüft. Manche Modelle optimieren diese beiden Szenarien separat. LangChain respektiert diese Unterscheidung im Interface.
from langchain_openai import OpenAIEmbeddings
# Embedding-Modell initialisieren
embeddings_model = OpenAIEmbeddings(model="text-embedding-3-small")
# Mehrere Dokumente einbetten
dokumente = [
"Künstliche Intelligenz verändert die Softwareentwicklung grundlegend.",
"Machine Learning erfordert große Datenmengen für das Training.",
"Neuronale Netze basieren auf biologischen Vorbildern.",
"Deep Learning ist eine Unterkategorie des maschinellen Lernens."
]
dokument_embeddings = embeddings_model.embed_documents(dokumente)
# Resultat: Liste von Vektoren
print(f"Anzahl Dokumente: {len(dokument_embeddings)}")
print(f"Dimensionalität: {len(dokument_embeddings[0])}")
# Ausgabe: Anzahl Dokumente: 4, Dimensionalität: 1536Für eine einzelne Query nutzen wir die dedizierte Methode:
# Eine Suchanfrage einbetten
query = "Wie funktionieren neuronale Netze?"
query_embedding = embeddings_model.embed_query(query)
print(f"Query-Dimensionalität: {len(query_embedding)}")
# Ausgabe: Query-Dimensionalität: 1536Die Dimensionalität ist identisch. Das ist wichtig. Query-Vektor und Dokument-Vektoren müssen im gleichen Raum leben, um vergleichbar zu sein. Ein 768-dimensionaler Query-Vektor kann nicht gegen 1536-dimensionale Dokument-Vektoren geprüft werden.
Wenn wir Vektoren haben, können wir sie vergleichen. Die Kosinus-Ähnlichkeit hat sich als De-facto-Standard etabliert, besonders für normierte Embeddings. Sie misst den Winkel zwischen zwei Vektoren, unabhängig von deren Länge.
import numpy as np
def cosine_similarity(vec1, vec2):
"""
Berechnet die Kosinus-Ähnlichkeit zwischen zwei Vektoren.
Wertebereich: -1 (entgegengesetzt) bis 1 (identisch)
Typischerweise liegen Ähnlichkeiten zwischen 0.0 und 1.0
"""
dot_product = np.dot(vec1, vec2)
norm_vec1 = np.linalg.norm(vec1)
norm_vec2 = np.linalg.norm(vec2)
return dot_product / (norm_vec1 * norm_vec2)
# Ähnlichkeit zwischen Query und erstem Dokument
similarity = cosine_similarity(query_embedding, dokument_embeddings[0])
print(f"Ähnlichkeit: {similarity:.4f}")Ein Wert nahe 1 bedeutet hohe Ähnlichkeit. Ein Wert nahe 0 bedeutet geringe Ähnlichkeit. Negative Werte sind theoretisch möglich, kommen aber in der Praxis bei guten Embeddings selten vor. OpenAI empfiehlt explizit Kosinus-Ähnlichkeit für ihre Embeddings. Andere Anbieter mögen andere Metriken präferieren – die Dokumentation gibt Aufschluss.
Embeddings haben ein Problem. Sie erzeugen einen Vektor fester Länge, unabhängig von der Eingabelänge. Ein Tweet wird zu 1536 Zahlen. Ein Buch wird ebenfalls zu 1536 Zahlen. Intuitiv klar: Die Qualität der Repräsentation leidet, wenn zu viel Information in zu wenige Dimensionen gepresst wird.
Dazu kommt: Embedding-Modelle haben Längenbeschränkungen. OpenAIs text-embedding-3-small verarbeitet maximal 8191 Tokens. Ein längeres Dokument wird abgeschnitten. Information geht verloren.
Die Lösung heißt Chunking.
Wir zerlegen große Dokumente in kleinere Abschnitte – Chunks. Jeder Chunk wird einzeln eingebettet. Das Ergebnis ist kein einzelner Vektor pro Dokument, sondern mehrere Vektoren pro Dokument. Bei der Suche werden diese Chunks einzeln durchsucht. Das erhöht die Granularität: Statt eines ganzen Dokuments finden wir den relevanten Abschnitt.
Doch Chunking ist keine triviale Operation. Wie groß sollte ein Chunk sein? Wo sollte man schneiden? Die Antworten hängen vom Kontext ab. LangChain bietet mehrere Splitter-Strategien, jede mit eigenen Vor- und Nachteilen.
Die naheliegendste Strategie: Wir schneiden nach einer bestimmten Länge. Entweder nach Zeichen oder nach Tokens. Token-basiertes Splitting ist präziser, da es die Tokenisierung des Modells respektiert. Ein Token ist nicht immer ein Wort – es kann ein Wortfragment sein. “Softwareentwicklung” könnte als drei Tokens tokenisiert werden: “Software”, “entwicklung”, oder als zwei: “Software”, “entwicklung”.
from langchain_text_splitters import CharacterTextSplitter
# Token-basierter Splitter mit tiktoken (OpenAI-Tokenizer)
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
encoding_name="cl100k_base", # Tokenizer für GPT-3.5/4
chunk_size=100, # 100 Tokens pro Chunk
chunk_overlap=0 # Keine Überlappung zwischen Chunks
)
dokument = """
Künstliche Intelligenz hat die Landschaft der Softwareentwicklung
grundlegend verändert. Machine Learning Modelle können heute Aufgaben
übernehmen, die früher manueller Programmierung bedurften. Deep Learning,
eine Unterkategorie des maschinellen Lernens, hat besonders in der
Bild- und Sprachverarbeitung beeindruckende Fortschritte erzielt.
"""
chunks = text_splitter.split_text(dokument)
print(f"Anzahl Chunks: {len(chunks)}")
for i, chunk in enumerate(chunks):
print(f"Chunk {i+1}: {chunk[:100]}...")Der Parameter chunk_overlap ist interessant. Ein Overlap
von 0 bedeutet: Chunks sind strikt getrennt. Ein Overlap von 20
bedeutet: Die letzten 20 Tokens eines Chunks werden als erste 20 Tokens
des nächsten Chunks wiederholt. Das verhindert, dass wichtige
Informationen an Chunk-Grenzen verloren gehen. Wenn ein Satz genau
zwischen zwei Chunks geteilt wird, könnte seine Bedeutung fragmentiert
werden. Ein Overlap mildert dieses Risiko.
Text hat Struktur. Absätze. Sätze. Überschriften. Warum nicht diese
Struktur nutzen? Der RecursiveCharacterTextSplitter tut
genau das. Er versucht zunächst, auf der höchsten Strukturebene zu
teilen – etwa bei Absätzen. Passt ein Absatz in die gewünschte
Chunk-Größe, bleibt er intakt. Ist er zu groß, geht der Splitter eine
Ebene tiefer – zu Sätzen. Ist ein Satz immer noch zu groß, geht es
weiter zu Wörtern.
from langchain_text_splitters import RecursiveCharacterTextSplitter
# Strukturbasierter Splitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # Zeichenlimit pro Chunk
chunk_overlap=50, # 50 Zeichen Überlappung
length_function=len, # Längenmessung via Zeichenanzahl
separators=["\n\n", "\n", ". ", " ", ""] # Trennzeichen hierarchisch
)
dokument = """
# Einführung in Machine Learning
Machine Learning ist ein Teilgebiet der künstlichen Intelligenz, das
Systemen ermöglicht, aus Daten zu lernen und sich zu verbessern, ohne
explizit programmiert zu werden.
## Supervised Learning
Beim überwachten Lernen wird das Modell mit gelabelten Daten trainiert.
Jedes Trainingsbeispiel besteht aus einem Input und dem gewünschten Output.
## Unsupervised Learning
Unüberwachtes Lernen arbeitet mit ungelabelten Daten. Das Modell muss
selbstständig Muster und Strukturen in den Daten entdecken.
"""
chunks = text_splitter.split_text(dokument)
print(f"Anzahl Chunks: {len(chunks)}")
for i, chunk in enumerate(chunks):
print(f"\n--- Chunk {i+1} ---\n{chunk}")Die Separatoren definieren die Hierarchie. Zuerst wird bei Doppel-Zeilenumbrüchen (Absätzen) getrennt. Dann bei einfachen Zeilenumbrüchen. Dann bei Satzenden (Punkt-Leerzeichen). Dann bei Leerzeichen. Erst als letzte Option wird mitten im Wort getrennt. Das Resultat: Chunks, die semantisch kohärenter sind, weil sie natürliche Einheiten respektieren.
Manche Dokumente haben inhärente Struktur, die über Absätze hinausgeht. Markdown mit Überschriften-Hierarchien. HTML mit Tags. JSON mit Objektgrenzen. Code mit Funktionen und Klassen. Für solche Formate bietet LangChain spezialisierte Splitter.
Ein Markdown-Splitter kann beispielsweise bei Überschriften trennen:
from langchain_text_splitters import MarkdownHeaderTextSplitter
# Markdown-Splitter, der bei Headers trennt
markdown_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=[
("#", "Header 1"),
("##", "Header 2"),
("###", "Header 3"),
]
)
markdown_dokument = """
# Datenbanken
## Relationale Datenbanken
Relationale Datenbanken organisieren Daten in Tabellen mit definierten
Beziehungen. SQL ist die Standardabfragesprache.
## NoSQL-Datenbanken
NoSQL-Datenbanken bieten flexible Schemas und horizontale Skalierbarkeit.
Beispiele sind MongoDB und Cassandra.
### Dokumentendatenbanken
Diese speichern Daten als Dokumente, typischerweise in JSON oder BSON.
### Key-Value-Stores
Key-Value-Stores sind die einfachste NoSQL-Form, optimiert für schnellen Zugriff.
"""
chunks = markdown_splitter.split_text(markdown_dokument)
for chunk in chunks:
print(f"\nMetadata: {chunk.metadata}")
print(f"Content: {chunk.page_content[:100]}...")Der Clou: Diese Splitter erhalten nicht nur den Text, sondern auch Metadaten über die Struktur. Ein Chunk kennt seine Überschriften-Hierarchie. Das erlaubt später präzisere Metadaten-Filter im Vector Store: “Finde mir Informationen, aber nur aus dem Abschnitt über NoSQL-Datenbanken.”
Die bisherigen Strategien nutzen Text- oder Dokumentstruktur als Proxy für semantische Bedeutung. Doch was, wenn wir direkt auf die Semantik schauen? Semantisches Splitting analysiert, wann sich die Bedeutung des Texts signifikant ändert, und trennt dort.
Der konzeptuelle Ansatz: Man erstellt Embeddings für überlappende Textfenster und vergleicht benachbarte Embeddings. Ein großer Sprung in der Ähnlichkeit signalisiert einen Themenwechsel – einen natürlichen Trennpunkt.
from langchain_openai import OpenAIEmbeddings
import numpy as np
def semantic_split(text, window_size=3, threshold=0.75):
"""
Teilt Text basierend auf semantischen Brüchen.
Args:
text: Der zu teilende Text
window_size: Anzahl Sätze pro Fenster
threshold: Ähnlichkeitsschwelle für Trennung
Returns:
Liste von semantisch kohärenten Chunks
"""
embeddings_model = OpenAIEmbeddings()
# Text in Sätze aufteilen (vereinfachte Version)
sentences = text.split('. ')
# Sliding window über Sätze
embeddings = []
for i in range(len(sentences) - window_size + 1):
window = '. '.join(sentences[i:i+window_size])
emb = embeddings_model.embed_query(window)
embeddings.append(emb)
# Ähnlichkeiten zwischen benachbarten Fenstern berechnen
splits = [0]
for i in range(len(embeddings) - 1):
similarity = cosine_similarity(embeddings[i], embeddings[i+1])
if similarity < threshold: # Signifikanter Bedeutungssprung
splits.append(i + window_size)
splits.append(len(sentences))
# Chunks basierend auf Splits erstellen
chunks = []
for i in range(len(splits) - 1):
chunk = '. '.join(sentences[splits[i]:splits[i+1]])
chunks.append(chunk)
return chunks
# Beispiel-Nutzung
dokument = """Machine Learning Modelle benötigen große Datenmengen. Das Training kann Tage oder Wochen dauern. Die Ergebnisse sind oft beeindruckend. Katzen sind beliebte Haustiere. Sie sind unabhängig und pflegeleicht. Viele Menschen schätzen ihre Gesellschaft. Die Quantenphysik revolutioniert unser Weltbild. Teilchen können gleichzeitig an mehreren Orten sein. Das widerspricht unserer Alltagserfahrung."""
chunks = semantic_split(dokument)
for i, chunk in enumerate(chunks):
print(f"\n--- Semantischer Chunk {i+1} ---\n{chunk}")Dieses Verfahren ist rechenintensiver – jedes Fenster muss eingebettet werden. Aber das Resultat sind Chunks, die thematisch kohärent sind. Der Sprung von Machine Learning zu Katzen wird erkannt und getrennt. Ebenso der Übergang zur Quantenphysik.
In einer realen Anwendung fügen sich diese Komponenten zusammen. Man wählt einen Splitter basierend auf der Dokumentstruktur und dem Anwendungsfall. Man wählt ein Embedding-Modell basierend auf Sprache, Domäne und Performance-Anforderungen. Dann orchestriert man den Prozess:
from langchain_core.documents import Document
from langchain_openai import OpenAIEmbeddings
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 1. Dokument vorbereiten
dokument = Document(
page_content="""
Vektordatenbanken sind spezialisierte Speichersysteme für Embeddings. Sie
ermöglichen effiziente semantische Suche über große Datenmengen. Moderne
Implementierungen nutzen Algorithmen wie HNSW für schnelle
Approximate-Nearest-Neighbor-Suche.
Die Wahl der richtigen Chunk-Größe ist entscheidend für die
Retrieval-Qualität. Zu kleine Chunks verlieren Kontext. Zu große Chunks
verwässern die semantische Präzision. Ein typischer Sweet Spot liegt bei
200-500 Tokens.
Metadaten spielen eine zentrale Rolle beim Filtern von Suchergebnissen.
Sie erlauben die Kombination von semantischer und strukturierter Suche.
Typische Metadaten umfassen Dokumenttyp, Autor, Datum und Kategorie.
""",
metadata={"source": "handbuch", "kapitel": "vector-stores"}
)
# 2. Splitter konfigurieren
splitter = RecursiveCharacterTextSplitter(
chunk_size=200,
chunk_overlap=30,
length_function=len
)
# 3. Dokument in Chunks aufteilen
chunks = splitter.split_documents([dokument])
print(f"Dokument wurde in {len(chunks)} Chunks aufgeteilt")
# 4. Embedding-Modell initialisieren
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
# 5. Vector Store mit Embeddings befüllen
vector_store = InMemoryVectorStore(embedding=embeddings)
vector_store.add_documents(
documents=chunks,
ids=[f"chunk_{i}" for i in range(len(chunks))]
)
# 6. Semantische Suche durchführen
query = "Wie beeinflusst die Chunk-Größe die Suchqualität?"
results = vector_store.similarity_search(query, k=2)
print("\n--- Suchergebnisse ---")
for i, doc in enumerate(results, 1):
print(f"\nErgebnis {i}:")
print(f"Content: {doc.page_content}")
print(f"Metadata: {doc.metadata}")Dieser Workflow ist das Rückgrat jeder Retrieval-Anwendung. Dokumente werden gesplittet, eingebettet und indexiert. Queries werden eingebettet und gegen den Index geprüft. Die relevantesten Chunks werden zurückgegeben. Alles andere – Prompts, Generierung, Chain-Orchestrierung – baut darauf auf.
Theorie ist gut. Praxis ist entscheidend. In produktionsnahen Systemen tauchen Fragen auf, die in Tutorials selten diskutiert werden.
Wie groß sollten Chunks wirklich sein? Die Antwort hängt von der Domäne ab. Technische Dokumentation profitiert von kleineren Chunks (100-300 Tokens), die präzise Konzepte isolieren. Narrative Texte brauchen größere Chunks (400-800 Tokens), um Kontext und Erzählfluss zu bewahren. Experimentieren ist unvermeidlich.
Was tun bei mehrsprachigen Dokumenten? Man kann entweder ein mehrsprachiges Embedding-Modell verwenden oder Dokumente vor dem Embedding übersetzen. Ersteres ist eleganter, letzteres manchmal präziser – abhängig vom Modell und der Sprachkombination.
Wie handhabt man Metadaten beim Splitting? LangChain-Splitter propagieren Metadaten von Parent-Dokumenten zu Chunks. Das ist der Standardfall. Manchmal will man aber chunk-spezifische Metadaten hinzufügen – etwa die Position des Chunks im Originaldokument. Das erfordert nachträgliche Anreicherung.
Was ist mit Updates? Dokumente ändern sich. In Vector Stores sollten IDs konsequent genutzt werden, um Updates statt Duplikate zu erzeugen. Ein Dokument wird re-gesplittet, alte Chunks mit denselben IDs werden überschrieben. Konsistenz bewahren erfordert Disziplin im ID-Management.
Embeddings und Chunking sind die unsichtbare Infrastruktur der semantischen Suche. Der Endnutzer sieht nur die Suchergebnisse. Doch darunter liegt ein sorgfältig orchestrierter Prozess: Text wird analysiert, strukturiert, zerlegt, eingebettet, indexiert. Jede Entscheidung – Modellwahl, Chunk-Größe, Splitting-Strategie – wirkt sich auf die Qualität aus.
LangChain abstrahiert die Komplexität, ohne sie zu verschleiern. Die Interfaces sind einheitlich, die Flexibilität bleibt erhalten. Man kann zwischen Modellen wechseln, zwischen Splittern tauschen, zwischen Vector Stores migrieren – alles mit minimalem Code-Umbau.
Was bleibt? Ein tieferes Verständnis dafür, dass semantische Suche keine Blackbox ist. Sie ist ein konstruiertes System aus Modellen, Algorithmen und Datenstrukturen. Embeddings übersetzen Bedeutung in Mathematik. Chunking strukturiert Chaos. Vector Stores organisieren den semantischen Raum. Und darauf bauen wir Anwendungen, die verstehen, was Nutzer wirklich suchen – nicht nur, welche Keywords sie eingeben.
Im nächsten Kapitel werden wir uns den Retrieval-Strategien widmen: Wie kombiniert man Suche mit Filterung? Was ist Maximal Marginal Relevance? Wie orchestriert man Hybrid Search? Doch das Fundament ist gelegt. Wir wissen jetzt, woher die Vektoren kommen und wie sie entstehen.