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.