RAG & search

Retrieval that respects permissions

Embeddings live in Postgres next to your data, so a similarity search filters by who is asking in the same query that ranks by distance.

pgvector
volcano cloud databases migration up --all -d app001_documents.sql   create extension vector002_embeddings.sql  chunks.embedding vector(1536)003_index.sql       ivfflat (embedding vector_cosine_ops)✓ retrieval ready · policies apply to similarity queries too# a search returns only chunks the caller may already read
Overview

Two datastores, and nothing keeping them in step

The standard retrieval stack keeps documents in a database and vectors in a separate service. Now every write has to reach both, deletes have to be mirrored, and permissions have to be re-implemented in a system that only understands vectors and metadata blobs.

Keep the embeddings in the same Postgres table as the text. A search is a SQL query, so it joins, filters by tenant and obeys the row-level policies you already wrote. Deleting a document deletes its vectors, because they were columns on the row.

Colocation

The vector is a column

Run CREATE EXTENSION vector and embeddings become part of the row they describe. There is nothing to synchronise, and a deleted document cannot leave orphaned vectors behind.

  • vector(1536) or whatever your model emits
  • Cascade deletes remove embeddings with the row
  • One transaction covers text and vector
psql
\d chunks id         | uuid doc_id     | uuid tenant_id  | uuid content    | text embedding  | vector(1536)one table · no sync job
Authorization

Similarity search obeys your policies

Row-level security applies to a vector query the same way it applies to a SELECT. A user retrieves only the chunks they were already allowed to read, so retrieval cannot become a data leak.

  • Filtered per caller through auth.uid()
  • Tenant predicate and ranking in one query
  • No permission model to duplicate
volcano.dev/dashboard/database/queries
Query statistics with a similarity search alongside ordinary reads
Ranking

Three distance operators and a real index

Cosine, L2 and inner product are all available, and an IVFFlat index keeps the search fast as the table grows. Because it is SQL, you can combine distance with recency or a text filter.

  • <=> cosine, <-> L2, <#> inner product
  • IVFFlat with vector_cosine_ops or vector_l2_ops
  • Blend distance with ordinary WHERE clauses
ranked, filtered, in one statement
select content, embedding <=> $1 as distance  from chunks where tenant_id = $2  order by distance limit 5;Refund policy for annual plans   0.118Cancelling mid-term              0.2045 rows · the policy already removed the rest
Path to production

How it works

  1. 01

    Turn on the extension

    CREATE EXTENSION vector in a migration, then add an embedding column to your chunk table.

  2. 02

    Embed on write

    A function calls your embedding provider and stores the vector with the text, in the same insert.

  3. 03

    Index once it matters

    Add an IVFFlat index on the distance operator you actually query with, after you have rows.

  4. 04

    Query with the caller

    Connect as the user so the policy narrows the candidates before ranking decides the order.

Code

Schema, ingest, retrieve, index

SQL
create extension if not exists vector; create table if not exists documents ( id uuid primary key default gen_random_uuid(), tenant_id uuid not null, title text not null, created_at timestamptz not null default now() ); create table if not exists chunks ( id uuid primary key default gen_random_uuid(), doc_id uuid not null references documents(id) on delete cascade, tenant_id uuid not null, content text not null, embedding vector(1536) ); alter table documents enable row level security; alter table chunks enable row level security; -- Retrieval inherits this. A similarity search cannot return a chunk the -- caller could not have selected directly. create policy tenant_documents on documents for select to authenticated using (exists ( select 1 from memberships m where m.tenant_id = documents.tenant_id and m.user_id = auth.uid() )); create policy tenant_chunks on chunks for select to authenticated using (exists ( select 1 from memberships m where m.tenant_id = chunks.tenant_id and m.user_id = auth.uid() ));
Return

What one datastore saves

One writeinstead of two

Text and embedding land in the same transaction, so they cannot drift apart.

0permission models to mirror

The policy guarding your rows already guards what retrieval can return.

1 queryto filter and rank

Tenant predicate, recency and similarity resolve in a single statement.

What you get

No sync job

Deleting a document removes its vectors with it.

Retrieval that cannot leak

Results are bounded by the reader's existing permissions.

Ordinary SQL

Filter, join and rank with the tools you already know.

Platform

Built on Volcano

Databases and vector
  • pgvector in your database

    Vector columns, cosine, L2 and inner-product operators, and IVFFlat indexes inside PostgreSQL 15 or 16.

Functions and agents
  • Embedding and retrieval

    Functions in Python, Node or Ruby that call your embedding provider and run the search.

Authentication
  • Who may retrieve what

    Sessions that reach the database as auth.uid(), so policies filter results per reader.

File storage
  • The source documents

    Original PDFs and uploads sit in a bucket, with policies matching the rows they produced.

Frequently asked questions

Read the docs
Does Volcano create the embeddings?

No. You call your own embedding provider from a function and store what it returns. Volcano provides the vector column, the distance operators and the index.

Which distance should I use?

Cosine (<=>) suits most text embedding models. L2 (<->) and inner product (<#>) are also available, and your index operator class has to match the operator you query with.

Can a search return documents the user cannot read?

No. Row-level security applies to vector queries, so the policy removes those rows before ranking. Connect as the caller rather than with the service key for this to hold.

When should I add an index?

Once you have a representative amount of data. IVFFlat clusters the rows present at build time, so an index created on an empty table will not help you.

What vector dimensions are supported?

Whatever your model produces — declare it on the column, for example vector(1536). Changing models means a new column and a re-embed, since dimensions have to match.

Do I need a separate vector database?

Not for retrieval that has to respect permissions and stay consistent with your rows. Keeping vectors in Postgres removes the sync path and the second authorization model entirely.

Ready to build retrieval?

Build, deploy, and scale on Volcano's global platform — free to start, with no infrastructure to manage.

Explore more solutions