a3s-vec is a native Rust, process-local retrieval engine for Coding Agent
workspaces. It combines dense and sparse vectors, scalar filtering, and BM25
inside one durable collection—without a server process or a C/C++ runtime.
The project is an active prototype. HNSW, IVF, HNSW/IVF RaBitQ, L2 Vamana, product-quantized L2 DiskANN with typed positioned or immutable mmap-snapshot traversal, scalar inverted indexes, and FTS are live; exact execution remains the correctness oracle whenever an index is missing, stale, or not selective enough.
Architecture · Roadmap · Reproducible benchmarks
What it delivers
| Need | Current implementation |
|---|---|
| Local semantic retrieval | Dense and sparse exact search, HNSW, IVF, HNSW/IVF RaBitQ, L2 Vamana, PQ/ADC DiskANN, and exact re-ranking |
| Workspace text search | BM25, Unicode n-grams, boolean groups, wildcard/fuzzy/range terms, ordered phrase proximity, boosts, and token filters |
| Structured narrowing | Typed scalar indexes, range/null/IN/wildcard predicates, and bitmap prefiltering |
| Durable embedding | WAL, checksummed snapshots, manifest commits, file locking, a validated derived-index cache, and a Vamana/DiskANN sector sidecar |
| Predictable failure | Typed validation errors and exact fallbacks instead of silent approximation |
All vector, scalar, and FTS indexes share one revisioned u64 ordinal domain.
That lets the planner compose bitmaps and candidates without building
query-sized primary-key maps, then resolve only the exact top-k documents.
Measured proof
cargo bench --bench structured_fts builds 25,000 workspace-shaped documents
and checks every indexed result and public score bit against scan execution.
The second of two consecutive post-change runs on the current development
machine produced:
| Query | Planner path | Candidates/query | Latency/query |
|---|---|---|---|
| Selective phrase | Indexed | 1 | 7.38 µs |
| Selective required + optional | Indexed | 1 | 4.62 µs |
| Selective wildcard | Indexed | 1 | 26.55 ms |
| Selective fuzzy | Indexed | 36 | 33.81 ms |
| Selective exact range | Indexed | 1 | 429.62 µs |
| Selective proximity | Indexed | 1 | 7.62 µs |
| Explicit scan controls | Scan | 25,000 | 38.07–98.36 ms |
| Common phrase | Automatic scan fallback | 25,000 | 40.81 ms |
| Broad boolean + NOT | Automatic scan fallback | 25,000 | 43.19 ms |
Five selective cases reduce scored candidates by 25,000×; fuzzy expansion reduces them from 25,000 to 36. Wildcard and fuzzy queries include a vocabulary expansion pass, while broad structured queries deliberately switch to the exact scan path when candidate-set work is unlikely to pay for itself. These are local regression measurements—not a cross-project zvec benchmark. Full methodology and repeated observations live in BENCHMARKS.md.
Quick start
The A3S monorepo consumes this repository as crates/vec. Until a crate
release is published, use a path dependency from the monorepo root:
[]
= { = "crates/vec" }
This complete example creates a durable FTS collection, inserts one workspace document, and executes a structured query:
use ;
Tokio applications can opt into scheduler-safe query methods without making the core collection runtime-dependent:
[]
= { = "crates/vec", = ["async"] }
use ;
async
query_async, multi_query_async, and group_by_async require an active
Tokio runtime and execute the complete synchronous snapshot, planner,
sidecar-I/O, fallback, and exact-refinement path on its blocking pool. They
produce the same results and telemetry as their synchronous counterparts; the
feature is an executor-safety boundary, not a latency claim. Tokio cannot
cancel spawn_blocking work after it starts, so dropping one of these futures
does not cancel its underlying query.
Executable compatibility examples
The examples directory is part of the regression
surface. examples/upstream/crud_operations.rs tracks
zvec-ai/zvec-rust@0d40cb1aef081bae175061fef35c89269e6a80f4 with only the
crate namespace changed; its executable wrapper adds only local lint
allowances. Asserted project-owned binaries cover vector/FTS and hybrid
retrieval, grouped top-k, isolated iteration, and durable schema evolution. CI
runs every binary instead of only checking that it compiles:
cargo run --locked --example crud_operations
cargo run --locked --example retrieval_workflows
cargo run --locked --example group_by
cargo run --locked --example schema_iteration
The pinned upstream CRUD fixture contains two incomplete replacement upserts;
both official zvec and a3s-vec reject them because the required id field is
absent. This known upstream fixture defect is preserved so the namespace-only
claim remains auditable. The asserted A3S-owned examples fail on any incorrect
result.
Retrieval capabilities
Vectors and indexes
- Dense FP16, FP32, FP64, INT4, INT8, INT16, Binary32, and Binary64 payloads.
- Sparse FP16 and FP32 payloads.
- Dense and sparse numeric queries accept either an explicit payload or a source document ID. Source-ID queries use the same exact scoring, filtering, radius, projection, persistence, and optional Tokio execution paths.
- Exact L2, inner product, cosine, and MIPS-L2 scoring with
f64intermediates. - Native HNSW and IVF candidate generation with exact full-vector re-ranking.
- Portable HNSW/IVF RaBitQ with deterministic random rotation, compact 1-to-9-bit codes, bounded refinement, and exact full-vector re-ranking.
- Deterministic two-pass L2 Vamana construction, bounded
list_sizesearch, incremental overlays, and exact full-vector re-ranking. - Deterministic product-quantizer training with up to 256 centroids per chunk, one-byte codes, query-local ADC tables, and exact full-vector re-ranking.
- Native 4 KiB-sector Vamana/DiskANN files with fixed full-vector or PQ-code records, CRC validation, bounded positioned reads or immutable anonymous mmap snapshots, and failure-closed in-memory fallback.
- Index-only FP16, symmetric INT8, and symmetric INT4 quantization.
- Scalar inverted indexes for equality, range,
IN, null, wildcard, prefix, suffix, and boolean filter composition.
Vamana accepts unquantized L2 vectors. IndexParams::diskann uses the same
deterministic graph and enables corpus-trained PQ when pq_chunk_num > 0;
zero selects full-vector graph scoring. A freshly built or rebuilt generation
traverses in memory. After a validated cache reopen, bounded queries use
portable positioned reads by default and retain a request-local sector/node
cache. IoBackend::Mmap instead copies the already validated sidecar into a
read-only anonymous memory map at open time and serves the same bounded extents
from that immutable snapshot. PQ queries build one centroid-distance table
and sum code distances during graph traversal. Incremental overlays share the
reader; a full rebuild retrains the codebook and invalidates the reader until
the next validated reopen. A short read or malformed record falls back to the
equivalent in-memory full-vector or ADC graph, and authoritative vectors still
perform final re-ranking. The file is an A3S-native format, not the Microsoft
DiskANN C++ format. The mmap snapshot is independent of later replacement or
truncation of the source file, but open performs a full sidecar copy and keeps
that additional memory for the handle's lifetime. The optional Tokio entry
points keep either backend off runtime workers; native async file reads and
direct file-backed mmap remain future accelerators.
Select mmap for one collection handle with a typed option:
use ;
The same query control selects the bounded list size for both index types:
use ;
RaBitQ is a separate HNSW/IVF index family. It trains deterministic centers,
applies a four-round signed Hadamard rotation, and uses the compact code only
for candidate traversal or refinement. The authoritative vector remains the
source of public scores. HNSW defaults to seven bits and 16 centers; the typed
options constructor exposes bit width, center count, and sample count. IVF
uses scale_factor * topk as the bounded exact-refiner set:
use ;
Non-L2 Vamana/DiskANN, Vamana graph saturation/occlusion tuning, and standalone Vamana quantization fail at schema validation until they have verified execution implementations.
Full-text search
The FTS pipeline analyzes documents and queries with the same ordered tokenizer and filter configuration.
| Component | Supported values |
|---|---|
| Tokenizer | standard, whitespace, Unicode ngram, optional jieba / jieba_accurate |
| Token filter | lowercase, ascii_folding, stemmer |
| Query syntax | AND, OR, NOT, parentheses, + required, - prohibited, escapes, * / ? wildcards, same-field qualifiers, ^ boosts, fuzzy terms, ordered phrase slop, and term ranges |
| Default operator | OR for compatibility, or explicit AND |
Omitting filters selects lowercase. Passing an explicit empty slice keeps
the standard, whitespace, and n-gram tokenizer output case-sensitive. Filters
run in declaration order on both indexed text and query text.
use ;
The Snowball stemmer supports Arabic, Danish, Dutch, English, Finnish, French, German, Greek, Hungarian, Italian, Norwegian, Portuguese, Romanian, Russian, Spanish, Swedish, Tamil, and Turkish. ASCII folding uses Unicode decomposition plus common Latin compatibility mappings; it is not advertised as byte-for-byte equivalent to every zvec folding table.
The n-gram tokenizer defaults to Unicode bigrams. ngram_min,
ngram_max, and token_chars configure its range and accepted Unicode
character classes:
use ;
For selective identifier/path queries, default_operator=AND starts from the
shortest posting. Structured expressions build exact boolean candidate sets;
phrases verify ordered proximity only for candidates. The planner falls back to
scan execution for broad expressions and keeps indexed refinement when a scalar
prefilter is available.
Wildcard (rust*, r?sty), fuzzy (rust~1 or rust~2), and range
([alpha TO omega], {alpha TO omega}) leaves expand once against the analyzed
term vocabulary. * is an unbounded range endpoint. Fuzzy terms, range bounds,
and wildcard literal fragments must each analyze to one term, and range
comparison is over the resulting lexicographic term order. A qualifier such as
body:rust must name the field already selected by SearchQuery::fts;
cross-field execution is rejected. Boosts are finite values in
(0, 1_000_000].
Quoted phrases accept an explicit slop from 0 through 1,024, for example
"vector engine"~2. Slop counts the total intervening tokens while preserving
term order; it does not enable transpositions. Both indexed and scan execution
use these same expansion, BM25, and proximity rules. Symbolic && and ||
aliases remain explicitly unsupported.
How execution stays exact
request
→ capture one immutable schema/document/index revision
→ validate route, type, dimension, limits, and syntax
→ derive scalar and FTS candidate ordinals when selective
→ run HNSW/IVF/RaBitQ/Vamana/DiskANN or the exact vector path
→ verify filters and phrases against authoritative documents
→ exact-score, deterministic top-k, projection, and optional fusion
- Flat vector and scan BM25 execution are always available as reference paths.
- Every derived index generation is immutable and tagged with its source revision.
- HNSW/IVF/RaBitQ/Vamana/DiskANN candidates are re-ranked with authoritative vectors.
- Indexed and scan FTS share
f64corpus/scoring primitives and produce bit-identical public scores in differential fixtures. - Equal scores use ascending primary key as the deterministic tie-break.
Persistence and recovery
Documents, snapshots, and WAL records are authoritative. The current storage format is version 4: checksummed MessagePack snapshots plus a manifest-committed WAL boundary. Version-3 JSON snapshots remain readable and upgrade at the next writable checkpoint.
ANN, scalar, FTS, and the shared ordinal table are persisted separately as a
non-authoritative derived cache. Cache format 10 includes RaBitQ rotations,
centers, compact codes, Vamana/DiskANN graphs, PQ codebooks/codes, parsed
tokenizer, and ordered filter state. A Vamana or
DiskANN generation additionally
requires indexes/diskann-graph.bin: an A3S-native 4 KiB-sector mirror bound to
the same revision, schema digest, and manifest identity. Its header, metadata,
padding, full vectors or PQ codes/codebooks, graph edges, and CRC are validated
before a cache hit. A missing, stale, corrupt, structurally invalid, or pre-v10
cache/sidecar pair
is ignored and rebuilt from recovered documents; read-only opens never repair
it.
The public API supports read-only handles, configurable durability and sidecar
I/O, explicit flush, targeted rebuild_index, whole-registry optimize, and
per-handle cache-hit/query/candidate plus DiskANN backend/sector-read telemetry.
Resource limits and accounting
Resource policy is a typed, collection-local option captured when a handle is created or opened:
use ;
max_documents and max_accounted_bytes are checked before a new collection
generation is published or appended to the WAL. Accounted bytes are the
deterministic bincode size of the authoritative document map plus the derived
index payload estimates reported by index statistics. They do not claim to
measure allocator overhead, temporary construction peaks, mapped files, or
process RSS. A deletion that would grow a tombstone overlay first compacts the
derived generation so deletion remains a practical way to recover capacity.
max_query_candidates bounds the planned exact/refinement candidates for one
query; multi-query branches share one cumulative budget. It does not represent
a wall-clock deadline or include every planner/index lookup. The write-batch
limit applies to insert, update, upsert, explicit delete inputs, and the matched
set of a filtered delete. A rejected generation is atomic and does not advance
the revision. stats and stats_snapshot expose the active policy, document
and index accounting, total accounted bytes, and a metadata-only rejection
counter; rejected query text and documents are never recorded.
Health and background maintenance
Collection::health reports an explicit healthy, degraded, unhealthy,
or closed state. It checks the in-memory revision against the committed
storage revision and requires every configured derived index to be ready,
complete, and sourced from that revision. WAL operations waiting for a
checkpoint are reported separately because they are normal under interval or
manual durability and do not make an otherwise recoverable collection
unhealthy.
Collection construction never starts a hidden thread. A writable collection can opt into one explicitly owned standard-thread scheduler:
use ;
use Duration;
Each due revision rebuilds the complete derived registry and checkpoints the
same authoritative generation while the writer gate is held; readers continue
using the previous immutable indexes during construction. Unchanged revisions
are skipped. Only one runtime may own a collection schedule, read-only handles
reject it, and close or Drop wakes and joins the worker before releasing
that ownership claim.
Current boundaries
| Area | Status |
|---|---|
| Flat, HNSW, IVF | Implemented |
| HNSW/IVF RaBitQ | Implemented for L2, inner product, and cosine with 1-to-9-bit codes and exact re-ranking |
| L2 Vamana traversal and incremental overlays | Implemented in memory and through positioned or immutable mmap-snapshot sidecar reads after reopen |
| L2 DiskANN PQ/ADC and incremental overlays | Implemented in memory and through positioned or immutable mmap-snapshot PQ-code reads after reopen |
| Sector-aligned native Vamana/DiskANN file | Implemented |
| Scalar inverted index | Implemented |
| BM25 + structured boolean/phrase FTS | Implemented |
| FTS wildcard/field/boost/fuzzy/proximity/range syntax | Implemented with bounded, analyzer-aware semantics |
| Dense/sparse source-ID query | Implemented; missing sources return NotFound and missing sparse payloads return FailedPrecondition |
| Collection health and background maintenance | Implemented with explicit ownership, bounded schedules, revision-aware skips, worker diagnostics, and joined shutdown |
| Collection resource admission | Implemented for retained documents/logical bytes, cumulative query candidates, write batches, and metadata-only rejection telemetry |
| DiskANN query reader | Portable positioned reads or a validated immutable anonymous mmap snapshot, plus optional Tokio blocking-pool query entry points; native async file reads and direct file-backed mmap remain roadmap |
| Product quantization / RaBitQ | PQ implemented for DiskANN / RaBitQ implemented for HNSW and IVF |
| Binary vector query execution | Not implemented |
| Alibaba C++ binary-format compatibility | Requires an explicit future importer/exporter |
a3s-vec follows zvec's Rust vocabulary where it is useful, but it is not a
binary-compatible clone. zvec-core remains a private pure-Rust algorithm
dependency; callers use only A3S-owned collection, schema, document, query, and
error contracts.
Quality gates
Run checks inside this crate:
RUSTDOCFLAGS="-D warnings"
Reproducible performance fixtures:
The default, no-default-feature, all-feature, strict Clippy, rustdoc, and Rust 1.75 gates are maintained separately. The optional Jieba dependency chain currently requires a newer Cargo because one transitive package uses a Rust 2024 manifest.
Platform and ownership
The portable correctness path targets Linux x86_64/aarch64, Windows x86_64,
and macOS arm64/x86_64, with macOS 12.0 as the Intel deployment target. It does
not require io_uring, a C/C++ runtime, or architecture-specific SIMD.
a3s-vec owns retrieval, persistence, and index execution. Workspace
scanning, embedding model runtimes, Agent sessions, and UI policy belong to
their callers. The cross-project boundary is documented in the
A3S local retrieval platform architecture.
This repository is A3S-Lab/Vec; the A3S monorepo consumes it as the
crates/vec submodule. Licensed under MIT.