vector search

every briven database is created with pgvector already switched on. that is the whole of what the platform does for you here — and it is the part that is usually a week of work.

what you actually get

When a database is created, CREATE EXTENSION IF NOT EXISTS vector is run inside it as superuser. Branches inherit it, because a branch is a copy of the whole database. That is it. That is the offering.

There is no platform-created vector column, no platform-created index, and no embedding service reaching into your database. You write your own DDL, your own index and your own queries, and you supply your own embeddings. What you are spared is the install: no extension request, no waiting on an operator, and no second, separate vector database to keep in step with the first one.

Being blunt about that is the point. “Semantic search, built-in” would read better and would be false the first time somebody went looking for the switch. What is true is smaller and more useful: your vectors sit in the same database as the rows they describe, so a similarity search is a join, not an integration.

a worked example, start to finish

Four statements. Paste them into psql, your ORM’s migration runner, or sql over http. The dimension — 1536 below — must match whatever model produced your embeddings, and cannot be changed later without rewriting the column.

sql · the table

CREATE TABLE article (
  id        bigserial PRIMARY KEY,
  title     text NOT NULL,
  body      text NOT NULL,
  embedding vector(1536)
);

sql · the index

CREATE INDEX article_embedding_hnsw_idx
  ON article USING hnsw (embedding vector_cosine_ops);

sql · a row

INSERT INTO article (title, body, embedding)
VALUES ('the pooler', 'what transaction pooling changes.', '[0.031, -0.114, ...]');

sql · the query

SELECT id, title, embedding <=> $1 AS distance
FROM article
ORDER BY embedding <=> $1
LIMIT 10;

Order ascending and always give it a LIMIT. Smaller distance means more similar, and the limit is what makes the index worth having — without one you have asked PostgreSQL to sort the whole table.

the three operators, and the trap

<=> is cosine distance. <-> is L2, ordinary straight-line distance. <#> is negative inner product. Which one is right depends on the model that produced your vectors; most text embedding models are trained for cosine.

The trap: the index opclass must match the operator you query with. An index built with vector_cosine_ops does nothing for a query written with the L2 operator — PostgreSQL will not error, it will simply ignore the index and scan the table, and you will conclude that pgvector is slow. Pick the operator first, then build the matching index.

trading recall against speed

HNSW is an approximate index: it can miss a true nearest neighbour in exchange for being fast. hnsw.ef_search is the dial. It defaults to 40; roughly 10 to 400 is the useful range. Higher means better recall and more time.

sql

BEGIN;
SET LOCAL hnsw.ef_search = 100;

SELECT id, title, embedding <=> $1 AS distance
FROM article
ORDER BY embedding <=> $1
LIMIT 10;

COMMIT;

It must be SET LOCAL, inside a transaction. briven pools connections at transaction level, so a plain SET would outlive your work and land on whichever unrelated query borrows that connection next. SET LOCAL is discarded at COMMIT, which is exactly the lifetime you want. Connecting covers what else transaction pooling changes.

Use HNSW rather than IVFFlat unless you have a specific reason not to. IVFFlat needs a training pass over data you already have, which means it cannot be built on an empty table and has to be rebuilt as the data grows. HNSW works from the first row and never needs that ceremony.

briven’s own documents feature

Separately from all of the above, briven has a documents feature of its own in the dashboard, at /documents. It is a different thing and it is worth keeping the two apart in your head: the section above is your database, this section is a briven product surface that stores documents for you.

It is tRPC only. It is not on the /api/v1 management api, and inventing an endpoint for it here would be worse than saying there is not one.

The configuration, for anyone comparing: 1024-dimension embeddings produced by the model Qwen/Qwen3-Embedding-0.6B served by text-embeddings-inference, indexed with HNSW and vector_cosine_ops at m = 16 and ef_construction = 64, queried with cosine distance <=>. The default similarity threshold is 0.5, the default is 10 results, and the maximum is 50.

Automatic embedding generation is built and proven end to end in tests, but it is not switched on in production yet. The queue, the trigger and the worker all exist and pass against a stand-in model server; what has not happened is the real model server running on production hardware. That is the exact state of it — no more, no less.

sql · the index briven uses for its own documents

CREATE INDEX IF NOT EXISTS document_embedding_hnsw_idx
  ON "Document" USING hnsw (embedding vector_cosine_ops)
  WITH (m = 16, ef_construction = 64)
  WHERE embedding IS NOT NULL;

The WHERE embedding IS NOT NULL is the part worth copying. A partial index keeps rows that have no vector yet out of the graph entirely, which is both smaller and faster, and it is what you want any time embeddings arrive after the row does.

where to go next

Run these statements over HTTP with sql over http, or open a real connection from psql, Prisma or Drizzle with connecting. Try a new index or a new dimension on a throwaway copy first — that is what branching is for — and if the experiment writes rows you want back, history and undo covers what can and cannot be reversed.