Supabase pgvector: Embeddings, HNSW Indexes, Filtering, and RAG Performance
- 35 minutes ago
- 8 min read
Supabase Vector is not a separate product with its own storage engine — it is pgvector, the open-source Postgres extension, wrapped in Supabase's managed Postgres, dashboard, client libraries, and a set of automation primitives (triggers, queues, Edge Functions) that turn a generic SQL column into a working embeddings pipeline. That framing matters for what follows: every constraint discussed here is a Postgres extension constraint, not a purpose-built vector engine's constraint, and every performance number reflects that a vector index sits on the same instance, the same buffer cache, and the same I/O path as the rest of the application's transactional workload.
Pgvector currently ships as v0.8.5 and requires Postgres 13 or later; Supabase pre-installs and manages upgrades to it. It supports four storage types — vector (single-precision float32), halfvec (half-precision float16), bit (binary), and sparsevec (sparse float) — and six distance operators: L2 (<->), inner product (<#>), cosine (<=>), L1 (<+>), Hamming (<~>), and Jaccard (<%>), the last two restricted to binary vectors. Which operator a query uses must match the operator class the index was built with, or the planner silently falls back to a sequential scan.
········
PGVECTOR DATA TYPES, DISTANCE OPERATORS, AND DIMENSION LIMITS.
How Postgres represents embeddings, and where hard ceilings apply to indexing versus storage.
Since pgvector 0.7.0, the dimension ceiling for indexed columns increased substantially, but the limits differ by whether a vector is merely stored or actually indexed. A plain vector column can hold up to 16,000 dimensions unindexed, but HNSW and IVFFlat indexes on it cap out at 2,000 dimensions — below the 3,072 dimensions of OpenAI's text-embedding-3-large at full size, which forces either dimension truncation (OpenAI's API supports this natively via the dimensions parameter) or a cast to halfvec, which raises the indexable ceiling to 4,000 dimensions at half the storage cost. A 1536-dimension float32 vector (OpenAI's text-embedding-3-small, or truncated -large) consumes roughly 6 KB per row before index overhead; the same vector cast to halfvec consumes about 3 KB.
Binary quantization via binary_quantize() converts a float vector to a bit vector, indexable up to 64,000 dimensions, at the cost of representational precision — in practice used as a coarse first-pass filter with a re-ranking step against the original float vectors, since Hamming distance on binarized embeddings alone typically loses meaningful recall. sparsevec targets sparse representations (SPLADE-style lexical-semantic hybrids) and supports HNSW indexing up to 16,000 non-zero elements, but not IVFFlat.
........
Type | Precision | Max dimensions (indexed / stored) | Approx. size at 1,536 dims | Typical use |
vector | float32 | 2,000 / 16,000 | ~6 KB | Standard embeddings (OpenAI, Cohere, open-source models) |
halfvec | float16 | 4,000 / 16,000 | ~3 KB | Large-dimension embeddings, RAM-constrained HNSW indexes |
bit | binary | 64,000 (indexed) | ~192 bytes (at 1,536 bits) | Quantized pre-filter stage with float re-ranking |
sparsevec | sparse float | 16,000 non-zero elements (HNSW only) | Depends on sparsity | Lexical-semantic hybrid vectors (e.g., SPLADE) |
........
········
HNSW INDEX BEHAVIOR, IVFFLAT TRADE-OFFS, AND BUILD COST AT SCALE.
Why HNSW is the default recommendation, and what it actually costs to build and keep resident at production volume.
HNSW indexes in pgvector default to m = 16 (maximum graph connections per node per layer) and ef_construction = 64 (candidate list size during build); query-time recall is controlled by hnsw.ef_search, defaulting to 40. Supabase's own guidance states plainly that HNSW "should be your default choice when creating a vector index," largely because the graph structure adapts incrementally as rows are inserted — it can be built immediately on an empty or partially populated table without degrading later. IVFFlat cannot: its inverted lists are formed via k-means clustering over the data present at build time, so building it before the table holds a representative sample produces skewed centroids and poor recall. IVFFlat's practical sizing rule of thumb is lists = rows / 1000 for tables up to roughly 1 million rows, and lists = sqrt(rows) beyond that, with query-time recall governed by ivfflat.probes (default 1 — searching a single list).
Build cost for HNSW at production scale is the more consequential trade-off. Third-party benchmarking (ParadeDB) reports that a 10-million-row table of 1,536-dimension embeddings can take on the order of hours to index on a single core, producing an index tens of gigabytes in size. Because HNSW query latency depends on the graph staying resident in memory, that index has to fit within shared_buffers/the instance's available RAM — once it spills to disk, latency degrades sharply rather than gracefully. This is the direct link between index size and Supabase compute-tier selection: casting embeddings to halfvec before indexing roughly halves the memory footprint of the same corpus, which in practice is what makes a mid-size compute add-on viable for a dataset that would otherwise require the next tier up.
HNSW also does not reclaim space from deleted vectors in place — tombstoned nodes remain in the graph structure until a full REINDEX, so workloads with heavy row churn (frequent re-embedding, content deletion, TTL-based cleanup) see gradual index bloat and slowly declining recall between rebuilds. There is no pgvector-native background compaction for this; it is an operational task the team has to schedule.
········
FILTERED SEARCH, ITERATIVE SCANS, AND THE AUTOMATIC EMBEDDINGS PIPELINE.
How pgvector handles metadata filters at query time, and how Supabase keeps embeddings synchronized with source rows without a separate ETL process.
Pgvector's documented weak point is combining an ANN index with a WHERE clause. By default, the index is scanned first — producing the top ef_search (HNSW) or probes-worth (IVFFlat) of approximate candidates — and the filter predicate is applied afterward. When the filter is highly selective (a rare tenant ID, a narrow date range, a small category), this can return noticeably fewer rows than the requested LIMIT, or silently drop recall, because the candidate set the index handed back never contained enough matching rows to begin with. Pgvector 0.8.0 introduced iterative index scans to address this directly: setting hnsw.iterative_scan to strict_order or relaxed_order makes the engine automatically walk further into the graph when the initial candidate set under-satisfies the filtered LIMIT, bounded by hnsw.max_scan_tuples (default 20,000) and governed by hnsw.scan_mem_multiplier (default 1). Strict order preserves exact distance ranking at the cost of more scanning; relaxed order allows minor reordering in exchange for stopping sooner.
For filters known ahead of time and stable — a tenant ID, a language, a document type — a partial index (an HNSW or IVFFlat index built with the predicate baked into the WHERE clause of the CREATE INDEX statement) sidesteps the post-filter problem entirely, at the cost of maintaining one index per predicate value or range instead of one shared index. Supabase's own applied guidance and third-party tuning writeups converge on the same fallback for very selective, unpredictable filters: partition the table, or fall back to an exact sequential scan over the pre-filtered subset, since brute-force distance computation over a few thousand rows is often faster and always exact.
Separately from search-time filtering, Supabase's automatic embeddings feature addresses keeping the vector column in sync with the row it describes, which is the actual operational burden in most RAG pipelines. The pipeline is trigger-driven: insert and column-scoped update triggers enqueue an embedding job into a pgmq queue (Postgres message queue) rather than compute the embedding inline; queued jobs are picked up and dispatched asynchronously to a Supabase Edge Function via pg_net, which makes the HTTP call to whatever embedding API the function is written against (Supabase's own examples use OpenAI's text-embedding-3-small at 1,536 dimensions, stored as halfvec). Failed jobs remain in the queue under a visibility timeout and are retried automatically rather than dropped, and an optional trigger nulls out the existing embedding on update so stale vectors are not served while a new one is in flight.
........
Filtering strategy | Mechanism | Recall behavior | Best fit |
Default post-filter | Index scanned first, WHERE clause applied to the candidate set | Degrades sharply as filter selectivity increases; may return fewer rows than LIMIT | Broad, low-selectivity filters |
Iterative scan — strict_order | Automatically re-scans deeper into the HNSW graph, preserves exact distance order | Higher recall; added latency scales with max_scan_tuples | Selective filters where ranking accuracy matters |
Iterative scan — relaxed_order | Same re-scan mechanism, allows minor reordering for speed | Better recall than default; some ordering drift | Selective filters where approximate ranking is acceptable |
Partial index | Separate index built with the filter predicate in the CREATE INDEX clause | Full recall within the predicate's scope | Known, stable filter dimensions (tenant, language, type) |
Sequential / exact scan | Brute-force distance computation, no index | 100% recall | Small filtered subsets or infrequent exact queries |
........
········
SIZING, COMPUTE COSTS, AND WHEN PGVECTOR ON SUPABASE IS THE RIGHT CHOICE.
What running a vector workload actually costs on Supabase's plan structure, and the point at which a dedicated vector database becomes the better trade-off.
Supabase's pricing separates the organization-level plan from project-level compute. The Free tier ($0) includes two active projects, 500 MB of database disk per project, 1 GB of file storage, 5 GB of egress, and pauses low-activity projects after seven days — workable for prototyping a RAG pipeline but not for a persistent embeddings store of any real size. Pro starts at $25/month per organization and includes $10/month in compute credit, which covers roughly one Micro compute instance; every additional project, or any larger instance size, is billed separately and hourly. Team starts at $599/month and adds compliance and collaboration controls not relevant to vector workload sizing. Compute add-ons scale from Micro (around $10/month) through Small, Medium, and Large (roughly $15, $60, and $111/month respectively) up to 16XL (in the range of $3,700/month) for teams that need the HNSW graph and working set fully resident in RAM at large row counts — Supabase's own documentation is the source for exact current rates, since compute pricing has changed more than once.
The storage math is worth doing before committing to a tier: 10 million rows of 1,536-dimension float32 embeddings is roughly 60 GB of raw vector data before HNSW graph overhead (commonly an additional 1.5–2x), meaning a corpus in that range typically needs a Medium-to-Large instance just to keep the index cached rather than paged from disk. A benchmark published by TigerData (maker of the competing pgvectorscale extension, so not a disinterested source, and testing pgvectorscale rather than stock pgvector) compared Postgres against Qdrant on 50 million 768-dimension Cohere embeddings on identical AWS hardware: at 99% recall, p50 latency was close (31.07 ms Postgres vs. 30.75 ms Qdrant) but Qdrant held a clear tail-latency advantage (36.73 ms vs. 60.42 ms p95; 38.71 ms vs. 74.60 ms p99), and built its index roughly 3.3x faster (3.3 hours vs. 11.1 hours, the latter on a single-threaded build path the authors note is due for optimization). The same report also showed Postgres/pgvectorscale far ahead on raw queries-per-second at both 90% and 99% recall targets — a result the source states without fully reconciling against its own tail-latency numbers, so it is reported here as published rather than resolved.
Choose Supabase pgvector when the embeddings corpus is expected to stay in the low tens of millions of rows or less, the filtering dimensions are known in advance so partial indexes apply, and the team's priority is keeping vectors, relational data, and application logic in one transactional database and one operational surface; move to a dedicated vector database once the working index no longer fits affordably in RAM on Supabase's largest practical compute tier, once filtered-query recall under iterative scans becomes unpredictable for the access patterns in production, or once row churn is high enough that HNSW's lack of in-place tombstone reclamation forces frequent, disruptive full reindexes.
FOLLOW US FOR MORE.
·····
DATA STUDIOS
·····



