Skip to Content
We are live but in Staging 🎉
Data EnginesVectorCore Concepts

Core Concepts — Vector

The vector data engine has no service of its own. A vector collection is a table with a vector<n> column on the tabled data plane, and everything below is grounded in that plane’s SQL — the type vocabulary in dodil-tables/crates/vector/src/vocab.rs, the KNN operators in crates/tables-sql/src/knn.rs, and the ANN index DDL in crates/tables-sql/src/ddl.rs.

Collections are created two ways, both landing on the same table. You either write vectors yourself into a vector<n> column (BYO — a plain CREATE TABLE), or a pipeline auto-embeds ingested objects into one. The auto-embed path — CreateVectorPipeline, VectorConfig, the embedding templates — is a control-plane concern and lives in Pipelines → Vector Collections. This page is about the data model underneath both.

The wire vocabulary is wider than the plane supports. The proto and pgvector surfaces name more embedding types, sparse modes and distance metrics than are actually reachable today. The working subset is small and the callouts below cite each limit: the float embedding type, vector<n> and halfvec<n> storage (a bit<n> column stores but only exact-scans), and the cosine / euclidean / dot metrics. Everything else — Hamming / Jaccard / BM25 metrics, learned-sparse (SPLADE) modes, binary ANN — is vocabulary the runtime does not honour. Full per-wire divergences: Vector → Wire Compatibility.

The column types

The plane has three vector-family column types. All three take a mandatory dimension — bare vector is rejected, because pgvector’s dim-less form defers typing to first insert and this plane does not.

TypeStorageWhat it holds
vector<n>Arrow List<Float32>Dense f32 embeddings — the common case
halfvec<n>Arrow Float16 (~half the on-disk size)Dense f16 embeddings
bit<n>packed bytes (n is the bit count)Binary vectors

vector<n> and VECTOR(n) are the same type — short aliases normalize and the type passes through uppercased. _vec_bench on rebuild-smoke, for example, describes its embedding column as emb vector<64> and its integer PK id as long.

CREATE TABLE ada_embeddings ( id VARCHAR PRIMARY KEY, content VARCHAR, embedding vector(1536) );

Distance metrics — operators and opclasses

KNN is plain SQL. Three pgvector operators are recognised by the dialect, each lowering to a distance function (knn.rs:40-46):

OperatorMetricLowers toOpclass (for the ANN index)
<=>cosinelist_cosine_distancevector_cosine_ops
<->L2 / Euclideanlist_distancevector_l2_ops
<#>inner product (dot)list_negative_inner_productvector_ip_ops
-- verified live against _vec_bench (emb vector<64>): ascending distances, closest first SELECT id, tag, emb <=> '[0.1, 0.1, …]' AS distance FROM _vec_bench ORDER BY distance LIMIT 5;

score / distance is a distance, not a similarity — lower is closer. Every path returns the raw metric distance: the SQL operators, the typed TablesVector.QueryVectors facet (VectorMatch.score), and the Qdrant wire, which passes the facet’s score through verbatim. A stock Qdrant client will happily print it as if higher were better. Sort ascending.

The plane’s honest metric surface is three values. Metric on dodil.tables.v1 is exactly COSINE, EUCLIDEAN, DOT_PRODUCT. There is no Hamming, Jaccard or BM25 metric on the query facet, QueryVectors carries no metadata filter or namespace, and DeleteVectors has one filter arm (ids or all: true, never both).

The ANN index

An optional approximate-nearest-neighbour index is a pgvector CREATE INDEX (ddl.rs:190-230):

CREATE INDEX ON ada_embeddings USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);
  • Methods: USING hnsw and USING ivfflat, plus the quantized kinds they promote to. Any other USING is ignored — the column stays exact-scan.
  • Opclasses: vector_l2_ops, vector_ip_ops, vector_cosine_ops — and nothing else. Omitting the opclass defaults to L2. An unknown opclass is a clean Unsupported error naming the three valid ones.
  • Params: WITH (m = …, ef_construction = …) for HNSW, WITH (lists = …) for IVFFlat.
  • Scalar quantization is opt-in via WITH (quantization = …): hnsw + f16 or i8 promotes to a quantized HNSW; ivfflat + sq8 promotes to IvfSq8. Any other (method, quantization) pair is a clean error, never a silent drop.

Which kind to ask for, what a build guarantees, and the serving knobs — including why hnsw_sq_i8 on an inner-product column is refused at DDL — are on ANN Indexes. How the engine keeps recall and build memory bounded as a collection grows into the millions is on Vector at Scale.

The plane’s storage coverage is narrower than pgvector’s full opclass set. bit<n>’s bit_hamming_ops / bit_jaccard_ops opclasses are rejected at CREATE INDEX on the vector index path — only the three vector_*_ops classes resolve. Binary HNSW is a hard error in the f32 engine. If you declare a bit<n> column, expect exact scan, not an ANN index.

Reaching the data — one table, many wires

The same vector<n> table answers on every wire the tabled plane exposes:

WireKNN callNotes
PostgresORDER BY col <=> '[…]' LIMIT kpg.uk-lon-1.dodil.io:5432, sslmode=require
Qdrantclient.search(collection_name, query_vector, limit)vector column hardcoded to vector; metrics Cosine / Euclid / Dot only
Pineconeindex.query(vector, top_k)table name from X-Dodil-Index header, not the SDK index name
Typed gRPC / RESTTablesVector.QueryVectors · POST /v1/vector/:table/querydodil data vsearch rides this facet

The Qdrant wire adapter is narrower than the SQL surface. Distance::from_wire accepts only Cosine, Euclid (or the long Euclidean) and Dot; Manhattan or anything else is a loud 400 (crates/adapter/src/qdrant.rs:59-68). It maps those to the same three opclasses / operators above.

Endpoints and credentials: Connect & wire adapters. The full BYO write/read/delete flow: Pipelines → External Collection.

The managed search route — dispatch model

POST /:bucket/search/vector is the one control-plane surface that stays with the vector engine. It is not raw KNN — it is a thin dispatcher over the per-modality *_embedding_search Scriptum templates, for pipeline-created collections (it reads store_entities rows with kind='vector'). There is no Search RPC; the route’s authz borrows PipelineService/ListPipelines.

  1. Resolve the target collections — explicit collectionNames win (names that match nothing → NOT_FOUND); else a file query auto-filters to its modality; else every collection in the bucket.
  2. Drop collections that are not active or have no bound search pipeline — the latter are skipped with an entry in the response’s warnings[].
  3. Group survivors by (embed_model, dimensions, embedding_type) — collections with different embed_model never co-mingle.
  4. One search-template thread per group embeds the query once and fans out to its members; hybrid + rerank are owned by the template, not the caller.
  5. RRF-merge (k=60) the groups’ ranked lists into the final topK. A single group is a pass-through — no fusion applied.

topK defaults to 10 and is floored at 10 when <= 0. searchMode, rerank, minScore and rerankText are parsed but feed no K3-side logic (services/search/search.rs:67-70). Full contract: API Reference → Search.

A pre-embedded vector query on this route returns UNIMPLEMENTED — “pre-embedded vector search retired with Milvus (#83) — pending the tabled KNN read-path” (services/search/search.rs:194-198). KNN by a vector you already hold is a data-plane operation: SQL, a stock Qdrant / Pinecone client, or dodil data vsearch --vector.

Hybrid is not live on the plane. dodil-tables/crates/vector-sparse/ is a learned-sparse index (SPLADE / BGE-M3 term weights) ranked by inner product — no IDF, no k1/b, and no binary depends on it, so it is unreachable. QueryVectorsRequest has no sparse, hybrid or filter field. Searches on the plane are dense-only; whether a search template does anything more is Scriptum’s business and not visible from these repos. See Recipes → Hybrid + Rerank.

Where collection creation lives now

Creating a collection — whether template-driven auto-embed or the manual mode — is a Pipelines concern. This engine documents the data model and the search route; the control-plane lifecycle is over there:


See also