zvec-rust
English | 中文
Safe, idiomatic Rust bindings for the zvec vector database.
Features
- RAII Resource Management — All C resources are automatically freed via
Drop - Builder Pattern — Fluent APIs for schema, query, and configuration
- Type Safety — Rust enums for all C constants with compile-time checks
- Comprehensive Error Handling — All FFI calls return
Result<T>with detailed error codes - Zero-Copy Where Possible — Minimizes data copying across the FFI boundary
- Prebuilt Libraries — Automatically downloads prebuilt
libzvec_c_apifrom GitHub Releases; advanced users can override withZVEC_LIB_DIR - FTS with jieba Out of the Box — Prebuilt packages ship the cppjieba dictionary (
data/jieba_dict);initializeauto-discovers it and registers the default jieba dict dir for thejiebaFTS tokenizer
Supported Platforms
| Platform | Architecture | CI Status | Notes |
|---|---|---|---|
| macOS | ARM64 (Apple Silicon) | ✅ Clippy + Test | Primary development platform |
| macOS | x86_64 (Intel) | ✅ Clippy + Test | |
| Linux | x86_64 | ✅ Clippy + Test + Fuzz + Coverage + Benchmark | Full CI coverage |
| Linux | ARM64 (AArch64) | ✅ Clippy + Test + Fuzz + Coverage | |
| Linux (musl) | x86_64 | ✅ Prebuilt | Alpine/musl distros; built in musllinux_1_2, same baseline as upstream zvec |
| Linux (musl) | ARM64 (AArch64) | ✅ Prebuilt | Alpine/musl distros; built in musllinux_1_2 |
| Windows | x86_64 (MSVC) | ✅ Clippy + Test | CMake + MSVC toolchain |
Linux gnu prebuilts target glibc 2.28 (
manylinux_2_28, same as upstream zvec), so they work on glibc distros as old as CentOS 8 / Ubuntu 20.04. musl prebuilts work on musl distros such as Alpine (musl 1.2+).
The dynamic library name varies by platform:
libzvec_c_api.dylib(macOS),libzvec_c_api.so(Linux),zvec_c_api.dll(Windows).
Architecture
zvec-rust/
├── zvec-sys/ # Low-level FFI bindings to libzvec_c_api
├── zvec/ # Safe, high-level Rust wrapper
└── fuzz/ # Fuzz testing targets
zvec-rust-sys— Rawextern "C"declarations, opaque pointer types, and constantszvec-rust— Safe wrappers with RAII, builders, iterators, and idiomatic Rust APIs
Prerequisites
The Rust SDK depends on the zvec C library (libzvec_c_api). Choose one of the following ways to provide it:
Option 1: Bundled Prebuilt Library (Zero Setup)
Add zvec-rust to your Cargo.toml. The default bundled feature automatically downloads the prebuilt libzvec_c_api for your platform from GitHub Releases:
[]
= "0.7.2"
cargo run / cargo test work out of the box because Cargo passes the
resolved library directory to the linker for you. A directly executed or
deployed binary, however, needs a runtime search path (rpath) pointing at
the shared library — Cargo does not add one automatically. Use the
zvec-rust-build build-script
helper from your binary crate to emit the rpath and stage the library beside
the executable:
[]
= "0.7.2"
[]
= "0.7.2"
// build.rs
Without the helper you must instead set DYLD_LIBRARY_PATH (macOS) /
LD_LIBRARY_PATH (Linux) at runtime.
Option 2: Custom Build
If you want to build the zvec C library yourself (e.g., for a custom configuration or unsupported platform), set the ZVEC_LIB_DIR environment variable:
# Build zvec from source
&&
&&
# Point zvec-rust to your custom build
Or use the built-in Makefile for local development:
Library Resolution Order
The build script resolves the C library in this order:
ZVEC_LIB_DIRenvironment variable (highest priority)- Sibling checkout:
../zvec/build/lib - Git submodule:
vendor/zvec/build/lib - Vendor directory:
vendor/lib/ - Prebuilt download: from GitHub Releases (automatic)
- Auto-build: clone and build from source (requires
git,cmake, C++17 compiler)
Set ZVEC_AUTO_BUILD=0 to disable steps 5 and 6.
Quick Start
use *;
Examples
Run any example with cargo run --example <name>:
| Example | Description |
|---|---|
basic |
End-to-end workflow: schema → insert → query → fetch → delete |
schema_builder |
Various schema configurations: field types, index types, quantization |
vector_search |
Vector query patterns: simple, builder, filter, output fields, HNSW params |
crud_operations |
Full CRUD: insert, fetch, update, upsert, delete, stats, flush |
config_logging |
Library configuration: memory limits, thread counts, logging |
# Using cargo directly (requires ZVEC_LIB_DIR / DYLD_LIBRARY_PATH)
API Overview
Initialization
| Function | Description |
|---|---|
initialize(config) |
Initialize the library (call once); pass None for defaults |
shutdown() |
Release all resources |
version() |
Get version string |
is_initialized() |
Check initialization status |
set_default_jieba_dict_dir(dir) |
Set the process-wide default jieba dict dir for the FTS jieba tokenizer |
get_default_jieba_dict_dir() |
Get the current default jieba dict dir ("" when unset) |
io_backend_type() / io_backend_type_name(code) |
Active DiskAnn I/O backend code and name (io_uring, libaio, pread, windows_overlapped) |
io_backend_description() |
Human-readable description of the active I/O backend |
Use ConfigBuilder to customize memory limits, thread counts, and logging:
let config = new
.memory_limit
.num_threads
.enable_console_log
.build;
initialize?;
jieba FTS tokenizer dictionary
The jieba FTS tokenizer needs cppjieba's dictionary files (jieba.dict.utf8
and hmm_model.utf8). The prebuilt libraries ship them under data/jieba_dict
next to libzvec_c_api, and initialize automatically discovers that
directory and registers it via zvec_set_default_jieba_dict_dir — Chinese
full-text search works with no extra configuration:
initialize?; // jieba dict auto-discovered and registered
let schema = builder
.add_field
.add_indexed_field
.build?;
To override the location (e.g. a custom dictionary), use one of:
ConfigBuilder::new().jieba_dict_dir("/path/to/jieba_dict")— perinitializecall (highest priority)set_default_jieba_dict_dir("/path/to/jieba_dict")— process-wide defaultZVEC_JIEBA_DICT_DIRenvironment variable — read by the zvec library at query time- per-field
extra_paramsJSON with"jieba_dict_dir"— per index
Schema Definition
let schema = builder
.add_field
.add_vector_field
.build?;
Collection Operations
| Method | Description |
|---|---|
Collection::create_and_open() |
Create a new collection |
Collection::open() |
Open an existing collection |
collection.insert(&docs) |
Insert documents |
collection.update(&docs) |
Update documents |
collection.upsert(&docs) |
Insert or update |
collection.delete(&pks) |
Delete by primary keys |
collection.delete_by_filter(filter) |
Delete documents matching a filter expression |
collection.query(&query) |
Scalar filtering, full-text, or vector similarity search |
collection.multi_query(&query) |
Multi-route search with RRF / weighted rerank |
collection.fetch(&pks) |
Fetch by primary keys |
collection.fetch_with_options(&pks, fields, include_vector) |
Fetch with output-field control |
collection.iter() / iter_with_options(fields, include_vector) |
Iterate over all documents (isolated snapshot) |
collection.create_index(field, params) / drop_index(field) |
Runtime index management |
collection.add_column(schema, default_expr) / drop_column(name) / alter_column(name, new_name, new_schema) |
Schema evolution (DDL) |
collection.optimize() |
Rebuild indexes / merge segments |
collection.stats() |
Get collection statistics |
collection.flush() |
Flush to disk |
Document Operations
let mut doc = new?;
doc.set_pk;
doc.add_string?;
doc.add_i64?;
doc.add_vector_f32?;
// Getters return `Result<Option<T>>` — `?` only unwraps the Result.
// Use `unwrap_or_default()` / `expect(..)` etc. to handle the Option.
let name: = doc.get_string?;
let count: = doc.get_i64?;
Scalar Query
Use SearchQuery::scalar(topk) to filter scalar fields without a query vector,
FTS payload, or target field. It also works on collections with no vector fields:
let mut query = scalar?;
query.set_filter?;
query.set_output_fields?;
let results = collection.query?;
topk limits the number of returned documents, and their order is unspecified.
A query without a filter returns up to topk documents; it does not enumerate
all matches when there are more than that limit.
SearchQuery::builder() produces the same query when neither vector(..) nor an
FTS clause is set; field_name(..) must then be omitted.
Vector Query
// Simple query
let query = new?;
// Builder pattern with filters
let query = builder
.field_name
.vector
.topk
.filter
.output_fields
.build?;
The builder infers the query kind from what was set: vector(..) builds a dense
vector query (adding an FTS clause makes it a hybrid search), an FTS clause
without a vector builds a keyword-only query, and neither builds a scalar query.
SearchQuery::fts(field, fts, topk) is the direct keyword-only constructor.
Multi-Query (Hybrid Search)
MultiQuery combines multiple sub-queries (dense vector, sparse vector, or FTS) with RRF or weighted reranking:
// FTS + vector hybrid search with RRF reranking
let mut sub_vec = new?;
sub_vec.set_field_name?;
sub_vec.set_query_vector?;
sub_vec.set_num_candidates?;
let mut fts = new?;
fts.set_match_string?;
let mut sub_fts = new?;
sub_fts.set_field_name?;
sub_fts.set_fts?;
sub_fts.set_num_candidates?;
let mut mq = new?;
mq.set_topk?;
mq.set_rerank_rrf?; // rank constant for RRF
mq.add_sub_query?;
mq.add_sub_query?;
let results = collection.multi_query?;
// Weighted reranking
mq.set_rerank_weighted?; // weights per sub-query
Supported Types
| Category | Types |
|---|---|
| Scalar | Bool, Int32, Int64, Uint32, Uint64, Float, Double, String, Binary |
| Vector | VectorFp16, VectorFp32, VectorFp64, VectorInt4, VectorInt8, VectorInt16, VectorBinary32, VectorBinary64 |
| Sparse | SparseVectorFp16, SparseVectorFp32 |
| Array | ArrayBool, ArrayInt32, ArrayInt64, ArrayUint32, ArrayUint64, ArrayFloat, ArrayDouble, ArrayString, ArrayBinary |
Index Types
Available distance metrics: L2, Ip, Cosine, MipsL2.
| Type | Constructor | Description |
|---|---|---|
| HNSW | IndexParams::hnsw(metric, m, ef) |
Graph index (recommended) |
| HNSW+Q | IndexParams::hnsw_with_quantize(...) |
HNSW with quantization |
| IVF | IndexParams::ivf(metric, nlist, niters, soar) |
Inverted file index |
| IVF RaBitQ | IndexParams::ivf_rabitq(metric, nlist, total_bits, sample_count) |
IVF with RaBitQ quantization |
| Flat | IndexParams::flat(metric) |
Brute-force index |
| DiskANN | IndexParams::diskann(metric, max_degree, list_size, pq_chunk_num) |
Disk-based graph index for large datasets (Linux x86_64/ARM64, macOS ARM64, Windows x86_64, 64-bit Android/iOS) |
| Invert | IndexParams::invert(range, wildcard) |
Scalar field index |
| FTS | IndexParams::fts(tokenizer, filters, extra) |
Full-text search index |
With Int8 / Int4 quantization, params.set_quantizer_enable_rotate(true) randomly rotates
vectors before quantization to reduce the quantization error. The rotation matrix is stored with
the index and applied to query vectors at search time.
Testing
# Using Makefile (recommended — auto-detects library paths)
# Using cargo directly (requires ZVEC_LIB_DIR / DYLD_LIBRARY_PATH)
# Fuzz tests (requires nightly)
# Benchmarks
# Code coverage
Keeping in Sync with zvec Core
This SDK tracks the zvec C-API. When the upstream C-API changes:
- Update
zvec-sys/src/lib.rswith new FFI declarations - Add safe wrappers in the
zveccrate - Update integration tests to cover new functionality
- Run the full test suite to verify compatibility
The CI pipeline automatically clones the latest zvec and builds the C library, ensuring FFI compatibility on every PR.
Contributing
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Ensure all tests pass (
cargo test) - Ensure code is formatted (
cargo fmt --all -- --check) - Ensure clippy is clean (
cargo clippy --workspace --all-targets -- -D warnings) - Submit a pull request
License
Apache-2.0 — see LICENSE.