Vectors
Store an embedding vector on a record, then search the collection with three retrieval modes — semantic (cosine), lexical (BM25), or hybrid (RRF-fused). Powers semantic search, recommendation engines, RAG retrieval, and similarity matching — backed by Postgres + pgvector with HNSW indexing for sub-millisecond lookups.
Declaring a vector field
Vector fields are declared per collection from the dashboard or CLI. Open a collection, click Vector Fields, enter a field name (e.g. embedding), pick a dimension, and add it. From that point, every record in the collection has a slot for that field — set it on the records that need it, leave it unset on the rest.
The dimension is the length of every vector you'll store on that field. It must match exactly what your embedding model produces — see Supported dimensions below for the values Koolbase supports.
Mobile SDKs don't declare schema
declareVectorField — vector fields are dashboard/CLI operations, the same way collections and storage buckets are created. An app consumes what's declared; it doesn't define it.Writing and reading vectors
Set a vector on a record from the SDK once you have an embedding from your model:
final embedding = await myEmbeddingModel.encode(article.content);
await Koolbase.db.doc(article.id).setVector(
'embedding',
embedding,
);Read it back later — useful for inspection or for re-running search on a known anchor:
final v = await Koolbase.db.doc(article.id).getVector('embedding');
print('${v.vector.length}-dim, updated ${v.updatedAt}');And remove a record's vector when you no longer need it. The field declaration stays — it's still settable on other records.
await Koolbase.db.doc(article.id).deleteVector('embedding');React Native
Koolbase.db.setVector(recordId, field, vector), Koolbase.db.getVector(recordId, field), Koolbase.db.deleteVector(recordId, field). Same wire protocol, idiomatic JS shape.Auto-embedding on write
You don't need a client-side embedding model. Configure a vector field with an embedding provider + model + source field, and Koolbase will embed records automatically on insert and update.
Configure the field from the dashboard (Database → your collection → Vector Fields → field settings) or via direct SQL:
UPDATE collection_vector_fields
SET embedding_provider = 'gemini',
embedding_model = 'gemini-embedding-001',
source_field = 'content'
WHERE collection_id = '<your collection id>'
AND field_name = 'content_embedding';From that point on, every record inserted or updated with a non-empty content field automatically gets a vector queued in embedding_jobs. A background worker processes the queue with 1s/2s/4s/8s/16s exponential backoff and writes the vector to record_vectors_<dim>. Same-text deduplication via content hash means re-embedding identical content from different records skips the API call entirely.
On the same write, Koolbase also populates a lexical (BM25) row in db_record_lexical for the configured source field. This makes lexical and hybrid search work out of the box — no extra configuration, no extra worker. The lexical index is a Postgres tsvector tokenized with the vector field's configured language (see Language for lexical indexing), so it's ready as soon as the row is written.
Source field must be a string
Setting up an AI provider
Koolbase supports two embedding providers in v1: Google Gemini (free tier, no payment method required) and OpenAI (requires billing). One config per project per provider.
Get a Gemini key at aistudio.google.com/apikey, then register it:
curl -X POST "https://api.koolbase.com/v1/projects/$PROJECT_ID/ai/providers" \
-H "Authorization: Bearer $DASHBOARD_JWT" \
-H "Content-Type: application/json" \
-d '{
"provider": "gemini",
"api_key": "AIza...",
"default_model": "gemini-embedding-001"
}'Validation happens inline — the response includes status: "valid" if the key works, or status: "invalid" with a validation_error message. Rate-limit errors (429) return status: "unvalidated"; click Re-test from the dashboard once the limit clears.
Provider keys are encrypted at rest
Searching: semantic, lexical, hybrid
Koolbase ships three retrieval modes from a single endpoint. Pick the one that matches your query characteristics, or use hybrid as a strong production default.
- Semantic — pure vector search via HNSW + cosine. Best for fuzzy / conceptual queries where exact words don't have to match.
- Lexical — pure BM25 over the field's source text via Postgres
ts_rank_cd. Best for exact terms, codes, names, acronyms. - Hybrid — vector + lexical fused with reciprocal rank fusion (k=60). Generally the strongest default; both rankers vote and the fused score promotes records that score well on either signal.
Flutter
// Semantic (default) — pure vector via HNSW + cosine.
final semantic = await Koolbase.db.collection('articles').searchSemantic(
field: 'content_embedding',
queryText: 'how do I configure CI/CD?',
limit: 10,
);
// Lexical — pure BM25 over the field's source text.
final lexical = await Koolbase.db.collection('articles').searchSemantic(
field: 'content_embedding',
queryText: 'CVE-2024-1234',
mode: KoolbaseSearchMode.lexical,
limit: 10,
);
// Hybrid — vector + lexical, RRF-fused (k=60).
final hybrid = await Koolbase.db.collection('articles').searchSemantic(
field: 'content_embedding',
queryText: 'production deploy pipeline',
mode: KoolbaseSearchMode.hybrid,
limit: 10,
);
for (final hit in hybrid.hits) {
print('${hit.record['title']} (${hit.distance.toStringAsFixed(3)})');
}React Native & Web
// Semantic (default) — pure vector via HNSW + cosine.
const semantic = await Koolbase.db.searchSemantic({
collection: 'articles',
field: 'content_embedding',
queryText: 'how do I configure CI/CD?',
limit: 10,
});
// Lexical — pure BM25 over the field's source text.
const lexical = await Koolbase.db.searchSemantic({
collection: 'articles',
field: 'content_embedding',
queryText: 'CVE-2024-1234',
mode: 'lexical',
limit: 10,
});
// Hybrid — vector + lexical, RRF-fused (k=60).
const hybrid = await Koolbase.db.searchSemantic({
collection: 'articles',
field: 'content_embedding',
queryText: 'production deploy pipeline',
mode: 'hybrid',
limit: 10,
});
for (const hit of hybrid.hits) {
console.log(`${hit.record.title} ${hit.distance.toFixed(3)}`);
}The pre-existing queryVector path still works for semantic mode — useful when you've already encoded the query client-side or want to use a different embedding model for the query than for the corpus. Lexical and hybrid modes require queryText (BM25 has no notion of vector queries).
Picking a mode
If you're unsure, start with hybrid. It combines the best of both rankers and degrades gracefully — a query that's a strong lexical match but a weak semantic match (e.g. an exact product code) still ranks high, and vice versa.
Use lexical when you specifically need keyword behavior (search by SKU, error code, person's name). Use semantic when you specifically want fuzzy meaning-based retrieval and don't want exact-term hits dominating.
Language for lexical indexing
Lexical and hybrid search use a Postgres text-search config to tokenize the source field — the config controls stemming (so running, runs, and ran can all match a query for run) and stop-word handling. Each vector field carries its own lexical_config, defaulting to simple.
Set it from the dashboard (Database → your collection → Vector Fields → the 🌐 Languages icon next to a field) or via the API:
curl -X PATCH "$API/v1/projects/$PROJECT_ID/collections/$COLLECTION/vector-fields/$FIELD" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"lexical_config": "english"}'Or with the CLI:
koolbase vector-fields update articles content_embedding \
--lexical-config english \
--project $PROJECT_IDSupported configs in v1 — pick the one that matches the dominant language of your source data:
| Config | Behavior |
|---|---|
| simple | Default. No stemming, no stop words — every token matches literally. Best for codes, names, multilingual mixes. |
| english | Snowball English stemmer + English stop words. running, runs, ran all match run. |
| french | French stemmer + French stop words. |
| german | German stemmer + German stop words. |
| spanish | Spanish stemmer + Spanish stop words. |
| italian | Italian stemmer + Italian stop words. |
| portuguese | Portuguese stemmer + Portuguese stop words. |
| dutch | Dutch stemmer + Dutch stop words. |
| russian | Russian stemmer + Russian stop words. |
| arabic | Arabic stemmer + Arabic stop words. |
Changing the config doesn't re-index existing data
Filtering weak matches
Pass minSimilarity (0..100) to drop results below a similarity threshold server-side. Saves bandwidth on weak matches and keeps client code simpler — no post-filtering needed.
final result = await Koolbase.db.collection('articles').searchSemantic(
field: 'content_embedding',
queryText: 'how do I configure CI/CD?',
mode: KoolbaseSearchMode.hybrid,
minSimilarity: 70, // only matches at least 70% similar
limit: 10,
);const result = await Koolbase.db.searchSemantic({
collection: 'articles',
field: 'content_embedding',
queryText: 'how do I configure CI/CD?',
mode: 'hybrid',
minSimilarity: 70,
limit: 10,
});Only valid for semantic and hybrid
minSimilarity on lexical mode with a 400 validation_error — BM25 rank scores aren't comparable to cosine similarity, and silently ignoring the parameter would produce confusing “I set 80, why did weak results return?” behavior. Values outside 0..100 are also rejected client-side before the request is sent.Backfilling existing records
Records that pre-date auto-embed or lexical indexing don't have vectors or lexical rows yet. Two backfill paths cover the two indexes — both are idempotent, both are exposed from the dashboard's Vector Fields modal.
Embedding backfill
For a single record, use embedText to queue an embedding job. Returns as soon as the job is queued — typically <100ms. The vector lands within ~1 second once the worker picks it up.
// Embed using the record's source_field value (whatever you configured):
await Koolbase.db.collection('articles').embedText(
recordId: article.id,
vectorField: 'content_embedding',
);
// Or override with custom text — useful for combining fields:
await Koolbase.db.collection('articles').embedText(
recordId: article.id,
vectorField: 'content_embedding',
text: '${article.title}\n\n${article.summary}',
);For a full collection at once, hit the admin endpoint (also available from the dashboard's Vector Fields modal via the ✨ wand icon):
curl -X POST "$API/v1/projects/$PROJECT_ID/collections/$COLLECTION/vector-fields/$FIELD/embed-all" \
-H "Authorization: Bearer $TOKEN"Lexical backfill
When you enable hybrid or lexical search on a collection that already has records, lexical rows need to be populated for the pre-existing data. (The write-path hook only covers writes that happen after the hook was wired.) The dashboard's Vector Fields modal has a 📖 book icon next to the ✨ wand for this; the corresponding endpoint:
curl -X POST "$API/v1/projects/$PROJECT_ID/collections/$COLLECTION/vector-fields/$FIELD/lexical-backfill" \
-H "Authorization: Bearer $TOKEN"Idempotent + no provider charges
already_current), so re-runs are cheap. Unlike embed backfill, lexical indexing is handled entirely by Postgres — no provider calls, no charges, no worker queue. Returns synchronously with the count of rows written.Combining with structured filters
All three modes accept a where filter — e.g. “find the 10 closest articles in the ‘tech’ category”:
final scoped = await Koolbase.db.collection('articles').searchSemantic(
field: 'content_embedding',
queryText: 'production deploy pipeline',
mode: KoolbaseSearchMode.hybrid,
limit: 10,
where: {'category': 'tech'},
);Filters apply after retrieval
where filters the top candidates after. With a very strict filter (matching only a tiny fraction of records) you may get fewer than limit hits — request a higher limit and re-filter client-side if needed.Supported dimensions
Vector dimensions match the output size of your embedding model. Koolbase supports these dimensions in v1, covering the most common open-source and commercial models:
| Dimensions | Example models |
|---|---|
| 384 | sentence-transformers/all-MiniLM-L6-v2 |
| 768 | sentence-transformers/all-mpnet-base-v2, BERT |
| 1024 | Cohere embed-v3, BGE-large |
| 1536 | OpenAI text-embedding-3-small, OpenAI text-embedding-ada-002, Gemini gemini-embedding-001 |
Using OpenAI's text-embedding-3-large
text-embedding-3-large isn't yet supported — pgvector's HNSW index caps at 2000 dimensions in our current version. Use the model's dimensions=1536 parameter (Matryoshka truncation) for full compatibility; this is the documented approach from OpenAI, not a hack. Full 3072-dim support arrives in a future release alongside a pgvector upgrade.Behavior & errors
Online-only
Vector operations don't cache locally and aren't queued offline — HNSW similarity search and BM25 ranking have no useful offline semantics, so deferring writes could corrupt your view of what's stored. Calls throw on network failure rather than queuing for sync.
Read rules apply post-search
All three search modes respect the collection's read rule the same way get() does. For owner, scoped, or conditional rules, records are filtered to the caller after the index lookup — so very strict rules may return fewer than limit hits.
Errors to handle
try {
await Koolbase.db.doc(id).setVector('embedding', vec);
} on KoolbaseVectorDimensionMismatchException catch (e) {
// vector.length didn't match the field's declared dimension
showError(e.message); // includes both expected and actual dims
} on KoolbaseNotFoundException {
// record_not_found, vector_field_not_found, or vector_not_found
showError('Record or field is missing.');
} on KoolbasePermissionException {
// The collection's write rule denied the operation
showError('Not allowed.');
}React Native
Exception → Error: KoolbaseVectorDimensionMismatchError, KoolbaseNotFoundError, KoolbasePermissionError, all caught with e instanceof ….