Traditionelle Keyword-Suche versagt lautlos. Ein Nutzer sucht nach «Laptop startet nicht», aber Ihre Dokumentation sagt «Computer bootet nicht». Ein Kunde fragt nach «Kündigungsrichtlinie», während Ihr Hilfe-Center «Abo-Beendigung» verwendet. Diese Vokabular-Mismatches passieren ständig — und Ihre Suche liefert nichts, während die perfekte Antwort in Ihrer Datenbank existiert.
Das Problem reicht tiefer als Synonyme. Keyword-Suche kann keine konzeptionellen Anfragen («Wie beschleunige ich meinen Workflow»), Negationen («Fehler ohne Internetverbindung») oder Fragen verarbeiten, bei denen die Antwort existiert, aber völlig andere Terminologie verwendet. In einer Welt, in der Nutzer Google-Level-Verständnis erwarten, fühlt sich traditionelle Suche kaputt an.
TL;DR: Semantische Suche nutzt Embeddings, um Anfragen nach Bedeutung statt Keywords zu matchen. Beste Ergebnisse kommen von Hybrid-Ansätzen, die Vektor-Ähnlichkeit mit BM25-Keyword-Matching kombinieren, plus Re-Ranking für Präzision. Dieser Leitfaden behandelt Embedding-Auswahl, Vektor-Speicherung mit pgvector, Hybrid-Suche-Implementierung, Cross-Encoder Re-Ranking und Relevanz-Tuning — mit Produktions-Code-Beispielen.
Keyword-Suche operiert auf einer einfachen Prämisse: Finde Dokumente, die die Suchbegriffe enthalten. Dieser Ansatz, über Jahrzehnte mit Techniken wie TF-IDF und BM25 verfeinert, funktioniert gut, wenn Nutzer und Content-Ersteller das gleiche Vokabular teilen. In der Praxis tun sie das oft nicht.
Betrachten Sie diese realen Suchfehler:
Synonym-Listen helfen, aber sie erfordern manuelle Pflege und können nicht skalieren, um jede mögliche Variation abzudecken. Nutzer drücken die gleiche Absicht auf unzählige Weisen aus.
Jenseits von Vokabular hat Keyword-Suche Schwierigkeiten mit Anfragen, die Verständnis erfordern:
Schlechte Suche hat messbare Konsequenzen:
Semantische Suche löst das Vokabular-Mismatch-Problem, indem sie Bedeutung statt Wörter vergleicht. Statt zu fragen «Welche Dokumente enthalten diese Begriffe?», fragt semantische Suche «Welche Dokumente handeln vom gleichen Thema wie diese Anfrage?»
Die Kerntechnologie ist das Embedding-Modell — ein neuronales Netzwerk, das trainiert ist, Text in dichte Vektoren (Arrays von Zahlen) zu konvertieren. Diese Vektoren haben eine bemerkenswerte Eigenschaft: semantisch ähnliche Texte produzieren Vektoren, die im Embedding-Raum nahe beieinander liegen.
«Auto-Reparatur» und «Fahrzeug-Wartung» haben fast keine Wort-Überlappung, aber ihre Embeddings sind nahezu identisch. Das Modell hat gelernt, dass diese Phrasen das Gleiche bedeuten.
Moderne Embedding-Modelle werden auf massiven Text-Korpora trainiert und lernen Beziehungen zwischen Konzepten. Sie verstehen, dass «CEO» ähnlich zu «Geschäftsführer» ist, dass «Python» in einem Programmierkontext sich von «Python» in einem Biologie-Kontext unterscheidet, und dass «laufen» Joggen, Software-Betrieb oder einen fliessenden Strom bedeuten kann — wobei der Kontext bestimmt, welche Bedeutung gilt.
Semantische Suche folgt diesem Ablauf:
Dieser Prozess matcht nach Bedeutung, unabhängig von spezifischen Wortwahlen. Eine Suche nach «Laptop bootet nicht» findet Dokumentation über «Computer-Startfehler», weil ihre Embeddings ähnlich sind.
Embeddings sind exzellent bei:
Embeddings haben Schwierigkeiten mit:
Diese Limitierungen sind der Grund, warum Produktionssysteme typischerweise semantische Suche mit Keyword-Matching kombinieren.
Die Wahl des richtigen Embedding-Modells beeinflusst die Suchqualität erheblich. Modelle unterscheiden sich in Dimensionen, Trainingsdaten, unterstützten Sprachen und Performance-Charakteristiken.
| Modell | Anbieter | Dimensionen | Stärken | Überlegungen |
|---|---|---|---|---|
| text-embedding-3-large | OpenAI | 3072 | Beste allgemeine Performance, einfache API | Kosten pro Token, erfordert API-Aufrufe |
| text-embedding-3-small | OpenAI | 1536 | Gute Balance von Qualität und Kosten | Leicht geringere Qualität als large |
| embed-v3 | Cohere | 1024 | Exzellent multilingual, Such-/Klassifikations-Varianten | Kommerzielle API |
| bge-large-en-v1.5 | BAAI (Open Source) | 1024 | Nahezu kommerzielle Qualität, self-hostable | Englisch-fokussiert |
| e5-large-v2 | Microsoft (Open Source) | 1024 | Starke Retrieval-Performance, Instruction-Tuned-Version verfügbar | Erfordert Query-Prefixe für beste Ergebnisse |
| all-MiniLM-L6-v2 | Sentence Transformers | 384 | Schnell, klein, gute Baseline | Geringere Qualität als grössere Modelle |
| multilingual-e5-large | Microsoft (Open Source) | 1024 | 100+ Sprachen, stark cross-lingual | Grössere Modellgrösse |
Wählen Sie basierend auf Ihren Anforderungen:
So generieren Sie Embeddings mit OpenAI und Open-Source-Modellen:
# OpenAI Embeddings
from openai import OpenAI
client = OpenAI()
def embed_openai(texts: list[str], model: str = "text-embedding-3-small") -> list[list[float]]:
"""Generiere Embeddings mit OpenAI API."""
response = client.embeddings.create(
input=texts,
model=model
)
return [item.embedding for item in response.data]
# Verwendung
documents = ["Laptop startet nicht", "Computer bootet nicht"]
embeddings = embed_openai(documents)
# Open-Source Embeddings mit sentence-transformers
from sentence_transformers import SentenceTransformer
# Modell einmal laden, für alle Embeddings wiederverwenden
model = SentenceTransformer("BAAI/bge-large-en-v1.5")
def embed_local(texts: list[str]) -> list[list[float]]:
"""Generiere Embeddings mit lokalem Modell."""
# BGE-Modelle empfehlen Instruction-Prefix für Queries
embeddings = model.encode(texts, normalize_embeddings=True)
return embeddings.tolist()
# Für BGE-Modelle: Prefix für Queries hinzufügen (nicht für Dokumente)
def embed_query_bge(query: str) -> list[float]:
"""Embedde eine Suchanfrage mit BGE Instruction-Prefix."""
prefixed = f"Represent this sentence for searching relevant passages: {query}"
return model.encode(prefixed, normalize_embeddings=True).tolist()
Sobald Sie Embeddings haben, brauchen Sie effiziente Speicherung und Retrieval. Für einen umfassenden Vergleich der Optionen siehe unseren Vektordatenbanken-Leitfaden. Hier fokussieren wir auf praktische Implementierung mit pgvector, das sich in bestehende PostgreSQL-Infrastruktur integriert.
pgvector fügt Vektor-Ähnlichkeitssuche zu PostgreSQL hinzu. Vorteile umfassen:
pgvector funktioniert gut für Datensätze bis zu einigen Millionen Vektoren. Für grössere Skalierung oder spezialisierte Features erwägen Sie zweckgebaute Vektordatenbanken wie Pinecone, Weaviate oder Qdrant.
-- pgvector Extension aktivieren
CREATE EXTENSION IF NOT EXISTS vector;
-- Tabelle für Dokument-Embeddings erstellen
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
embedding vector(1536), -- Passend zu den Dimensionen Ihres Modells
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP DEFAULT NOW()
);
-- HNSW Index für schnelle Ähnlichkeitssuche erstellen
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- Index für Metadaten-Filterung
CREATE INDEX ON documents USING GIN (metadata);
import psycopg2
from psycopg2.extras import execute_values
def store_documents(conn, documents: list[dict]):
"""Speichere Dokumente mit ihren Embeddings."""
with conn.cursor() as cur:
execute_values(
cur,
"""
INSERT INTO documents (title, content, embedding, metadata)
VALUES %s
""",
[
(doc["title"], doc["content"], doc["embedding"], doc.get("metadata", {}))
for doc in documents
],
template="(%(title)s, %(content)s, %(embedding)s::vector, %(metadata)s::jsonb)"
)
conn.commit()
def semantic_search(conn, query_embedding: list[float], limit: int = 10) -> list[dict]:
"""Finde Dokumente mit höchster Ähnlichkeit zum Query-Embedding."""
with conn.cursor() as cur:
cur.execute(
"""
SELECT id, title, content, metadata,
1 - (embedding <=> %s::vector) AS similarity
FROM documents
ORDER BY embedding <=> %s::vector
LIMIT %s
""",
(query_embedding, query_embedding, limit)
)
columns = [desc[0] for desc in cur.description]
return [dict(zip(columns, row)) for row in cur.fetchall()]
def filtered_search(conn, query_embedding: list[float],
filters: dict, limit: int = 10) -> list[dict]:
"""Semantische Suche mit Metadaten-Filtern."""
with conn.cursor() as cur:
cur.execute(
"""
SELECT id, title, content, metadata,
1 - (embedding <=> %s::vector) AS similarity
FROM documents
WHERE metadata @> %s::jsonb
ORDER BY embedding <=> %s::vector
LIMIT %s
""",
(query_embedding, filters, query_embedding, limit)
)
columns = [desc[0] for desc in cur.description]
return [dict(zip(columns, row)) for row in cur.fetchall()]
HNSW-Index-Parameter beeinflussen den Genauigkeit/Geschwindigkeit-Tradeoff:
SET hnsw.ef_search = 100. Höhere Werte verbessern Recall auf Kosten der Latenz.Beginnen Sie mit Defaults und tunen Sie basierend auf Ihren Genauigkeitsanforderungen. Benchmarken Sie mit repräsentativen Queries, um die richtige Balance zu finden.
Reine semantische Suche verpasst exakte Matches. Reine Keyword-Suche verpasst semantische Matches. Hybrid-Suche kombiniert beide und erfasst die Stärken jedes Ansatzes.
Betrachten Sie diese Queries:
Hybrid-Suche handhabt alle drei Fälle, indem sie beide Suchtypen ausführt und Ergebnisse kombiniert.
Fügen Sie Volltext-Suchfähigkeit neben Vektorsuche hinzu:
-- tsvector-Spalte für BM25-artige Suche hinzufügen
ALTER TABLE documents ADD COLUMN search_vector tsvector;
-- Suchvektor aus Titel und Content befüllen
UPDATE documents SET search_vector =
setweight(to_tsvector('german', coalesce(title, '')), 'A') ||
setweight(to_tsvector('german', coalesce(content, '')), 'B');
-- GIN Index für schnelle Textsuche erstellen
CREATE INDEX ON documents USING GIN (search_vector);
-- Trigger um search_vector aktuell zu halten
CREATE OR REPLACE FUNCTION update_search_vector() RETURNS trigger AS $$
BEGIN
NEW.search_vector :=
setweight(to_tsvector('german', coalesce(NEW.title, '')), 'A') ||
setweight(to_tsvector('german', coalesce(NEW.content, '')), 'B');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER documents_search_update
BEFORE INSERT OR UPDATE ON documents
FOR EACH ROW EXECUTE FUNCTION update_search_vector();
Reciprocal Rank Fusion (RRF) ist eine einfache, effektive Methode, um gerankte Ergebnisse aus mehreren Quellen zu kombinieren:
def reciprocal_rank_fusion(
rankings: list[list[str]],
k: int = 60
) -> list[tuple[str, float]]:
"""
Kombiniere mehrere Rankings mit Reciprocal Rank Fusion.
Args:
rankings: Liste von gerankten Dokument-ID-Listen
k: Konstante um zu verhindern, dass hohe Ränge dominieren (Standard 60)
Returns:
Liste von (doc_id, score) Tupeln, sortiert nach fusioniertem Score
"""
scores = {}
for ranking in rankings:
for rank, doc_id in enumerate(ranking, start=1):
if doc_id not in scores:
scores[doc_id] = 0
scores[doc_id] += 1 / (k + rank)
return sorted(scores.items(), key=lambda x: x[1], reverse=True)
def hybrid_search(conn, query: str, query_embedding: list[float],
limit: int = 10) -> list[dict]:
"""
Kombiniere semantische und Keyword-Suche mit RRF.
"""
# Semantische Suchergebnisse holen
with conn.cursor() as cur:
cur.execute(
"""
SELECT id::text FROM documents
ORDER BY embedding <=> %s::vector
LIMIT 50
""",
(query_embedding,)
)
semantic_ids = [row[0] for row in cur.fetchall()]
# Keyword-Suchergebnisse holen
with conn.cursor() as cur:
cur.execute(
"""
SELECT id::text FROM documents
WHERE search_vector @@ plainto_tsquery('german', %s)
ORDER BY ts_rank(search_vector, plainto_tsquery('german', %s)) DESC
LIMIT 50
""",
(query, query)
)
keyword_ids = [row[0] for row in cur.fetchall()]
# Rankings fusionieren
fused = reciprocal_rank_fusion([semantic_ids, keyword_ids])
top_ids = [doc_id for doc_id, score in fused[:limit]]
# Vollständige Dokumente abrufen
with conn.cursor() as cur:
cur.execute(
"""
SELECT id, title, content, metadata
FROM documents
WHERE id = ANY(%s::int[])
""",
([int(i) for i in top_ids],)
)
docs = {row[0]: dict(zip(["id", "title", "content", "metadata"], row))
for row in cur.fetchall()}
# In fusionierter Rang-Reihenfolge zurückgeben
return [docs[int(doc_id)] for doc_id in top_ids if int(doc_id) in docs]
Initiales Retrieval (ob semantisch, Keyword oder hybrid) optimiert für Recall — relevante Dokumente in die Kandidatenmenge zu bekommen. Re-Ranking optimiert für Präzision — die besten Ergebnisse an die Spitze zu setzen.
Embedding-Modelle sind Bi-Encoder: Sie kodieren Queries und Dokumente unabhängig. Das ermöglicht schnelles Retrieval, limitiert aber die Genauigkeit, weil das Modell Query und Dokument nicht direkt vergleichen kann.
Cross-Encoder verarbeiten Query und Dokument zusammen und ermöglichen tiefere Interaktion zwischen ihnen. Sie sind genauer, aber zu langsam für die Suche in grossen Sammlungen. Die Lösung: Nutzen Sie Bi-Encoder für initiales Retrieval, dann Cross-Encoder um die Top-Kandidaten zu re-ranken.
from sentence_transformers import CrossEncoder
# Cross-Encoder Modell laden
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-12-v2")
def rerank_results(query: str, documents: list[dict],
top_k: int = 10) -> list[dict]:
"""
Re-ranke Suchergebnisse mit einem Cross-Encoder.
Args:
query: Suchanfrage des Nutzers
documents: Initiale Suchergebnisse mit 'content' Feld
top_k: Anzahl der Ergebnisse nach Re-Ranking
Returns:
Re-gerankte Dokumente mit Relevanz-Scores
"""
if not documents:
return []
# Query-Dokument-Paare erstellen
pairs = [(query, doc["content"]) for doc in documents]
# Alle Paare scoren
scores = reranker.predict(pairs)
# Scores hinzufügen und sortieren
for doc, score in zip(documents, scores):
doc["rerank_score"] = float(score)
reranked = sorted(documents, key=lambda x: x["rerank_score"], reverse=True)
return reranked[:top_k]
# Alternative: Cohere Rerank API für Produktion
import cohere
co = cohere.Client("your-api-key")
def rerank_cohere(query: str, documents: list[dict], top_k: int = 10) -> list[dict]:
"""Re-ranke mit Coheres Rerank API."""
response = co.rerank(
query=query,
documents=[doc["content"] for doc in documents],
model="rerank-english-v3.0",
top_n=top_k
)
return [
{**documents[result.index], "rerank_score": result.relevance_score}
for result in response.results
]
Für maximale Genauigkeit können LLMs Relevanz direkt evaluieren. Das ist langsamer und teurer, kann aber Cross-Encoder bei komplexen Queries übertreffen:
from openai import OpenAI
client = OpenAI()
def rerank_with_llm(query: str, documents: list[dict], top_k: int = 5) -> list[dict]:
"""
Re-ranke mit einem LLM zur Relevanz-Evaluation.
Am besten für komplexe Queries, wo Cross-Encoder Schwierigkeiten haben.
"""
prompt = f"""Bewerte die Relevanz jedes Dokuments zur Anfrage auf einer Skala von 1-10.
Anfrage: {query}
Dokumente:
"""
for i, doc in enumerate(documents):
prompt += f"\n[{i}] {doc['content'][:500]}\n"
prompt += "\nAntworte mit JSON: {\"scores\": [score0, score1, ...]}"
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"}
)
import json
scores = json.loads(response.choices[0].message.content)["scores"]
for doc, score in zip(documents, scores):
doc["rerank_score"] = score
reranked = sorted(documents, key=lambda x: x["rerank_score"], reverse=True)
return reranked[:top_k]
Suche bauen ist iterativ. Initiale Implementierungen brauchen immer Tuning basierend auf realen Nutzungsmustern. Systematische Evaluation ermöglicht datengetriebene Verbesserungen.
Ein Evaluationsset enthält Queries gepaart mit ihren relevanten Dokumenten (Ground Truth):
# Evaluationsset-Struktur
evaluation_set = [
{
"query": "Wie Passwort zurücksetzen",
"relevant_docs": ["doc_123", "doc_456"], # Dokument-IDs
"notes": "Sollte sowohl Self-Service als auch Admin-Reset-Prozeduren finden"
},
{
"query": "ERR_CONNECTION_TIMEOUT python requests",
"relevant_docs": ["doc_789"],
"notes": "Technischer Fehlercode - braucht exakten Match"
},
# ... 50-200 Queries für aussagekräftige Evaluation
]
Quellen für Evaluations-Queries:
from typing import List, Dict
import numpy as np
def precision_at_k(retrieved: List[str], relevant: List[str], k: int) -> float:
"""Precision@K: Welcher Anteil der Top-k Ergebnisse ist relevant?"""
retrieved_k = retrieved[:k]
relevant_set = set(relevant)
hits = sum(1 for doc in retrieved_k if doc in relevant_set)
return hits / k
def recall_at_k(retrieved: List[str], relevant: List[str], k: int) -> float:
"""Recall@K: Welcher Anteil der relevanten Docs ist in Top-k?"""
retrieved_k = set(retrieved[:k])
relevant_set = set(relevant)
if not relevant_set:
return 0.0
hits = len(retrieved_k & relevant_set)
return hits / len(relevant_set)
def mrr(retrieved: List[str], relevant: List[str]) -> float:
"""Mean Reciprocal Rank: Wie hoch ist das erste relevante Ergebnis?"""
relevant_set = set(relevant)
for rank, doc in enumerate(retrieved, start=1):
if doc in relevant_set:
return 1.0 / rank
return 0.0
def ndcg_at_k(retrieved: List[str], relevant: List[str], k: int) -> float:
"""NDCG@K: Normalized Discounted Cumulative Gain."""
relevant_set = set(relevant)
# DCG: Summe von Relevanz / log2(rank+1)
dcg = sum(
1.0 / np.log2(rank + 2) # +2 weil rank 0-indiziert ist
for rank, doc in enumerate(retrieved[:k])
if doc in relevant_set
)
# Ideal DCG: alle relevanten Docs ganz oben
ideal_length = min(len(relevant), k)
idcg = sum(1.0 / np.log2(i + 2) for i in range(ideal_length))
return dcg / idcg if idcg > 0 else 0.0
def evaluate_search(search_fn, evaluation_set: List[Dict], k: int = 10) -> Dict:
"""
Führe Evaluation über alle Queries aus.
Args:
search_fn: Funktion die Query nimmt und Liste von Doc-IDs zurückgibt
evaluation_set: Liste von {query, relevant_docs} Dicts
k: Cutoff für Metriken
Returns:
Dict mit durchschnittlichen Metriken
"""
metrics = {"precision": [], "recall": [], "mrr": [], "ndcg": []}
for item in evaluation_set:
retrieved = search_fn(item["query"])
relevant = item["relevant_docs"]
metrics["precision"].append(precision_at_k(retrieved, relevant, k))
metrics["recall"].append(recall_at_k(retrieved, relevant, k))
metrics["mrr"].append(mrr(retrieved, relevant))
metrics["ndcg"].append(ndcg_at_k(retrieved, relevant, k))
return {
f"precision@{k}": np.mean(metrics["precision"]),
f"recall@{k}": np.mean(metrics["recall"]),
"mrr": np.mean(metrics["mrr"]),
f"ndcg@{k}": np.mean(metrics["ndcg"]),
}
Sobald Sie Qualität messen können, testen Sie systematisch Verbesserungen:
Verfolgen Sie Metriken über Zeit, während sich Ihr Content und Ihre Query-Muster entwickeln.
Der Übergang vom Prototyp zur Produktion erfordert Aufmerksamkeit für Zuverlässigkeit, Performance und Wartbarkeit.
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import psycopg2
from sentence_transformers import SentenceTransformer, CrossEncoder
from contextlib import contextmanager
import os
app = FastAPI(title="Semantic Search API")
# Modelle einmal beim Start initialisieren
embedding_model = SentenceTransformer("BAAI/bge-large-en-v1.5")
rerank_model = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-12-v2")
# Datenbank Connection Pool
from psycopg2 import pool
db_pool = pool.ThreadedConnectionPool(
minconn=2,
maxconn=10,
dsn=os.environ["DATABASE_URL"]
)
@contextmanager
def get_db():
conn = db_pool.getconn()
try:
yield conn
finally:
db_pool.putconn(conn)
class SearchRequest(BaseModel):
query: str
limit: int = 10
filters: dict = {}
use_rerank: bool = True
class SearchResult(BaseModel):
id: int
title: str
content: str
score: float
metadata: dict
class SearchResponse(BaseModel):
results: list[SearchResult]
query: str
total_candidates: int
@app.post("/search", response_model=SearchResponse)
def search(request: SearchRequest):
"""
Hybride semantische Suche mit optionalem Re-Ranking.
"""
# Query-Embedding generieren
query_prefixed = f"Represent this sentence for searching relevant passages: {request.query}"
query_embedding = embedding_model.encode(query_prefixed, normalize_embeddings=True).tolist()
with get_db() as conn:
# Hybrid-Suche: semantische und Keyword-Ergebnisse kombinieren
candidates = hybrid_search(
conn,
request.query,
query_embedding,
filters=request.filters,
limit=50 # Mehr Kandidaten für Re-Ranking holen
)
total_candidates = len(candidates)
# Re-ranken falls angefordert und wir Ergebnisse haben
if request.use_rerank and candidates:
pairs = [(request.query, doc["content"]) for doc in candidates]
scores = rerank_model.predict(pairs)
for doc, score in zip(candidates, scores):
doc["score"] = float(score)
candidates.sort(key=lambda x: x["score"], reverse=True)
else:
# Similarity-Scores vom initialen Retrieval verwenden
for i, doc in enumerate(candidates):
doc["score"] = doc.get("similarity", 1.0 - i * 0.01)
results = [
SearchResult(
id=doc["id"],
title=doc["title"],
content=doc["content"][:500], # Für Response kürzen
score=doc["score"],
metadata=doc.get("metadata", {})
)
for doc in candidates[:request.limit]
]
return SearchResponse(
results=results,
query=request.query,
total_candidates=total_candidates
)
@app.post("/index")
def index_document(title: str, content: str, metadata: dict = {}):
"""Füge ein Dokument zum Suchindex hinzu."""
embedding = embedding_model.encode(content, normalize_embeddings=True).tolist()
with get_db() as conn:
with conn.cursor() as cur:
cur.execute(
"""
INSERT INTO documents (title, content, embedding, metadata)
VALUES (%s, %s, %s::vector, %s::jsonb)
RETURNING id
""",
(title, content, embedding, metadata)
)
doc_id = cur.fetchone()[0]
conn.commit()
return {"id": doc_id, "status": "indexed"}
@app.get("/health")
def health_check():
"""Health-Check Endpoint."""
with get_db() as conn:
with conn.cursor() as cur:
cur.execute("SELECT 1")
return {"status": "healthy"}
Vermeiden Sie diese häufigen Fehler bei semantischen Such-Implementierungen:
Bei Virtido helfen wir Unternehmen, produktionsreife Such-Infrastruktur zu bauen, die Bedeutung versteht, nicht nur Keywords — kombiniert mit Vektorsuche-Expertise und praktischem AI Engineering durch unseren KI-Hub.
Wir haben semantische Suchsysteme für Kunden in FinTech, LegalTech, Gesundheitswesen und Enterprise-Software gebaut. Unser Staff-Augmentation-Modell bietet geprüfte Talente mit Schweizer Verträgen und vollem IP-Schutz.
Semantische Suche repräsentiert einen fundamentalen Wandel darin, wie Anwendungen Nutzer mit Informationen verbinden. Statt von Nutzern zu verlangen, die exakten Wörter zu erraten, die Content-Ersteller verwendet haben, versteht semantische Suche die Absicht und matcht nach Bedeutung. Die Technologie ist ausgereift, die Tools sind produktionsbereit, und die User-Experience-Verbesserungen sind substanziell.
Die effektivsten Implementierungen kombinieren mehrere Techniken: Embeddings für semantisches Verständnis, BM25 für exakte Matches und Re-Ranking für Präzision. Dieser hybride Ansatz handhabt das volle Spektrum an Queries, die Nutzer tatsächlich stellen — von konzeptionellen Fragen bis zu spezifischen Fehlercodes. Frühes Aufbauen von Evaluations-Infrastruktur ermöglicht datengetriebene Verbesserungen statt Raterei.
Ob Sie eine Wissensbasis, Dokumentensuche oder die Retrieval-Schicht für ein RAG-System bauen — semantische Suche transformiert die Frustration «Ich weiss, die Antwort existiert irgendwo» in die Zufriedenheit «beim ersten Versuch gefunden». Die Implementierungsmuster in diesem Leitfaden bieten ein Fundament für Suche, die wirklich versteht, wonach Nutzer suchen.
Keyword-Suche findet Dokumente, die bestimmte Wörter enthalten — sie erfordert exakte oder nahezu exakte Übereinstimmungen zwischen Suchbegriffen und Dokumenttext. Semantische Suche nutzt Embeddings, um nach Bedeutung zu matchen und findet relevante Dokumente, selbst wenn sie völlig andere Wörter verwenden. «Auto-Reparatur» findet «Fahrzeug-Wartung» mit semantischer Suche, weil die Embeddings erfassen, dass beide Phrasen das Gleiche bedeuten.
Für die meisten Anwendungen bietet OpenAIs text-embedding-3-small die beste Balance von Qualität und Einfachheit. Wenn Sie Self-Hosted Embeddings brauchen, um API-Kosten zu vermeiden, bieten BGE-large oder E5-large nahezu kommerzielle Qualität. Für multilingualen Content verwenden Sie Cohere embed-v3 oder multilingual-e5. Beginnen Sie mit einem Allzweckmodell, dann evaluieren Sie domänenspezifische Optionen, wenn die Qualität nicht ausreicht.
Kosten haben drei Komponenten: Embedding-Generierung, Vektor-Speicherung und Compute für Suche. OpenAI-Embeddings kosten etwa 0,02 € pro Million Tokens (etwa 750'000 Wörter). Vektor-Speicherung in pgvector nutzt Ihre bestehende PostgreSQL-Infrastruktur. Managed-Vektordatenbanken wie Pinecone starten bei etwa 65 €/Monat für Produktions-Workloads. Self-Hosted-Optionen reduzieren Pro-Abfrage-Kosten, erfordern aber Infrastruktur-Management. Ein typischer 1-Million-Dokument-Index kostet 50-180 €/Monat im Betrieb.
Hybrid-Suche wird für fast alle Produktionssysteme empfohlen. Reine Vektorsuche verpasst exakte Matches — wichtig für Fehlercodes, Produktnamen und technische Begriffe. Hybrid-Suche kombiniert semantische Ähnlichkeit mit Keyword-Matching (BM25) und erfasst sowohl konzeptionelle als auch exakte Treffer. Der Overhead ist minimal, und Hybrid-Suche performt selten schlechter als jede Methode allein.
Re-Ranking verbessert die Präzision für die Top-Ergebnisse, was für die User Experience am wichtigsten ist. Cross-Encoder sind genauer als Bi-Encoder, weil sie Query und Dokument direkt vergleichen können. Wenn Ihre Suchergebnisse «fast, aber nicht ganz richtig» sind, korrigiert Re-Ranking oft die Reihenfolge. Überspringen Sie Re-Ranking, wenn Latenz kritisch ist oder wenn die initiale Retrieval-Qualität bereits ausreicht.
Bauen Sie ein Evaluationsset mit Queries und ihren relevanten Dokumenten (Ground Truth). Schlüsselmetriken umfassen Precision@K (welcher Anteil der Top-k Ergebnisse ist relevant), Recall@K (welcher Anteil der relevanten Docs ist in Top-k), MRR (Mean Reciprocal Rank — wie hoch ist das erste relevante Ergebnis) und NDCG (berücksichtigt Ranking-Position). Sammeln Sie Queries aus Such-Logs und Support-Tickets, um reale Nutzungsmuster widerzuspiegeln.
Verwenden Sie multilinguale Embedding-Modelle wie Cohere embed-v3 oder multilingual-e5-large, die vergleichbare Embeddings über 100+ Sprachen erstellen. Eine Anfrage auf Englisch matcht relevante Dokumente auf Deutsch, Spanisch oder Japanisch. Für beste Ergebnisse erwägen Sie sprachspezifisches Tuning und stellen Sie sicher, dass Ihr Evaluationsset sprachübergreifende Queries enthält. Einige Anwendungen profitieren von separaten Indexen pro Sprache mit Spracherkennungs-Routing.
Die Update-Strategie hängt von Anforderungen an Content-Aktualität ab. Near-Real-Time-Indexierung (Sekunden bis Minuten) eignet sich für dynamischen Content wie Support-Tickets oder Chat-Historie. Tägliche Batch-Indexierung funktioniert für Dokumentation oder Wissensbasen, die sich selten ändern. Verwenden Sie immer inkrementelle Updates wo möglich — re-embedden Sie nur geänderte Dokumente statt den gesamten Index neu zu bauen.
Embedding-Generierung braucht 10-50ms pro Query, abhängig von Modell und Hardware. Vektorsuche mit HNSW-Indexen liefert Ergebnisse in 5-20ms für Millionen-Skala-Indexe. Re-Ranking fügt 50-200ms hinzu, abhängig von Kandidatenanzahl und Re-Ranker-Modell. Gesamte End-to-End-Latenz von 100-300ms ist typisch für Produktionssysteme. Latenz steigt mit Indexgrösse und Ergebnisgrösse.
Nicht unbedingt. PostgreSQL mit pgvector handhabt Millionen von Vektoren und integriert sich in bestehende Infrastruktur — oft der beste Startpunkt. Zweckgebaute Vektordatenbanken (Pinecone, Weaviate, Qdrant) werden wertvoll bei grösserer Skalierung (zig Millionen Vektoren), wenn Sie fortgeschrittene Features wie eingebaute Hybrid-Suche brauchen, oder wenn die operative Einfachheit eines Managed Service die Kosten-Prämie überwiegt.