page.tsx222 lines · main
1import Link from 'next/link';
2
3import { DocsShell } from '../../components/shell';
4
5export const metadata = { title: 'vector search' };
6
7export default function VectorSearchPage() {
8 return (
9 <DocsShell>
10 <h1 className="font-mono text-2xl tracking-tight">vector search</h1>
11 <p className="mt-2 font-mono text-sm text-[var(--color-text-muted)]">
12 briven supports pgvector as a first-class column type and exposes a `ctx.db.vectorSearch`
13 chain for nearest-neighbour queries inside any function. embeddings can come from
14 anywhere — you pass the vector in; briven runs the search.
15 </p>
16
17 <Section title="schema declaration">
18 <p>
19 declare a `vector(N)` column on any table. `N` is the embedding dimension your model
20 produces — 768 for nomic-embed-text, 1536 for OpenAI text-embedding-3-small, etc.
21 </p>
22 <Snippet>{`import { schema, table, text, timestamp, vector } from '@briven/cli/schema';
23
24export default schema({
25 documents: table({
26 columns: {
27 id: text().primaryKey(),
28 title: text().notNull(),
29 body: text().notNull(),
30 embedding: vector(768).notNull(), // 768 dims = nomic-embed-text
31 createdAt: timestamp().notNull().default('now()'),
32 },
33 indexes: [
34 // index the vector column for fast ANN search. without the index,
35 // postgres falls back to a sequential scan — fine for <10k rows,
36 // painful past that.
37 { columns: ['embedding'], unique: false },
38 ],
39 }),
40});`}</Snippet>
41 <p>
42 briven&apos;s migration apply path runs <code>CREATE EXTENSION IF NOT EXISTS vector</code>{' '}
43 automatically before applying any schema that declares a vector column; you don&apos;t
44 need to enable pgvector manually on the project shard.
45 </p>
46 </Section>
47
48 <Section title="search query">
49 <p>
50 inside a function, call <code>ctx.db(&apos;documents&apos;).vectorSearch(...)</code> with
51 a query vector + the column name + optional distance / limit / where filter.
52 </p>
53 <Snippet>{`// briven/functions/searchDocs.ts
54import { query, type Ctx } from '@briven/cli/server';
55
56interface Args {
57 embedding: number[];
58 topK?: number;
59}
60
61export default query(async (ctx: Ctx, args: Args) => {
62 return ctx
63 .db('documents')
64 .vectorSearch({
65 column: 'embedding',
66 vector: args.embedding,
67 distance: 'cosine', // 'l2' (default) | 'inner_product' | 'cosine'
68 limit: args.topK ?? 10,
69 })
70 .where({ archived: false }) // optional predicate filter
71 .select(['id', 'title', 'body']);
72});`}</Snippet>
73 <p>
74 the returned rows are ordered by similarity to the query vector — closest first.
75 the vector column itself is omitted from the response unless you explicitly include it
76 in <code>.select()</code>.
77 </p>
78 </Section>
79
80 <Section title="generating embeddings">
81 <p>
82 briven doesn&apos;t generate embeddings — that&apos;s your call. three common shapes:
83 </p>
84 <ul className="list-disc pl-5">
85 <li>
86 <strong>self-hosted via the briven ollama proxy</strong>: if your operator has
87 <code> nomic-embed-text </code> available (the briven.tech proxy does today), POST to{' '}
88 <code>/api/embeddings</code> from inside an <code>action()</code> function — same auth
89 shape as the AI features.
90 </li>
91 <li>
92 <strong>third-party API</strong>: OpenAI, Voyage, Cohere — call from inside an{' '}
93 <code>action()</code> (not a query/mutation; embeddings are slow + idempotent and
94 should live outside the transaction).
95 </li>
96 <li>
97 <strong>at write time</strong>: when you insert a document, compute its embedding in
98 the same mutation and store it. queries become cheap.
99 </li>
100 </ul>
101 <Snippet>{`// briven/functions/indexDocument.ts — embed at write time
102import { mutation, type Ctx } from '@briven/cli/server';
103import { ulid } from '@briven/shared';
104
105interface Args { title: string; body: string }
106
107export default mutation(async (ctx: Ctx, args: Args) => {
108 // embedded inline; the briven ollama proxy serves nomic-embed-text
109 // alongside the chat models. swap for OpenAI / Voyage as needed.
110 const res = await fetch('https://ai.flndrn.com/api/embeddings', {
111 method: 'POST',
112 headers: {
113 'content-type': 'application/json',
114 'x-api-key': ctx.env.OLLAMA_API_KEY ?? '',
115 },
116 body: JSON.stringify({ model: 'nomic-embed-text:latest', prompt: args.body }),
117 });
118 const { embedding } = (await res.json()) as { embedding: number[] };
119
120 const id = ulid('doc');
121 const [row] = await ctx
122 .db('documents')
123 .insert({ id, title: args.title, body: args.body, embedding })
124 .returning();
125 return row;
126});`}</Snippet>
127 </Section>
128
129 <Section title="distance operators">
130 <p>three operators are supported, mapping to pgvector&apos;s native ones:</p>
131 <ul className="list-disc pl-5">
132 <li>
133 <code>l2</code> (default) — euclidean distance. pgvector <code>&lt;-&gt;</code>.
134 best for embeddings where magnitude matters.
135 </li>
136 <li>
137 <code>cosine</code> — cosine distance. pgvector <code>&lt;=&gt;</code>. best for
138 text embeddings where similarity is direction, not magnitude.
139 </li>
140 <li>
141 <code>inner_product</code> — negative inner product. pgvector{' '}
142 <code>&lt;#&gt;</code>. fastest; use when your embeddings are already normalised.
143 </li>
144 </ul>
145 <p>
146 if you&apos;re unsure, start with <code>cosine</code> for text embeddings and{' '}
147 <code>l2</code> for image embeddings. the choice affects ranking, not correctness — a
148 query that finds the right neighbours under one operator generally does under the
149 others.
150 </p>
151 </Section>
152
153 <Section title="indexes and recall">
154 <p>
155 for tables past ~10,000 rows, declare an index on the vector column. briven&apos;s
156 schema apply path creates an HNSW index when it sees an index on a vector column —
157 ANN search becomes sub-millisecond instead of seconds.
158 </p>
159 <p>
160 HNSW trades exactness for speed: a query may not return the absolute closest neighbour
161 every time, but the recall is &gt; 95% in typical configurations. for cases where
162 exact recall matters (medical / legal retrieval), remove the index and accept the
163 sequential scan — fine for &lt; 50k rows on a decent box.
164 </p>
165 </Section>
166
167 <Section title="what's NOT in v1">
168 <ul className="list-disc pl-5">
169 <li>
170 <strong>hybrid search</strong> (vector + text BM25). pgvector + tsvector both exist
171 in postgres; combining them is a manual <code>ctx.db.execute(raw_sql)</code> today.
172 a first-class hybrid helper is queued for the wrapper SDK&apos;s year-two work.
173 </li>
174 <li>
175 <strong>embedding generation inside the query</strong>. briven leaves embedding to
176 your code so we don&apos;t pick a model for you. when the ai docs assistant ships
177 a generic embed endpoint, this guide will update.
178 </li>
179 <li>
180 <strong>per-tenant vector indexes</strong>. all rows in a table share one HNSW
181 index. for multi-tenant projects, scope by a <code>where</code> clause — the index
182 still helps; just at the candidate-set selection step.
183 </li>
184 </ul>
185 </Section>
186
187 <p className="mt-12 font-mono text-xs text-[var(--color-text-subtle)]">
188 related:{' '}
189 <Link href="/schema" className="underline">
190 schema dsl
191 </Link>{' '}
192 ·{' '}
193 <Link href="/functions" className="underline">
194 functions
195 </Link>{' '}
196 ·{' '}
197 <Link href="/ai" className="underline">
198 ai features
199 </Link>
200 </p>
201 </DocsShell>
202 );
203}
204
205function Section({ title, children }: { title: string; children: React.ReactNode }) {
206 return (
207 <section className="mt-10">
208 <h2 className="font-mono text-lg">{title}</h2>
209 <div className="mt-2 space-y-3 font-mono text-sm text-[var(--color-text-muted)]">
210 {children}
211 </div>
212 </section>
213 );
214}
215
216function Snippet({ children }: { children: string }) {
217 return (
218 <pre className="overflow-x-auto rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-4 font-mono text-xs">
219 <code>{children}</code>
220 </pre>
221 );
222}