Créer un outil de recherche de billets « Support » avec Python
Points clés à retenir
- La recherche sémantique aide les équipes d'support s à retrouver des tickets déjà résolus, même lorsque les nouvelles demandes sont formulées de manière totalement différente.
- Ce tutoriel utilise Python, SQL et l'IA d'Actian Zen V17 pour Embarquer le texte d'un ticket, stocker des vecteurs et retrouver des problèmes similaires survenus par le passé.
- La recherche par similarité vectorielle permet de faire correspondre des tickets en fonction de leur signification, plutôt qu’en se basant sur des mots-clés ou des valeurs de champ exacts.
- Les jointures SQL et les filtres d'état garantissent que les agents ne présentent que les tickets pertinents dont la résolution a déjà été vérifiée.
- Ce même modèle peut support la recherche dans la base de connaissances, la détection des doublons de bogues et la mise en correspondance des FAQ lors de l'intégration, sans nécessiter de base de données vectorielle distincte.
Un nouveau ticket arrive : « L'application ne me laisse pas me connecter depuis la dernière mise à jour. » Quelque part dans votre historique se trouve un ticket presque identique, déjà résolu, avec la solution indiquée. Une recherche par mot-clé ne le trouvera pas si la formulation ne correspond pas. Une recherche sémantique, en revanche, le trouvera, et vous pouvez la mettre en place uniquement avec Python et SQL, sans avoir besoin d'une base de données vectorielle distincte.
Le problème
Support Les équipes se retrouvent à résoudre sans cesse les mêmes problèmes, car les solutions antérieures sont enfouies dans un système de tickets qui ne permet que des recherches par mot-clé ou par champ exact. « Impossible de se connecter après la mise à jour » et « L’application ne me laisse pas me connecter depuis la dernière mise à jour » correspondent au même problème aux yeux d’un agent d’ support , mais une requête de type « LIKE ‘%login%’ requête » ne permettra pas de les relier.
Ce tutoriel vous explique comment créer un petit outil qui résout ce problème : il encode le texte de chaque ticket sous forme de vecteur, le stocke dans Actian Zen V17 AI, et lorsqu’un nouveau ticket arrive, il recherche les correspondances les plus proches sur le plan sémantique, puis filtre ces correspondances pour ne retenir que celles qui ont effectivement été résolues.
CE DONT VOUS AUREZ BESOIN :Python 3.8+, les paquets pyodbc et sentence-transformers, ainsi qu’une base de données Actian Zen V17 AI accessible via ODBC. Aucune clé API n’est requise ; les embeddings s’exécutent localement à l’aide de ces paquets.
Présentation générale de l'architecture
Avant d’écrire la moindre ligne de code, voici une vue d’ensemble de ce que vous allez connecter : votre processus Python , le moteur SQL de Zen V17 et la couche VDE (Vector Data Engine) qui le sous-tend. Le VDE s’exécute en tant que processus compagnon intégré à Zen (localhost:6573 REST / 6574 gRPC, tableau de bord sur le port 6575).
| Python Application
sentence_transformers + pyodbc |
→ | Moteur SQL Zen V17
SRDE · dbo.vector_* |
→ | Moteur VDE
VectorAI DB · localhost:6573 |
→ | Magasin Vector
Collections d'index HNSW sur disque |
Python se connecte à Zen V17 AI via pyodbc — le moteur SQL fait le lien entre les représentations d'apprentissage automatique et la recherche vectorielle VDE.
Chaque requête effectue le même parcours aller-retour de six sauts, que ce soit pour indexer un ticket ou pour en rechercher un :
| 1
Saisie de texte Chaîne de sujet du ticket |
→ | 2
model.encode() sentence_transformers → tableau de nombres à virgule flottante de 384 dimensions |
→ | 3
pyodbc Exécuter cursor.execute(sql) via ODBC |
→ | 4
Moteur Zen SQL dbo.vector_point_upsert / search() |
→ | 5
Recherche VDE ANN Similitude cosinus, top-K |
→ | 6
Python Résultats u64_id, score, payload |
Flux de données complet, depuis le texte brut jusqu'au modèle d'apprentissage automatique, en passant par la connexion ODBC, le moteur Zen SQL, le moteur de recherche VDE, puis de nouveau vers Python.
Si l'on classe les éléments par responsabilité, ce même système se présente comme suit :
| Couche application
Python 3,8+ · sentence_transformers · pyodbc |
| couche de données
pyodbc.connect(DSN) · cursor.execute(sql) |
| Couche du moteur SQL
Actian Zen V17 · SRDE · Fonctions dbo.vector_* |
| Couche du moteur vectoriel
VectorAI DB (VDE) · REST : 6573 · gRPC : 6574 |
| Couche de stockage
Index HNSW · Fichiers binaires Float32 · Charges utiles JSON |
Une architecture à cinq couches — chaque couche assume une responsabilité spécifique. Vous n'écrirez du code que pour les deux couches supérieures.
ÉTAPE 1 – Installation, connexion et authentification
pip install pyodbc sentence-transformers
Connectez-vous ensuite. Certaines instances exigent également des identifiants de session VDE avant tout appel à une fonction vector_* — configurez-les une fois par connexion si c'est le cas pour la vôtre :
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'")
ÉTAPE 2 – Créer une collection pour les représentations vectorielles des billets
Une collection, dont la dimension est adaptée au modèle d'intégration, distance cosinus pour le texte :
cursor.execute("""
dbo.vector_collection_create (
'ticket_embeddings',
'{"dimension": 384, "distance_metric": "cosine"}'
)
""")
conn.commit()
print('Collection ready')
REMARQUE : La dimension est fixée lors de la création. Si vous changez par la suite de modèle d'intégration, supprimez la collection puis recréez-la ; il n'est pas possible de modifier la taille sur place.
ÉTAPE 3 – Répertorier vos tickets existants
Voici la liste des tickets en attente que nous sommes en train d'indexer : cinq tickets résolus, dont l'objet contient tous la mention « Embarqué » :
| ID | Objet | Statut |
|---|---|---|
| 101 | Impossible de se connecter après la mise à jour | Résolution |
| 102 | L'e-mail de réinitialisation du mot de passe n'arrive jamais | Résolution |
| 103 | Le fichier PDF de la facture ne se télécharge pas | Résolution |
| 104 | Paiement refusé lors du paiement | Résolution |
| 105 | Code de vérification à deux facteurs non reçu | Ouvrir |
Une seule fonction gère à la fois l'intégration et l'écriture — le processus est le même, qu'il s'agisse de votre premier billet ou de votre centième :
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")
CONSEIL DE PERFORMANCE : Pour un véritable backlog, encodez tous les sujets en une seule fois à l'aide de model.encode().
ÉTAPE 4 – Recherche : trouver des billets similaires déjà vendus
Un nouveau ticket arrive, dont la formulation ne ressemble en rien à celle du ticket déjà enregistré dans votre système :
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")
Résultats (mesurés par rapport à une exécution réelle de MiniLM-L6-v2) :
| Billet | Similitude | Objet |
|---|---|---|
| #101 | 74% | Impossible de se connecter après la mise à jour |
Aucun mot du nouveau ticket ne correspond exactement à « login », « cannot » ou « update » : le modèle a effectué la correspondance en fonction du sens, et non du vocabulaire. C'est là tout l'intérêt de la recherche sémantique par rapport à la recherche par mots-clés pour ce charge de travail.
REMARQUE : L’écart réel entre les scores mérite ici d’être souligné : le ticket n° 101 obtient un score de 0,74, tandis que le ticket le plus proche (n° 102, « L’e-mail de réinitialisation du mot de passe n’arrive jamais ») n’obtient qu’un score de 0,32 — bien en dessous du seuil de 0,5 — et n’apparaît donc pas, ce qui est tout à fait normal. C’est précisément cet écart net entre les correspondances réelles et toutes les autres qui permet à un seuil fixe de bien fonctionner ; dans le catalogue de produits du tutoriel SQL associé, les scores étaient beaucoup plus regroupés et nécessitaient un seuil plus bas. Vérifiez toujours l’écart entre vos propres données avant de choisir une valeur.
ÉTAPE 5 – Afficher uniquement les tickets qui ont effectivement été résolus
Un ticket dont le contenu est similaire sur le plan sémantique et qui est toujours ouvert n'aide pas un agent. Effectuez une jointure (JOIN) entre les résultats de la requête VECTOR et votre table de tickets, puis filtrez par statut, comme pour n'importe quelle autre requête SQL requête:
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()
Sur l'requête de connexion ci-dessus, seul le ticket n° 101 dépasse le seuil de similitude — cette recherche particulière ne déclenche donc pas le filtre de statut. Mais nul besoin d'un scénario hypothétique pour comprendre pourquoi cela a de l'importance. Essayez plutôt un autre ticket entrant :
find_resolved_matches("verification code never received")
Le ticket n° 105 (« Code de validation à deux facteurs non reçu », toujours en cours) est de loin celui qui présente la meilleure correspondance sémantique, avec un score de similarité de 0,52, largement supérieur au seuil de 0,5 et nettement devant tous les autres tickets. Si l'on effectue une recherche simple, sans filtre, il apparaît en première position :
| Billet | Similitude | Statut |
|---|---|---|
| #105 | 52% | Ouvrir |
| #102 | 40% | Résolution |
| #104 | 32% | Résolution |
Cependant, si l’on exécute la fonction `find_resolved_matches()` sur le même texte, le ticket n° 105 disparaît complètement, ET la condition `t.status = ‘Resolved’` le supprime même s’il s’agit de la meilleure correspondance de similarité de tout le backlog. Ce n’est pas une hypothèse : c’est exactement ce que fait le JOIN dès qu’un ticket ouvert réel obtient un bon score. Le filtre relationnel fait exactement ce que fait toujours une clause WHERE ; il filtre simplement la sortie de l’IA au lieu d’une colonne classique.
Dans quels autres cas ce modèle s'applique-t-il ?
Remplacez l'objet du ticket par un autre champ de texte court : les cinq fonctions restent inchangées :
- Recherche dans la base de connaissances interne, titres et résumés d’articles de l’ Embarquer , recherche à partir de la question d’un salarié.
- Détection des doublons de bogues, Embarquer nouveaux rapports de bogues : signalez tout élément présentant un taux de similitude supérieur à 0,90 avec un ticket ouvert existant avant qu’il ne soit signalé deux fois.
- Correspondance avec la FAQ d'intégration : Embarquer : lorsqu'un nouvel employé pose une question sur Slack, affichez la réponse existante la plus pertinente au lieu de faire appel immédiatement à un collaborateur.
Choisir votre modèle d'intégration
« all-MiniLM-L6-v2 » est le paramètre par défaut idéal pour les textes courts, comme les objets de tickets : il est suffisamment rapide et léger pour fonctionner sur un ordinateur portable. Remplacez-le si vos besoins en termes de volume ou de précision évoluent :
| Modèle | Dimensions | Vitesse | Idéal pour | |
|---|---|---|---|---|
| all-MiniLM-L6-v2 | 384 | environ 50 ms par message | Usage général — ce tutoriel | |
| paraphrase-MiniLM-L3-v2 | 384 | environ 20 ms par SMS | Volume élevé de tickets, précision moindre | |
| all-mpnet-base-v2 | 768 | environ 120 ms par message | Triage plus précis | |
| text-embedding-ada-002 | 1536 | environ 200 ms par appel | Précision optimale (nécessite une clé API) | |
REMARQUE : Quel que soit le modèle que vous choisissez, créez la collection Zen en respectant les dimensions exactes de ce modèle. Les dimensions sont immuables une fois la collection créée : mélanger les modèles entre l’insertion et la recherche génère silencieusement des scores dénués de sens.
Ce que vous avez construit
En cinq étapes, vous avez indexé un carnet de tickets sous forme de vecteurs, effectué une recherche à l'aide de texte en langage naturel plutôt que de mots-clés, puis filtré les résultats pour ne retenir que les corrections qu'un agent peut réellement transmettre, le tout à l'aide de pyodbc et du SQL standard, sans avoir recours à une base de données vectorielle distincte ni à un client REST.
À partir de là, intégrez la fonction `find_resolved_matches()` dans votre webhook de création de tickets afin que les solutions proposées s'affichent dès qu'un nouveau ticket est créé, et remplacez les appels individuels à `index_ticket()` par une opération d'upsert par lots dès que vous commencez à reconstituer un historique réel au lieu d'utiliser cinq lignes d'exemple.