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
- 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 db = /* build as above */;
// 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 db = /* build as above */;
// 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;
// … insert more vectors …
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 = new; // CPU backend (default)
| Field | Type | Description |
|---|---|---|
dims |
usize |
Number of dimensions per vector. Must be > 0. |
backend_preference |
BackendPreference |
Backend selection. Only Cpu is available today. |
RingDb<T> — builder
| Method | Description |
|---|---|
RingDb::new(config) |
Create an empty database. |
add_vector(v, payload) |
Insert one vector and its associated payload. |
build() |
Seal the database; returns SealedRingDb<T>. |
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
RangeQuery<'a>
RangeQuery
DiskQuery<'a>
DiskQuery
// 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>:
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 — the temp file is deleted automatically when
SealedRingDbis dropped, on all platforms (Windows: mmap is dropped before deletion).
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() │ │
└─────────────┼─────────────────────────┼──────────────┘
▼ ▼
┌──────────────────────────────────────────────────────┐
│ 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.
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 (memory-mapped, no load time)
- Python bindings (PyO3)
License
MIT OR Apache-2.0