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.

Version
v 1.0.14
Last reviewed
Nov 4, 2026
ExperimentalThe Rust SDK is experimental. It is outside CocoIndex's compatibility promise: any release, including a minor one, may change or remove its API. It is not yet published on crates.io.

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 cached zvec_rust::Collection handle, for queries.
  • open_or_create(name, &schema).await — Open the collection, creating it from a CollectionSchema if 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).

Different from Python

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:

Note

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 the ManagedConnection.
  • 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.

Different from Python

The zvec schema has no with_vector_schema: take the dimension from the embedder and pass it to with_vector_dim.

Parameters:

  • T — A SchemaFields struct whose fields define the document structure.
  • primary_key — Exactly one column name. Its value becomes the document id.
Single primary key

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.

At least one vector field

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 Typezvec DataType
boolBOOL
i16, i32INT32
i64INT64
f32FLOAT
f64DOUBLE
StringSTRING
Vec<u8>STRING (base64)
UuidSTRING
DecimalSTRING
NaiveDate, NaiveTime, DateTimeSTRING (ISO format)
DurationDOUBLE (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.

Different from Python

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>,
}
Different from Python

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.

Different from Python

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)?;