Erstellen Sie ein Suchtool für Support-Tickets mit Python
Wichtigste Erkenntnisse
- Die semantische Suche hilft Support-Teams dabei, bereits gelöste Tickets zu finden, selbst wenn neue Anfragen eine völlig andere Formulierung aufweisen.
- In diesem Tutorial werden Python, SQL und Actian Zen V17 AI verwendet, um den Text von einbetten -Tickets zu , Vektoren zu speichern und ähnliche frühere Fälle abzurufen.
- Die vektorbasierte Ähnlichkeitssuche gleicht Tickets anhand ihrer Bedeutung ab, anstatt sich auf exakte Schlüsselwörter oder Feldwerte zu stützen.
- SQL-Verbindungen und Statusfilter stellen sicher, dass die Agenten nur relevante Tickets anzeigen, für die bereits bestätigte Lösungen vorliegen.
- Dasselbe Muster kann die Suche in Wissensdatenbanken, die Erkennung doppelter Fehlermeldungen und den Abgleich von FAQs beim Onboarding unterstützen, ohne dass eine separate Vektordatenbank erforderlich ist.
Es geht eine neue Anfrage ein: „Nach dem letzten Update kann ich mich nicht mehr in der App anmelden.“ Irgendwo in Ihrem Archiv befindet sich eine fast identische Anfrage, die bereits gelöst wurde und bei der die Lösung festgehalten ist. Bei einer Stichwortsuche wird sie nicht gefunden, wenn der Wortlaut nicht übereinstimmt. Eine semantische Suche findet sie jedoch, und Sie können diese rein mit Python und SQL erstellen, ganz ohne separate Vektordatenbank.
Das Problem
Support-Teams müssen immer wieder dieselben wenigen Probleme lösen, weil frühere Lösungen in einem Ticket-System vergraben sind, das nur eine Suche nach Stichwörtern oder nach exakten Feldangaben zulässt. „Kann mich nach dem Update nicht mehr anmelden“ und „Die App lässt mich nach dem letzten Update nicht mehr einloggen“ sind aus Sicht eines Support-Mitarbeiters dasselbe Problem, aber eine Suche nach „LIKE ‚%login%‘“ abfragen stellt keinen Zusammenhang zwischen den beiden her.
In diesem Tutorial wird ein kleines Tool erstellt, das dieses Problem behebt: Der Text jedes Tickets wird als Vektor kodiert, in Actian Zen V17 AI gespeichert, und wenn ein neues Ticket eingeht, wird nach den semantisch am besten passenden Treffern gesucht und diese anschließend auf diejenigen gefiltert, die tatsächlich gelöst wurden.
WAS SIE BENÖTIGEN:Python 3.8+, die Pakete „pyodbc“ und „sentence-transformers“ sowie eine Actian Zen V17 AI-Datenbank, auf die über ODBC zugegriffen werden kann. Es sind keine API-Schlüssel erforderlich; die Einbettungen werden mit diesen Paketen lokal ausgeführt.
Architekturübersicht
Bevor Sie mit dem Programmieren beginnen, hier ein Überblick über die Komponenten, die Sie miteinander verbinden: Ihren „ Python “-Prozess, die SQL-Engine von Zen V17 und die darunter liegende VDE-Schicht (Vector Data Engine). VDE läuft als Begleitprozess, der im Lieferumfang von Zen enthalten ist (localhost:6573 REST / 6574 gRPC, dashboard auf Port 6575).
| Python App
sentence_transformers + pyodbc |
→ | Zen V17 SQL-Engine
SRDE · dbo.vector_* |
→ | VDE-Motor
VectorAI DB · localhost:6573 |
→ | Vektor-Speicher
HNSW-Index-Sammlungen auf Festplatte |
Python Stellt über pyodbc eine Verbindung zu Zen V17 AI her – die SQL-Engine bildet eine Brücke zwischen ML-Einbettungen und der VDE-Vektorsuche.
Jede Anfrage durchläuft denselben Rundlauf mit sechs Hops, unabhängig davon, ob Sie ein Ticket indexieren oder danach suchen:
| 1
Texteingabe Betreffzeile des Tickets |
→ | 2
model.encode() sentence_transformers → 384-dim float[] |
→ | 3
pyodbc Ausführen cursor.execute(sql) über ODBC |
→ | 4
Zen SQL-Engine dbo.vector_point_upsert / search() |
→ | 5
VDE ANN-Suche Kosinus-Ähnlichkeit, Top-K |
→ | 6
Python Ergebnisse u64_id, Punktzahl, Nutzlast |
Der gesamte Datenfluss vom Rohtext über das ML-Modell, die ODBC-Verbindung, die Zen-SQL-Engine und die VDE-Suchmaschine zurück zu Python.
Nach Zuständigkeiten gegliedert sieht dasselbe System wie folgt aus:
| Anwendungsschicht
Python 3.8+ · sentence_transformers · pyodbc |
| Data Layer
pyodbc.connect(DSN) · cursor.execute(sql) |
| SQL-Engine-Ebene
Actian Zen V17 · SRDE · dbo.vector_*-Funktionen |
| Vektor-Engine-Ebene
VectorAI DB (VDE) · REST: 6573 · gRPC: 6574 |
| Speicherschicht
HNSW-Index · Float32-Binärdaten · JSON-Nutzdaten |
Fünfschichtiger Aufbau – jede Schicht übernimmt eine bestimmte Aufgabe. Sie schreiben Code ausschließlich für die beiden obersten Schichten.
SCHRITT 1 – Installieren, anschließen und authentifizieren
pip install pyodbc sentence-transformers
Stellen Sie dann eine Verbindung her. Bei einigen Instanzen sind vor jedem Aufruf einer `vector_*`-Funktion außerdem Anmeldedaten für eine VDE-Sitzung erforderlich – legen Sie diese in diesem Fall einmal pro Verbindung fest:
import pyodbc, json
from sentence_transformers import SentenceTransformer
# all-MiniLM-L6-v2: 384-dim, ~90MB, runs locally, ~50ms per ticket
model = SentenceTransformer('all-MiniLM-L6-v2')
conn = pyodbc.connect(
'Driver={Pervasive ODBC Interface};serverName=localhost;DBQ=Demodata;'
)
cursor = conn.cursor()
# Only needed if your instance requires it — if this raises
# "Invalid SET statement", your instance authenticates via the
# ODBC connection itself and you can skip this line.
# cursor.execute("SET VDE_CREDENTIAL = 'your_vde_token'")
SCHRITT 2 – Erstellen einer Sammlung für Ticket-Einbettungen
Eine Sammlung, deren Dimension an das Einbettungsmodell angepasst ist, Kosinus-Abstand für Text:
cursor.execute("""
dbo.vector_collection_create (
'ticket_embeddings',
'{"dimension": 384, "distance_metric": "cosine"}'
)
""")
conn.commit()
print('Collection ready')
HINWEIS: Die Dimension wird bei der Erstellung festgelegt. Wenn Sie später das Einbettungsmodell wechseln, löschen Sie die Sammlung und erstellen Sie sie neu; eine Größenanpassung vor Ort ist nicht möglich.
SCHRITT 3 – Indizieren Sie Ihre vorhandenen Tickets
Hier ist der Rückstand, den wir gerade indexieren: fünf abgeschlossene Tickets, deren Betreffzeilen jeweils „ eingebettet “ lauten:
| ID | Betreff | Status |
|---|---|---|
| 101 | Nach dem Update kann ich mich nicht mehr anmelden | Beschlossen |
| 102 | Die E-Mail zum Zurücksetzen des Passworts kommt nie an | Beschlossen |
| 103 | Die PDF-Rechnung lässt sich nicht herunterladen | Beschlossen |
| 104 | Zahlung beim Bezahlvorgang abgelehnt | Beschlossen |
| 105 | Zweifaktor-Code nicht erhalten | Öffnen |
Eine Funktion übernimmt sowohl das Einbetten als auch das Schreiben – der Ablauf ist derselbe, egal ob es sich um Ihr erstes Ticket oder Ihr hundertstes handelt:
def index_ticket(ticket_id, subject):
# Step A: text -> 384-dim vector, entirely local
embedding = model.encode(subject).tolist()
# Step B: upsert — inserts if new, updates if ticket_id exists.
# Inline the JSON directly in the SQL text — dbo.vector_*
# calls don't reliably support ODBC "?" parameter binding.
# Escape single quotes first: real text has apostrophes
# ("won't", "can't") that would otherwise break out of the
# SQL string literal and raise a syntax error.
payload = json.dumps({'subject': subject}).replace("'", "''")
cursor.execute(f"""
dbo.vector_point_upsert (
'ticket_embeddings',
'{{"u64_id": {ticket_id}, "dim": 384}}',
'{json.dumps(embedding)}',
'{payload}'
)
""")
conn.commit()
index_ticket(101, "Cannot login after update")
index_ticket(102, "Password reset email never arrives")
index_ticket(103, "Invoice PDF won't download")
index_ticket(104, "Payment declined at checkout")
index_ticket(105, "Two-factor code not received")
TIPP ZUR LEISTUNGSOPTIMIERUNG: Um einen echten Backlog zu erzeugen, codieren Sie alle Subjekte in einem einzigen „model.encode()“-Batch.
SCHRITT 4 – Suche: Ähnliche frühere Tickets finden
Es geht ein neues Ticket ein, dessen Wortlaut sich völlig von dem des bereits in Ihrem System vorhandenen unterscheidet:
def find_similar(new_ticket_text, top_k=3):
query_embedding = model.encode(new_ticket_text).tolist()
# Inline the vector directly — dbo.vector_* calls don't
# reliably support ODBC "?" parameter binding.
cursor.execute(f"""
SELECT TOP ({top_k})
a.u64_id, a."SIMILARITY_SCORE", a.payload
FROM dbo.vector_collection_search (
'ticket_embeddings', 'vector',
'{json.dumps(query_embedding)}', {top_k}, 384
) a
WHERE a."SIMILARITY_SCORE" > 0.5
ORDER BY a."SIMILARITY_SCORE" DESC
""")
for u64_id, score, payload in cursor.fetchall():
subj = json.loads(payload)["subject"]
print(f" #{u64_id} {score:.0%} {subj}")
find_similar("App won't let me sign in after the last update")
Ergebnis (gemessen anhand eines tatsächlichen Durchlaufs mit ausschließlich MiniLM-L6-v2):
| Fahrkarte | Ähnlichkeit | Betreff |
|---|---|---|
| #101 | 74% | Nach dem Update kann ich mich nicht mehr anmelden |
Kein Wort im neuen Ticket stimmt genau mit „login“, „cannot“ oder „update“ überein – das Modell hat anhand der Bedeutung und nicht anhand des Wortschatzes abgeglichen. Darin liegt für diesen Workload der gesamte Mehrwert der semantischen Suche gegenüber der Stichwortsuche.
HINWEIS: Die tatsächliche Punktedifferenz ist hier bemerkenswert: Ticket Nr. 101 erreicht 0,74 Punkte, während das nächstgelegene Ticket (Nr. 102, „E-Mail zum Zurücksetzen des Passworts kommt nie an“) nur 0,32 Punkte erreicht – deutlich unter dem Schwellenwert von 0,5, sodass es zu Recht nicht angezeigt wird. Ein deutlicher Abstand wie dieser zwischen der echten Übereinstimmung und allen anderen Einträgen ist der Grund dafür, dass ein fester Schwellenwert gut funktioniert; im Produktkatalog des begleitenden SQL-Tutorials lagen die Bewertungen viel enger beieinander und erforderten einen niedrigeren Schwellenwert. Überprüfen Sie immer den Abstand in Ihren eigenen Daten, bevor Sie einen Wert festlegen.
SCHRITT 5 – Nur Tickets anzeigen, die tatsächlich gelöst wurden
Ein semantisch ähnliches Ticket, das noch offen ist, hilft einem Mitarbeiter nicht weiter. Führen Sie ein JOIN der Vektor-Ergebnisse mit Ihrer Ticket-Tabelle durch und filtern Sie nach Status, genau wie bei jedem anderen SQL- abfragen:
def find_resolved_matches(new_ticket_text, top_k=3):
query_embedding = model.encode(new_ticket_text).tolist()
cursor.execute(f"""
SELECT TOP ({top_k})
t.ticket_id, t.subject, t.resolution_notes,
sr."SIMILARITY_SCORE"
FROM dbo.vector_collection_search (
'ticket_embeddings', 'vector', '{json.dumps(query_embedding)}',
{top_k * 2}, 384 -- over-fetch for filter headroom
) sr
INNER JOIN tickets t
ON CAST(sr.u64_id AS INTEGER) = t.ticket_id
WHERE sr."SIMILARITY_SCORE" > 0.5
AND t.status = 'Resolved'
ORDER BY sr."SIMILARITY_SCORE" DESC
""")
return cursor.fetchall()
Auf der oben genannten Anmeldeseite abfragen erfüllt lediglich Ticket Nr. 101 die Ähnlichkeitsschwelle – daher wird der Statusfilter bei dieser konkreten Suche nicht angewendet. Aber man braucht kein hypothetisches Beispiel, um zu erkennen, warum dies von Bedeutung ist. Probieren Sie stattdessen ein anderes eingehendes Ticket aus:
find_resolved_matches("verification code never received")
Ticket Nr. 105 („Zwei-Faktor-Code nicht erhalten“, noch offen) weist mit einer Ähnlichkeit von 0,52 die mit Abstand größte semantische Übereinstimmung auf – deutlich über dem Schwellenwert von 0,5 und mit großem Abstand vor allen anderen Tickets. Bei einer einfachen, ungefilterten Suche erscheint es auf Platz 1:
| Fahrkarte | Ähnlichkeit | Status |
|---|---|---|
| #105 | 52% | Öffnen |
| #102 | 40% | Beschlossen |
| #104 | 32% | Beschlossen |
Führt man jedoch find_resolved_matches() auf denselben Text an, verschwindet #105 vollständig, UND t.status = „Resolved“ entfernt es, obwohl es die mit Abstand beste Übereinstimmung im gesamten Backlog ist. Das ist kein hypothetisches Szenario: Genau das macht der JOIN tatsächlich, sobald ein echtes offenes Ticket zufällig eine hohe Punktzahl erzielt. Der relationale Filter tut genau das, was eine WHERE-Klausel immer tut; er filtert lediglich die KI-Ausgabe anstelle einer einfachen Spalte.
Wo dieses Muster sonst noch zum Tragen kommt
Ersetzen Sie das Ticket-Betreff-Feld durch ein anderes Kurztextfeld, und die gleichen fünf Funktionen bleiben unverändert erhalten:
- Suche in der internen Wissensdatenbank, nach Titeln und Zusammenfassungen von Artikeln auf einbetten sowie nach der Frage eines Mitarbeiters.
- Erkennung doppelter Fehler, einbetten neue Fehlerberichte; markiere alle Einträge mit einer Ähnlichkeit von über 0,90 zu einem bereits offenen Problem, bevor sie doppelt gemeldet werden.
- Abgleich von FAQs beim Onboarding: einbetten – Bei einer Slack-Frage eines neuen Mitarbeiters wird die am besten passende vorhandene Antwort angezeigt, anstatt zunächst einen Mitarbeiter hinzuzuziehen.
Auswahl Ihres Einbettungsmodells
„all-MiniLM-L6-v2“ ist die richtige Standardeinstellung für kurze Texte wie Ticket-Betreffzeilen – schnell und klein genug, um auf einem Laptop ausgeführt zu werden. Ersetzen Sie sie, wenn sich Ihre Anforderungen hinsichtlich Volumen oder Genauigkeit ändern:
| Modell | Abmessungen | Geschwindigkeit | Am besten geeignet für | |
|---|---|---|---|---|
| all-MiniLM-L6-v2 | 384 | ~50 ms/Text | Allgemeines – dieses Tutorial | |
| Paraphrase-MiniLM-L3-v2 | 384 | ~20 ms/Text | Hohes Ticketvolumen, geringere Genauigkeit | |
| all-mpnet-base-v2 | 768 | ~120 ms/Text | Triage mit höherer Genauigkeit | |
| text-embedding-ada-002 | 1536 | ~200 ms/Anruf | Höchste Genauigkeit (erfordert einen API-Schlüssel) | |
HINWEIS: Egal, für welches Modell Sie sich entscheiden: Erstellen Sie die Zen-Sammlung mit den genauen Abmessungen dieses Modells. Die Abmessungen sind nach der Erstellung unveränderlich – das Mischen von Modellen zwischen „Einfügen“ und „Suchen“ führt unbemerkt zu sinnlosen Partituren.
Was du geschaffen hast
In fünf Schritten haben Sie einen Ticket-Rückstand als Vektoren indexiert, ihn mit normalem Text statt mit Schlüsselwörtern durchsucht und die Ergebnisse auf Lösungen eingegrenzt, die ein Mitarbeiter tatsächlich weitergeben kann – und das alles mit pyodbc und Standard-SQL, ohne separate Vektordatenbank oder REST-Client.
Binden Sie von hier aus die Funktion `find_resolved_matches()` in Ihren Webhook zur Ticket-Erstellung ein, damit die vorgeschlagenen Lösungen sofort angezeigt werden, sobald ein neues Ticket erstellt wird, und ersetzen Sie die einzelnen Aufrufe von `index_ticket()` durch einen gebündelten Upsert-Vorgang, sobald Sie einen echten Verlauf nachträglich einpflegen, anstatt nur fünf Beispielzeilen.