153 lines
4.8 KiB
Python
153 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Beispiel: Embeddings lokal berechnen und in DuckDB speichern.
|
|
|
|
Gedacht als Startpunkt für Team B (AI-gestütztes Matching im
|
|
Verbund-Szenario). Zeigt:
|
|
|
|
1. Wie pro Kunde ein Kontakt-Embedding mit Ollama erzeugt wird.
|
|
2. Wie die Vektoren als DuckDB-Spalte FLOAT[768] gespeichert werden.
|
|
3. Wie über array_cosine_similarity Kandidatenpaare für das
|
|
anschliessende LLM-Matching ermittelt werden.
|
|
|
|
Das Skript ist BEWUSST minimal gehalten und reduziert nicht
|
|
auf Produktivqualitaet. Ziel ist eine arbeitsfaehige Vorlage,
|
|
nicht eine fertige Pipeline.
|
|
|
|
Voraussetzungen:
|
|
|
|
pip install duckdb ollama pandas
|
|
ollama pull nomic-embed-text
|
|
|
|
Aufruf:
|
|
|
|
python beispiel_embeddings.py
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import duckdb
|
|
import ollama
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
DB = HERE / "embedding_demo.duckdb"
|
|
SCHEMA = "embeddings"
|
|
TABLE = f"{SCHEMA}.kunde_embedding"
|
|
MODEL = "nomic-embed-text" # 768-dim, ~270 MB lokales Modell
|
|
DIM = 768
|
|
THRESHOLD = 0.75 # Cosine-Similarity-Schwellwert fuer Kandidatenpaare
|
|
|
|
|
|
def build_text(row: dict) -> str:
|
|
"""Baut den Eingabetext, aus dem das Embedding berechnet wird.
|
|
|
|
Die Reihenfolge der Felder ist wichtig: das Modell gewichtet
|
|
fruehe Tokens staerker. Name kommt zuerst, dann Adresse, dann
|
|
Kontaktinformationen.
|
|
"""
|
|
parts = [
|
|
(row.get("vorname") or "") + " " + (row.get("nachname") or ""),
|
|
row.get("strasse") or "",
|
|
(row.get("plz") or "") + " " + (row.get("ort") or ""),
|
|
row.get("telefon") or "",
|
|
row.get("email") or "",
|
|
]
|
|
return " | ".join(p.strip() for p in parts if p and p.strip())
|
|
|
|
|
|
def main() -> int:
|
|
con = duckdb.connect(str(DB))
|
|
|
|
# ---- 1. VSS-Extension fuer Vektor-Operationen --------------
|
|
con.execute("INSTALL vss;")
|
|
con.execute("LOAD vss;")
|
|
|
|
# ---- 2. Schema + Tabelle vorbereiten -----------------------
|
|
con.execute(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA};")
|
|
con.execute(f"""
|
|
CREATE OR REPLACE TABLE {TABLE} (
|
|
quell_id VARCHAR,
|
|
praxis VARCHAR,
|
|
text VARCHAR,
|
|
embedding FLOAT[{DIM}]
|
|
);
|
|
""")
|
|
|
|
# ---- 3. Beispieldaten laden --------------------------------
|
|
# Wir nehmen die Juckstadt-CSV als Demo. In der echten Pipeline
|
|
# zieht Team B aus dem final.verbund_kunde (siehe Anhang III).
|
|
juck_path = HERE / "praxis_juckstadt_kunden.csv"
|
|
rows = con.execute(f"""
|
|
SELECT
|
|
kunden_nr::VARCHAR AS quell_id,
|
|
'JUCK' AS praxis,
|
|
vorname, nachname, strasse, plz, ort, telefon, email
|
|
FROM read_csv_auto('{juck_path.as_posix()}',
|
|
sep=';', header=true, all_varchar=true)
|
|
""").fetchall()
|
|
|
|
print(f"Berechne Embeddings fuer {len(rows)} Kunden ...")
|
|
t0 = time.time()
|
|
for r in rows:
|
|
quell_id, praxis, vor, nach, str_, plz, ort, tel, mail = r
|
|
text = build_text({
|
|
"vorname": vor, "nachname": nach, "strasse": str_,
|
|
"plz": plz, "ort": ort, "telefon": tel, "email": mail,
|
|
})
|
|
resp = ollama.embeddings(model=MODEL, prompt=text)
|
|
con.execute(
|
|
f"INSERT INTO {TABLE} VALUES (?, ?, ?, ?)",
|
|
[quell_id, praxis, text, resp["embedding"]],
|
|
)
|
|
dt = time.time() - t0
|
|
print(f" fertig in {dt:.1f}s "
|
|
f"({dt / max(len(rows), 1) * 1000:.0f}ms pro Kunde)\n")
|
|
|
|
# ---- 4. HNSW-Index fuer schnelle KNN-Suche -----------------
|
|
con.execute(f"""
|
|
CREATE INDEX IF NOT EXISTS idx_kunde_emb
|
|
ON {TABLE}
|
|
USING HNSW (embedding) WITH (metric = 'cosine');
|
|
""")
|
|
|
|
# ---- 5. Kandidatenpaare ueber Cosine-Similarity ------------
|
|
paare = con.execute(f"""
|
|
SELECT a.quell_id AS a_id,
|
|
b.quell_id AS b_id,
|
|
a.text AS a_text,
|
|
b.text AS b_text,
|
|
round(array_cosine_similarity(a.embedding, b.embedding), 3) AS sim
|
|
FROM {TABLE} a
|
|
JOIN {TABLE} b
|
|
ON a.quell_id < b.quell_id
|
|
WHERE array_cosine_similarity(a.embedding, b.embedding) >= {THRESHOLD}
|
|
ORDER BY sim DESC
|
|
LIMIT 20
|
|
""").fetchall()
|
|
|
|
print(f"Kandidatenpaare (sim >= {THRESHOLD}):\n")
|
|
for a_id, b_id, a_text, b_text, sim in paare:
|
|
print(f" sim={sim:.3f} {a_id} vs {b_id}")
|
|
print(f" A: {a_text}")
|
|
print(f" B: {b_text}\n")
|
|
|
|
# ---- 6. Modell-Metadaten festhalten (Reproduzierbarkeit) ---
|
|
con.execute(f"""
|
|
CREATE OR REPLACE TABLE {SCHEMA}.modell_meta (
|
|
modell VARCHAR,
|
|
dim INTEGER,
|
|
erstellt_am TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
INSERT INTO {SCHEMA}.modell_meta (modell, dim)
|
|
VALUES (?, ?);
|
|
""", [MODEL, DIM])
|
|
|
|
print(f"DB: {DB}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|