Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f6f644b
chore: align .cora.yaml with current cora schema (#4)
ajianaz Aug 26, 2026
a66cc23
ci: add release workflow (crates.io publish via tag) (#6)
ajianaz Aug 26, 2026
d31f363
chore: bump version to 0.1.1 (#8)
ajianaz Aug 26, 2026
1a68582
ci: add cora AI code review workflow (parity with uteke) (#13)
ajianaz Aug 28, 2026
71f73d0
fix(ci): correct CLA bot exemption to match actual bot logins (#14)
ajianaz Aug 28, 2026
ea68774
chore(deps): bump rand from 0.8.8 to 0.10.2 (#2)
dependabot[bot] Aug 28, 2026
6531476
chore(deps): bump actions/github-script from 7 to 9 (#3)
dependabot[bot] Aug 28, 2026
0589864
chore(cora): raise review llm max_tokens to 16384 (#15)
ajianaz Aug 28, 2026
4e21226
feat(core): keyed API — add_keyed/remove_keyed/tombstones (#10) (#16)
ajianaz Aug 28, 2026
8128e3d
feat(core): x86_64 AVX2 scoring path (parity with NEON, bit-identity …
ajianaz Aug 28, 2026
e5f40ef
docs: canonical SQLite BLOB storage pattern (#12) (#18)
ajianaz Aug 28, 2026
e3e838e
chore: bump version to 0.2.0 (#19)
ajianaz Aug 28, 2026
b694d5f
feat(core): Matryoshka-aware working_dim truncation (#30)
ajianaz Aug 28, 2026
6e11b17
feat(core): keyed parity with usearch — relabel + multi-vectors-per-k…
ajianaz Aug 28, 2026
553bd45
bench: head-to-head vs TurboQuant and RaBitQ at 4 bits (#28) (#34)
ajianaz Aug 28, 2026
41546b4
docs: add 0.2.0 changelog entry (#31) (#35)
ajianaz Aug 28, 2026
4832d8e
fix(core): persist the keyed map — file format v1.3 (#32) (#36)
ajianaz Aug 28, 2026
af6e480
feat(core): cascade search — 2-bit prefilter + 4-bit rescore (#22) (#37)
ajianaz Aug 28, 2026
2128a81
feat(core): residual quantization — opt-in recall mode, exact-norm tw…
ajianaz Aug 29, 2026
eb26d67
feat(core): configurable Lloyd-Max width (4/5/6-bit), default 5-bit s…
ajianaz Aug 29, 2026
639ca1d
docs: benchmark width matrix (#39/#40) + changelog for Unreleased (#42)
ajianaz Aug 29, 2026
4dda528
feat(core): zero-copy VecqView — serve an index straight from bytes (…
ajianaz Aug 29, 2026
9b87a4c
docs: README overhaul for the width/view era + stale-claim sweep (#44)
ajianaz Aug 29, 2026
a1db1ce
chore: bump version to 0.3.0 (#45)
ajianaz Aug 30, 2026
c8e3fec
docs(contributing): add CLA section + state contributions are unpaid …
ajianaz Aug 30, 2026
54f6b69
docs: README badges + reproduce folds; crate metadata honesty fixes (…
ajianaz Sep 4, 2026
9110eac
chore(deps): bump softprops/action-gh-release from 2 to 3 (#48)
dependabot[bot] Sep 4, 2026
5fd1559
bench: server-scale profile — 100K real vectors, cascade + working_di…
ajianaz Sep 13, 2026
e61c419
chore(deps): bump usearch from 2.26.1 to 2.26.2 (#50)
dependabot[bot] Sep 14, 2026
1e49932
chore: bump version to 0.3.1 (#54)
ajianaz Sep 14, 2026
2cd2637
chore: release v0.3.1 to main
ajianaz Sep 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ jobs:
fi

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [0.3.1] — 2026-09-14

### Added
- Server-scale benchmark profile (#52): seeded 100K-vector dataset generator (`scripts/gen_dataset.py`) and a `server_scale` harness measuring plain / `working_dim=256` / cascade modes against exact f32 ground truth, with results, a 2K/10K/100K scale curve, and positioning in the new "Server scale" section of `docs/BENCHMARK.md`. Headline: compression holds at 4.78x/100K; recall is N-dependent (5-bit r@10 0.974 → 0.850) because true-neighbor margins collapse as N grows — vecq's measured sweet spot is the local/on-device profile up to ~10K vectors (r@10 0.932 @ 18 ms/q single-thread); the 2-bit cascade is not a single-threaded throughput win at server N.

### Changed
- Crate metadata honesty pass (#49): new `vecq-core` description — "Training-free vector quantization (4/5/6-bit) and search — the SQLite profile for edge vector storage" — plus `homepage`, `keywords`, and `categories`, so the crates.io page reflects the current scope; README gains badges and benchmark-reproduce folds.
- Dependency/CI bumps: `softprops/action-gh-release` 2 → 3 (#48), usearch 2.26.1 → 2.26.2 in the bench harness (#50). No library code changes since 0.3.0.

## [0.3.0] — 2026-08-30

### Added
Expand Down
24 changes: 24 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,27 @@ This document helps you decide *whether* and *how* to contribute in a way that's
## Security

See [SECURITY.md](SECURITY.md).

## CLA

All contributions (code, docs, tests, configuration) require a signed
Contributor License Agreement before a pull request can be merged:

- 📋 **Individual?** → [Sign the Individual CLA](https://codecoradev.github.io/cla/?type=individual)
- 🏢 **Contributing on behalf of a company?** → [Sign the Corporate CLA](https://codecoradev.github.io/cla/?type=corporate)

The CLA is a license agreement, not a copyright assignment — you keep
ownership of your work. Signing takes a couple of minutes and is stored
in the [codecoradev/.github](https://github.com/codecoradev/.github)
repository; a bot checks it automatically on every pull request.

## Contributions are unpaid

Contributing to this project is **voluntary and unpaid**. There is no
compensation, payment, bounty, or financial reward of any kind for
contributions — now or in the future. You contribute on your own time,
at your own discretion, because you want to improve the project.

If any paid-contribution program is ever introduced, it will be announced
explicitly and this document will be updated. Until then, assume every
contribution is volunteer work under the license terms above.
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ resolver = "2"
members = ["crates/vecq-core", "crates/vecq-bench"]

[workspace.package]
version = "0.3.0"
version = "0.3.1"
edition = "2021"
license = "Apache-2.0"
repository = "https://github.com/codecoradev/vecq"
homepage = "https://github.com/codecoradev/vecq"
keywords = ["vector", "quantization", "search", "embedded", "no-std"]
keywords = ["vector", "quantization", "search", "embedded", "mobile"]
categories = ["algorithms", "data-structures", "science"]
authors = ["CodeCora <[email protected]>"]

Expand Down
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# vecq

[![CI](https://github.com/codecoradev/vecq/actions/workflows/ci.yml/badge.svg)](https://github.com/codecoradev/vecq/actions/workflows/ci.yml)
[![Crates.io](https://img.shields.io/crates/v/vecq-core.svg)](https://crates.io/crates/vecq-core)
[![docs.rs](https://img.shields.io/docsrs/vecq-core)](https://docs.rs/vecq-core)
[![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)

> Training-free vector quantization at configurable width (4/5/6-bit) and search — the "SQLite profile" for vector storage on edge devices.

`vecq` compresses dense embeddings **~5x** into a single deterministic file, using a zero-dependency pure-Rust crate. No training pass, no server, no C++.
Expand All @@ -13,6 +18,21 @@ vecq residual: 1,028 bytes/vector (3.0x smaller, recall@10 0.984)

It is the semantic-search engine for on-device and offline-first workloads — the layer below [uteke](https://github.com/codecoradev/uteke), the SQLite-based memory engine, where it is available as an optional search backend.

## Installation

```sh
cargo add vecq-core
```

or in `Cargo.toml`:

```toml
[dependencies]
vecq-core = "0.3"
```

The core crate is dependency-free — adding it pulls nothing beyond `std`. The benchmarking harness used for every number in this README lives in [`crates/vecq-bench`](crates/vecq-bench).

## Why vecq

| | vecq | HNSW libraries (usearch etc.) | server engines (Qdrant) |
Expand Down Expand Up @@ -46,6 +66,17 @@ Based on techniques validated in the RaBitQ / MonaVec line of research (random r

Real EmbeddingGemma, 768-dim, aarch64 release, n=2,000 — full methodology and the width matrix in [`docs/BENCHMARK.md`](docs/BENCHMARK.md).

<details>
<summary>Reproduce</summary>

```sh
cargo run --release -p vecq-bench --bin widths # full 4/5/6-bit + residual matrix
cargo run --release -p vecq-bench --bin real # recall/latency vs exact cosine ground truth
```

The dataset is 2,000 base vectors + 100 queries (768-dim) embedded with EmbeddingGemma 300M (Q4 ONNX) over a synthetic corpus — setup described in [`docs/BENCHMARK.md`](docs/BENCHMARK.md). Recall values are deterministic across runs and must match the tables bit-for-bit; latencies are aarch64-host numbers (ordering replicates everywhere, absolute times vary by machine).
</details>

## Usage

```rust
Expand Down Expand Up @@ -115,6 +146,17 @@ let hits = index.search(&query, 10);

Default width (5-bit), aarch64, single-threaded, 2,000 real EmbeddingGemma vectors (768-dim): search **3.21 ms/query**, build **75 ms**, recall@10 **0.979**. The 4-bit width trades to 0.89 ms/query @ 0.958; residual trades up to 0.984 @ 1.76 ms/query. Full methodology, the width matrix, the usearch comparison, and the per-architecture scoring-path matrix (NEON / AVX2 / scalar, all bit-identical) in [`docs/BENCHMARK.md`](docs/BENCHMARK.md).

<details>
<summary>Reproduce</summary>

```sh
cargo run --release -p vecq-bench --bin real # recall + latency vs exact f32 cosine ground truth
cargo run --release -p vecq-bench --bin vs_usearch # head-to-head against usearch HNSW (f32)
```

Ground truth is exact brute-force cosine on the same queries. Recall is deterministic across runs; timings vary by host — the scoring-path matrix in [`docs/BENCHMARK.md`](docs/BENCHMARK.md) lists the expected per-architecture behavior.
</details>

## Persistence & serving

- **SQLite BLOB** for mutable, transactional, embedded storage: schema, save/load pattern, atomicity, measured latencies at 1k/10k/50k vectors, and pitfalls — [`docs/SQLITE.md`](docs/SQLITE.md).
Expand Down
78 changes: 78 additions & 0 deletions crates/vecq-bench/src/bin/scale_probe.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//! Diagnostic: does in-memory scoring (f32 scales) differ from the persisted
//! artifact (f16 scales, reloaded from to_bytes)?
use std::fs;

use vecq_core::VecqIndex;

fn load_f32(path: &str, n: usize, dim: usize) -> Vec<Vec<f32>> {
let bytes = fs::read(path).expect("read file");
(0..n)
.map(|i| {
(0..dim)
.map(|j| {
f32::from_le_bytes(
bytes[(i * dim + j) * 4..(i * dim + j) * 4 + 4]
.try_into()
.unwrap(),
)
})
.collect()
})
.collect()
}

fn meta_get(meta: &str, k: &str) -> usize {
let i = meta.find(&format!("\"{k}\"")).expect(k) + k.len() + 4;
let rest = &meta[i..];
let end = rest.find(|c: char| !c.is_ascii_digit()).unwrap();
rest[..end].parse().unwrap()
}

fn main() {
let dir = "/tmp/vecq-bench";
let meta = fs::read_to_string(format!("{dir}/meta.json")).unwrap();
let (nb, nq, dim) = (
meta_get(&meta, "n_base"),
meta_get(&meta, "n_query"),
meta_get(&meta, "dim"),
);
let base = load_f32(&format!("{dir}/base.f32"), nb, dim);
let queries = load_f32(&format!("{dir}/queries.f32"), nq, dim);

for &bits in &[5u8, 4] {
let mut idx = VecqIndex::new(dim, 42);
if bits != 5 {
idx.set_bits(bits);
}
for v in &base {
idx.add(v);
}
let bytes = idx.to_bytes();
let reloaded = VecqIndex::from_bytes(&bytes).expect("reload");

let mut score_diffs = 0usize;
let mut max_delta = 0.0f32;
let mut list_diffs = 0usize;
for q in &queries {
let a = idx.search(q, 10);
let b = reloaded.search(q, 10);
if a != b {
list_diffs += 1;
}
// compare scores for the ids returned by the in-memory search
for (id, sa) in &a {
let sb = b.iter().find(|(i, _)| i == id).map(|(_, s)| *s);
if let Some(sb) = sb {
let d = (sa - sb).abs();
if d > 0.0 {
score_diffs += 1;
max_delta = max_delta.max(d);
}
}
}
}
println!(
"bits={bits}: top10 list diffs in-memory vs reloaded = {list_diffs}/{nq}, nonzero score deltas = {score_diffs}, max |delta| = {max_delta:.2e}"
);
}
}
Loading