# zvec connector

> **CocoIndex v1, Rust SDK (experimental).** This page documents the CocoIndex **v1** Rust SDK (the `cocoindex` crate). The Rust SDK is experimental: any release may change or remove its API, and it is not yet published on crates.io. The Python SDK pages are at https://cocoindex.io/docs/.
>
> Source: https://cocoindex.io/docs/rust/connectors/zvec/ · Rust docs index: https://cocoindex.io/docs/rust/llms.txt · Rust SDK tour: https://github.com/cocoindex-io/cocoindex/blob/main/rust/sdk/SHOWCASE.md · Rust examples: https://github.com/cocoindex-io/cocoindex/tree/main/examples/rust

The `zvec` connector writes documents to [zvec](https://zvec.org), an embedded, in-process vector database. zvec runs inside your application (no server or daemon) and stores each collection in a directory on disk.

```rust
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.

```rust
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).

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](/docs/rust/programming_guide/context/#contextkey-as-stable-identity) before renaming.

```rust
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.

```rust
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](#collection-schema-from-a-row-struct)).

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:

```rust
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`:

```rust
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` — A `SchemaFields` struct whose fields define the document structure.
- `primary_key` — Exactly one column name. Its value becomes the document `id`.

**Note — 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.

**Note — At least one vector field**
zvec is a vector database: every collection must declare at least one vector field.

**Example:**

```rust
#[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:

```rust
// 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:

```rust
#[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.

```rust
#[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`:

```rust
#[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

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