Vector Search
AnvilBase supports vector embeddings natively through pgvector. Because it’s a
Postgres extension, your vectors live in the same database as the rest of your data
— you can JOIN embeddings against application rows, filter them with RLS, and
back them up with everything else. No separate vector database to run.
1. Enable pgvector
create extension if not exists vector;Run this once per project via the SQL Editor or exec_sql.
2. Add a vector column
The Schema API and CLI accept the vector type directly. Pick a dimension that
matches your embedding model (e.g. 1536 for OpenAI text-embedding-3-small, 768
for many open models):
{ "columns": [ {"name":"id","type":"uuid","primary_key":true,"default":"gen_random_uuid()"}, {"name":"content","type":"text","nullable":false}, {"name":"embedding","type":"vector(1536)"} ], "enable_rls": true}anvilbase schema table create --project <id> --name documents --from ./documents.jsonOr in SQL:
create table documents ( id uuid primary key default gen_random_uuid(), content text not null, embedding vector(1536));3. Add an index
For anything beyond a handful of rows, build an approximate-nearest-neighbor index. HNSW gives the best recall/speed trade-off:
-- cosine distance (most common for normalized embeddings)create index on documents using hnsw (embedding vector_cosine_ops);
-- or IVFFlat (faster build, needs ANALYZE; choose lists ~ rows/1000)create index on documents using ivfflat (embedding vector_l2_ops) with (lists = 100);Operator classes: vector_cosine_ops (cosine), vector_l2_ops (Euclidean),
vector_ip_ops (inner product).
StreamingDiskANN — the performance tier (diskann)
For large embedding tables, AnvilBase bundles pgvectorscale, Timescale’s
DiskANN-backed index that scales further than pgvector’s built-in indexes. It’s
enabled per project automatically (alongside pgvector), so you can build a
diskann index directly:
-- StreamingDiskANN over cosine distancecreate index on documents using diskann (embedding vector_cosine_ops);When to reach for it:
- Use
diskannwhen your table is large (hundreds of thousands of rows and up) and you want high recall with low query latency without holding the whole index in RAM — DiskANN streams from disk and uses SBQ compression to keep the in-memory footprint small. It also builds incrementally as you insert. - Use
hnsw(above) for small-to-medium tables where the index fits comfortably in memory — it’s simplest and has excellent recall/latency. - Use
ivfflatwhen you need the fastest build time and can runANALYZE, and approximate recall is acceptable.
If you enable it manually (e.g. in the SQL Editor), it depends on pgvector:
create extension if not exists vectorscale cascade;Operator classes mirror pgvector: vector_cosine_ops, vector_l2_ops,
vector_ip_ops. A diskann index accelerates the same distance operators
(<=>, <->, <#>) — including the REST API’s
order=embedding.cosine.[...] / order=embedding.l2.[...]
vector ordering, so server-driven similarity queries get the index automatically.
Availability depends on the Postgres image. The AnvilBase image ships pgvectorscale; a stock Postgres without it still serves vector search via the
hnsw/ivfflatindexes above.
4. Insert embeddings
Generate embeddings in your application, then write them as a bracketed array
literal. Distance operators: <=> cosine, <-> L2, <#> negative inner product.
// TypeScript — embed with your model, then insert via the SDKconst embedding = await embed(content) // number[] of length 1536await db.from('documents').insert({ content, embedding: JSON.stringify(embedding), // '[0.01, -0.23, ...]'})# Pythonemb = embed(content) # list[float]db.table("documents").insert({ "content": content, "embedding": str(emb), # '[0.01, -0.23, ...]'}).execute()5. Query by similarity
The cleanest way to expose similarity search to clients is a Postgres function you call over RPC — it keeps the vector math server-side and stays subject to RLS:
create or replace function match_documents( query_embedding vector(1536), match_count int default 5)returns table (id uuid, content text, similarity float)language sql stableas $$ select d.id, d.content, 1 - (d.embedding <=> query_embedding) as similarity from documents d order by d.embedding <=> query_embedding limit match_count$$;Call it from any client:
const queryEmbedding = await embed('how do I reset my password?')const { data: matches } = await db.rpc('match_documents', { query_embedding: JSON.stringify(queryEmbedding), match_count: 5,})curl -X POST "http://localhost:39001/v1/rest/<project_id>/rpc/match_documents" \ -H "apikey: $ANON_KEY" -H "Content-Type: application/json" \ -d '{"query_embedding":"[0.01,-0.23, ...]","match_count":5}'Hybrid search and filtering
Because it’s all SQL, you can combine vector similarity with ordinary predicates and full-text search in one query — and RLS still applies, so a user only matches documents they’re allowed to see:
select id, content, 1 - (embedding <=> $1) as similarityfrom documentswhere org_id = current_setting('app.current_user_id')::uuid -- RLS-aligned filter and content ilike '%billing%'order by embedding <=> $1limit 10;Tips
- Normalize embeddings if your model isn’t already normalized, and use cosine distance for text.
- Set
hnsw.ef_search(orivfflat.probes) per session to trade recall for latency:set hnsw.ef_search = 100;. - Keep the dimension in the column type (
vector(1536)) so inserts of the wrong size fail loudly. - Embeddings count toward your project’s
max_db_size_mbquota — they’re large.
Next: Migrations.