TutorialPostgreSQL

pg_pinecone: El Índice Vectorial de Pinecone Dentro del Postgres que Ya Tenés

3 de noviembre de 2026
13 min de lectura
Una piña de pino junto a un cubo translúcido lleno de pequeños puntos de luz, los dos apoyados sobre una losa de pizarra oscura
Compartir:

El 27 de octubre de 2026 Pinecone liberó pg_pinecone, una extensión de Postgres que corre el motor de búsqueda que Pinecone usa a escala dentro del Postgres que ya operás. Conservás tus tablas y tus queries de pgvector; lo que cambia es el índice detrás de ORDER BY embedding <=> $1. Por defecto nunca llama a Pinecone, y cuando un servidor no alcanza, una sola llamada SQL mueve la búsqueda de una tabla a un índice de Pinecone mientras Postgres conserva todas las filas.

El 3 de noviembre de 2026 leí la documentación de pg_pinecone, el repo en GitHub (README, referencia, runbook, changelog, reportes de benchmarks) y el post de lanzamiento de Pinecone. Cada comando y sentencia SQL de abajo sale de esas páginas, salvo el harness de recall del paso 2, que marco como mío. Docker no estaba corriendo en la computadora donde escribí esto, así que no ejecuté nada: es una receta contrastada con la documentación, no un reporte de laboratorio.

1. El Problema: El Índice Supera al Servidor Antes que los Datos

El HNSW de pgvector es un grafo, y los grafos quieren RAM. En la prueba del propio Pinecone con 1 millón de vectores de 768 dimensiones, HNSW ocupó 4.1 GB con vectores completos y 2.0 GB sobre halfvec, contra 136 MB del índice native de pg_pinecone (datos del vendor; la nota de Techzine repite las cifras). Con un límite de 4 GB de memoria, el índice HNSW de precisión completa se volvió entre 2.8 y 3.8 veces más lento con recall de 0.95 a 0.99 en el reporte over-RAM del vendor, mientras que HNSW sobre halfvec y native no se frenaron.

El segundo dolor son los filtros. pgvector aplica el WHERE después del index scan, así que con el hnsw.ef_search por defecto de 40, un filtro que matchea el 10% de las filas deja unos 4 resultados en promedio. Los iterative scans (pgvector 0.8.0+) ayudan, y son lo que recomiendo en mis posts anteriores de pgvector. pg_pinecone toma otro camino: empuja un filtro de igualdad, como un tenant ID, adentro del propio scan.

📦

Última versión

0.11.0

Publicada en GitHub el 26 oct 2026, anunciada el 27 oct

⚖️

Licencia

Apache-2.0

Escrita en Rust con pgrx 0.19.2

🐘

Postgres

17 y 18

Solo self-managed: necesita superuser y pgvector

⭐

GitHub

~55 stars

pinecone-io/pg_pinecone al 3 nov 2026, creado el 26 oct

Chequeo de madurez: un proyecto 0.x que se mueve rápido

La 0.10.0 (17 de octubre) renombró el proyecto desde pinegres sin camino de upgrade y cambió el formato en disco de native, así que los índices native de la 0.9 quedan offline hasta un REINDEX. La 0.11.0 (26 de octubre) hizo de native el motor por defecto de pinecone.create_index(); hasta que corras ALTER EXTENSION pg_pinecone UPDATE después de actualizar, una llamada sin engine sigue armando HNSW. Fijá la versión y leé el changelog antes de cada upgrade.

2. Los Conceptos Mínimos

Seis ideas cubren todo lo que toca el tutorial. Todas salen de la documentación y de la referencia SQL de la 0.11.0.

🔀

Tres motores detrás de un índice

El motor es una propiedad del único índice de pg_pinecone de la tabla: pgvector (su propio HNSW o IVFFlat), native (el default) o remote. Las queries no cambian, y Postgres conserva cada fila en los tres, así que todo cambio se puede revertir reconstruyendo desde la tabla.

🧮

IVF + RaBitQ, y después un rescore exacto

Native agrupa los vectores en listas (IVF) y guarda cada uno como un código RaBitQ de más o menos un bit por dimensión (116 bytes por registro a 768 dimensiones). Una query sondea las listas más cercanas, se queda con los mejores candidatos por distancia estimada y los vuelve a puntuar exactos contra los vectores completos del heap. Pensalo como un IVFFlat comprimido, no como HNSW.

🎯

Defaults calibrados

Cada CREATE INDEX o REINDEX elige la cantidad de listas a sondear y el pool de rescore más baratos que llegan a target_recall (0.95 por defecto, recall@10 sobre una muestra de filas de la tabla) y muestra la elección en un NOTICE. Los inserts no recalibran; los centroides solo se mueven con un REINDEX.

🪶

Flat por debajo de 10.000 filas

Con menos de 10.000 filas un índice native es flat: un scan exacto de todas las filas, correcto pero cada vez más lento a medida que la tabla crece. Armalo después de la carga masiva, o hacé REINDEX más adelante.

🏷️

Una columna de filtro, dos modos de scan

filter_column empuja WHERE columna = valor adentro del scan. Con una selectividad estimada de 3% o menos, el modo L puntúa exacto cada fila que matchea; por encima, el modo H sondea hasta la mitad de las listas y chequea la key por bloque. El resto de las condiciones corre sobre cada lote que devuelve el índice.

☁️

Remote: Pinecone busca, Postgres conserva las filas

Las escrituras confirmadas se encolan en Postgres y un background worker las copia a Pinecone, normalmente en segundos; por eso la extensión tiene que estar precargada. No hay read-your-writes, y una búsqueda que Pinecone rechaza falla en el acto en vez de reintentarse.

Dónde encaja junto a mis otros posts de Postgres

En la guía de pgvector vs Qdrant, la búsqueda con filtros pesados a escala era la razón principal para sumar un segundo sistema. Native es una tercera opción: te quedás en Postgres y cambiás solo el índice. pg_pinecone también trae búsqueda full-text, que dejo afuera; el tutorial de búsqueda híbrida cubre la pata léxica dentro de Postgres.

3. Paso 1: Corré pg_pinecone en Docker y Armá un Primer Índice

El camino más rápido es la imagen oficial: pgvector/pgvector más la extensión, un tag por versión mayor de Postgres. Estos son los comandos del quickstart, que fijan 0.11.0-pg17.

1. Bajá la imagen, levantala y conectate (del quickstart)

# The image is pgvector/pgvector plus pg_pinecone (amd64 and arm64).
# Pin the version: "latest" currently points at the pg18 build.
docker pull pineconeteam/pg_pinecone:0.11.0-pg17

# shared_preload_libraries isn't needed for the native engine, but the remote
# engine and full-text search need it: setting it now saves a restart later
docker run -d --name pg_pinecone \
  -e POSTGRES_PASSWORD=secret \
  pineconeteam/pg_pinecone:0.11.0-pg17 \
  -c shared_preload_libraries=pg_pinecone

# No port is published: connect through the container as the postgres superuser
docker exec -it pg_pinecone psql -U postgres

Revisá tu CPU antes de bajar la imagen

En x86_64, la imagen y los archivos para Linux necesitan una CPU con x86-64-v3 (Intel Haswell o más nueva), y los archivos necesitan glibc 2.39 o superior, como Ubuntu 24.04 o Debian 13. En Debian 12, Ubuntu 22.04, RHEL 9 y CPUs más viejas se compila desde el código con Rust y cargo-pgrx 0.19.2.

2. Creá las dos extensiones y anotá las versiones

-- pgvector first: pg_pinecone builds on its vector type and distance operators
CREATE EXTENSION vector;
CREATE EXTENSION pg_pinecone;

-- The two versions the support page asks for in every bug report
SELECT extversion FROM pg_extension WHERE extname = 'pg_pinecone';
SELECT version();

3. El quickstart de la documentación: una tabla, un índice native, una query y su plan

-- The docs quickstart: hand-made 3-d vectors (food, nature, places)
CREATE TABLE docs (
    id        bigint PRIMARY KEY,
    body      text,
    embedding vector(3)
);

INSERT INTO docs (id, body, embedding) VALUES
    (1, 'The Eiffel Tower was completed in 1889 for the World''s Fair in Paris.', '[0.1, 0.1, 0.9]'),
    (2, 'Photosynthesis converts sunlight, water, and carbon dioxide into glucose.', '[0.05, 0.9, 0.1]'),
    (3, 'The Great Barrier Reef is the largest coral reef system in the world.', '[0.1, 0.8, 0.3]'),
    (4, 'Espresso is brewed by forcing hot water through finely ground coffee.', '[0.95, 0.05, 0.05]'),
    (5, 'Mount Everest sits on the border between Nepal and China.', '[0.05, 0.4, 0.8]'),
    (6, 'Honeybees communicate the location of flowers with a waggle dance.', '[0.2, 0.85, 0.05]'),
    (7, 'A croissant is a flaky pastry made from laminated, buttered dough.', '[0.95, 0.05, 0.1]'),
    (8, 'The Amazon rainforest produces a large share of the world''s oxygen.', '[0.05, 0.9, 0.3]');

-- native is the default engine since 0.11.0; naming it is explicit, not required
SELECT pinecone.create_index('docs', 'embedding', engine => 'native');

-- Eight rows: force the index so you can watch it work (tests only)
SET enable_seqscan = off;

SELECT id, body
FROM docs
ORDER BY embedding <=> '[1, 0, 0]'
LIMIT 2;
--  4 | Espresso is brewed by forcing hot water through finely ground coffee.
--  7 | A croissant is a flaky pastry made from laminated, buttered dough.

-- Which native path served that scan: flat, h or l (expect flat: under 10,000 rows)
SELECT pinecone.last_scan_mode();

EXPLAIN (COSTS OFF)
SELECT id
FROM docs
ORDER BY embedding <=> (SELECT embedding FROM docs WHERE id = 4)
LIMIT 2;
-- The plan should include: Index Scan using docs_embedding_pinecone_idx

Qué demuestra este primer índice y qué no

Con ocho filas el índice es flat y el planner prefiere leer todas las filas; de ahí el enable_seqscan = off. Esto demuestra que la extensión carga, arma el índice y responde una query común de pgvector a través de docs_embedding_pinecone_idx. No dice nada de velocidad ni de recall; para eso necesitás tus propios datos.

4. Paso 2: Native vs pgvector HNSW con Tus Propios Embeddings

Esta es la comparación que importa, y solo se puede correr con tus embeddings y tus queries reales. El plan sigue la guía de la documentación para pasar de pgvector a native: medís HNSW, armás native al lado, borrás HNSW y volvés a medir. Igual que el harness de Pinecone, nunca deja que dos índices compitan por la misma query.

1. La tabla, un set de queries reales y la línea base HNSW

-- Your corpus. STORAGE PLAIN keeps 768-d vectors in the heap row instead of
-- TOAST, which the native engine's exact rescore reads for every candidate
CREATE TABLE items (
    id        bigint PRIMARY KEY,
    tenant_id bigint,
    body      text,
    embedding vector(768) STORAGE PLAIN
);

-- Real query embeddings from the same model, kept apart from the corpus
CREATE TABLE eval_queries (id serial PRIMARY KEY, embedding vector(768));

-- Load both with your usual pipeline, THEN build the pgvector baseline
CREATE INDEX items_hnsw ON items USING hnsw (embedding vector_cosine_ops);
SET hnsw.ef_search = 100;

SELECT pg_size_pretty(pg_relation_size('items_hnsw'));

2. Recall@10 y latencia: pasada exacta una vez, pasada aproximada por índice

-- recall@10 of whichever index serves items right now, against exact answers.
-- My glue, plain Postgres. The exact pass forces sequential scans, the way
-- Pinecone's bench harness builds its baseline. Run it once.
SET enable_indexscan = off;
SET enable_bitmapscan = off;
SET pinecone.am_enabled = off;
CREATE TEMP TABLE exact AS
SELECT q.id AS qid, n.id
FROM eval_queries q
CROSS JOIN LATERAL (
    SELECT i.id FROM items i ORDER BY i.embedding <=> q.embedding LIMIT 10
) n;
RESET enable_indexscan;
RESET enable_bitmapscan;
RESET pinecone.am_enabled;

-- The approximate pass: rerun after every index change. \timing gives the
-- total, divide by the number of queries (check the plan with EXPLAIN first)
\timing on
DROP TABLE IF EXISTS approx;
CREATE TEMP TABLE approx AS
SELECT q.id AS qid, n.id
FROM eval_queries q
CROSS JOIN LATERAL (
    SELECT i.id FROM items i ORDER BY i.embedding <=> q.embedding LIMIT 10
) n;

SELECT round(count(*) / (10.0 * (SELECT count(*) FROM eval_queries)), 3) AS recall_at_10
FROM approx JOIN exact USING (qid, id);

Este harness es mío, así que revisalo

El runbook describe el método en prosa: recall@10 de queries reales contra las respuestas exactas, que obtenés con SET pinecone.am_enabled = off. El SQL es mi versión en Postgres puro, más los settings del planner que usa el harness del vendor para su línea base exacta. No lo corrí, así que primero hacé EXPLAIN de una query interna y confirmá que es un Index Scan sobre el índice que querés probar.

3. Pasá a native, inspeccionalo y conocé el camino de vuelta

-- 1. Build the native index next to HNSW. Cosine is create_index's default
--    metric, matching vector_cosine_ops (use metric => 'l2' or 'ip' otherwise).
--    Reads keep using HNSW meanwhile; writes wait for the build.
SELECT pinecone.create_index('items', 'embedding', engine => 'native');
-- NOTICE:  pg_pinecone: calibrated for recall@10 0.95: probing ... lists and rescoring ... rows

-- 2. Drop the pgvector index: queries move to the native one
DROP INDEX items_hnsw;

-- 3. What the build chose, and what it weighs
SELECT * FROM pinecone.index_calibration('items_embedding_pinecone_idx');
SELECT pg_size_pretty(pg_relation_size('items_embedding_pinecone_idx'));

-- 4. Rerun the approximate pass and compare recall_at_10 and \timing

-- Going back is two statements, from the same guide
DROP INDEX items_embedding_pinecone_idx;
CREATE INDEX items_hnsw ON items USING hnsw (embedding vector_cosine_ops);

4. Ajustalo si el recall no llega

-- Session overrides for a sweep; both win over the index's calibrated defaults
SET pinecone.scan_fraction = 0.05;     -- share of IVF lists probed (nprobe / n_lists)
SET pinecone.rescore_multiplier = 5;   -- rescore pool = 5 x 100 rows

-- LIMIT 100 workloads: pin a 1,000-row pool, then rebuild (probes recalibrate)
ALTER INDEX items_embedding_pinecone_idx SET (rescore_multiplier = 10);
REINDEX INDEX items_embedding_pinecone_idx;

-- Or calibrate on real queries (a table of 50+ distinct ones, never eval_queries)
ALTER INDEX items_embedding_pinecone_idx SET (calibration_queries = 'public.calib_queries');
REINDEX INDEX items_embedding_pinecone_idx;

Las dos perillas, y cuándo fijarlas

scan_fraction (listas sondeadas) y rescore_multiplier (filas re-puntuadas, en cientos) son overrides de sesión para hacer barridos. La calibración apunta a recall@10 y muchas veces se queda con el pool mínimo de 100 filas, lo que deja corto a LIMIT 100: Pinecone midió un recall@100 de 0.754 a 0.799 con los defaults y de 0.88 a 0.98 con un pool de 1.000 filas. Si tus queries son preguntas y tus filas pasajes, calibrá con queries reales.

No hagas este benchmark con vectores aleatorios

IVF necesita estructura de clusters, y los vectores aleatorios uniformes no la tienen. Con 300.000 de ellos, la referencia cuenta que la calibración terminó sondeando las 548 listas con un pool de 2.000 filas, para un recall de 0.928. Los embeddings reales forman clusters; los aleatorios hacen que native parezca un scan exhaustivo.

5. Paso 3: Búsqueda con Filtros Sin Perder Filas

La búsqueda multi-tenant es donde native se gana su lugar. Declarás una columna de filtro al crear el índice, y la igualdad sobre ella se aplica mientras el índice escanea, así que un filtro selectivo no deja la lista de resultados con hambre.

1. Reconstruí el índice con una columna de filtro y consultá un tenant

-- One pg_pinecone index per table: rebuild it with the tenant as filter column
-- (smallint, integer, bigint, text or varchar)
DROP INDEX items_embedding_pinecone_idx;
SELECT pinecone.create_index('items', 'embedding', engine => 'native', filter_column => 'tenant_id');

-- "=" on the filter column runs inside the scan, not after it
SELECT id, body FROM items
WHERE tenant_id = 42
ORDER BY embedding <=> (SELECT embedding FROM eval_queries WHERE id = 1)
LIMIT 10;

SELECT pinecone.last_scan_mode();   -- l: key at or below 3% of rows, h: above

Modo L o modo H: decide la estimación del planner

Con 3% o menos de filas estimadas que matchean, native usa el modo L: lee cada fila que matchea y la puntúa exacta, así que el recall es 1.0. Por encima, el modo H sondea listas IVF y prueba la key por bloque, salteando los bloques sin ningún match. pinecone.last_scan_mode() muestra qué camino tomó el último scan de tu sesión.

2. Condiciones que el índice no puede empujar, y cómo leer el scan

-- Any other condition (a range, IN, ILIKE, a second column) filters each batch the
-- index returns. On native, pinecone.iterative_scan = auto (the default) keeps
-- scanning until LIMIT is filled; rows after the first batch arrive in batch
-- order, so re-sort them when exact order matters:
WITH relaxed AS MATERIALIZED (
    SELECT id, embedding <=> (SELECT embedding FROM eval_queries WHERE id = 1) AS distance
    FROM items
    WHERE tenant_id = 42 AND body ILIKE '%invoice%'
    ORDER BY distance
    LIMIT 10
)
SELECT * FROM relaxed ORDER BY distance + 0;

-- Did the scan stop at pinecone.max_scan_tuples (20,000 rows by default)?
SELECT batches, rescored, capped FROM pinecone.last_scan_stats();

-- On the pgvector engine, pgvector's own switch applies instead (0.8.0+)
SET hnsw.iterative_scan = relaxed_order;

El planner no le pone precio al tamaño de una key empujada

La referencia dice que la estimación de costo le da el mismo costo a una key del 1% y a una del 10%, así que puede elegir el modo L donde un sequential scan es más rápido. La corrida de 10M filas de Pinecone muestra lo que puede costar el modo L: 669 ms para un filtro del 1% contra 391 ms de un sequential scan con filtro. Revisá los tenants lentos con EXPLAIN ANALYZE; SET pinecone.am_enabled = off aleja a una query del índice.

6. Paso 4: El Motor Remote (Búsqueda en Pinecone, Filas en Postgres)

Remote es para una tabla que ya no entra en un solo servidor de Postgres. pg_pinecone crea un índice serverless en tu proyecto de Pinecone y copia los vectores en segundo plano, escribiendo en los dos hasta que termina la copia. Los comandos salen de la guía para pasar a remote y de la página de storage engines.

Cambiá de motor, seguí la copia, consultá y volvé

-- Prerequisites: shared_preload_libraries = 'pg_pinecone' (the docker run in
-- step 1 sets it), a single-column primary key, and an API key in the database
SELECT pinecone.set_api_key('YOUR_API_KEY');

-- Creates a serverless index (aws us-east-1 unless you set pinecone.am_cloud and
-- pinecone.am_region first) and copies the vectors in the background.
-- Queries keep using the native index until the copy finishes.
SELECT pinecone.set_engine('items', 'remote');

-- mode is 'promoting' during the copy, 'remote' with backlog 0 once Pinecone serves
SELECT table_name, mode, index_name, backlog FROM pinecone.status();

-- The same query, unchanged: the filter column travels as a metadata filter
SELECT id, body FROM items
WHERE tenant_id = 42
ORDER BY embedding <=> (SELECT embedding FROM eval_queries WHERE id = 1)
LIMIT 10;

-- Back to native: a blocking local rebuild, then the Pinecone index is deleted
SELECT pinecone.set_engine('items', 'native');
SELECT table_name, mode FROM pinecone.status();   -- done when items no longer appears

Lo que resignás en remote

Una query justo después de una escritura puede no verla: las escrituras suelen llegar a Pinecone en segundos, en minutos bajo carga pesada. Las búsquedas con rate limit o error no se reintentan, así que sumá backoff en tu app. La copia tarda hasta cerca de una hora por millón de filas, y volver bloquea lecturas y escrituras mientras se reconstruye native. Es un índice estándar en tu proyecto de Pinecone, así que aparece en esa factura.

Remote tiene sentido cuando

  • • La tabla ya no entra en un servidor, pero las filas, los joins y las transacciones siguen perteneciendo a Postgres.
  • • Cada tenant necesita su propio namespace: una partition_column NOT NULL obliga a cada query a nombrar su tenant.
  • • Ya pagás Pinecone y querés sacarte de encima un job de sincronización que mantenés vos.

Quedate en native o pgvector cuando

  • • Necesitás read-your-writes, como buscar justo después de subir algo.
  • • Los datos no pueden salir de tu red, o una llamada de red por query es inaceptable.
  • • La tabla entra en un servidor: el índice native pesó 1.3 GB con 10M × 768 en la corrida de Pinecone, contra 41 GB de HNSW.

7. Native vs Remote vs pgvector

Los tres motores lado a lado, según la documentación y la referencia SQL de la 0.11.0. Los tamaños son las mediciones de Pinecone con 1M × 768.

MotorDónde corre la búsquedaFiltrosConsistenciaIdeal paraOjo con
native (default)Dentro de Postgres: IVF + RaBitQ, rescore exacto desde el heap; sin cuenta ni llamadas de redUna columna de filtro por igualdad dentro del scan; el resto, por loteTransaccional, como cualquier índice de PostgresBúsqueda con filtros y multi-tenant en servidores con poca RAM (índice de 136 MB)0.x; sin builds CONCURRENTLY; flat por debajo de 10.000 filas; tope de 2.038 dimensiones; REINDEX para mover centroides
remoteÍndice serverless de Pinecone; las filas quedan en Postgres; necesita API key y precargaLa columna de filtro viaja como filtro de metadata; una partition column le da un namespace a cada tenantEventual (segundos); sin read-your-writes; las lecturas no se reintentanUna tabla que ya no entra en un servidorFactura de Pinecone, latencia de red, us-east-1 por defecto, la vuelta bloquea
pgvectorEl HNSW o IVFFlat propio de pgvector, en PostgresDespués del index scan; hnsw.iterative_scan (0.8.0+) sigue escaneandoTransaccionalTablas con HNSW que no filtran; builds CONCURRENTLYEl grafo quiere RAM: 4.1 GB a precisión completa, 2.0 GB sobre halfvec

pg_pinecone maneja un índice por tabla, así que el motor se elige tabla por tabla, y cada cambio conserva las filas.

8. Los Benchmarks de Pinecone, Leídos con Cuidado

Son números del vendor, pero publicados con harness y configs, que es más de lo que dan la mayoría de los lanzamientos. Las filas sin filtro salen del reporte de halfvec, las filtradas del reporte con filtros. Las queries out-of-region caen lejos de las filas que matchean el filtro; las in-region (estilo tenant) caen cerca.

Métrica (1M × 768)pg_pinecone nativeHNSW sobre halfvecHNSW con vectores completos
Tamaño del índice136 MB2.0 GB4.1 GB
Heap + índice4.3 GB (conserva los vectores completos)3.7 GB con columna halfvec, 6.2 GB como índice de expresión8.2 GB
Tiempo de build45 s55–62 s79 s
p50 con recall@10 de 0.951.27 ms1.71 ms2.67 ms
p50 con recall@10 de 0.992.28 ms4.65 ms6.72 ms
Filtro del 1%, out-of-region0.981 @ 8.3 ms0.924 @ 55.7 ms0.926 @ 57.8 ms
Filtro del 10%, out-of-region1.000 @ 12.3 ms0.922 @ 5.1 ms0.920 @ 5.9 ms
Estilo tenant, in-region1.000 @ 8.5–12.8 ms0.983–0.989 @ 3.2 ms0.981–0.987 @ 3.7–4.5 ms

Las condiciones detrás de la tabla

Una AWS r6id.4xlarge (16 vCPUs, 128 GB), wiki_dpr 1M × 768, L2, k = 10, 500 queries reservadas, un cliente, pgvector 0.8.7. Sin filtro: PostgreSQL 17.11, HNSW armado con m = 16 y pg_pinecone 0.10.0 compilado desde main antes del release (16 oct). Con filtro: PostgreSQL 18.6, el release 0.10.0, ef_search 160 con relaxed_order (22 oct). El anuncio dice 5.9 ms para HNSW con 10% y su gráfico 5.1 ms: el reporte tiene los dos, para vectores completos y para halfvec.

Lo que me llevo

  • • Un índice chico no es una huella chica: native re-puntúa contra los vectores completos, así que el heap de 4.1 GB se queda (4.3 GB en total, más que HNSW sobre una columna halfvec). Lo que se achica es lo que tiene que vivir en RAM.
  • • Los filtros son donde la diferencia es real, y corta para los dos lados: con 10% HNSW fue más rápido pero se frenó cerca de 0.92 de recall, y en queries estilo tenant HNSW le ganó en latencia a native con recall de 0.98 o más.
  • • La escala cambia el camino: con 10M en la 0.11.0, un filtro del 1% cayó en modo L y perdió contra un sequential scan, mientras HNSW quedaba cerca de 0.85 de recall.
  • • halfvec cierra buena parte de la brecha sin filtros, sin costo de recall medible en este dataset. Probalo antes de cambiar de motor.
  • • Un vendor, una máquina y un dataset, solo L2, con fsync y jit apagados. Todavía no encontré una reproducción independiente.

9. Errores Comunes (y la Solución de Cada Uno)

Lo que esperaría encontrarme en la primera semana, cada punto respaldado por la documentación, la referencia o el runbook.

Un índice flat, o ningún índice

Síntoma: En una tabla chica el plan muestra Seq Scan, o last_scan_mode() dice flat: por debajo de 10.000 filas native escanea todas las filas.

Solución: Armá el índice después de la carga masiva o hacé REINDEX cuando la tabla crezca. enable_seqscan = off es solo para pruebas.

Cada fila re-puntuada paga un fetch de TOAST

Síntoma: Native va más lento de lo esperado, y CREATE INDEX mostró un NOTICE sobre STORAGE PLAIN: pgvector guarda un vector de 768 dimensiones (3.080 bytes) fuera de la fila.

Solución: ALTER TABLE items ALTER COLUMN embedding SET STORAGE PLAIN, y después VACUUM FULL items (lock ACCESS EXCLUSIVE). UPDATE … SET embedding = embedding no mueve las filas existentes.

Métrica equivocada después del cambio

Síntoma: Reemplazaste un índice HNSW de L2 o de producto interno y las queries dejaron de usar un índice: create_index() usa coseno por defecto.

Solución: Pasá metric => 'l2' (vector_l2_ops) o 'ip' (vector_ip_ops), y mantené el operador de la query en línea con el índice.

La calibración dice 0.97, tu evaluación dice 0.92

Síntoma: El build calibra con filas de la tabla, y los embeddings de preguntas no se parecen a los de pasajes.

Solución: Apuntá calibration_queries a 200 a 1.000 queries reales (no tu set de evaluación) y hacé REINDEX, o subí target_recall.

LIMIT 100 devuelve peores vecinos que LIMIT 10

Síntoma: La calibración apunta a recall@10, y el pool no sigue al LIMIT.

Solución: ALTER INDEX … SET (rescore_multiplier = 10), y después REINDEX.

Las escrituras se traban durante builds y mantenimiento

Síntoma: Native rechaza CREATE INDEX CONCURRENTLY y REINDEX CONCURRENTLY por diseño: los builds bloquean escrituras, y un REINDEX también bloquea las lecturas que pasan por el índice.

Solución: Usá una ventana de mantenimiento, seguí pinecone.index_drift() y dejá pinecone.auto_maintain apagado salvo que te puedas bancar un freno desatendido.

Tus embeddings son demasiado anchos

Síntoma: CREATE INDEX falla con vectores de 3.072 dimensiones: native topea en 2.038 dimensiones con páginas de 8 KB, según un test de límite del propio código.

Solución: Usá un modelo de 768 a 1.536 dimensiones o embeddings recortados, o pgvector HNSW sobre halfvec (hasta 4.000 dimensiones).

No se instala en tu Postgres administrado

Síntoma: CREATE EXTENSION pg_pinecone falla: necesita superuser y archivos en el directorio de extensiones del servidor.

Solución: La documentación la limita a Postgres 17 y 18 self-managed, y ninguna de mis fuentes nombra un proveedor administrado que la ofrezca.

Mi regla práctica

Medí antes de cambiar: recall@10 y latencia de queries reales, con filtros reales, primero con HNSW y después con native. Cambiá solo las tablas donde native gana con tus números, y mantené el CREATE INDEX de HNSW en tus migraciones para que volver sean dos sentencias.

Conclusión

pg_pinecone es una alternativa seria a HNSW dentro de Postgres: un índice IVF + RaBitQ unas 30 veces más chico que HNSW a precisión completa en la prueba de Pinecone, que empuja un filtro de igualdad adentro del scan y no te toca las queries. El motor remote es una salida de emergencia razonable, con un camino de vuelta documentado.

También tiene ocho días como proyecto público, con un default que cambió la semana pasada y un formato en disco que cambió la semana anterior. Mi opinión: probalo esta semana sobre una copia de tus datos, con el harness del paso 2 y tus filtros reales, y llevalo a producción solo donde le gane a HNSW con tus números, en Postgres self-managed y con una ventana de mantenimiento para el REINDEX.

Sello de verificación

Documentación: las páginas de pg_pinecone en docs.pinecone.io, leídas el 3 nov 2026. Repo: pinecone-io/pg_pinecone en v0.11.0 y main, incluidos la referencia, el runbook, el changelog y los reportes de bench/vector. Tags de la imagen revisados en Docker Hub; README de pgvector en v0.8.7. Todos los benchmarks son de Pinecone. No ejecuté nada: Docker no estaba corriendo en esta computadora.

Fuentes

Diego Rodriguez

Diego Rodriguez

Ingeniero Senior Full-Stack & AI

Diego tiene mas de 9 anos de experiencia construyendo aplicaciones potenciadas por IA de produccion, desde orquestacion de LLMs y pipelines RAG hasta deteccion de riesgos con ML y sistemas de trading algoritmico.

Conoce mas sobre Diego →