Skip to content

AI & Machine Learning·5 min read

Why pgvector Replaced Qdrant in Our LLM Feedback Pipeline

We migrated 5 million vector embeddings from a standalone Qdrant cluster to PostgreSQL 16 with pgvector 0.7. Here is how operational overhead dropped while maintaining sub-50 ms search latencies.

Anatoli NavahrodskiFounder & CEO, GlanitPublished 27 August 2026

Why did we drop a dedicated vector database?

Managing a dedicated Qdrant cluster for 5 million vector embeddings in our Feedback AI pipeline added unnecessary operational friction. While Qdrant served sub-10 ms queries effortlessly, maintaining dual-write consistency between PostgreSQL 16 (where user feedback metadata lived) and Qdrant created synchronization edge cases. When pgvector 0.7 introduced parallel HNSW index builds and iterative index scans, vector search latencies settled at 34 ms p95 directly inside Postgres. Consolidating our storage stack eliminated cross-database network latency, simplified transaction management, and cut infrastructure costs by running vector search on existing database read replicas.

In our Feedback AI project, incoming user feedback is converted into 1 536-dimensional vectors using OpenAI text-embedding-3-small. We initially ran Qdrant v1.8 alongside Postgres to store these vectors. As the dataset crossed 5 million rows, maintaining separate snapshot schedules, sync pipelines, and auth tokens for two distinct database clusters consumed far more engineering time than tuning SQL queries.

Moving embeddings into Postgres 16 meant treating vectors like any other data type. We lost 20 ms of raw query speed, but gained single-query joins, standard ACID guarantees, and zero extra infrastructure ops.

Dual-writes and the 3 a.m. sync failure

Dual-writing to PostgreSQL and Qdrant broke atomic transactions during background network blips. Our Python 3.12 worker task had to save feedback metadata in Postgres and push the corresponding embedding vector to Qdrant over HTTP. When Qdrant timed out under peak ingestion on 12 November 2024, 4 120 metadata records lacked matching vectors, forcing us to write a 140-line reconciliation script. Moving vectors into PostgreSQL columns wrapped inside standard BEGIN...COMMIT blocks eliminated partial updates entirely.

With pgvector 0.7, vector insertion happens inside the exact same transaction block as metadata persistence. If vector processing fails or the client disconnects, Postgres rolls back the metadata and vector simultaneously.

-- Insert feedback text, metadata, and 1536-d embedding in one transaction
BEGIN;
INSERT INTO feedback_items (id, tenant_id, content, created_at)
VALUES ('fb_98231', 'tenant_42', 'Great response times on API', NOW());

INSERT INTO feedback_embeddings (feedback_id, embedding)
VALUES ('fb_98231', '[0.012,-0.043,...0.008]'::vector(1536));
COMMIT;

This snippet guarantees that zero orphaned records exist in our production schema. We deleted 450 lines of background retry logic from our Celery codebase after adopting single-transaction writes.

Qdrant vs pgvector 0.7 performance benchmark

Comparing Qdrant v1.8 and pgvector 0.7 on 5 million 1 536-dimensional vectors shows distinct resource trade-offs. Qdrant keeps vectors in RAM or memory-mapped files for ultra-fast p95 search latencies under 12 ms, but requires dedicated cluster management. Postgres with pgvector HNSW indexing consumes 28% less RAM when configured with m=16, ef_construction=64, yielding p95 search latencies under 35 ms while keeping data co-located with relational tables.

Benchmark on 5M 1 536-d vectors running on AWS RDS db.r6g.xlarge (32 GB RAM)
MetricQdrant v1.8pgvector 0.7 (HNSW)
p95 Query Latency11.8 ms34.2 ms
RAM Usage (Index + Data)19.8 GB14.2 GB
Index Build Time (5M vectors)26 minutes42 minutes
Filtered Vector Latency410 ms (cold payload index)38 ms (composite B-tree + HNSW)
Transaction SafetyEventual (Dual-write sync)ACID Compliant
Benchmark on 5M 1 536-d vectors running on AWS RDS db.r6g.xlarge (32 GB RAM)

How does HNSW tuning impact memory and recall?

Tuning HNSW parameters in pgvector 0.7 directly dictates memory consumption and search accuracy for LLM feedback clustering. Setting m=16 and ef_construction=64 achieved 97.4% recall@10 on our 5 million dataset while consuming 14.2 GB of storage for the vector index. Increasing hnsw.ef_search from 40 to 100 during search queries added 11 ms to query time but boosted recall for edge-case semantic queries. We found defaults insufficient for high-dimensional vectors, requiring explicit parameter tuning in our migration scripts.

We tested three different indexing strategies during the Feedback AI migration. IVFFlat indices built faster but suffered severe recall drops (under 81%) when new embeddings arrived post-indexing. HNSW maintained high recall across continuous writes.

-- HNSW index creation tailored for 1536-d cosine similarity
CREATE INDEX CONCURRENTLY idx_feedback_embeddings_hnsw
ON feedback_embeddings
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);

Building this index concurrently took 42 minutes on our primary production database without blocking read or write queries to the main table. In pgvector 0.5, building indexes locked tables, which prevented live production deployments.

38 seconds to 38 ms: Querying metadata with vector filters

Filtering vector searches by structured metadata was slow in Qdrant when filtering across cold payload fields, but Postgres handles mixed filters using existing composite indexes. In our Feedback AI workload, retrieving feedback vectors filtered by tenant ID and date range required passing payload indexes to Qdrant, which degraded when matching strict multi-column criteria. Postgres executes filtered HNSW scans natively using iterative index scans, combining vector distance with standard B-tree index checks without payload indexing workarounds.

Before pgvector 0.7, filtering vector queries in Postgres often defaulted to full table scans if the filter returned a small subset of rows. Iterative scans introduced in version 0.7 resolve this by stepping through the HNSW graph until enough rows matching the relational WHERE clause are collected.

Our standard production query filters by tenant identity and date windows prior to semantic rank matching:

SET hnsw.ef_search = 64;

SELECT f.id, f.content, e.embedding <=> :query_vector AS distance
FROM feedback_items f
JOIN feedback_embeddings e ON f.id = e.feedback_id
WHERE f.tenant_id = 'tenant_99'
  AND f.created_at >= '2024-01-01'
ORDER BY distance ASC
LIMIT 10;

This query returns in 38 ms against 5 million records. In Qdrant, unindexed payload fields regularly triggered 400 ms timeouts under multi-tenant load.

What breaks when scaling pgvector past 5M vectors?

Scaling pgvector beyond 5 million vectors exposes RAM limitations and prolonged index build times on standard cloud instances. Building an HNSW index on 5M 1 536-d vectors took 42 minutes on an AWS RDS db.r6g.xlarge instance with 32 GB RAM, spiking CPU utilization to 98%. Additionally, if shared_buffers is sized below the combined table and HNSW index footprint, search latencies degrade rapidly from 34 ms to over 280 ms as Postgres falls back to disk I/O.

We had to adjust PostgreSQL configuration parameters explicitly for vector search workloads. Setting maintenance_work_mem = 4GB was mandatory to prevent out-of-memory worker crashes during parallel index builds.

If your vector count grows past 20 million rows, Postgres RAM requirements escalate sharply. At that scale, partitioning tables by tenant ID or date ranges is necessary to keep individual HNSW indexes small enough to fit inside RAM buffers.

Frequently asked questions