OriginChainDB docs
schema · vector

Vector schema.

Vector search answers "what is closest to this?" — semantic retrieval for RAG, recommendations, deduplication, and anything where the query is an embedding rather than a predicate. You hand the engine a query vector and it returns the k nearest ids with their similarity scores.

Reach for it when meaning matters more than wording. If you need exact keyword matching, full-text search is cheaper and more precise; if you know the predicate, plain SQL beats both.

1

Before you start.

Every example on this page uses the shop.orders table from the quickstart, with an embedding of the order's notes field attached to each row's id.

vectors are not a column type

There is no vector column type, and vectors cannot be written through the rows endpoint. They live in their own keyspace, addressed by (tenant, table, id), and are written with the dedicated /vector/… endpoints on the Vector reference. Using the same id on both sides is what lets you take a vector hit and read the full row back with SQL.

The :table path segment is free-form — it does not have to name a registered schema. Registering one anyway is worth it: it gives you SQL access to the same rows, and the optional [vector] block turns a dimension mistake into a readable error.

schemas/orders.toml
# The row schema. Vectors do NOT live in a column - they sit in their
# own keyspace, addressed by the same id. Registering the table is what
# lets you read the row back with SQL after a vector hit gives you an id.

namespace   = "shop"
table       = "orders"
primary_key = ["id"]

[[columns]]
name = "id"
ty   = "str"
required = true

[[columns]]
name = "customer"
ty   = "str"

[[columns]]
name = "amount_cents"
ty   = "i64"

[[columns]]
name = "status"
ty   = "str"

[[columns]]
name = "notes"
ty   = "str"

[[columns]]
name = "placed_ms"
ty   = "u64"

# Optional. Declares the dimensionality the collection expects so a
# wrong-sized vector is refused with a readable message instead of a
# generic mismatch. `distance` accepts cosine | l2 | dot | manhattan | l1,
# defaults to cosine, and resolves the metric queries actually run under.
[vector]
dim      = 768
distance = "cosine"
the [vector] block checks dim and metric

dim is enforced on every write and query. distance is enforced too: it resolves the metric a put and a top-k actually run under. Declare a non-default distance (l2, dot, manhattan, l1) and send a contradicting metric, and both paths return a 400; declare one and send no metric, and the request runs under the declared metric rather than defaulting to cosine. The block accepts all five values — cosine, l2, dot, manhattan and l1 — the same set the runtime metric field takes. The one exception is a declared "cosine": it is byte-identical on disk to declaring nothing, so it resolves an omitted metric but never refuses one.

2

Metrics and dimensions.

metric When to use it
"cosine" The default, and the right answer for almost every text embedding model. Angle only — magnitude is ignored. A zero-magnitude vector scores 0.
"dot" Inner product. Use it on unit-normalised vectors, where it is equal to cosine. On a corpus with a real magnitude spread it is a known hazard: the graph's neighbour lists end up ordered by vector length, and the smallest-norm vectors lose their last in-edge and stop being reachable. Normalise first, or use cosine.
"l2" Euclidean distance, returned negated. Common for image and audio embeddings. Note the accepted string is l2"euclidean" is not recognised.
"manhattan" L1 distance, returned negated. "l1" is accepted as an alias — both spellings are also valid in the [vector] schema block.
an unrecognised metric is a 400, not a fallback

Metric parsing is case-insensitive, but an unrecognised value is refused: "euclidean", "hamming" or a typo like "cosin" is a 400 listing the accepted spellings. Until 2026-07-29 all three fell back to cosine behind a 200, so an older client may still be sending one. An omitted metric is not an error - it resolves to cosine, or to the declared [vector].distance where there is one.

Dimensions.

The only hard rule is dim > 0 — there is no upper bound on dimensionality in the engine. What matters is consistency: every vector in a collection, and every query against it, must agree.

A mismatch is a 400. With a [vector] block registered you get the friendly form:

vector has 512 dims but collection "shop.orders" expects 768.
Use the same model, or re-index the collection.

Without one the engine falls back to the width already stored, and the same message reads already holds vectors of 768 in place of expects 768. A different error, vec: dimension mismatch: expected 768, got 512, means the request contradicted itself - the dim field did not match the length of the array you sent.

Picking a dimensionality.

  • Storage is dim × 4 bytes per raw vector, so 1536-dim costs exactly twice 768-dim before any index overhead.
  • Graph search cost scales with dim too — every distance computation touches every component.
  • Many modern embedding models support truncation (Matryoshka-style). Halving dimensions usually costs a little recall and saves a lot of memory; measure on your own data before committing, because the collection's dim is fixed once you have written vectors at it.

Related.