zvec connector
Write documents to zvec, an embedded in-process vector database. Covers collection setup, declaring documents with dense vectors plus scalar, array and full-text fields, schema-from-row type mapping, and lifecycle management.
The zvec connector writes documents to zvec, an embedded, in-process vector database. zvec runs inside your application (no server or daemon) and stores each collection in a directory on disk.
use cocoindex::connectors::zvec::{
self, CollectionSchema, ConnectOptions, ManagedConnection, ScalarType, VectorMetric,
VectorQuantize, ZvecFtsType, ZvecType, ZvecVectorDef,
};The module is compiled with the zvec Cargo feature of the cocoindex crate. It builds on the zvec-rust bindings, whose build script downloads the prebuilt libzvec_c_api shared library. cargo run and cargo test find it; a binary that runs on its own needs it on the library path, or a build.rs that calls zvec_rust_build::configure().
Connection setup
connect
connect() creates a ManagedConnection rooted at a base directory. Each collection lives in a subdirectory under it.
pub fn connect(base_path: impl AsRef<Path>, options: ConnectOptions) -> Result<ManagedConnection>
pub struct ConnectOptions {
/// Whether zvec uses memory-mapped I/O for data files. Defaults to `true`.
pub enable_mmap: bool,
}
let conn = zvec::connect(tempdir.path().join("zvec"), ConnectOptions::default())?;Parameters:
base_path— Directory under which collections are stored. Created if missing.options.enable_mmap— Whether zvec uses memory-mapped I/O for data files.
ManagedConnection
A handle to the base directory. zvec takes an exclusive write lock per open collection, so ManagedConnection caches open handles by collection name and reuses them.
Methods:
base_path()— The directory under which collections are stored.collection_path(name)— Path to a collection’s directory.open_existing(name).await— Open an existing collection and return its cachedzvec_rust::Collectionhandle, for queries.open_or_create(name, &schema).await— Open the collection, creating it from aCollectionSchemaif it does not exist, and return its cached handle. A system-managed collection target does this itself; call it to set up a user-managed collection.destroy(name).await— Permanently delete the collection from disk, closing its cached handle first. A missing collection is not an error.close().await— Release all cached collection handles (drops their write locks).
A collection with a live handle cannot be destroyed: drop the handles from open_existing and open_or_create before destroy, and before a run that destroys the collection (a schema change, or the target no longer declared).
There is no managed_connection() context manager: call conn.close().await? on shutdown.
As target
The zvec connector tracks which documents should exist in a collection and automatically handles upserts and deletions. zvec’s native upsert is used directly, and documents are removed by id when they are no longer declared.
Declaring target states
Setting up a connection
Declare a context key for a ManagedConnection to identify your connection, then provide the connection at startup:
The key name is load-bearing across runs — it’s the stable identity CocoIndex uses to track managed documents. See ContextKey as stable identity before renaming.
use cocoindex::connectors::zvec::{self, ConnectOptions, ManagedConnection};
cocoindex::context_key!(static ZVEC: ManagedConnection);
// At startup: provide the connection with `.provide_key(&ZVEC, conn.clone())`.
let conn = zvec::connect("./zvec_data", ConnectOptions::default())?;context_key!(static ZVEC: ManagedConnection) declares the key; its name is the stable identity. context_key!(static DB: ManagedConnection, key = "zvec_test") sets a different stable name from the identifier.
Collections (parent state)
Declares a collection as a target state. Returns a CollectionTarget for declaring documents.
pub fn declare_collection_target(
ctx: &Ctx,
db: &ContextKey<ManagedConnection>,
collection_name: impl Into<String>,
schema: CollectionSchema,
) -> Result<CollectionTarget>
pub async fn mount_collection_target(
ctx: &Ctx,
db: &ContextKey<ManagedConnection>,
collection_name: impl Into<String>,
schema: CollectionSchema,
) -> Result<CollectionTarget>
// A user-managed collection: documents only, no create or destroy.
let target = zvec::mount_collection_target_with_options(
&ctx,
&DB,
COL,
simple_schema(),
ManagedTargetOptions::user_managed(),
)
.await?;declare_collection_target returns a handle whose documents resolve when the current component commits; mount_collection_target(...).await? returns a handle that accepts documents at once.
Parameters:
db— The context key of theManagedConnection.collection_name— Name of the collection (a subdirectory under the connection’s base path).schema— Schema definition (see Collection schema).
The *_with_options variants take a ManagedTargetOptions: ManagedTargetOptions::system_managed() (the default) creates and destroys the collection, and ManagedTargetOptions::user_managed() assumes it already exists and writes documents only. Create a user-managed collection with conn.open_or_create(name, &schema).
Documents (child states)
Once a CollectionTarget is resolved, declare documents to be upserted:
impl CollectionTarget {
pub fn declare_row<R: Serialize>(&self, ctx: &Ctx, row: &R) -> Result<()>
}
target.declare_row(&ctx, &doc)?;The primary-key value becomes the document id (converted to a string). row is any Serialize value that serializes to an object with the schema’s columns; extra fields are dropped and missing ones are null.
Collection schema: from a row struct
Define the collection structure using a struct that derives SchemaFields:
impl CollectionSchema {
pub fn from_row<T: SchemaFields>(
primary_key: impl IntoIterator<Item = impl Into<String>>,
) -> Result<Self>
pub fn with_vector_dim(self, column: &str, dim: usize) -> Result<Self>
pub fn with_vector_def(self, column: &str, def: ZvecVectorDef) -> Result<Self>
pub fn with_column_type(self, column: &str, zvec_type: ZvecType) -> Result<Self>
pub fn with_fts(self, column: &str, fts: ZvecFtsType) -> Result<Self>
}The row type is a struct that derives SchemaFields (and Serialize). Per-column overrides are builder calls on the derived schema: with_vector_dim for a vector dimension, with_vector_def for the index, with_column_type for a scalar or array type, with_fts for a full-text field.
The zvec schema has no with_vector_schema: take the dimension from the embedder and pass it to with_vector_dim.
Parameters:
T— ASchemaFieldsstruct whose fields define the document structure.primary_key— Exactly one column name. Its value becomes the documentid.
zvec documents have a single string id, so primary_key must name exactly one column. Its value is converted to a string to form the id. Composite primary keys are not supported. The key column must be a String, Uuid or integer column, and not an Option; from_row returns an error otherwise.
zvec is a vector database: every collection must declare at least one vector field.
Example:
#[derive(Clone, Serialize, SchemaFields)]
struct SimpleDoc {
id: String,
title: String,
year: i64,
#[coco(vector = 4)]
embedding: Vec<f32>,
}
fn simple_schema() -> CollectionSchema {
CollectionSchema::from_row::<SimpleDoc>(["id"]).unwrap()
}#[coco(vector = N)] fixes the dimension in the struct; a bare #[coco(vector)] leaves it to with_vector_dim.
Rust field types map to zvec field types as follows:
| Rust Type | zvec DataType |
|---|---|
bool | BOOL |
i16, i32 | INT32 |
i64 | INT64 |
f32 | FLOAT |
f64 | DOUBLE |
String | STRING |
Vec<u8> | STRING (base64) |
Uuid | STRING |
Decimal | STRING |
NaiveDate, NaiveTime, DateTime | STRING (ISO format) |
Duration | DOUBLE (total seconds) |
Vec<bool>, Vec<i64>, Vec<f64>, … with #[coco(type = "array_bool")], "array_int64", "array_double", … | ARRAY_BOOL, ARRAY_INT64, ARRAY_DOUBLE, … |
other Vec<T>, maps, nested structs, #[coco(json)] | STRING (JSON) |
Vec<f32> with #[coco(vector)] | VECTOR_FP32 |
An array field is opt-in: a Vec<T> without a type attribute or a with_column_type override is stored as a JSON string. The array types are array_bool, array_int32, array_int64, array_uint32, array_uint64, array_float and array_double.
Scalar fields get an invert index by default so they can be used in query filters. The primary-key column maps to the document id and is not stored as a separate field.
There is no array of strings (ARRAY_STRING): a Vec<String> is stored as a JSON string. Only float32 dense vectors are written: a #[coco(vector = N, half)] field is an error, because the zvec-rust bindings have no float16 setter. For smaller storage, keep a float32 vector and set quantize.
ZvecType
Override the scalar type, encoder, or indexing for a field:
// Store `small` as INT32 instead of the derived type, without a filter index.
let schema = CollectionSchema::from_row::<TypedDoc>(["id"])?.with_column_type(
"small",
ZvecType {
data_type: ScalarType::Int32,
indexed: false,
},
)?;ScalarType has Bool, Int32, Int64, Uint32, Uint64, Float, Double and String, and the array types ArrayBool, ArrayInt32, ArrayInt64, ArrayUint32, ArrayUint64, ArrayFloat and ArrayDouble. A #[coco(type = "int32")] attribute on the field selects the type too (the names are case-insensitive), but keeps the index. Array fields take the same attribute:
#[derive(Clone, Serialize, SchemaFields)]
struct ArrayDoc {
id: String,
#[coco(type = "array_int64")]
ints: Vec<i64>,
#[coco(type = "array_double")]
floats: Vec<f64>,
#[coco(type = "array_bool")]
flags: Vec<bool>,
#[coco(vector = 4)]
embedding: Vec<f32>,
}ZvecType has no encoder: put #[serde(serialize_with = "…")] on the field to change how its value is written.
ZvecFtsType
Mark a string field as full-text (FTS) indexed. The field is stored as a STRING field with a zvec FTS index, so you can run full-text match queries against it directly through zvec. Requires zvec >= 0.5.
#[derive(Clone, Serialize, SchemaFields)]
struct FtsDoc {
id: String,
body: String,
#[coco(vector = 4)]
embedding: Vec<f32>,
}
let schema =
CollectionSchema::from_row::<FtsDoc>(["id"])?.with_fts("body", ZvecFtsType::default())?;ZvecFtsType has the fields tokenizer_name (default "standard"), filters (default ["lowercase"]) and extra_params.
The connector writes the field and its FTS index; querying it happens directly against zvec, since this connector only handles the write path. Query through the handle from conn.open_existing(name).await? and the re-exported zvec_rust API.
Vectors
A collection can declare multiple named vector fields in one schema. zvec supports querying across them with reranking at read time.
Sparse vectors and float16 dense vectors are not supported: the zvec-rust bindings have no document setter for them, so the connector writes float32 dense vectors only.
Dense vectors
A Vec<f32> field marked #[coco(vector)] becomes a dense vector (VECTOR_FP32). For smaller storage, set quantize. Tune the HNSW index with ZvecVectorDef:
#[derive(Clone, Serialize, SchemaFields)]
struct VectorDoc {
id: String,
#[coco(vector)]
embedding: Vec<f32>,
#[coco(vector = 2)]
small: Option<Vec<f32>>,
}
let schema = CollectionSchema::from_row::<VectorDoc>(["id"])?
.with_vector_dim("embedding", 3)?
.with_vector_def(
"embedding",
ZvecVectorDef {
metric: VectorMetric::L2,
quantize: Some(VectorQuantize::Int8),
},
)?;VectorMetric has Cosine (default), Ip and L2; quantize is None or Some(VectorQuantize::Fp16 | Int8 | Int4).
Full example
use cocoindex::connectors::zvec::{self, CollectionSchema, ConnectOptions, ManagedConnection, ZvecFtsType};
cocoindex::context_key!(static ZVEC: ManagedConnection);
#[derive(Serialize, SchemaFields)]
struct Doc {
id: String,
body: String,
#[coco(vector)]
embedding: Vec<f32>,
}
// At startup: provide the connection with `.provide_key(&ZVEC, conn.clone())`.
let conn = zvec::connect("./zvec_data", ConnectOptions::default())?;
// In the app: the collection and its documents follow what is declared.
let schema = CollectionSchema::from_row::<Doc>(["id"])?
.with_vector_dim("embedding", 384)?
.with_fts("body", ZvecFtsType::default())?;
let docs = zvec::mount_collection_target(&ctx, &ZVEC, "docs", schema).await?;
docs.declare_row(&ctx, &doc)?;