Skip to content
Tech Stack3 August 2026 · 7 min readUpdated 30 September 2026

Postgres + pgvector for AI-Powered SaaS Features in 2026

Vector search can live in the Postgres you already run. The first thing that breaks is the tenant filter, which fails silently, so learn iterative scans, index choice and hybrid search before you buy a second database.

Postgres + pgvector for AI-Powered SaaS Features in 2026

What you'll learn

  • Do You Need a Separate Vector Database for a SaaS AI Feature?
  • Why Does Filtered Vector Search Return Too Few Results?
  • HNSW or IVFFlat, and How Much Memory Will You Need?
Article contents

Before you sign up for a vector database, run one query on your own data: how many rows does your largest tenant own, and what share of the table is that? The answer tells you whether your first problem will be scale or filtering, and for most SaaS features it's filtering.

Most teams shopping for a vector database are about to add a second source of truth to a product that has only one. The AI feature they want is usually small: semantic search across a customer's own documents, or a "find similar tickets" button next to a support queue. The data already lives in Postgres, scoped by organization_id, protected by policies someone wrote and reviewed. pgvector puts the embeddings in that same table, so similarity search becomes a SELECT that joins to the rest of your schema inside one transaction.

Do You Need a Separate Vector Database for a SaaS AI Feature?

A small square cardboard shipping box still sealed with packing tape, its tape curling loose at one corner, sitting unopened on a warm beige desk beside an already-open wooden drawer packed with cream index cards

Usually not. pgvector stores embeddings in the Postgres table that already holds your content, so search, tenant filters, joins and deletes share one transaction, one backup and one permission model.

The argument for staying in one database is mostly about consistency. When a user deletes a document, the row and its embedding disappear together or neither does. With a separate vector service, that becomes a cross-system delete you have to reconcile yourself. For a B2B SaaS product where each customer uploads a modest number of documents, that operational simplicity is the point. I don't have a primary-source benchmark for where a single Postgres instance stops being enough, so treat any "up to N million vectors" figure you read as a claim to test on your own hardware, not a rule.

Cost is the smaller argument, and it's checkable. Pinecone's pricing page, read on 30 September 2026, lists a free Starter plan, Builder at $20 per month, Standard at a $50 per month minimum and Enterprise at a $500 per month minimum. Those minimums are small money. They're also another vendor, another SDK and another line in your security questionnaire for a feature that might not survive the next roadmap review.

pgvector is actively maintained. Its changelog lists 0.8.6 on 29 July 2026, following 0.8.2 on 25 February 2026 and three more patch releases between them and 0.8.6, and the PostgreSQL versioning page lists majors 14 through 18 as currently supported. If you're choosing foundations, PostgreSQL is the boring default, and pgvector is what keeps it a sensible default once AI features arrive.

How do pgvector and Pinecone compare for a small SaaS feature?

pgvectorPinecone
Cost at small scaleIncluded in the Postgres you already runFree Starter, then $20 Builder, $50 Standard minimum, $500 Enterprise minimum (pricing page, 30 Sep 2026)
Consistency with relational dataSame transaction, same backupA separate store to keep in sync (my inference from the architecture)
Tenant filteringWHERE clause, plus iterative scans or partial indexesHandled by the vendor's own filtering, which I did not verify here
Hybrid keyword and vector searchOne SQL query with Postgres full-text searchNot verified here
Operational surfaceOne databaseA second service, SDK and vendor

Only the first row and the operational row rest on sources I fetched or on plain architecture. If a decision hinges on the others, check Pinecone's documentation directly.

Why Does Filtered Vector Search Return Too Few Results?

A fine wire mesh sieve held tilted above a shallow white ceramic bowl in hard raking sidelight, with a scatter of small dark seeds resting on the desk beside the bowl and not one seed inside it

Approximate indexes apply your WHERE clause after scanning the index, so a selective tenant filter can leave fewer results than you asked for, and the query returns them without any error.

The pgvector README states this directly and gives the arithmetic: if a condition matches 10% of rows, then with HNSW and the default hnsw.ef_search of 40, only 4 rows will match on average. Now picture a tenant that owns 1% of the table. The index walks its graph, hands back roughly the 40 closest candidates globally, and the tenant filter throws most of them away. Nothing errors, and every query still returns quickly, which is why this survives testing. One customer eventually reports that search "feels broken".

The fix arrived in pgvector 0.8.0, which the changelog dates to 30 October 2024. It added iterative index scans: with hnsw.iterative_scan set to strict_order or relaxed_order, the index keeps scanning until enough results are found or it reaches hnsw.max_scan_tuples, which is 20,000 by default. Relaxed ordering gives better recall but can return results slightly out of order by distance, and the README shows a materialized CTE if you need strict order back. Raising hnsw.ef_search above its default of 40 is the other lever.

Iterative scans, partial indexes or partitions?

OptionFits whenCost
Iterative scanThe filter varies per query: a date range, a folder, a document typeDeeper scans on selective filters, bounded by hnsw.max_scan_tuples
Partial HNSW indexA stable filter you query constantly, such as one large tenantOne index per filter value, so it stops scaling with thousands of tenants (my reasoning)
Table partitioningAccess follows a natural key like tenant or timeReal operational weight, so try it after the cheaper options

The README shows the partial index form (CREATE INDEX ... USING hnsw (...) WHERE (category_id = 123)) and PARTITION BY LIST for partitioning.

Row level security sits on top of any of these. Supabase's RAG with permissions guide states that semantic search over protected rows "will continue to respect these RLS policies". That's the same argument for keeping isolation in the database that I make in the React and Supabase stack with row level security. Consider a Postgres-backed app on Supabase, like my React and Supabase ordering platform Pizzeria Bestek: adding vector search there would be a hypothetical extension and a policy, not a new service. One caution from that same reasoning: RLS adds a predicate the planner has to weigh alongside the index scan, so measure recall and latency with EXPLAIN ANALYZE on a realistic spread of tenant sizes, not a seed database where every tenant is equal.

HNSW or IVFFlat, and How Much Memory Will You Need?

An antique brass balance scale on a bare wooden desk, one pan loaded with a thick stack of folded cream paper and tipped down, the other pan holding a single smooth grey river stone, a green tarnish streak running down one chain

HNSW has the better speed-recall tradeoff but slower builds and higher memory use, while IVFFlat builds faster and uses less memory but has lower query performance, according to the pgvector README.

HNSWIVFFlat
Query performanceBetter speed-recall tradeoffLower
Build timeSlowerFaster
MemoryHigherLower
Training stepNone, can be created on an empty tableNeeds data to train lists
Iterative scan settinghnsw.iterative_scanivfflat.iterative_scan

For sizing, the README gives exact storage formulas. A vector takes 4 * dimensions + 8 bytes and a halfvec takes 2 * dimensions + 8, so a 1,536-dimension embedding is 6,152 bytes at full precision and 3,080 bytes as a half vector. Both types can be stored up to 16,000 dimensions, but the README limits indexing to 2,000 dimensions for vector and 4,000 for halfvec. Those are per-row numbers, and the README also advises using the default index options unless you see low recall, so resist tuning m and ef_construction before you have a measurement that justifies it. The index adds its own footprint, so measure it instead of guessing: SELECT pg_size_pretty(pg_relation_size('index_name')).

Do indexes need to fit in memory? The README says no, but you'll likely see better performance if they do. Its scaling section lists the levers in order: use halfvec instead of vector, use binary quantization for indexes with re-ranking, scale vertically, then scale horizontally with replicas or sharding tools. That ordering matches how I'd approach it: cheap storage wins first, architecture later.

What happens when you re-embed everything?

Plan for it as a maintenance event. The README notes that vacuuming can take a while for HNSW indexes and suggests reindexing first with REINDEX INDEX CONCURRENTLY. If you change embedding models, my recommendation is to write the new vectors to a new column and cut over behind a flag, rather than rewriting the old column in place.

How Do You Add Hybrid Search in Postgres?

Run a full-text query and a vector query over the same tenant-scoped rows, then combine the two rankings with Reciprocal Rank Fusion or a cross-encoder, both of which the pgvector README points to.

Pure vector search can miss exact tokens. Someone searching for an invoice number or a surname gets documents that are about invoices, not the one containing INV-20418. Full-text search is good at exact terms, vectors are good at meaning, and fusion gets both. The README's hybrid search section shows the full-text side with ts_rank_cd, and the pgvector-python RRF example shows the whole fusion in one SQL statement. The procedure:

  1. Run the vector query, take the top 20 by cosine distance, and keep each row's ID and rank.
  2. Run the keyword query over the same tenant-scoped rows with a GIN index on to_tsvector, take the top 20 and keep ID and rank.
  3. Full outer join the two sets on ID and score each row as 1.0 / (k + vector_rank) + 1.0 / (k + text_rank), treating a missing rank as zero. The example uses k = 60.
  4. Order by the fused score. If quality matters more than latency, rerank the top results with a cross-encoder.

Keep the tenant predicate inside both branches from the first commit. Retrofitting isolation into a search path is rework in the same category as retrofitting billing, which is why it sits where it does in my SaaS MVP stack recommendations. Also measure recall against a hand-labelled set of real queries before calling it done, since the README suggests monitoring recall by comparing approximate results with exact search.

Now go and look at your own numbers. How many vectors will your largest tenant own in a year, and what fraction of the table is that? If it's a small slice, you have a filtering problem to solve before you have a scale problem, and SET hnsw.iterative_scan = relaxed_order is the first thing to try tonight.

Free resource

Free SaaS MVP Scope Template

A Notion document with the full feature checklist, MVP vs. nice-to-have table, pre-build questions, and cost signals — so you walk into any developer call knowing exactly what to ask for.

Get the template →
DL

Dusko Licanin

Full-Stack Developer · Banja Luka, Bosnia

Full-stack developer shipping SaaS MVPs, web apps, and mobile apps using AI-augmented workflows — without agency coordination overhead. Live portfolio: BookBed, Callidus, Pizzeria Bestek.

Frequently Asked Questions

Is pgvector good enough to replace Pinecone for a SaaS product?

For many SaaS features, yes, because pgvector keeps embeddings in the Postgres you already run, so deletes, permissions and joins share one transaction. Pinecone's pricing page, checked on 30 September 2026, lists a free Starter plan, Builder at $20 per month, Standard with a $50 monthly minimum and Enterprise with a $500 monthly minimum, and it adds a second store to keep in sync. I have no primary benchmark for the point where one Postgres instance stops being enough, so test recall and latency with your own data and tenant sizes. Revisit the decision if index size, latency or re-embedding cost outgrows what one instance handles.

How do I run vector search in Postgres without a separate service?

Install the extension with CREATE EXTENSION vector, add a vector or halfvec column to the table that holds the content, create an HNSW index on it, and order by a distance operator. The query is an ordinary SELECT, so your existing tenant predicates, joins and row level security policies apply. Two settings matter early: hnsw.ef_search, which defaults to 40, and hnsw.iterative_scan, added in pgvector 0.8.0, which lets the index keep scanning when a WHERE clause removes most candidates. Set them before customers depend on search, then measure recall against exact search on a sample of real queries.

Where should embeddings live in a multi-tenant SaaS schema?

Put the embedding column on the table that owns the content, next to the tenant key you already filter by. Then the delete cascade, the backup and the row level security policy you wrote for that table cover the vectors as well. A separate embeddings table is not wrong, but it needs its own tenant column and its own policy, which is one more place to get isolation wrong. Either way, include the tenant predicate in the retrieval query from the first commit, because adding it later means auditing every call site that searches.

Should I use HNSW or IVFFlat in pgvector?

Start with HNSW unless build time or memory is your constraint. The pgvector README says HNSW has a better speed-recall tradeoff than IVFFlat but slower builds and higher memory use, and that it can be created on an empty table because it has no training step. IVFFlat builds faster and uses less memory but has lower query performance. Both support iterative scans through hnsw.iterative_scan and ivfflat.iterative_scan. If memory becomes the problem, the README suggests halfvec storage or binary quantization with re-ranking before you look at bigger architecture changes.

What breaks first when you scale pgvector?

Recall under filtering usually breaks before raw speed does. The pgvector README explains that approximate indexes apply filtering after the index is scanned, so with a condition matching 10% of rows and the default hnsw.ef_search of 40, only about 4 rows match on average. A tenant with a small share of the table can get near-empty results without any error. Enable iterative scans, consider partial indexes or partitioning for stable filters, and monitor recall against exact search. Memory and index maintenance come next, and the README covers halfvec, quantization and reindexing before vacuum.