Créer un moteur de recommandation de produits en SQL pur
Points clés à retenir
- Actian Zen V17 AI intègre directement dans SQL les représentations vectorielles, la recherche par similarité et les opérations vectorielles.
- Ce tutoriel permet de créer un moteur de recommandation de produits sémantique sans avoir besoin d'une base de données vectorielle ni d'une couche d'synchronisation des données .
- Les représentations des produits sont stockées dans une collection de vecteurs et font l'objet d'une recherche par similarité afin d'identifier les articles associés.
- Les jointures SQL standard associent les classements vectoriels à des règles métier en temps réel, telles que les stocks, les prix et la disponibilité des produits.
- Ces mêmes modèles d'Embarquer, de recherche et de JOIN peuvent support la recherche sémantique, détection des fraudes, la déduplication et d'autres applications pilotée par l’IA .
Si vous développez des applications de type « pilotée par l’IA », vous devez probablement jongler entre plusieurs systèmes : votre base de données principale, un magasin vectoriel distinct, voire une couche de mise en cache intermédiaire. Et si vous pouviez vous débarrasser de toute cette complexité ?
Grâce à l’ support native de VectorAI, Actian Zen V17 AI vous permet de gérer les représentations vectorielles, d’effectuer des recherches par similarité et d’effectuer des opérations vectorielles directement en SQL. Pas de changement de contexte, pas d’appels d’API externes, pas de synchronisation de données entre les systèmes : simplement le SQL que vous connaissez déjà, avec une nouvelle fonction en plus.
Le problème
Un client de votre site de commerce en ligne ouvre la page d'un « ordinateur portable de jeu Pro ». Vous souhaitez afficher en dessous une colonne « Les clients ont également consulté », basée sur la similarité sémantique et non pas simplement sur l'appartenance à la « même catégorie ».
La méthode habituelle pour mettre cela en place consiste à déployer une base de données vectorielle distincte, à créer une tâche de synchronisation pour la maintenir à jour par rapport à votre table de produits, et à ajouter un saut réseau entre votre application et deux bases de données au lieu d'une seule.
Actian Zen V17 AI intègre le magasin de vecteurs directement dans le moteur SQL. Les représentations vectorielles, la recherche par similarité et les opérations CRUD sur les « collections » de vecteurs s'exécutent toutes sous forme d'instructions SQL classiques via la même connexion que celle que vous utilisez déjà pour la table « products ». Ce tutoriel vous permet de mettre en place la fonctionnalité de recommandation décrite ci-dessus, de A à Z, en environ 20 minutes.
CE DONT VOUS AUREZ BESOIN: Actian Zen V17 AI, une connexion ODBC (pyodbc fonctionne très bien) et Python avec Sentence-Transformers installé pour générer des embeddings localement ; aucune clé OpenAI n'est requise pour ce tutoriel.
Présentation générale de l'architecture
Avant la première requête, voici ce qui se passe réellement en arrière-plan de la fonction dbo.vector_collection_search() : votre application communique avec le moteur SQL de Zen V17 exactement comme elle l'a toujours fait, et le moteur SQL communique à votre place avec une nouvelle couche VDE (Vector Data Engine).
| Votre candidature
Python · Node.js · Java · C# |
→ | Moteur SQL Zen V17
SRDE · Fonctions de table DBO |
→ | Moteur VDE
VectorAI DB · localhost:6573 |
→ | Magasin Vector
Collections sur disque |
Architecture VectorAI d'Actian Zen V17 : le moteur SQL fait le lien entre les données relationnelles et la recherche vectorielle VDE.
Chaque requête, qu'il s'agisse d'une lecture ou d'une écriture, suit le même parcours aller-retour :
| 1
Générer un encodage L'application convertit du texte et des images en tableaux de flottants |
→ | 2
DÉFINIR VDE_CREDENTIAL Authentification au niveau de la session, une fois par connexion |
→ | 3
Appel de dbo.vector_* Instruction SQL brute via ODBC, aucun appel CALL nécessaire |
→ | 4
Processus VDE ANN : recherche ou écriture dans l'index vectoriel |
→ | 5
Post-traitement SQL WHERE / JOIN / ORDER BY sur l'ensemble de résultats |
→ | 6
L'application reçoit les résultats u64_id, score, payload |
Le flux de données va de la génération de l'intégration de l'application, en passant par le moteur SQL, jusqu'au VDE, puis revient en sens inverse.
Il convient de mettre l'accent sur la procédure d'échange d'informations d'identification, car c'est l'étape où l'on risque le plus de se tromper la première fois :
| Jeton d'authentification
Émis par le VDE, commence par « vdai_… » — n’est jamais enregistré dans le texte d’ requête s ni dans les journaux |
| Variable de session
SET VDE_CREDENTIAL = « token » — à définir une seule fois par connexion ; expire à la déconnexion |
| Accès VDE accordé
Permet d'utiliser les méthodes `vector_collection_create`, `vector_point_upsert` et `vector_collection_search`, ainsi que toutes les opérations CRUD |
Dans le flux d'authentification VDE, le jeton est stocké sous forme de variable de session et est exclu de tous les journaux et pistes d'audit.
ÉTAPE 1 – Connexion et authentification
Connectez-vous via ODBC à l'aide du pilote Zen. Certaines instances nécessitent également des identifiants de session VDE (Vector Data Engine) avant tout appel vector_* ; si c'est le cas, définissez-les une seule fois par connexion.
import pyodbc
conn = pyodbc.connect(
"Driver={Pervasive ODBC Interface};serverName=localhost;DBQ=Demodata;"
)
cur = 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.
# cur.execute("SET VDE_CREDENTIAL = 'vdai_your_token_here'")
POURQUOI C'EST IMPORTANT : Dans les cas où il est utilisé, le jeton est stocké dans une variable de session plutôt que dans le texte « requête », de sorte qu’il n’apparaît jamais dans le cache de planification, le journal « slow-requête » ni la piste d’audit. ÉTAPE 2 Créer une collection vectorielle
Une collection est l'endroit où sont stockées les représentations de vos produits. Choisissez une dimension adaptée à votre modèle de représentation et une métrique de distance adaptée à vos données ; la métrique cosinus est le choix standard pour les représentations de texte.
-- 384 dimensions matches the sentence-transformers model we'll use below
dbo.vector_collection_create ( 'product_embeddings',
'{
"dimension" : 384,
"distance_metric" : "cosine"
}' );
REMARQUE : La dimension est fixée au moment de la création. Si vous changez de modèle d'intégration par la suite, supprimez la collection puis recréez-la — il n'est pas possible de modifier la taille en place.
ÉTAPE 3 – Générer et charger les représentations vectorielles des produits
Voici le catalogue que nous indexons : cinq produits, chacun doté d’un nom et d’une description qui contiennent l’ Embarqué:
| ID | Nom | Description |
| 1 | Ordinateur portable de jeu Pro | 32 Go de RAM, carte graphique RTX, écran à 165 Hz |
| 2 | Ultrabook professionnel | Léger, autonomie d'une journée, lecteur d'empreintes digitales |
| 3 | Clavier mécanique | Commutateurs à rétroéclairage RVB, remplaçables à chaud |
| 4 | Écran 4K 27 pouces | HDR, USB-C, socle réglable en hauteur |
| 5 | Souris de jeu sans fil | 16 000 DPI, boutons programmables |
Encodez chaque description à l'aide d'un modèle d'intégration local, puis chargez-la dans la collection :
from sentence_transformers import SentenceTransformer
import json
model = SentenceTransformer('all-MiniLM-L6-v2') # 384-dim, runs locally
products = [
(1, 'Gaming Laptop Pro', '32GB RAM, RTX graphics, 165Hz display'),
(2, 'Business Ultrabook', 'Lightweight, all-day battery, fingerprint reader'),
(3, 'Mechanical Keyboard', 'RGB backlit, hot-swappable switches'),
(4, '4K Monitor 27"', 'HDR, USB-C, height-adjustable stand'),
(5, 'Wireless Gaming Mouse', '16000 DPI, programmable buttons'),
]
# Batch-encode all descriptions in one model call…
texts = [f"{name}. {desc}" for _, name, desc in products]
vectors = model.encode(texts).tolist()
# …then load them all in a single dbo.vector_point_upsert_batch call.
ids = [pid for pid, _, _ in products]
payloads = [json.dumps({'name': name}) for _, name, _ in products]
# Escape single quotes in the assembled JSON — real product names/
# descriptions can contain apostrophes, which would otherwise break
# out of the SQL string literal below.
ids_json = json.dumps(ids).replace("'", "''")
vectors_json = json.dumps(vectors).replace("'", "''")
payloads_json = json.dumps(payloads).replace("'", "''")
cur.execute(f"""
dbo.vector_point_upsert_batch (
'product_embeddings',
'{ids_json}',
null,
'{vectors_json}',
'{payloads_json}',
{len(ids)},
384
)
""")
conn.commit()
REMARQUE : Il existe également une fonction dbo.vector_point_upsert_batch qui regroupe plusieurs vecteurs en un seul appel.
ÉTAPE 4 – Lancer la recherche de similitudes
Et voici le résultat : un client consulte la page « Gaming Laptop Pro ». Embarquer sur requête de la même manière que vous Embarqué le catalogue, puis effectuez une recherche :
query_vector = model.encode("Gaming Laptop Pro. 32GB RAM, RTX graphics, 165Hz display").tolist()
cur.execute(f"""
SELECT TOP (3)
a.u64_id, a."SIMILARITY_SCORE", a.payload
FROM dbo.vector_collection_search (
'product_embeddings', 'vector',
'{json.dumps(query_vector)}', 3, 384
) a
WHERE a.u64_id != 1 -- exclude the product being viewed
ORDER BY a."SIMILARITY_SCORE" DESC
""")
for row in cur.fetchall():
# Positional indexing — unquoted identifiers get case-folded
# by the engine, so row.<name> attribute access isn't reliable.
print(row[0], row[1], row[2]) # u64_id, SIMILARITY_SCORE, payload
Résultats de ce catalogue (mesurés par rapport à une exécution réelle entièrement sur MiniLM-L6-v2 — vos chiffres exacts peuvent varier légèrement selon la version du modèle, mais le classement reste le même) :
| Classement | Produit | Similitude |
| 1 | Souris de jeu sans fil | 0.44 |
| 2 | Écran 4K 27 pouces | 0.37 |
| 3 | Clavier mécanique | 0.25 |
La souris de jeu arrive en tête de liste, suivie de près par l'écran et le clavier — le modèle assimile le vocabulaire commun lié aux caractéristiques techniques et à l'électronique, et non une catégorie « gaming » prédéfinie. Notez que les scores eux-mêmes sont modestes : la similitude cosinus réelle sur des expressions techniques courtes, avec un petit modèle comme celui-ci, se situe généralement bien en dessous de 1,0, même pour des éléments véritablement liés — ne vous attendez pas à des scores proches de la perfection en dehors de textes quasi identiques.
REMARQUE : Si vous avez l’habitude d’intégrer des démos présentant une similitude de 80 à 90 % pour des éléments connexes, cela concerne généralement des textes plus longs et plus riches sur le plan sémantique. Les phrases courtes issues de fiches techniques comme celles-ci se compressent de manière moins distincte ; il est donc normal d’obtenir ici des scores compris entre 0,2 et 0,5 pour des éléments véritablement connexes.
ÉTAPE 5 – Ajouter des règles métier à l'aide d'une jointure (JOIN)
Un classement basé uniquement sur la similarité ne constitue pas encore une recommandation prête à être mise en production ; vous devez également tenir compte des niveaux de stock et des prix. Comme les résultats sont renvoyés sous la forme d'un ensemble de résultats standard, vous devez effectuer une jointure (JOIN) avec votre table de produits, comme pour toute autre requête « requête » :
cur.execute(f"""
SELECT TOP (3)
p.product_id, p.name, p.price, p.in_stock,
sr."SIMILARITY_SCORE"
FROM dbo.vector_collection_search (
'product_embeddings', 'vector',
'{json.dumps(query_vector)}', 10, 384
) sr
INNER JOIN products p ON CAST(sr.u64_id AS INTEGER) = p.product_id
WHERE sr."SIMILARITY_SCORE" > 0.15
AND p.in_stock = 1
AND p.product_id != 1
ORDER BY sr."SIMILARITY_SCORE" DESC
""")
Notez que la recherche renvoie 10 candidats au lieu de 3 : prévoyez une marge avant l’application des filtres relationnels, afin qu’un article en rupture de stock ne réduise pas votre sélection finale à deux produits. Dans ce catalogue, le moniteur 4K affiche en réalité le deuxième meilleur score de similarité brute (0,37, devant le clavier qui affiche 0,25), mais il est en rupture de stock. Le JOIN l'exclut donc correctement, et le clavier ainsi que l'ultrabook prennent sa place. C'est là tout l'intérêt de superposer des filtres SQL à la recherche vectorielle : le classement généré par un modèle et celui que vous pouvez réellement proposer sont deux choses différentes.
ÉTAPE 6 – Choisir un seuil de similarité
Le seuil indiqué ci-dessus (0,15) est volontairement bas : il sert à illustrer le fonctionnement de la jointure (JOIN), et non à constituer une recommandation pour la production. Les scores de similarité vectorielle n’ont de sens que par rapport à votre cas d'usage, à votre modèle d’embedding et à la formulation typique de vos données. Considérez le tableau ci-dessous comme un point de départ pour effectuer vos réglages, et non comme une constante universelle :
| cas d'usage | Plage relative typique | Pourquoi ? |
| Déduplication | Niveau le plus élevé | Uniquement les enregistrements quasi identiques |
| Recommandation formelle | Haut de gamme | Même gamme de produits |
| Recommandation (ce tutoriel) | Niveau intermédiaire, entre 0,15 et 0,40 ici | Connexes, mais pas identiques |
| Recherche sémantique | Gamme moyenne-basse | En gros, ça concerne le sujet |
| anomalie / détection des fraudes | Niveau le plus bas | Signaler ce qui ne correspond pas aux modèles connus |
Ne considérez pas ces chiffres comme transposables : nos propres résultats de l’étape 4 (de 0,44 à 0,25 pour les produits véritablement liés) se situent bien en dessous de la fourchette de 0,65 à 0,85 que vous verrez citée dans les tutoriels basés sur des textes plus longs et plus riches sur le plan sémantique. Le modèle, la dimension d’encodage et la longueur des expressions modifient tous l’échelle absolue. Ce qui reste valable, c’est l’ordre relatif : déduplication > correspondance stricte > recommandation approximative > correspondance thématique large > valeur aberrante. Commencez par une approche souple, enregistrez les scores que vos requêtes renvoient réellement en production, puis resserrez le seuil une fois que vous aurez une vision claire de votre propre distribution réelle.
Dans quels autres cas ce modèle s'applique-t-il ?
Le moteur de recommandation ci-dessus est une variante d'un modèle plus général : Embarquer → recherche → JOIN avec des règles relationnelles. Ces trois étapes s'appliquent directement :
- Recherche sémantique dans les documents — remplacez les descriptions de produits par des articles d'support s ou des entrées de base de connaissances ; même requête de recherche, meilleur classement « top-N ».
- détection des fraudes — Embarquer les modèles de transaction plutôt que le texte du produit, puis regroupez les valeurs SIMILARITY_SCORE renvoyées en niveaux de risque à l'aide d'une instruction CASE.
- Déduplication — même appel de recherche, seuil relevé au-dessus de 0,95, utilisée pour signaler les fiches clients ou les fiches catalogue quasi-identiques avant leur insertion.
Guide de référence rapide : les fonctions que vous avez utilisées
Tout ce qui se trouve au-dessus de l'API est du SQL brut, sans wrapper de procédure stockée. Gardez ce tableau à portée de main pendant que vous développez :
| Fonctionnement | Fonction |
| Créer une collection | dbo.vector_collection_create(nom, config) |
| Insérer ou mettre à jour un vecteur | dbo.vector_point_upsert(collection, id, vector, payload) |
| Insérer plusieurs vecteurs | dbo.vector_point_upsert_batch(collection, ids, uuids, vectors, payloads, count, dim) |
| Recherche | dbo.vector_collection_search(collection, champ, requête_vector, topN, dim) |
| Supprimer un vecteur | dbo.vector_point_delete(collection, id) |
| Supprimer plusieurs vecteurs | dbo.vector_point_delete_batch(collection, ids, uuids, count) |
| Créer une collecte | dbo.vector_collection_drop(nom) |
Ce que vous avez construit
En six étapes, et sans quitter SQL, vous avez : créé une collection vectorielle, chargé des représentations vectorielles réelles de produits à l'aide d'un seul appel par lots, effectué une recherche de similarité sémantique, filtré les résultats en fonction des stocks et des prix en temps réel à l'aide d'une simple instruction JOIN, et choisi un seuil justifiable plutôt que de le deviner.
À partir de là, vous pouvez appliquer ce même principe à votre catalogue : remplacez l'exemple à cinq lignes par une tâche batch portant sur l'intégralité de votre table de produits, orientez la recherche vers le produit que le client connecté est en train de consulter, puis mettez en cache les N premiers résultats pour votre colonne « Les clients ont également consulté ».
À ESSAYER ENSUITE : Reprenez la variante de détection des fraudes présentée dans la section « Autres cas d’application de ce modèle » et adaptez les étapes 4 à 6 ci-dessus pour classer SIMILARITY_SCORE en niveaux de risque à l’aide d’une instruction CASE au lieu d’une clause WHERE.