page.tsx222 lines · main
| 1 | import Link from 'next/link'; |
| 2 | |
| 3 | import { DocsShell } from '../../components/shell'; |
| 4 | |
| 5 | export const metadata = { title: 'vector search' }; |
| 6 | |
| 7 | export 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 | |
| 24 | export 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'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'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('documents').vectorSearch(...)</code> with |
| 51 | a query vector + the column name + optional distance / limit / where filter. |
| 52 | </p> |
| 53 | <Snippet>{`// briven/functions/searchDocs.ts |
| 54 | import { query, type Ctx } from '@briven/cli/server'; |
| 55 | |
| 56 | interface Args { |
| 57 | embedding: number[]; |
| 58 | topK?: number; |
| 59 | } |
| 60 | |
| 61 | export 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't generate embeddings — that'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 |
| 102 | import { mutation, type Ctx } from '@briven/cli/server'; |
| 103 | import { ulid } from '@briven/shared'; |
| 104 | |
| 105 | interface Args { title: string; body: string } |
| 106 | |
| 107 | export 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's native ones:</p> |
| 131 | <ul className="list-disc pl-5"> |
| 132 | <li> |
| 133 | <code>l2</code> (default) — euclidean distance. pgvector <code><-></code>. |
| 134 | best for embeddings where magnitude matters. |
| 135 | </li> |
| 136 | <li> |
| 137 | <code>cosine</code> — cosine distance. pgvector <code><=></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><#></code>. fastest; use when your embeddings are already normalised. |
| 143 | </li> |
| 144 | </ul> |
| 145 | <p> |
| 146 | if you'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'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 > 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 < 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'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'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 | |
| 205 | function 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 | |
| 216 | function 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 | } |