Aller au contenu principal

Cas d'Usage - Python ML & Recherche Vectorielle

Guide pratique : À quoi servent les embeddings et la recherche vectorielle dans Aaperture

🎯 Concept de Base

Qu'est-ce qu'un embedding ?

Un embedding est une représentation numérique d'un texte sous forme de vecteur (liste de nombres). Des textes similaires ont des vecteurs proches dans l'espace vectoriel.

Exemple :

  • "Consultation psychologique" → [0.1, 0.3, -0.2, 0.5, ...] (384 nombres)
  • "Thérapie de couple" → [0.12, 0.28, -0.18, 0.48, ...] (très proche)
  • "Facture d'électricité" → [-0.5, 0.1, 0.8, -0.3, ...] (très différent)

Pourquoi utiliser Qdrant ?

Qdrant est une base de données vectorielle qui permet de :

  1. Stocker des millions de vecteurs efficacement
  2. Rechercher rapidement les vecteurs les plus similaires
  3. Filtrer par métadonnées (orgId, userId, etc.)

📋 Cas d'Usage Concrets dans Aaperture

1. 🔍 Recherche Sémantique dans les Sessions

Problème actuel :

  • La recherche textuelle classique ne trouve que les mots exacts
  • "Consultation" ne trouve pas "Thérapie" ou "Séance"

Solution avec embeddings :

// 1. Indexer toutes les sessions existantes (une fois)
const sessions = await db.selectFrom("sessions")
.selectAll()
.where("orgId", "=", orgId)
.execute();

// Générer les embeddings pour chaque session
const texts = sessions.map(s => `${s.title} ${s.notes || ""}`);
const embeddings = await workersQueueService.enqueueMlEmbedTexts({
v: 1,
orgId,
entityId: "batch-sessions",
texts,
});

// Indexer dans Qdrant
await workersQueueService.enqueueMlQdrantUpsert({
v: 1,
orgId,
entityId: "index-sessions",
collectionName: `sessions-${orgId}`,
points: sessions.map((session, idx) => ({
id: session.id,
vector: embeddings.embeddings[idx],
payload: {
sessionId: session.id,
title: session.title,
orgId,
createdAt: session.createdAt,
},
})),
});

// 2. Rechercher des sessions similaires
const queryEmbedding = await workersQueueService.enqueueMlEmbedText({
v: 1,
orgId,
entityId: "query-embedding",
text: "Consultation pour anxiété",
});

const results = await workersQueueService.enqueueMlQdrantSearch({
v: 1,
orgId,
entityId: "search-sessions",
collectionName: `sessions-${orgId}`,
queryVector: queryEmbedding.embedding,
limit: 10,
scoreThreshold: 0.7, // Seulement les résultats très similaires
filter: { orgId }, // Filtrer par organisation
});

// results.results contient les sessions les plus similaires !

Résultat :

  • L'utilisateur tape "Consultation anxiété"
  • Le système trouve automatiquement :
    • "Séance pour troubles anxieux"
    • "Thérapie contre l'anxiété"
    • "Consultation stress et anxiété"
  • Même si les mots exacts ne correspondent pas !

2. 👥 Recommandations de Contacts Similaires

Cas d'usage :

  • "Trouve-moi des contacts qui ont des besoins similaires à ce client"
// Indexer les contacts avec leurs descriptions
const contacts = await db.selectFrom("contacts")
.selectAll()
.where("orgId", "=", orgId)
.execute();

const contactTexts = contacts.map(c =>
`${c.firstName} ${c.lastName} - ${c.notes || ""} - ${c.tags?.join(" ") || ""}`
);

// Générer et indexer
const embeddings = await workersQueueService.enqueueMlEmbedTexts({
v: 1,
orgId,
entityId: "batch-contacts",
texts: contactTexts,
});

await workersQueueService.enqueueMlQdrantUpsert({
v: 1,
orgId,
entityId: "index-contacts",
collectionName: `contacts-${orgId}`,
points: contacts.map((contact, idx) => ({
id: contact.id,
vector: embeddings.embeddings[idx],
payload: {
contactId: contact.id,
name: `${contact.firstName} ${contact.lastName}`,
orgId,
},
})),
});

// Rechercher des contacts similaires
const targetContact = contacts.find(c => c.id === targetContactId);
const targetEmbedding = await workersQueueService.enqueueMlEmbedText({
v: 1,
orgId,
entityId: "target-embedding",
text: `${targetContact.firstName} ${targetContact.lastName} - ${targetContact.notes}`,
});

const similarContacts = await workersQueueService.enqueueMlQdrantSearch({
v: 1,
orgId,
entityId: "find-similar-contacts",
collectionName: `contacts-${orgId}`,
queryVector: targetEmbedding.embedding,
limit: 5,
filter: {
orgId,
contactId: { $ne: targetContactId }, // Exclure le contact lui-même
},
});

Résultat :

  • "Montre-moi des clients similaires à Jean Dupont (anxiété, thérapie cognitive)"
  • Le système trouve automatiquement d'autres contacts avec des profils similaires

3. 📄 Classification Automatique de Documents

Cas d'usage :

  • Classifier automatiquement les documents uploadés (facture, contrat, questionnaire, etc.)
// Après OCR d'un document
const documentText = await extractTextFromDocument(documentId);

// Générer l'embedding du document
const docEmbedding = await workersQueueService.enqueueMlEmbedText({
v: 1,
orgId,
entityId: documentId,
text: documentText,
});

// Rechercher dans une collection de documents types connus
const documentTypes = await workersQueueService.enqueueMlQdrantSearch({
v: 1,
orgId,
entityId: "classify-document",
collectionName: "document-types",
queryVector: docEmbedding.embedding,
limit: 1,
scoreThreshold: 0.8,
});

if (documentTypes.results.length > 0) {
const detectedType = documentTypes.results[0].payload.type; // "INVOICE", "CONTRACT", etc.
await updateDocumentType(documentId, detectedType);
}

Résultat :

  • Un document est uploadé
  • Le système détecte automatiquement : "C'est une facture" ou "C'est un contrat"
  • Plus besoin de sélectionner manuellement le type !

4. 💡 Suggestions Intelligentes

Cas d'usage :

  • "Quelles sessions sont similaires à celle-ci ?"
  • "Quels contacts pourraient bénéficier de ce type de service ?"
// Quand l'utilisateur consulte une session
const currentSession = await getSession(sessionId);

// Trouver des sessions similaires pour suggestions
const similarSessions = await workersQueueService.enqueueMlQdrantSearch({
v: 1,
orgId,
entityId: "suggest-sessions",
collectionName: `sessions-${orgId}`,
queryVector: currentSession.embedding, // Stocké lors de l'indexation
limit: 3,
filter: {
orgId,
sessionId: { $ne: sessionId }, // Exclure la session actuelle
},
});

// Afficher : "Sessions similaires : ..."

🚀 Workflow Complet : Indexation Initiale

Étape 1 : Créer la collection (une fois par organisation)

await workersQueueService.enqueueMlQdrantCreateCollection({
v: 1,
orgId: "org-123",
entityId: "collection-sessions",
collectionName: "sessions-org-123",
vectorSize: 384, // Dimension des embeddings (all-MiniLM-L6-v2)
distance: "Cosine",
});

Étape 2 : Indexer les données existantes (migration)

// Par batch de 100 pour éviter la surcharge
const batchSize = 100;
for (let i = 0; i < sessions.length; i += batchSize) {
const batch = sessions.slice(i, i + batchSize);

// Générer les embeddings
const texts = batch.map(s => `${s.title} ${s.notes || ""}`);
const embeddings = await workersQueueService.enqueueMlEmbedTexts({
v: 1,
orgId,
entityId: `batch-${i}`,
texts,
});

// Indexer dans Qdrant
await workersQueueService.enqueueMlQdrantUpsert({
v: 1,
orgId,
entityId: `upsert-${i}`,
collectionName: `sessions-${orgId}`,
points: batch.map((session, idx) => ({
id: session.id,
vector: embeddings.embeddings[idx],
payload: {
sessionId: session.id,
title: session.title,
orgId,
},
})),
});
}

Étape 3 : Indexer les nouvelles données (en temps réel)

// Quand une nouvelle session est créée
const newSession = await createSession({ title, notes, ... });

// Générer l'embedding
const embedding = await workersQueueService.enqueueMlEmbedText({
v: 1,
orgId,
entityId: newSession.id,
text: `${newSession.title} ${newSession.notes || ""}`,
});

// Indexer immédiatement
await workersQueueService.enqueueMlQdrantUpsert({
v: 1,
orgId,
entityId: `index-${newSession.id}`,
collectionName: `sessions-${orgId}`,
points: [{
id: newSession.id,
vector: embedding.embedding,
payload: {
sessionId: newSession.id,
title: newSession.title,
orgId,
},
}],
});

💰 Avantages vs OpenAI Embeddings

Coûts

SolutionCoût par 1000 embeddings
OpenAI (text-embedding-3-small)~$0.02
sentence-transformers (local)$0.00

Économie : Pour 100,000 sessions indexées :

  • OpenAI : ~$2,000
  • Local : $0

Performance

  • Latence : Local est plus rapide (pas de réseau)
  • Privacy : Les données ne quittent jamais votre infrastructure
  • Scalabilité : Pas de limites de rate limiting

🎯 Résumé

Les embeddings + Qdrant permettent de :

  1. Recherche intelligente : Trouver du contenu même sans mots exacts
  2. Recommandations : Suggérer des sessions/contacts similaires
  3. Classification automatique : Détecter le type de document
  4. Économies : $0 vs $0.02 par 1000 embeddings
  5. Privacy : Tout reste local

Workflow typique :

  1. Créer une collection (une fois)
  2. Indexer les données existantes (migration)
  3. Indexer les nouvelles données (temps réel)
  4. Rechercher quand nécessaire

C'est comme avoir un Google Search interne pour votre CRM, mais qui comprend le sens et pas juste les mots ! 🚀