Supabase Vector

Beginner4 min

Vectors as a column, so the filter is a WHERE clause on columns you already have and a row and its embedding land in one transaction.

#vector-search
#postgres

Keeping embeddings in the database you already run

Supabase Vector is pgvector, a Postgres extension that adds a vector column type and index types to work with it. If your application data already lives in Postgres, your embeddings can live in the same table as the rows they describe.

That is the whole proposition, and it is an argument about operations rather than about speed. One database to back up, one connection pool, one place where a row and its embedding either both exist or neither does.

Declaring a vector column

The column carries its dimension count:

create table documents (
  id          bigserial primary key,
  title       text not null,
  body        text not null,
  owner_id    uuid references auth.users not null,
  created_at  timestamptz default now(),
  embedding   extensions.vector(384)
);

The number has to match what your embedding model produces. Get it wrong and inserts fail, which is the good outcome; the bad one is picking a model later whose output does not fit the column you already filled.

Everything else on that table is ordinary. owner_id is a foreign key. You can put an index on created_at. Row-level security works the way it does on any other table, which means access control on your vectors is access control you already wrote.

Querying with pgvector's distance operators

Three operators, each a different metric:

OperatorMetric
<->Euclidean distance
<#>negative inner product
<=>cosine distance

Cosine is the usual starting point for text embeddings. Supabase notes that "dot product tends to be the fastest if your vectors are normalized," and for normalized vectors the two rank results identically, so the choice is about speed. Performing similarity search covers when that equivalence holds.

A search orders by distance and takes the closest rows:

select id, title, body
from documents
order by embedding <=> $1
limit 5;

Add an index once the table grows. Supabase documents the index types separately, and the choice affects build time and recall rather than the query you write.

Filtering with an ordinary WHERE clause

This is where the single-database argument stops being about operations and starts being about code you do not write.

select id, title, body
from documents
where owner_id = $2
  and created_at > now() - interval '90 days'
order by embedding <=> $1
limit 5;

No metadata query language, no syncing a filter attribute into a second system, no wondering whether the vector store's copy of owner_id is current. The filter runs against the same column your application writes, and the planner handles both conditions.

Compare that with keeping vectors elsewhere. You copy the fields you filter on into the vector store, and then you own the problem of keeping two copies agreeing. Every field you might filter by later has to be copied too, or you discover you cannot ask the question.

Writing a row and its embedding together

Because the embedding is a column, an insert is one statement and one transaction:

insert into documents (title, body, owner_id, embedding)
values ($1, $2, $3, $4);

Either the document and its vector both land or neither does. A separate vector store gives you two writes across two systems, and the failure between them leaves a document nothing can find or a vector pointing at a document that does not exist. Reconciling those is a background job you now maintain.

When to outgrow pgvector

The pressure shows up in two places.

Index builds and vector search compete with your transactional workload for memory and CPU on the same instance. When search traffic starts affecting your application queries, you want to scale them separately, and one Postgres does not let you.

The second is capacity. Vector indexes want to sit in memory, and a large corpus at full dimensions will outgrow what you want to provision for a database that is also serving your application.

Neither has a row count attached, because it depends on your vector dimensions, your query volume, and what else the database is doing. Watch the interference rather than a threshold. If you can cut dimensions instead, do that first; it is cheaper than an extra system.

Further reading

Knowledge check

Question 1 of 3

Your application stores documents in Postgres and embeddings in a separate vector service. A write succeeds in Postgres and fails in the vector store. What state are you in, and what does pgvector change?

Sign in to save your progress and pick up where you left off.

Open this article on its own page