← Files InsForgeARCHIVED FILE
skills/insforge-cli/references/database/vector.md
6.47 KB · Oct 4, 2026 · 12:28 UTC
# Database Vector Search
Use this reference when configuring pgvector with the InsForge CLI: vector
extension setup, embedding columns, similarity search functions, HNSW/IVFFlat
indexes, and vector-specific RLS considerations.
For app code that generates embeddings through OpenRouter and inserts vectors
with `@insforge/sdk`, use the `insforge` app-integration skill's AI/RAG guidance after this backend
schema is in place.
## Migration Pattern
DDL belongs in a migration. Create a migration file with
`npx @insforge/cli db migrations new <name>`, put SQL like the example below in
that file, then apply it with `npx @insforge/cli db migrations up --all`.
```sql
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE public.documents (
id BIGSERIAL PRIMARY KEY,
owner_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
content TEXT NOT NULL,
embedding vector(1536) NOT NULL,
embedding_model TEXT NOT NULL DEFAULT 'openai/text-embedding-3-small',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
ALTER TABLE public.documents ENABLE ROW LEVEL SECURITY;
CREATE POLICY "owners can read documents"
ON public.documents
FOR SELECT TO authenticated
USING (owner_id = (SELECT auth.uid()));
CREATE POLICY "owners can insert documents"
ON public.documents
FOR INSERT TO authenticated
WITH CHECK (owner_id = (SELECT auth.uid()));
GRANT SELECT, INSERT ON public.documents TO authenticated;
CREATE OR REPLACE FUNCTION public.match_documents(
query_embedding vector(1536),
match_count INT DEFAULT 5,
match_threshold DOUBLE PRECISION DEFAULT 0.78
)
RETURNS TABLE (
id BIGINT,
content TEXT,
similarity DOUBLE PRECISION
)
LANGUAGE sql
STABLE
SECURITY INVOKER
AS $$
SELECT
public.documents.id,
public.documents.content,
1 - (public.documents.embedding <=> query_embedding) AS similarity
FROM public.documents
WHERE 1 - (public.documents.embedding <=> query_embedding) >= match_threshold
ORDER BY public.documents.embedding <=> query_embedding
LIMIT match_count;
$$;
GRANT EXECUTE ON FUNCTION public.match_documents(vector, INT, DOUBLE PRECISION)
TO authenticated;
CREATE INDEX documents_owner_id_idx ON public.documents (owner_id);
CREATE INDEX documents_embedding_hnsw_idx
ON public.documents
USING hnsw (embedding vector_cosine_ops);
```
## Dimensions
Match `vector(N)` to the embedding model output dimension.
| Model | Dimensions |
| ------------------------------- | ---------- |
| `openai/text-embedding-3-small` | 1536 |
| `openai/text-embedding-3-large` | 3072 |
| `openai/text-embedding-ada-002` | 1536 |
| `google/gemini-embedding-001` | 3072 |
A vector column's dimension cannot be altered in place. To change models with a
different dimension, create a new vector column/table and re-embed data.
## Distance Operators
Pick one distance operator and use the matching index operator class.
| Operator | Distance | Operator class | Typical use |
| -------- | ---------------------- | ------------------- | --------------------------------- |
| `<=>` | Cosine | `vector_cosine_ops` | Default for normalized embeddings |
| `<->` | L2 | `vector_l2_ops` | Un-normalized embeddings |
| `<#>` | Inner product, negated | `vector_ip_ops` | Advanced ranking patterns |
For cosine distance, lower distance is closer. If exposing a similarity score,
use `1 - (embedding <=> query_embedding)` and keep ordering by raw distance.
## Indexing
Without an index, pgvector performs exact nearest-neighbor scans. That is
correct but linear. Add an index before production-sized workloads.
HNSW is usually the default choice and is safe to create on empty tables:
```sql
CREATE INDEX documents_embedding_hnsw_idx
ON public.documents
USING hnsw (embedding vector_cosine_ops);
```
IVFFlat uses less memory, but build it only after representative data exists:
```sql
CREATE INDEX documents_embedding_ivfflat_idx
ON public.documents
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
```
Index columns used with vector filters, such as `owner_id`, `tenant_id`,
`document_type`, or `created_at`. The vector index helps nearest-neighbor order;
normal B-tree indexes help metadata filters.
## SQL Inserts and Queries
For small SQL fixtures or debugging, cast a JSON-array literal to the exact
vector dimension:
```sql
CREATE TABLE public.vec_demo (
id BIGSERIAL PRIMARY KEY,
embedding vector(3) NOT NULL
);
INSERT INTO public.vec_demo (embedding)
VALUES ('[0.12,0.34,0.56]'::vector(3));
SELECT *
FROM public.vec_demo
ORDER BY embedding <=> '[0.10,0.30,0.55]'::vector(3)
LIMIT 5;
```
For real app data, generate embeddings in server-side app code and insert a
`number[]` with the InsForge SDK.
## RLS and RPCs
Standard RLS applies to vector tables. A `SECURITY INVOKER` match function runs
under the caller's role, so table policies still filter rows.
If a vector search function must be `SECURITY DEFINER`, re-check `auth.uid()` or
tenant membership inside the function body. Do not bypass RLS and return vectors
or documents across users/tenants by accident.
## Common Mistakes
| Mistake | Fix |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| Creating `pgvector` extension instead of `vector` | Use `CREATE EXTENSION IF NOT EXISTS vector;` |
| Dimension mismatch between model and column | Set `vector(N)` to the model's exact output dimension |
| Ordering similarity descending while thresholding distance | Keep distance and similarity semantics explicit |
| Operator class does not match query operator | Pair `<=>` with `vector_cosine_ops`, `<->` with `vector_l2_ops`, `<#>` with `vector_ip_ops` |
| IVFFlat on an empty table | Use HNSW, or build IVFFlat after representative rows exist |
| Client-side distance math | Put search/ranking in SQL or an RPC |
| SECURITY DEFINER vector RPC without user/tenant filter | Re-filter inside the function body |
SHA-256: 3f2bdcb2b1799d4694391ee9ea96946dcde4175562378f31a485a5edeb192be0