Crear un motor de recomendación de productos en SQL puro
Principales conclusiones
- Actian Zen V17 AI incorpora representaciones vectoriales, búsqueda por similitud y operaciones vectoriales directamente en SQL.
- El tutorial explica cómo crear un motor de recomendación semántica de productos sin necesidad de una base de datos vectorial independiente ni de una capa de sincronización de datos.
- Las representaciones de los productos se almacenan en una colección de vectores y se buscan por similitud para identificar artículos relacionados.
- Las operaciones JOIN estándar de SQL combinan clasificaciones vectoriales con reglas de negocio en tiempo real, como el inventario, los precios y la disponibilidad de los productos.
- El mismo patrón de integración, búsqueda y JOIN puede servir de base para la búsqueda semántica, la detección de fraudes, la deduplicación y otras aplicaciones basadas en la inteligencia artificial.
Si estás desarrollando aplicaciones basadas en inteligencia artificial, probablemente tengas que gestionar varios sistemas a la vez: tu base de datos principal, un almacén de vectores independiente y, quizá, una capa de caché entre ambos. ¿Y si pudieras prescindir de toda esa complejidad?
Gracias a la compatibilidad nativa con VectorAI, Actian Zen V17 AI te permite gestionar representaciones vectoriales, realizar búsquedas por similitud y llevar a cabo operaciones vectoriales directamente en SQL. Sin cambios de contexto, sin llamadas a API externas, sin sincronización de datos entre sistemas: solo el SQL que ya conoces, más una nueva función.
El problema
Un cliente de tu tienda online abre la página de un producto llamado «Portátil para videojuegos Pro». Quieres mostrar debajo una columna titulada «Los clientes también han visto», basada en la similitud semántica y no solo en que sean de la «misma categoría».
La forma habitual de hacerlo consiste en crear una base de datos vectorial independiente, programar una tarea de sincronización para mantenerla actualizada con la tabla de productos y añadir un salto de red entre la aplicación y los dos almacenes de datos, en lugar de uno solo.
Actian Zen V17 AI integra el almacén vectorial en el propio motor SQL. Las representaciones vectoriales, la búsqueda por similitud y las operaciones CRUD sobre «colecciones» vectoriales se ejecutan como sentencias SQL normales a través de la misma conexión que ya utilizas para la tabla de productos. En este tutorial se crea la función de recomendación anterior, de principio a fin, en unos 20 minutos.
LO QUE NECESITARÁS: Actian Zen V17 AI, una conexión ODBC (pyodbc funciona bien) y Python con sentence-transformers instalado para generar representaciones de forma local; no se necesita ninguna clave de OpenAI para este tutorial.
Descripción general de la arquitectura
Antes de que se realice la primera consulta, esto es lo que ocurre realmente en segundo plano en la función dbo.vector_collection_search(): tu aplicación se comunica con el motor SQL de Zen V17 exactamente como lo ha hecho siempre, y el motor SQL se comunica con una nueva capa VDE (Vector Data Engine) en tu nombre.
| Tu solicitud
Python · Node.js · Java · C# |
→ | Motor SQL Zen V17
SRDE · Funciones de tabla DBO |
→ | Motor VDE
VectorAI DB · localhost:6573 |
→ | Tienda Vector
Colecciones en disco |
Arquitectura VectorAI de Actian Zen V17: el motor SQL integra los datos relacionales con la búsqueda vectorial VDE.
Cada solicitud, ya sea de lectura o de escritura, sigue el mismo proceso de ida y vuelta:
| 1
Generar representación La aplicación convierte texto e imágenes en matrices de flotantes |
→ | 2
ESTABLECER VDE_CREDENTIAL Autenticación con ámbito de sesión, una vez por conexión |
→ | 3
Llamada a dbo.vector_* Instrucción SQL sin formato a través de ODBC, sin necesidad de CALL |
→ | 4
Procesos de VDE ANN: buscar o escribir en el índice vectorial |
→ | 5
Posprocesamiento de SQL WHERE / JOIN / ORDER BY en el conjunto de resultados |
→ | 6
La aplicación recibe los resultados u64_id, puntuación, carga útil |
El flujo de datos va desde la generación de la integración de la aplicación, pasando por el motor SQL, hasta el VDE y viceversa.
Merece la pena destacar por separado el intercambio de credenciales, ya que es el único paso en el que es fácil equivocarse la primera vez:
| Token de autenticación
Emitido por VDE, comienza por vdai_… — nunca se almacena en el texto de la consulta ni en los registros |
| Variable de sesión
SET VDE_CREDENTIAL = «token» — se establece una vez por conexión y caduca al desconectarse |
| Acceso a VDE concedido
Permite el uso de `vector_collection_create`, `vector_point_upsert` y `vector_collection_search`, así como todas las operaciones CRUD |
En el flujo de credenciales VDE, el token se almacena como una variable de sesión y se filtra de todos los registros y rastros de auditoría.
PASO 1: Conectarse e identificarse
Conéctate a través de ODBC utilizando el controlador Zen. Algunas instancias también requieren unas credenciales de sesión VDE (Vector Data Engine) antes de cualquier llamada a vector_*; si es tu caso, configúralas una vez por conexión.
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'")
POR QUÉ ES IMPORTANTE: En los casos en los que sí se utiliza, el token se almacena en una variable de sesión en lugar de en el texto de la consulta, por lo que nunca aparece en la caché de planes, en el registro de consultas lentas ni en el registro de auditoría. PASO 2: Crear una colección vectorial
Una colección es donde se almacenan las representaciones de tus productos. Elige una dimensión que se ajuste a tu modelo de representación y una métrica de distancia adecuada para los datos; la métrica del coseno es la opción habitual para las representaciones de texto.
-- 384 dimensions matches the sentence-transformers model we'll use below
dbo.vector_collection_create ( 'product_embeddings',
'{
"dimension" : 384,
"distance_metric" : "cosine"
}' );
NOTA: La dimensión se fija en el momento de la creación. Si más adelante cambias los modelos de incrustación, elimina la colección y vuelve a crearla; no es posible cambiar el tamaño in situ.
PASO 3 – Generar y cargar las representaciones de los productos
Este es el catálogo que estamos indexando: cinco productos, cada uno con un nombre y una descripción que se incluyen:
| ID | Nombre | Descripción |
| 1 | Portátil para videojuegos Pro | 32 GB de RAM, tarjeta gráfica RTX, pantalla de 165 Hz |
| 2 | Ultrabook empresarial | Ligero, batería con autonomía para todo el día, lector de huellas dactilares |
| 3 | Teclado mecánico | Interruptores con retroiluminación RGB y sustituibles en caliente |
| 4 | Monitor 4K de 27″ | HDR, USB-C, soporte regulable en altura |
| 5 | Ratón inalámbrico para videojuegos | 16 000 DPI, botones programables |
Codifica cada descripción con un modelo de incrustación local y, a continuación, añádela a la colección:
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()
NOTA: También existe una función dbo.vector_point_upsert_batch que agrupa varios vectores en una sola llamada.
PASO 4 – Realizar la búsqueda de similitudes
Ahora viene lo mejor: un cliente está viendo el «Gaming Laptop Pro». Incrusta la consulta del mismo modo que incrustaste el catálogo y, a continuación, realiza la búsqueda:
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
Resultados de este catálogo (comparados con una ejecución real de MiniLM-L6-v2; tus cifras exactas pueden variar ligeramente según la versión del modelo, pero la clasificación se mantiene):
| Clasificación | Producto | Similitud |
| 1 | Ratón inalámbrico para videojuegos | 0.44 |
| 2 | Monitor 4K de 27″ | 0.37 |
| 3 | Teclado mecánico | 0.25 |
El ratón para videojuegos encabeza la lista, seguido de cerca por el monitor y el teclado; el modelo está captando vocabulario compartido en torno a las especificaciones y la electrónica, no una categoría de «videojuegos» predefinida. Fíjate en que las puntuaciones en sí mismas son modestas: la similitud coseno real en frases técnicas cortas de un modelo pequeño como este suele situarse muy por debajo de 1,0, incluso para elementos genuinamente relacionados; no esperes puntuaciones casi perfectas fuera de textos prácticamente idénticos.
NOTA: Si estás acostumbrado a incluir demostraciones que muestran una similitud del 80-90 % para elementos relacionados, eso suele darse con textos más largos y con mayor riqueza semántica. Las frases breves de las fichas técnicas como estas se comprimen de forma menos distintiva, por lo que aquí es normal obtener puntuaciones en el rango de 0,2-0,5 para elementos realmente relacionados.
PASO 5 – Añadir reglas de negocio con una operación JOIN
Una clasificación basada únicamente en la similitud aún no constituye una recomendación lista para su implementación; es necesario tener en cuenta los niveles de existencias y el precio. Dado que los resultados se devuelven como un conjunto de resultados normal, debes realizar una unión (JOIN) con tu tabla de productos como en cualquier otra consulta:
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
""")
Fíjate en que la búsqueda muestra 10 candidatos en lugar de 3: deja un margen antes de que se apliquen los filtros relacionales, para que un artículo agotado no reduzca tu selección final a solo dos productos. En este catálogo, el monitor 4K tiene, de hecho, la segunda puntuación de similitud bruta más alta (0,37, por delante del 0,25 del teclado), pero está agotado, por lo que la operación JOIN lo descarta correctamente y el teclado y el ultrabook ocupan su lugar. Ese es precisamente el objetivo de superponer filtros SQL a la búsqueda vectorial: la clasificación que genera un modelo y la clasificación que realmente puedes ofrecer son dos cosas diferentes.
PASO 6 – Elige un umbral de similitud
El umbral anterior (0,15) es deliberadamente bajo: su finalidad es ilustrar el funcionamiento de JOIN, no servir como recomendación para entornos de producción. Las puntuaciones de similitud vectorial solo tienen sentido en función de tu caso de uso, tu modelo de incrustación y la forma habitual de expresar las frases en tus datos. Considera la tabla siguiente como un punto de partida a partir del cual realizar ajustes, no como una constante universal:
| Caso de uso | Rango relativo típico | ¿Por qué? |
| Desduplicación | Nivel más alto | Solo registros prácticamente idénticos |
| Recomendación firme | Nivel superior | La misma línea de productos |
| Recomendación (este tutorial) | Nivel medio, entre 0,15 y 0,40 aquí | Relacionados, no idénticos |
| Búsqueda semántica | Gama baja-media | En líneas generales, sobre el tema |
| Detección de anomalías y fraudes | Nivel más bajo | Marca lo que no se ajuste a los patrones conocidos |
No consideres que las cifras concretas son extrapolables: nuestros propios resultados del Paso 4 (de 0,44 a 0,25 para productos realmente relacionados) se sitúan muy por debajo del rango de 0,65–0,85 que verás citado en tutoriales basados en textos más largos y con mayor riqueza semántica. El modelo, la dimensión de la incrustación y la longitud de la frase influyen en la escala absoluta. Lo que sí se mantiene es el orden relativo: deduplicación > coincidencia estricta > recomendación aproximada > coincidencia temática amplia > valor atípico. Empieza con un enfoque flexible, registra las puntuaciones que tus consultas devuelven realmente en producción y ajusta el umbral una vez que puedas ver tu propia distribución real.
En qué otros casos se aplica este patrón
El motor de recomendaciones anterior es una variante de un patrón más general: incrustación → búsqueda → JOIN con reglas relacionales. Estos mismos tres pasos se aplican directamente:
- Búsqueda semántica de documentos: sustituye las descripciones de productos por artículos de ayuda o entradas de la base de conocimientos; misma consulta de búsqueda, mejor clasificación «top-N».
- Detección de fraudes: incorpora patrones de transacciones en lugar del texto del producto y, a continuación, clasifica el valor SIMILARITY_SCORE obtenido en niveles de riesgo mediante una instrucción CASE.
- Deduplicación: misma llamada de búsqueda, umbral elevado por encima de 0,95, utilizada para señalar registros de clientes o de catálogo casi idénticos antes de su inserción.
Referencia rápida: Las funciones que has utilizado
Todo lo que hay por encima de la API es SQL puro, sin envoltorios de procedimientos almacenados. Ten esta tabla a mano mientras desarrollas:
| Funcionamiento | Función |
| Crear una colección | dbo.vector_collection_create(nombre, configuración) |
| Insertar o actualizar un vector | dbo.vector_point_upsert(colección, id, vector, carga útil) |
| Insertar varios vectores | dbo.vector_point_upsert_batch(colección, ids, uuids, vectores, cargas útiles, recuento, dim) |
| Buscar en | dbo.vector_collection_search(colección, campo, vector_de_búsqueda, topN, dim) |
| Eliminar un vector | dbo.vector_point_delete(colección, id) |
| Eliminar varios vectores | dbo.vector_point_delete_batch(colección, ids, uuids, recuento) |
| Deja una donación | dbo.vector_collection_drop(nombre) |
Lo que has construido
En seis pasos, y sin salir de SQL, has: creado una colección vectorial, cargado representaciones vectoriales reales de productos con una sola llamada por lotes, realizado una búsqueda de similitud semántica, filtrado los resultados según el inventario y los precios en tiempo real con un simple JOIN, y elegido un umbral justificado en lugar de adivinarlo.
A partir de aquí, el mismo patrón se puede aplicar a tu catálogo real: sustituye el ejemplo de cinco filas por una tarea por lotes que abarque toda tu tabla de productos, dirige la búsqueda al producto que esté viendo en ese momento un cliente que haya iniciado sesión y almacena en caché los N primeros resultados para tu barra de «los clientes también vieron».
PRUÉBALO A CONTINUACIÓN: Toma la variante de detección de fraudes de «Dónde más se aplica este patrón» y adapta los pasos 4 a 6 anteriores para clasificar SIMILARITY_SCORE en niveles de riesgo con una instrucción CASE en lugar de una cláusula WHERE.