Optimización del diseño de datos

Completado

Las decisiones de modelado de datos afectan significativamente al rendimiento de la búsqueda vectorial. La forma de estructurar tablas, elegir tipos de datos para metadatos y crear índices auxiliares determina si las consultas se ejecutan de forma eficaz a medida que crece el conjunto de datos.

Nota:

En los ejemplos de código de esta unidad se muestran los patrones de diseño de esquemas para los datos vectoriales con metadatos. Adapte estos patrones a los requisitos de consulta y modelo de datos específicos.

Consideraciones sobre el almacenamiento de vectores

Las columnas vectoriales consumen recursos de procesamiento y almacenamiento considerables. Comprender las características de almacenamiento le ayuda a tomar decisiones fundamentadas sobre el diseño del esquema.

Cada dimensión vectorial agrega 4 bytes de almacenamiento (para float de precisión única) más una sobrecarga fija. La relación entre dimensiones y almacenamiento es lineal:

Dimensiones Bytes por vector 1 millón de vectores
384 ~1,5 KB ~1,5 GB
768 ~3 kB aproximadamente 3 GB
1536 ~6 KB ~6 GB
3072 ~12 kB ~12 GB

Para un catálogo de productos con dos millones de elementos con incrustaciones dimensionales de 1536, la columna vectorial solo requiere aproximadamente 12 GB de almacenamiento. Agregar índices HNSW aumenta esto en aproximadamente un 50%.

Muchos modelos de inserción ofrecen varias opciones de dimensión. Las dimensiones más bajas reducen los costos de almacenamiento y cálculo, a la vez que mantienen una calidad razonable para muchos casos de uso. La especificación de dimensiones en la definición de columna proporciona validación. Los intentos de insertar vectores con diferentes dimensiones producen un error, lo que evita errores sutiles en los modelos de inserción no coincidentes. Defina la tabla con una restricción de dimensión explícita mediante embedding vector(768) en la definición de columna.

Algunas aplicaciones necesitan vectores de diferentes modelos. Por ejemplo, puede almacenar incrustaciones de título de producto, incrustaciones de imágenes e incrustaciones de comportamiento del usuario por separado. Cada columna vectorial necesita su propio índice porque no se puede crear un único índice que abarque varias columnas vectoriales.

CREATE TABLE products (
    id BIGSERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    title_embedding vector(768),      -- Text embedding model
    image_embedding vector(512),       -- Image embedding model
    category_id INTEGER,
    price NUMERIC(10,2)
);

-- Create separate indexes for each embedding type
CREATE INDEX ON products USING hnsw (title_embedding vector_cosine_ops);
CREATE INDEX ON products USING hnsw (image_embedding vector_cosine_ops);

Tipos de datos de metadatos: columnas estructuradas frente a JSONB

Las recomendaciones de productos rara vez usan la similitud vectorial por sí sola. Las consultas suelen filtrar por categoría, intervalo de precios, disponibilidad u otros atributos antes o junto con la búsqueda vectorial. Cómo almacenar estos metadatos afecta al rendimiento de las consultas.

Las columnas estructuradas usan los tipos de datos nativos de PostgreSQL (INTEGER, TIMESTAMP, NUMERIC, TEXT) con esquema explícito. Estas columnas ofrecen ventajas de rendimiento de las consultas porque los tipos nativos permiten índices de árbol B eficaces para consultas de igualdad y rango, eficiencia de almacenamiento a través de formatos de almacenamiento optimizados, seguridad de tipos a través de la validación en tiempo de inserción y optimización de consultas a través de estadísticas precisas del planificador. Use columnas estructuradas cuando se conozcan atributos en tiempo de diseño, filtre o ordene con frecuencia por atributos específicos o el rendimiento de las consultas sea fundamental.

CREATE TABLE products (
    id BIGSERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    embedding vector(1536),
    category_id INTEGER NOT NULL,
    price NUMERIC(10,2) NOT NULL,
    in_stock BOOLEAN DEFAULT true,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    brand TEXT,
    rating NUMERIC(2,1)
);

JSONB almacena datos semiestructurados como JSON binario, lo que ofrece flexibilidad para los atributos dinámicos. JSONB proporciona flexibilidad de esquema (distintos productos pueden tener atributos diferentes), una evolución sencilla (agregar nuevos atributos sin migraciones de esquema) y estructuras anidadas (almacenar datos jerárquicos complejos). Sin embargo, JSONB tiene sobrecarga de consulta (la extracción de valores requiere análisis), limitaciones de índice (los índices GIN funcionan para consultas de contención, pero no para consultas de intervalo), y la incertidumbre del planificador (las estadísticas son menos precisas para el contenido JSONB).

En el caso de las búsquedas vectoriales filtradas, el rendimiento del filtro de metadatos afecta directamente al tiempo total de consulta. Las columnas estructuradas con índices de árbol B permiten a PostgreSQL restringir rápidamente los candidatos antes de los cálculos de distancia de vector, mientras que JSONB requiere diferentes patrones de consulta y tipos de índice. Un filtro de columna estructurado como WHERE category_id = 5 AND price BETWEEN 100 AND 500 puede usar un índice de árbol B en (category_id, price) para identificar rápidamente las filas candidatas. Un filtro JSONB como WHERE attributes @> '{"category": "electronics"}' AND (attributes->>'price')::numeric BETWEEN 100 AND 500 requiere un índice GIN (que no ayuda con las consultas de rango en el precio) o un examen secuencial de la columna JSONB.

Muchas aplicaciones se benefician de la combinación de columnas estructuradas y JSONB: use columnas estructuradas para atributos filtrados con frecuencia en los que importa el rendimiento de las consultas y JSONB para atributos dinámicos o raramente filtrados en los que la flexibilidad del esquema es más valiosa. Este patrón le permite optimizar el caso común sin sacrificar la flexibilidad de los casos perimetrales.

CREATE TABLE products (
    id BIGSERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    embedding vector(1536),
    -- Structured columns for common filters
    category_id INTEGER NOT NULL,
    price NUMERIC(10,2) NOT NULL,
    in_stock BOOLEAN DEFAULT true,
    -- JSONB for dynamic attributes
    attributes JSONB DEFAULT '{}'
);

Índices de metadatos para búsquedas filtradas

Los índices de metadatos complementan los índices vectoriales al acelerar la fase de filtrado de las consultas. Sin índices de metadatos adecuados, PostgreSQL podría necesitar examinar todas las filas para aplicar filtros antes de la búsqueda de vectores.

Cree índices de B-tree en columnas utilizadas en las cláusulas WHERE. Los índices de una sola columna controlan coincidencias exactas, mientras que los índices compuestos controlan combinaciones de filtros. Los índices compuestos son más eficaces cuando las consultas filtran por las columnas situadas más a la izquierda. Un índice en (category_id, price) maneja WHERE category_id = 5 y WHERE category_id = 5 AND price < 100 de forma eficaz, pero no ayuda con WHERE price < 100 por sí solo porque el precio no es la columna más a la izquierda.

-- Single-column index for exact matches
CREATE INDEX idx_products_category ON products (category_id);

-- Composite index for common filter combinations
CREATE INDEX idx_products_category_price ON products (category_id, price);

Si la mayoría de las consultas filtran por la misma condición (por ejemplo, productos en existencias), un índice parcial reduce el tamaño del índice y mejora el rendimiento. Este índice es menor que un índice completo y solo se usa para las consultas que incluyen WHERE in_stock = true. Para un motor de recomendaciones de comercio electrónico en el que casi todas las consultas tienen como destino productos disponibles, esto puede reducir significativamente la sobrecarga de mantenimiento de índices. Cree un índice parcial con CREATE INDEX idx_products_instock_category ON products (category_id) WHERE in_stock = true;.

Si usa JSONB para atributos, los índices GIN admiten consultas de contención mediante los operadores @> (contiene), <@ (contenido por), ? (la clave existe) y ?|/?& (cualquier/todas las claves existen). No aceleran las consultas de rango ni las expresiones arbitrarias de ruta de acceso JSON. Cree un índice GIN con CREATE INDEX idx_products_attributes ON products USING gin (attributes);. Para los campos JSONB consultados con frecuencia que necesitan consultas de intervalo, considere los índices de expresión. Cree un índice de expresión en un campo JSONB extraído como numérico con CREATE INDEX idx_products_json_price ON products (((attributes->>'price')::numeric)); para habilitar consultas de rango en ese campo.

Combinación de la búsqueda vectorial con filtros de metadatos

PostgreSQL ejecuta consultas mediante la combinación de exámenes de índice con filtrado. Comprender los patrones de ejecución le ayuda a escribir consultas eficaces.

El patrón más eficaz aplica primero filtros de metadatos, lo que reduce el conjunto de vectores que necesitan cálculos de similitud. PostgreSQL usa índices de metadatos para identificar los productos que coinciden con los filtros y, a continuación, aplica la similitud vectorial solo a esos candidatos. Si 5% de productos coinciden con los filtros, busca 100 000 vectores en lugar de 2 millones.

-- Efficient: filter narrows candidates before vector search
SELECT id, name, embedding <=> $1 AS distance
FROM products
WHERE category_id = 5
  AND in_stock = true
  AND price BETWEEN 100 AND 500
ORDER BY embedding <=> $1
LIMIT 10;

Utilice EXPLAIN ANALYZE para comprobar que las consultas usan índices esperados e identificar cuellos de botella en el rendimiento. El plan de consulta muestra si PostgreSQL usa los índices de metadatos para filtrar candidatos antes de la búsqueda de vectores o si recurre a exámenes secuenciales que examinan cada fila. Busque Escaneo de índice o Escaneo de índice de mapa de bits en columnas de metadatos (eficiente), Escaneo de índice mediante el índice vectorial (eficiente) y Escaneo secuencial en tablas grandes (potencialmente ineficaz). Si ve exámenes secuenciales inesperados, compruebe que existen índices adecuados y que las estadísticas están actualizadas mediante ANALYZE products;.

Algunas consultas no se prestan a un filtrado eficaz. Cuando los filtros no eliminan muchas filas (por ejemplo WHERE price > 0 , que casi todos los productos coinciden), PostgreSQL podría omitir los índices de metadatos por completo y confiar en el índice vectorial por sí solo. Este es el comportamiento esperado porque el optimizador toma decisiones basadas en costos.

A veces, necesita resultados del índice vectorial que también satisfacen restricciones que no se pueden filtrar de forma eficaz de antemano. El patrón posterior al filtrado captura más candidatos similares a vectores de los necesarios y, a continuación, aplica filtros. Ajuste el LÍMITE interno en función de la selectividad de filtro esperada.

-- Get more candidates than needed, then filter
WITH candidates AS (
    SELECT id, name, price, in_stock, embedding <=> $1 AS distance
    FROM products
    ORDER BY embedding <=> $1
    LIMIT 100
)
SELECT id, name, distance
FROM candidates
WHERE in_stock = true AND price BETWEEN 100 AND 500
ORDER BY distance
LIMIT 10;

Creación de particiones de tablas para conjuntos de datos grandes

La creación de particiones divide una tabla grande en partes más pequeñas y manejables. En el caso de las cargas de trabajo vectoriales, la creación de particiones puede mejorar el rendimiento de las consultas y simplificar el mantenimiento.

Considere la posibilidad de crear particiones cuando las tablas superen decenas de millones de filas, las consultas filtran naturalmente por clave de partición (fecha, categoría, inquilino), debe quitar de forma eficaz los datos antiguos (eliminación de particiones) o los tiempos de compilación de índices se vuelven prohibitivos en la tabla completa.

En el caso de las aplicaciones que procesan incrustaciones de series temporales (como vectores de actividad de usuario o contenido publicado a lo largo del tiempo), la creación de particiones de intervalo por fecha es efectiva. Las consultas que filtran por fecha examinan solo las particiones pertinentes. Cada partición tiene sus propios índices, lo que hace que el mantenimiento sea más fácil de administrar.

-- Create partitioned table
CREATE TABLE user_interactions (
    id BIGSERIAL,
    user_id BIGINT NOT NULL,
    embedding vector(768),
    created_at TIMESTAMP NOT NULL,
    interaction_type TEXT
) PARTITION BY RANGE (created_at);

-- Create partitions for each month
CREATE TABLE user_interactions_2025_01
    PARTITION OF user_interactions
    FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');

En el caso de aplicaciones multiinquilino o catálogos de productos con categorías naturales, la creación de particiones hash o listas puede ayudar. Las consultas filtradas por categoría examinan solo la partición pertinente, lo que reduce tanto el tamaño de los datos examinados como del índice.

Cree índices en la tabla primaria para crear automáticamente índices coincidentes en todas las particiones mediante CREATE INDEX ON products USING hnsw (embedding vector_cosine_ops);. Cada partición tiene su propio índice, que se puede compilar o volver a generar de forma independiente. Esto es útil para grandes conjuntos de datos en los que la regeneración de un único índice global tardaría horas.

La creación de particiones agrega complejidad. Las consultas que abarcan muchas particiones pueden ser más lentas que en una sola tabla. Las restricciones únicas entre particiones requieren que la clave de partición esté incluida en la restricción. Es posible que la lógica de la aplicación necesite conocer los límites de partición. Evalúe si los patrones de consulta se alinean con posibles claves de partición antes de implementar la creación de particiones.

Recursos adicionales