ring-db
A Rust library for ring queries in high-dimensional vector spaces.
Instead of nearest-neighbour search, ring-db retrieves every vector whose
Euclidean distance to a query falls inside a specified interval.
Three query shapes are supported out of the box:
| Query type | Interval | Typical use |
|---|---|---|
RingQuery |
[d − λ, d + λ] | Hollow shell at distance d |
RangeQuery |
[d_min, d_max] | Arbitrary distance band |
DiskQuery |
[0, d_max] | Full ball / nearest-within-radius |
RingQuery RangeQuery DiskQuery
┌──── λ ───┐ ┌──────────┐ ┌──────────────┐
d ─────────── q d_min d_max q 0 d_max q
└──────────┘
Table of contents
- Why ring queries?
- Use cases
- Quick start
- API overview
- Payload storage
- Persistence
- Architecture
- Performance
- Running the benchmarks
- Running the tests
- CLI example
- Roadmap
Why ring queries?
Most vector databases answer "what is closest to X?". Ring queries answer a different question: "what is at distance d from X, give or take λ?"
This matters when the magnitude of similarity has semantic meaning and you want to filter by it rather than rank by it. Nearest-neighbour returns a ranked list; a ring query returns a boolean membership set — every result satisfies the same geometric constraint.
The three query types let you express the constraint directly:
RingQuery— symmetric band defined by a centre distancedand half-widthλ: interval[d-λ, d+λ].RangeQuery— arbitrary interval[d_min, d_max], no conversion needed.DiskQuery— full ball of radiusd_max, equivalent toRangeQuery { d_min: 0, d_max }.
Internally, the library avoids computing square roots by working with squared L2 distances:
lower_sq = d_min²
upper_sq = d_max²
dist_sq = ‖x‖² + ‖q‖² − 2·(x · q)
The dot product and norms are computed with four independent accumulators so
the compiler can emit fused multiply-add instructions (fmla/vfmadd).
Use cases
| Domain | Query | Ring semantics |
|---|---|---|
| Anomaly detection | Is this embedding at an unusual distance from the cluster centroid? | Query at d = expected_radius, flag hits at d ± small_λ |
| Content deduplication | Find near-duplicates, exclude exact copies and fully different items | Set d to a "suspicious similarity" threshold, narrow λ |
| Recommendation diversity | Return items similar to but not the same as the seed | Exclude the d ≈ 0 ball with a positive lower bound |
| Spatial shells | Find all POIs within a GPS distance band (e.g. 500 m–1 km) | Direct geometric interpretation |
| Music / audio fingerprinting | Match recordings at a specific perceptual distance | Encodes "sounds like but is not identical" |
| Security / threat intel | Find embeddings of known-bad patterns at a specific mutation distance | Ring encodes "one edit away from known threat" |
Quick start
Add to Cargo.toml:
[]
= { = "." } # or version once published
= { = "1", = ["derive"] }
Ring query — hollow shell at distance d ± λ
use ;
let mut db = new.unwrap;
db.add_vector.unwrap;
db.add_vector.unwrap;
db.add_vector.unwrap; // dist = 5.0 from origin
let db = db.build.unwrap;
// Find all vectors at distance ≈ 5 from the origin (band: [4.5, 5.5])
let result = db.query.unwrap;
println!; // [1, 2]
println!; // "cpu"
println!;
Range query — explicit [d_min, d_max] band
use ;
let mut db = new.unwrap;
db.add_vector.unwrap;
db.add_vector.unwrap;
let db = db.build.unwrap;
// Find all vectors between distance 3.0 and 6.0 from the query
let result = db.query_range.unwrap;
Disk query — everything within radius d_max
use ;
let mut db = new.unwrap;
db.add_vector.unwrap;
db.add_vector.unwrap;
let db = db.build.unwrap;
// Find all vectors within distance 5.0 from the query (full ball)
let result = db.query_disk.unwrap;
With a typed payload
use ;
use ;
let mut db: = new.unwrap;
db.add_vector.unwrap;
db.add_vector.unwrap;
let query = ;
let db = db.build.unwrap;
let result = db.query.unwrap;
// Fetch payloads for matching IDs
let docs = db.fetch_payloads.unwrap;
for doc in &docs
API overview
RingDbConfig
let config = RingDbConfig::new(dims); // in-memory, CPU backend (defaults)
let config = RingDbConfig::new(dims).with_persist_dir("/my/db"); // persisted to disk
| Field | Type | Description |
|---|---|---|
dims |
usize |
Number of dimensions per vector. Must be > 0. |
backend_preference |
BackendPreference |
Backend selection. Only Cpu is available today. |
persist_dir |
Option<PathBuf> |
Directory to persist the database. None = in-memory only. |
| Builder method | Description |
|---|---|
with_persist_dir(path) |
Enable persistence to the given directory (created automatically). |
with_backend_preference(p) |
Override the backend (default: BackendPreference::Cpu). |
RingDb<T> — builder
| Method | Description |
|---|---|
RingDb::new(config) |
Create an empty in-memory (or persist-configured) database. |
RingDb::load(dir, backend) |
Rehydrate a previously persisted database from dir. |
add_vector(v, payload) |
Insert one vector and its associated payload. |
build() |
Seal the database; returns SealedRingDb<T>. Writes files to disk if persist_dir is set. |
len() / is_empty() |
Number of staged vectors. |
dims() / backend_name() |
Configuration introspection. |
Vectors are assigned sequential IDs starting from 0 in insertion order.
SealedRingDb<T> — query
| Method | Description |
|---|---|
query(q: &RingQuery) |
Ring query: interval [d-λ, d+λ]. |
query_range(q: &RangeQuery) |
Range query: explicit [d_min, d_max]. |
query_disk(q: &DiskQuery) |
Disk query: full ball [0, d_max]. |
fetch_payload(id) |
Deserialize the payload for a single ID. |
fetch_payloads(ids) |
Deserialize payloads for a slice of IDs, in order. |
len() / is_empty() / dims() |
Introspection. |
RingQuery<'a>
RingQuery {
query: &[f32], // query vector, length == dims
d: f32, // centre of the ring (target distance)
lambda: f32, // half-width; interval = [max(0, d-λ), d+λ]
}
RangeQuery<'a>
RangeQuery {
query: &[f32], // query vector, length == dims
d_min: f32, // lower bound of the distance interval (inclusive, ≥ 0)
d_max: f32, // upper bound of the distance interval (inclusive, ≥ d_min)
}
DiskQuery<'a>
DiskQuery {
query: &[f32], // query vector, length == dims
d_max: f32, // radius of the ball (inclusive, ≥ 0)
}
// Equivalent to RangeQuery { d_min: 0.0, d_max }
QueryResult
result.ids // Vec<u32> — matching vector IDs
result.backend_used // &'static str — e.g. "cpu"
result.elapsed // Duration — wall-clock query time
Error handling
All fallible operations return Result<_, RingDbError>:
pub enum RingDbError {
DimensionMismatch { expected: usize, got: usize },
Payload(String), // serialization / deserialization error
Io(std::io::Error),
Corrupt(String), // missing / inconsistent persistence files
}
Payload storage
Payloads are designed to consume zero hot memory after the build phase:
-
Build phase — each call to
add_vectorserializes the payload immediately (viabincode) and streams it to a temporary file. -
Query phase — after
build(), the temp file is mmap-ed read-only. The OS can page payload bytes out under memory pressure. The only hot-path data is the offset table (8 bytes × number of vectors). -
Cleanup — when no
persist_diris set, the temp file is deleted automatically whenSealedRingDbis dropped. Whenpersist_diris set, the files are kept on disk and can be reloaded later withRingDb::load().
Persistence
ring-db can save the full database to disk and reload it in a later process. No re-insertion is required — the original vectors, norms, and payloads are reconstructed exactly.
Saving
Set persist_dir on the config before calling build():
use ;
let mut db = new.unwrap;
db.add_vector.unwrap;
db.add_vector.unwrap;
let _sealed = db.build.unwrap; // writes files to /tmp/mydb
build() creates the directory if it does not exist and writes:
| File | Content |
|---|---|
meta.bin |
dims + n_vectors as little-endian u64 |
vectors.bin |
Raw f32 vectors (row-major, n_vectors × dims floats) |
norms_sq.bin |
Raw f32 squared L2 norms (n_vectors floats) |
payloads.bin |
Concatenated bincode-serialized payload bytes |
offsets.bin |
Byte offsets (u64) into payloads.bin, one per vector |
Loading
Pass the directory and your chosen backend to RingDb::load():
use ;
use Path;
let db = load.unwrap;
// db is a fully usable SealedRingDb — query immediately
let result = db.query.unwrap;
load() validates the file sizes against the stored metadata and returns
RingDbError::Corrupt if anything is inconsistent.
With a typed payload
use ;
use ;
use Path;
// --- save ---
let mut db: = new.unwrap;
db.add_vector.unwrap;
let _sealed = db.build.unwrap;
// --- load in a new process ---
let db = load.unwrap;
let result = db.query.unwrap;
let docs = db.fetch_payloads.unwrap;
The payload type T must implement serde::Serialize + serde::DeserializeOwned.
Use T = () when no payload is needed — the file is never written in that case.
Architecture
┌──────────────────────────────────────────────────────┐
│ RingDb<T> (builder, mutable) │
│ │
│ ┌─────────────────────┐ ┌──────────────────────┐ │
│ │ vectors: Vec<f32> │ │ PayloadStoreBuilder │ │
│ │ norms_sq: Vec<f32> │ │ (BufWriter → tmpfile│ │
│ └──────────┬──────────┘ └──────────┬───────────┘ │
│ │ build() │ │
└─────────────┼─────────────────────────┼──────────────┘
│ │
│ (if persist_dir set) │
▼ ▼
┌─────────────────────────────────────┐
│ Disk (/persist_dir/) │
│ meta.bin vectors.bin norms_sq.bin│
│ payloads.bin offsets.bin │
└──────────────┬──────────────────────┘
│ RingDb::load(dir, backend)
▼
┌──────────────────────────────────────────────────────┐
│ SealedRingDb<T> (immutable, Send + Sync) │
│ │
│ ┌─────────────────────────┐ ┌────────────────────┐ │
│ │ RingComputeBackend │ │ PayloadStore │ │
│ │ (trait object) │ │ mmap + offsets │ │
│ │ │ │ (cold, page-able) │ │
│ │ ┌─────────────────┐ │ └────────────────────┘ │
│ │ │ CpuBackend │ │ │
│ │ │ Rayon parallel │ │ │
│ │ │ 4-acc dot prod │ │ │
│ │ └─────────────────┘ │ │
│ └─────────────────────────┘ │
└──────────────────────────────────────────────────────┘
Backend trait
The RingComputeBackend trait separates the upload step from the query step,
reflecting the intended usage: upload once, then run many queries against
resident data without re-uploading.
pub trait RingComputeBackend: Send + Sync {
fn name(&self) -> &'static str;
fn upload_f32_dataset(&mut self, dims: usize, vectors: Vec<f32>, norms_sq: Vec<f32>) -> Result<()>;
fn ring_query_f32(&self, dims: usize, query: &[f32], d_min: f32, d_max: f32) -> Result<Vec<u32>>;
}
All three public query methods (query, query_range, query_disk) translate
their respective inputs into a (d_min, d_max) pair and delegate to this single
backend method.
New backends (WGPU, CUDA, AVX-512 hand-rolled) can be added without touching the public API.
CPU backend implementation
- Parallelism — Rayon
par_chunks_exactsplits the vector matrix across all logical cores. - SIMD-friendly dot product — four independent float accumulators break the
dependency chain so the compiler emits
fmla.4s(NEON) orvfmadd231ps(AVX), yielding ~4× FP throughput on the reduction. - Precomputed norms —
‖x‖²is stored at insertion time; only a dot product is computed per vector at query time (no square root). - Squared bounds — both bounds are squared once before the loop to avoid
sqrtentirely.
Performance
Benchmarks run on 30 000 000 randomly generated float32 vectors in [-1, 1]^d,
with a ring width of 0.05 % of the expected inter-vector distance.
Measured with Criterion.
Ring query throughput (no payload)
| Dimensions | Vectors | Query time | Hit rate |
|---|---|---|---|
| 64 | 30 M | ~68 ms | ~0.55 % |
| 128 | 30 M | ~138 ms | ~0.61 % |
At 64 dims this is ~440 M vectors/second scanned on a single machine.
Payload fetch (100-byte string payload, mmap)
Fetching all payloads for matching vectors after a query (ring=[6.529, 6.535], ~164 K hits out of 30 M):
| Dimensions | Hits | Fetch time |
|---|---|---|
| 64 | ~164 K | ~24 ms |
| 128 | ~184 K | ~26 ms |
Both are O(hits), not O(dataset), because payloads are addressed by offset table and read directly from the mmap.
Running the benchmarks
# Full benchmark suite (HTML report in target/criterion/)
# Single group
# With native CPU optimisations (recommended)
RUSTFLAGS="-C target-cpu=native"
Results are written to target/criterion/ as an interactive HTML report.
Running the tests
# All tests
# Correctness tests only (hand-crafted, analytically verified)
# Randomised sanity tests
# With output (useful to see ring stats)
CLI example
A small CLI that builds a random dataset and runs a ring query:
# 100 000 vectors, 64 dims, ring centred at d=5.0 with λ=0.2
Sample output:
ringdb demo
dataset : 100000 vectors × 64 dims
ring : d=5, λ=0.2
Building database … done in 45.12ms
backend : cpu
Running ring query … done
Results:
backend : cpu
hits : 312
query time : 2.34ms
hit IDs : [142, 891, … 312 total]
Roadmap
- GPU backend via
wgpu(compute shaders, cross-platform) - CUDA backend for datacenter workloads
- AVX-512 hand-rolled kernel (skipping Rayon for very small datasets)
- Approximate ring search (LSH / quantisation) for datasets > 1 B vectors
- Persistent on-disk format (
build()writes,RingDb::load()rehydrates) - Python bindings (PyO3)
License
MIT OR Apache-2.0