- veröffentlicht am:
- Lesedauer:
- Lesedauer: 5 min
Semantische Dublettensuche
- Autoren
Thomas Schefter
Überblick
Echte KI-Hilfe abseits von Hype und Agenten: Dublettensuche in 200.000 Texten – lokal, kostenlos, in 5 Minuten.
Bisher hatte ich keine gute Lösung, um Dubletten in meiner Sammlung von kurzen Texten zu finden. Typische Datenbanken finden nur exakte Übereinstimmungen, aber keine inhaltlichen.
D. h.
„Wer so spricht, dass er verstanden wird, spricht immer gut." und
„Wer so redet, dass er verstanden wird, redet immer gut."
sind für die meisten Maschinen keine Dublette.
KI-Systeme arbeiten mit Vektor-Indizes. Diese funktionieren anders als herkömmliche Datenbanken: Texte werden in mathematische Vektoren umgewandelt, die Bedeutung abbilden. Das Ergebnis: Mit 50 Zeilen Python konnte ich lokal über 200.000 kurze Texte binnen 5 Minuten in einen Vektor-Index (FAISS) speichern und über 400 Dublettenpaare finden, inklusive Export als CSV.
Sogar anderssprachige Dubletten wurden erkannt:
→ „Unser Körper ist die Harfe unserer Seele."
→ „Notre corps est la harpe de notre âme."
Dieses Dokument beschreibt den technischen Workflow zur Erkennung semantischer Dubletten in einer Sammlung von 200.000 Texten mittels Vektor-Embeddings und FAISS-Index.
Workflow
CSV-Datei (200k Texte)
↓
Sentence Transformer (Embeddings erzeugen)
↓
FAISS Index (Vektoren speichern & normalisieren)
↓
Similarity Search (Top-k Nachbarn finden)
↓
Filterung (Score > 0.95)
↓
CSV-Export (Dublettenkandidaten)
Input-Daten
Die CSV-Datei hat eine einfache Struktur:
id,text
1,"Der Geist, der stets verneint, ist jener Geist, der Böses will und Gutes schafft."
2,"Ein Geist, der alles verneint und doch Gutes bewirkt."
3,"In der Ehe schweigt man, um sich zu verstehen."
- id: Primärschlüssel aus der MySQL-Datenbank zur eindeutigen Zuordnung
- text: Der zu analysierende Text
Technischer Ansatz
1. Embeddings statt Textvergleich
Traditionelle Datenbanken arbeiten mit exakten String-Matches. Semantische Ähnlichkeit erfordert einen anderen Ansatz:
- Embeddings: Texte werden in hochdimensionale Vektoren (512 Dimensionen) umgewandelt
- Bedeutung: Ähnliche Bedeutungen liegen im Vektorraum nahe beieinander
- Mehrsprachig: Das Modell erkennt auch Übersetzungen als ähnlich
2. FAISS für schnelle Suche
FAISS (Facebook AI Similarity Search) ist eine optimierte Bibliothek für:
- Effiziente Nearest-Neighbor-Suche in großen Vektormengen
- Speicher-optimierte Indizes
- Parallelisierung
Gewählte Index-Strategie: IndexFlatIP (Inner Product)
- Exakte Suche ohne Approximation
- Bei 200k Vektoren noch performant genug
- Inner Product entspricht nach L2-Normalisierung der Cosine Similarity
3. Similarity Threshold
- Score-Bereich: 0.0 (komplett unähnlich) bis 1.0 (identisch)
- Gewählter Threshold: 0.95
- Begründung: Hohe Präzision, um False Positives zu vermeiden
Das Python-Script
import pandas as pd
from sentence_transformers import SentenceTransformer
import faiss
import numpy as np
# CRITICAL: Fix for macOS ARM + FAISS + OpenMP bug
# Without this, you'll get "resource_tracker: leaked semaphore" warnings
faiss.omp_set_num_threads(1)
# Step 1: Load CSV
df = pd.read_csv("zitate.csv")
# Step 2: Load pre-trained multilingual model
# distiluse-base-multilingual-cased supports 50+ languages
model = SentenceTransformer("distiluse-base-multilingual-cased")
# Step 3: Generate embeddings
# convert_to_numpy=True for FAISS compatibility
embeddings = model.encode(df["text"].tolist(), convert_to_numpy=True)
# Step 4: Build FAISS index
dimension = embeddings.shape[1] # 512 for this model
index = faiss.IndexFlatIP(dimension) # Inner Product index
# Normalize embeddings for cosine similarity
faiss.normalize_L2(embeddings)
index.add(embeddings)
# Step 5: Search for similar vectors
k = 4 # Top 4 neighbors (including self)
D, I = index.search(embeddings, k)
# D = distances/scores, I = indices
# Step 6: Print potential duplicates
print("Potenzielle Dubletten:")
for i, neighbors in enumerate(I):
for j, neighbor_idx in enumerate(neighbors[1:]): # Skip first (self)
score = D[i][j+1]
if score > 0.95:
print(f"- Ähnlich ({score:.2f}):")
print(f" [{df.iloc[i]['id']}] {df.iloc[i]['text']}")
print(f" [{df.iloc[neighbor_idx]['id']}] {df.iloc[neighbor_idx]['text']}\n")
# Step 7: Build CSV output (deduplicated pairs)
seen = set()
rows = []
for i, neighbors in enumerate(I):
for j, neighbor_idx in enumerate(neighbors[1:]):
# Create sorted tuple to avoid duplicate pairs (A,B) and (B,A)
pair = tuple(sorted((df.iloc[i]["id"], df.iloc[neighbor_idx]["id"])))
score = D[i][j+1]
if score >= 0.95 and pair not in seen:
seen.add(pair)
rows.append({
"id_1": df.iloc[i]["id"],
"text_1": df.iloc[i]["text"],
"id_2": df.iloc[neighbor_idx]["id"],
"text_2": df.iloc[neighbor_idx]["text"],
"score": round(float(score), 4)
})
# Step 8: Write CSV
df_out = pd.DataFrame(rows)
df_out.to_csv("dublettenkandidaten.csv", index=False)
print(f"\n✅ {len(df_out)} Dublettenkandidaten gespeichert in 'dublettenkandidaten.csv'")
Technische Details
Warum distiluse-base-multilingual-cased?
- Mehrsprachig: Unterstützt 50+ Sprachen ohne Spracherkennung
- Kompakt: DistilBERT-Architektur, schneller als BERT
- Bewährt: Standard-Modell für semantische Textsuche
Performance-Optimierungen
- Batch-Encoding: Alle Texte werden in einem Durchgang encodiert
- NumPy-Arrays: Direkte FAISS-Kompatibilität
- L2-Normalisierung: Ermöglicht Inner Product statt teurerem Cosine Distance
- Deduplizierung:
seen-Set verhindert doppelte Paare (A→B und B→A)
Platform-spezifische Anpassungen
macOS ARM (M1/M2/M3) + FAISS Bug
faiss.omp_set_num_threads(1)
Problem: Bekannter Kombinationsfehler zwischen macOS ARM, FAISS und OpenMP führt zu:
resource_tracker: There appear to be 1 leaked semaphore objects to clean up at shutdown
warnings.warn('resource_tracker: There appear to be %d '
Ursache: Inkompatibilität zwischen Apple Silicon, OpenMP-Threading und Python's multiprocessing-Modul
Lösung: Single-Threading für FAISS erzwingt
- Kostet minimale Performance (bei 200k Texten kaum messbar)
- Verhindert Semaphore-Leaks und Race Conditions
- Nur auf macOS ARM nötig, auf Linux/Windows optional
Skalierung
Bei 200.000 Texten:
- Embedding-Zeit: ~3-4 Minuten (CPU-abhängig)
- Index-Aufbau: <1 Sekunde
- Search: <1 Sekunde (auch mit Single-Threading)
- RAM-Bedarf: ~1-2 GB
Output
Die erzeugte dublettenkandidaten.csv hat folgendes Format:
id_1,text_1,id_2,text_2,score
42,"Wer so spricht, dass er verstanden wird...",1337,"Wer so redet, dass er verstanden wird...",0.9823
105,"Unser Körper ist die Harfe unserer Seele.",106,"Notre corps est la harpe de notre âme.",0.9645
Erkenntnisse
Stärken
✅ Erkennt semantische Ähnlichkeit (nicht nur exakte Matches)
✅ Mehrsprachige Dubletten werden gefunden
✅ Schnell und lokal (keine Cloud/API-Kosten)
✅ Reproduzierbar und transparent
Grenzen
❌ Homophone werden nicht erkannt ("ist" vs. "isst")
❌ Threshold muss manuell justiert werden
❌ Keine Berücksichtigung von Kontext/Autor
Weiterführende Ideen
- Clustering: Nicht nur Paare, sondern ganze Gruppen ähnlicher Texte
- Deduplication-Pipeline: Automatisches Zusammenführen in Datenbank
- Fine-Tuning: Custom Model für spezifische Textdomäne trainieren
- Monitoring: Score-Verteilung analysieren für besseren Threshold