pgvector for Personalization: When Postgres Is Enough — and When It Isn't
Most teams wire up pgvector in a weekend and assume they have personalization. They don't. This post maps the real ceiling: latency budgets, the filtered-ANN trap, cold-start gaps, and the exact moment a dedicated vector DB pays for itself.
pgvector for Personalization: When Postgres Is Enough — and When It Isn't
TL;DR.
- pgvector gives you ANN search inside Postgres; it does not give you personalization — those are different problems.
- The filtered-ANN problem hits personalization harder than generic search: every practical recommendation query filters by category, availability, or user-segment before ranking, and that is exactly where pgvector's index breaks down.
- At under 500K items and p50 budgets above 20 ms, pgvector is a legitimate production choice — above that, you're fighting the library instead of shipping.
- In 2026, pgvector 0.8.x's iterative index scan and parallel HNSW build close the gap with dedicated vector DBs for many workloads, but not for real-time embedding updates or sub-10 ms filtered ANN at millions of items.
- If you only remember one thing: pgvector is the right layer for ANN — not the whole personalization stack; you still need the user-state, business-rules, and cold-start layers that no vector index provides.
The pgvector setup post is everywhere. Add the extension, create a vector(768) column, build an HNSW index, call <=> in a SELECT — you're done in an afternoon. What those posts don't tell you is what you haven't built: a recency signal, a cold-start strategy, a filtering layer that actually uses the index, or any model of what the user wants beyond the last embedding you shoved into the column. The gap between "vector similarity search" and "personalization" is large, and in production it shows up as a p99 spike when your catalog hits 2 million items, a 40% click-through drop for new users, and a recommendation list that returns out-of-stock items because your WHERE clause bypassed the index.
This post is not about how to install pgvector. It is about what you can and cannot build on top of it, where the ceiling is, and how to structure the layers above it so the system behaves like personalization rather than a nearest-neighbor lookup.
What pgvector Actually Gives You
pgvector gives you three things: a vector column type, distance operators (<-> L2, <=> cosine, <#> inner product), and two index types — IVFFlat and HNSW — that make those operators fast at scale (pgvector GitHub).
IVFFlat partitions the vector space into nlist inverted lists during build. At query time it scans nprobe lists. Build is fast and memory-light. Recall degrades as the dataset grows because the Voronoi partition becomes a worse approximation of the true neighborhood. It is a good choice under 100K vectors or in environments where rebuild cost matters more than recall.
HNSW (Hierarchical Navigable Small World) builds a layered proximity graph. Query traversal is O(log n) in the number of layers, which gives it much better recall-vs-latency characteristics at scale (Malkov & Yashunin, 2016). The tradeoffs: HNSW index build is slow (minutes for millions of vectors), the in-memory footprint is large (a 1M-row HNSW index on 768-dimensional vectors can consume 4–6 GB of shared buffers depending on m and ef_construction), and ef_search must be tuned to get high recall without blowing your latency budget.
pgvector 0.5.0 (mid-2023) added HNSW. Version 0.7.x added parallel index builds and iterative index scans — the latter being the most important change for personalization workloads, which we'll get to. In 2026, 0.8.x is the stable production baseline.
What pgvector does not give you: user state, interaction history, recency weighting, diversity constraints, business eligibility rules, or any mechanism for a user who has zero interaction history. Those are the hard parts of personalization. pgvector is an index, not a recommender.
The Filtered-ANN Problem Is Worse for Personalization
The core issue with vector search in production is that almost no real query is unfiltered. A generic document search might be — "find the 10 most semantically similar documents to this query embedding." Personalization queries are never that clean.
A typical recommendation query in a content app looks like:
find the 20 items most similar to user_embedding
WHERE category IN ('drama', 'comedy')
AND language = 'en'
AND availability = 'active'
AND NOT IN (user's already-seen item_ids)
AND published_at > NOW() - INTERVAL '90 days'
This is a filtered ANN problem, and it is notoriously hard for any vector index. There are two approaches, both imperfect:
Pre-filtering. Apply the WHERE clause first, build a candidate subset, then do ANN over that subset. The vector index cannot be used — Postgres falls back to a sequential scan over the filtered rows. At 2M items with a 10% eligible fraction, you're doing ANN over 200K rows without an index on every request.
Post-filtering. Run ANN over the full index to get the top-k candidates, then filter by metadata. This is fast but produces the wrong number of results: if you ask for 20 and half the candidates are out-of-stock, you get 10. You then have to oversample — ask for 200, filter, take 20 — which changes your recall properties entirely.
pgvector 0.7+ addresses this with iterative index scans: the HNSW traversal keeps going until it has accumulated k passing rows rather than k raw candidates. This is real progress. The implementation still has a cost: you pay for the extra graph traversal proportional to the filter selectivity, and it doesn't help when the filter is very selective (< 1% of rows match). For moderate selectivity (5–20% match rate), iterative scans work well in 2026.
Dedicated vector databases — Qdrant, Weaviate, Milvus — handle this differently. Qdrant's payload index lets it prune the HNSW graph during traversal by intersecting with an inverted index on metadata fields. This is architecturally distinct from post-filtering and significantly better for highly selective filters (Qdrant documentation). Weaviate similarly does hybrid filtered ANN natively. If your recommendation queries have more than 2–3 filter predicates or regularly hit < 5% selectivity, this architectural difference matters.
The Real Latency Numbers
Benchmarks are context-dependent, but order-of-magnitude guidance is useful. The ANN benchmarks project (ann-benchmarks.com) gives a good baseline for HNSW recall-vs-latency tradeoffs across libraries.
For pgvector HNSW with 768-dimensional vectors, tuned ef_search, and Postgres on a reasonably provisioned instance (16–32 GB RAM, SSD):
- 100K items: p50 around 2–5 ms for unfiltered ANN. Well within most budgets.
- 1M items: p50 around 8–20 ms for unfiltered ANN, depending on
ef_searchand recall target. Moderately filtered (20% selectivity with iterative scan): 15–40 ms. - 10M items: p50 around 30–80 ms unfiltered. Index may not fit in shared buffers — buffer cache misses start dominating. Filtered queries degrade badly.
Our internal target at ×marble is p50 < 30 ms for the full retrieval-and-ranking pass, not just ANN. At 10M items, pgvector is already consuming most of that budget on ANN alone, before any ranking model, re-scoring, or business rule application.
The memory constraint is underappreciated. HNSW requires the entire index in memory for low-latency access. A 5M-row index on 1536-dimensional vectors (OpenAI's standard embedding dimension) at m=16 consumes roughly 15–20 GB. If that doesn't fit in shared_buffers, Postgres pages it through the OS buffer cache with significant p99 variance. This is the wall most teams hit before they hit any algorithmic limit.
The Cold-Start Gap: What Vectors Cannot Solve
pgvector is silent on the question of new users. An embedding-based recommender works by finding items close to the user's embedding in vector space. A new user has no interaction history — no clicks, no watches, no purchases — and therefore no meaningful embedding. This is the day-zero personalization problem, and it is not a pgvector limitation; it's a fundamental issue with any purely embedding-based approach.
The common workarounds all have costs:
Popularity fallback. Show trending or globally popular items. No personalization. Reduces day-1 retention for users with niche tastes.
Demographic clustering. Assign the user to a cluster based on signup attributes (age, location, device). The cluster centroid becomes their initial embedding. Works if your demographic signals are predictive, which they often aren't for content preferences.
Onboarding preference capture. Explicit interest selection during signup. Users drop off if the flow is long; the signals are coarse.
Synthetic clone. Infer a prior distribution over user preferences from the user's context signals — IP geolocation, device, referral source, time of day, app version — and initialize the embedding from a pre-trained prior that maps those signals to a preference distribution. This is the approach we use at ×marble. It requires a knowledge graph that encodes those relationships — pure vector search cannot represent them because they're structural, not geometric.
The point is that no matter how well you tune your HNSW index, the cold-start problem requires something architecturally different from ANN over embeddings. If you design your personalization stack as "pgvector plus nothing else," you've already decided to fail for new users.
Building the Full Stack: pgvector as One Layer
The right mental model is pgvector as a retrieval layer — fast approximate candidate generation — and separate layers for everything else.
The retrieval layer
Item embeddings stored in pgvector. HNSW index. ANN against user embedding to get top-200 candidates in < 20 ms. This is the part pgvector is actually good at.
User embeddings need to be updated as the user interacts. The update strategy matters: write user embeddings to a separate table with a last_updated timestamp, and use the most recent embedding at query time. For users with rapid interaction patterns (a session with 50 item interactions in 10 minutes), you need either a write-through cache (Redis or Dragonfly) or accept that the embedding lags by one request.
The ranking layer
Take the 200 ANN candidates and apply a lightweight ranking model: recency decay, diversity penalties, business eligibility filters, user-specific interaction history exclusions. This can run in the same Postgres query using scalar functions, or be pushed to an application layer. Either way, this is where you apply the business rules that no vector index encodes.
A collaborative filtering signal belongs here too — if similar users liked item X, bump X's score even if it's not close in content-embedding space. This requires a user-item interaction matrix or a precomputed item-item similarity matrix, not a vector index.
The cold-start layer
For users without an embedding — defined as fewer than some interaction threshold, typically 5–10 events — route to a separate serving path. This might be popularity-weighted within the user's inferred segment, a content-based prior, or the synthetic clone approach. The key is that this path must be fast and must not depend on a user embedding that doesn't exist yet.
The knowledge graph layer
Structural relationships between entities — director → film, artist → genre → mood, brand → product category — are not well-represented in a flat embedding space. A knowledge graph lets you traverse these relationships explicitly: "user likes director A, director A's new film B is releasing — show B to user even though user has no interaction history with B's content embedding." pgvector cannot express this reasoning; it requires a graph traversal, which is either a recursive CTE in Postgres, a dedicated graph database, or a pre-materialized relationship table.
In 2026, the practical implementation for most teams is a Postgres-native hybrid: pgvector for ANN, JSONB columns or a separate relational table for structured signals and relationships, and application-layer orchestration for the ranking pass. It's not architecturally elegant but it avoids introducing a second database technology.
When pgvector Is the Right Choice
Be honest about when pgvector genuinely fits:
Scale is right. Under 500K items, HNSW on pgvector will deliver p50 ANN in under 10 ms with room to spare. Under 100K, even IVFFlat works. If you're not past these thresholds, a dedicated vector DB adds operational cost with no measurable benefit.
Filter selectivity is moderate. If your recommendation queries filter to 10–30% of items (common in mid-size catalogs where most content is eligible), pgvector 0.8.x's iterative index scan handles this well. You only need to escape to a dedicated DB when filters are very selective or composed from many predicates simultaneously.
You're already running Postgres. The operational argument is real. A separate vector database means a second cluster to provision, monitor, back up, and failover. For a team of 3–10 engineers, this is a real cost. If Postgres already handles your transactional data, keeping embeddings in the same database simplifies the architecture significantly.
Embedding updates are infrequent. If your item catalog is relatively stable — a few thousand new items per day, not millions — and user embeddings are batch-updated nightly rather than in real-time, pgvector's rebuild and update characteristics are acceptable. HNSW in pgvector currently does not support efficient single-row index updates at high write throughput; each insert appends to the graph, but mass rebuilds are the safe path for maintaining recall after large updates.
When to Reach for a Dedicated Vector Database
The signal that you've outgrown pgvector is not a single number — it's a pattern:
p99 spikes on filtered queries. If you're seeing your recommendation endpoint blow its latency SLA at the tail, and traces show the vector search step as the culprit, you've probably hit either the memory ceiling or the filtered-ANN problem at scale.
Catalog exceeds 2M items. At this scale, HNSW's memory requirements plus Postgres's buffer pool management make consistent p50 performance difficult without dedicated infrastructure.
Real-time embedding updates. If users' embeddings need to reflect interactions within seconds (session-level personalization), and you have high write concurrency, pgvector's write path becomes a bottleneck. Qdrant and Milvus are designed for concurrent writes to large HNSW graphs.
Multi-vector search. Some personalization approaches use multiple embeddings per user (one per interest cluster, one per content modality) and search against all of them simultaneously. pgvector supports one vector column per query; multi-vector retrieval requires application-layer orchestration or a DB that handles it natively.
Complex compound filters. If your query routinely filters on 4+ predicates with selectivities below 5%, Qdrant's payload index or Weaviate's filtered ANN will outperform pgvector's iterative scan by a substantial margin. This is an architectural difference, not a tuning problem.
What to Build First
If you're starting a personalization stack in 2026 and already have Postgres, the right sequence is:
1. Get pgvector running with HNSW and real data. Benchmark your actual query pattern — your catalog size, your filter predicates, your embedding dimensions — not synthetic benchmarks. Measure p50 and p99 at your realistic QPS. This gives you an honest read on whether you're in pgvector territory or not.
2. Separate user embeddings from item embeddings. User embeddings are high-write (updated per session), item embeddings are lower-write (updated when catalog changes). Different tables, potentially different indexing strategies.
3. Build the cold-start path before you need it. It is much harder to retrofit than the ANN layer. Define your fallback — segment-based popularity, explicit onboarding, or synthetic clone — and instrument it from day one. Track the conversion rate of cold-start users separately from warm users; it is the most important product metric you'll have in the first month.
4. Add the ranking layer. ANN gives you candidates. Re-scoring with recency, interaction history exclusions, and lightweight collaborative signals is where most of the measurable lift comes from. This is typically a 10–30% improvement in click-through rate over pure ANN ranking.
5. Benchmark again at 3× your current catalog size. Don't wait until you hit the ceiling. If the numbers suggest you'll need a dedicated vector DB in 6 months, start the evaluation now — migration is non-trivial because it involves re-indexing, API changes, and operational ramp-up.
The shortest path to wrong is treating pgvector as a personalization system rather than a retrieval component. It is a very good retrieval component. Everything above it — user modeling, cold-start, ranking, knowledge graph traversal — is still your problem to solve.
×marble is the personalization graph.
One API. A living knowledge graph per user. Day-zero ready, explainable by construction. We built it so you don't have to.