Skip to content

Commit 18c7b32

Browse files
committed
Add CI, edge-case tests, contributor docs, and issue templates
- GitHub Actions CI: test, clippy, fmt, WASM build on push/PR - 12 edge-case integration tests + 9 new unit tests in distance/search - CONTRIBUTING.md with build, test, and PR instructions - Bug report and feature request issue templates - GitHub Pages redirect to altorlab.dev - README: add CI badge, live demo link, "How it works" section - Fix pre-existing clippy warnings (doc comments, useless vec) - Format codebase with cargo fmt
1 parent 682d953 commit 18c7b32

14 files changed

Lines changed: 488 additions & 25 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
name: Bug Report
3+
about: Report a bug in altor-vec
4+
title: ""
5+
labels: bug
6+
assignees: ""
7+
---
8+
9+
**Describe the bug**
10+
A clear description of what the bug is.
11+
12+
**To reproduce**
13+
Steps or code snippet to reproduce the behavior:
14+
15+
```rust
16+
// your code here
17+
```
18+
19+
**Expected behavior**
20+
What you expected to happen.
21+
22+
**Environment**
23+
- altor-vec version:
24+
- Rust version (`rustc --version`):
25+
- OS:
26+
- Browser (if WASM):
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
name: Feature Request
3+
about: Suggest an idea for altor-vec
4+
title: ""
5+
labels: enhancement
6+
assignees: ""
7+
---
8+
9+
**Problem**
10+
What problem does this feature solve?
11+
12+
**Proposed solution**
13+
How you'd like it to work.
14+
15+
**Alternatives considered**
16+
Any other approaches you've thought about.

.github/workflows/ci.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
env:
10+
CARGO_TERM_COLOR: always
11+
12+
jobs:
13+
test:
14+
name: Test
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
- uses: dtolnay/rust-toolchain@stable
19+
- uses: Swatinem/rust-cache@v2
20+
- run: cargo test --all-features
21+
22+
clippy:
23+
name: Clippy
24+
runs-on: ubuntu-latest
25+
steps:
26+
- uses: actions/checkout@v4
27+
- uses: dtolnay/rust-toolchain@stable
28+
with:
29+
components: clippy
30+
- uses: Swatinem/rust-cache@v2
31+
- run: cargo clippy --all-targets --all-features -- -D warnings
32+
33+
fmt:
34+
name: Format
35+
runs-on: ubuntu-latest
36+
steps:
37+
- uses: actions/checkout@v4
38+
- uses: dtolnay/rust-toolchain@stable
39+
with:
40+
components: rustfmt
41+
- run: cargo fmt --all -- --check
42+
43+
wasm:
44+
name: WASM Build
45+
runs-on: ubuntu-latest
46+
steps:
47+
- uses: actions/checkout@v4
48+
- uses: dtolnay/rust-toolchain@stable
49+
with:
50+
targets: wasm32-unknown-unknown
51+
- uses: Swatinem/rust-cache@v2
52+
- uses: jetli/wasm-pack-action@v0.4.0
53+
- run: cd wasm && wasm-pack build --target web --release

CONTRIBUTING.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Contributing to altor-vec
2+
3+
Thanks for your interest in contributing! Here's how to get started.
4+
5+
## Building from source
6+
7+
```bash
8+
git clone https://github.com/altor-lab/altor-vec.git
9+
cd altor-vec
10+
cargo build
11+
```
12+
13+
## Running tests
14+
15+
```bash
16+
cargo test # core tests
17+
cargo test --all-features # include serialization tests
18+
```
19+
20+
## Building WASM
21+
22+
```bash
23+
cargo install wasm-pack # one-time setup
24+
cd wasm && wasm-pack build --target web --release
25+
```
26+
27+
## Code style
28+
29+
We use standard Rust tooling — please run these before submitting a PR:
30+
31+
```bash
32+
cargo fmt # format code
33+
cargo clippy --all-targets --all-features -- -D warnings # lint
34+
```
35+
36+
## Pull request process
37+
38+
1. Open an issue describing the change you'd like to make.
39+
2. Fork the repo and create a feature branch from `main`.
40+
3. Make your changes, add tests, and ensure `cargo test --all-features` passes.
41+
4. Run `cargo fmt` and `cargo clippy` with no warnings.
42+
5. Submit a PR — we'll review it as soon as we can.
43+
44+
## Questions?
45+
46+
Open an issue or email [anshul@altorlab.dev](mailto:anshul@altorlab.dev).

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@
66
<p align="center">
77
<a href="https://www.npmjs.com/package/altor-vec"><img src="https://img.shields.io/npm/v/altor-vec?color=blue&label=npm" alt="npm version"></a>
88
<a href="https://www.npmjs.com/package/altor-vec"><img src="https://img.shields.io/npm/dm/altor-vec?color=green" alt="npm downloads"></a>
9+
<a href="https://github.com/altor-lab/altor-vec/actions/workflows/ci.yml"><img src="https://github.com/altor-lab/altor-vec/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
910
<a href="https://github.com/altor-lab/altor-vec/stargazers"><img src="https://img.shields.io/github/stars/altor-lab/altor-vec?style=social" alt="GitHub stars"></a>
1011
<a href="https://github.com/altor-lab/altor-vec/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="License"></a>
1112
<img src="https://img.shields.io/badge/WASM-54KB_gzipped-orange" alt="WASM size">
1213
</p>
14+
<p align="center">
15+
<a href="https://altorlab.dev"><img src="https://img.shields.io/badge/%F0%9F%9A%80_Try_Live_Demo-altorlab.dev-blueviolet?style=for-the-badge" alt="Try Live Demo"></a>
16+
</p>
1317
</p>
1418

1519
---
@@ -149,6 +153,12 @@ const output = await embed('your query', { pooling: 'mean', normalize: true });
149153
const results = JSON.parse(engine.search(new Float32Array(output.data), 5));
150154
```
151155

156+
## How it works
157+
158+
altor-vec uses **HNSW (Hierarchical Navigable Small World)** — the same algorithm behind Pinecone, Qdrant, and pgvector. HNSW builds a multi-layer graph where each node is a vector and edges connect nearby neighbors. Upper layers act as express lanes for coarse navigation; the bottom layer contains all vectors for fine-grained search. A query enters at the top and greedily descends to find the nearest neighbors in O(log n) time.
159+
160+
All vectors are L2-normalized at insert time, so dot product distance equals cosine similarity — no extra computation at search time.
161+
152162
## Architecture
153163

154164
```
@@ -175,7 +185,7 @@ cd wasm && wasm-pack build --target web --release # build WASM
175185

176186
## Contributing
177187

178-
We welcome contributions! Open an issue to discuss what you'd like to change, then submit a PR.
188+
We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for build instructions, code style, and PR process.
179189

180190
## License
181191

benches/search_bench.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
use criterion::{criterion_group, criterion_main, Criterion};
21
use altor_vec::distance::normalize;
32
use altor_vec::HnswIndex;
3+
use criterion::{criterion_group, criterion_main, Criterion};
44
use rand::Rng;
55

66
fn random_unit_vector(dims: usize, rng: &mut impl Rng) -> Vec<f32> {
@@ -49,5 +49,10 @@ fn bench_search_1k(c: &mut Criterion) {
4949
});
5050
}
5151

52-
criterion_group!(benches, bench_search_ef50, bench_search_ef500, bench_search_1k);
52+
criterion_group!(
53+
benches,
54+
bench_search_ef50,
55+
bench_search_ef500,
56+
bench_search_1k
57+
);
5358
criterion_main!(benches);

docs/index.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta http-equiv="refresh" content="0; url=https://altorlab.dev">
6+
<title>altor-vec — Redirecting</title>
7+
</head>
8+
<body>
9+
<p>Redirecting to <a href="https://altorlab.dev">altorlab.dev</a>...</p>
10+
</body>
11+
</html>

src/distance.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,54 @@ mod tests {
4242
let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
4343
assert!((norm - 1.0).abs() < 1e-6);
4444
}
45+
46+
#[test]
47+
fn test_dot_product_zero_vector() {
48+
let a = vec![1.0, 2.0, 3.0];
49+
let b = vec![0.0, 0.0, 0.0];
50+
assert!((dot_product(&a, &b)).abs() < 1e-6);
51+
}
52+
53+
#[test]
54+
fn test_dot_product_identical_vectors() {
55+
let a = vec![0.6, 0.8];
56+
// dot(a, a) = 0.36 + 0.64 = 1.0
57+
assert!((dot_product(&a, &a) - 1.0).abs() < 1e-6);
58+
}
59+
60+
#[test]
61+
fn test_dot_product_high_dimensions() {
62+
let dims = 1536;
63+
let a: Vec<f32> = (0..dims).map(|i| (i as f32).sin()).collect();
64+
let b: Vec<f32> = (0..dims).map(|i| (i as f32).cos()).collect();
65+
// Just verify it runs without panicking and produces a finite result
66+
let result = dot_product(&a, &b);
67+
assert!(result.is_finite());
68+
}
69+
70+
#[test]
71+
fn test_normalize_zero_vector() {
72+
let mut v = vec![0.0, 0.0, 0.0];
73+
let norm = normalize(&mut v);
74+
assert!((norm - 0.0).abs() < 1e-6);
75+
// Vector should remain zero
76+
for x in &v {
77+
assert!((*x).abs() < 1e-6);
78+
}
79+
}
80+
81+
#[test]
82+
fn test_normalize_already_unit() {
83+
let mut v = vec![1.0, 0.0, 0.0];
84+
let norm = normalize(&mut v);
85+
assert!((norm - 1.0).abs() < 1e-6);
86+
assert!((v[0] - 1.0).abs() < 1e-6);
87+
}
88+
89+
#[test]
90+
fn test_normalize_returns_original_norm() {
91+
let mut v = vec![3.0, 4.0];
92+
let norm = normalize(&mut v);
93+
assert!((norm - 5.0).abs() < 1e-6);
94+
}
4595
}

src/hnsw/construction.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@ pub fn random_layer(m: usize, rng: &mut impl Rng) -> usize {
1919

2020
/// Insert a new vector into the HNSW graph.
2121
/// The vector should already be L2-normalized.
22-
pub fn insert(graph: &mut Graph, vector: Vec<f32>, ef_construction: usize, rng: &mut impl Rng) -> usize {
22+
pub fn insert(
23+
graph: &mut Graph,
24+
vector: Vec<f32>,
25+
ef_construction: usize,
26+
rng: &mut impl Rng,
27+
) -> usize {
2328
let new_layer = random_layer(graph.m, rng);
2429
let new_id = graph.add_node(vector, new_layer);
2530

@@ -49,11 +54,7 @@ pub fn insert(graph: &mut Graph, vector: Vec<f32>, ef_construction: usize, rng:
4954
let results = search_layer(graph, &query, &current_entry_points, ef_construction, layer);
5055

5156
// Select M neighbors (use M, not Mmax — paper Algorithm 2)
52-
let neighbors: Vec<usize> = results
53-
.iter()
54-
.take(graph.m)
55-
.map(|&(id, _)| id)
56-
.collect();
57+
let neighbors: Vec<usize> = results.iter().take(graph.m).map(|&(id, _)| id).collect();
5758

5859
// Connect new node to neighbors
5960
graph.set_neighbors(new_id, layer, neighbors.clone());
@@ -107,7 +108,7 @@ mod tests {
107108
fn test_random_layer_distribution() {
108109
let mut rng = rand::thread_rng();
109110
let m = 16;
110-
let mut layers = vec![0usize; 10];
111+
let mut layers = [0usize; 10];
111112
for _ in 0..10000 {
112113
let l = random_layer(m, &mut rng);
113114
if l < layers.len() {

src/hnsw/graph.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
/// Layered graph structure for HNSW.
2-
///
3-
/// Each node stores its vector and a list of neighbor lists (one per layer it exists in).
4-
/// Neighbors are stored as (node_id, distance) pairs sorted by distance.
1+
//! Layered graph structure for HNSW.
2+
//!
3+
//! Each node stores its vector and a list of neighbor lists (one per layer it exists in).
4+
//! Neighbors are stored as (node_id, distance) pairs sorted by distance.
55
66
/// A single node in the HNSW graph.
7-
#[cfg_attr(feature = "serialization", derive(serde::Serialize, serde::Deserialize))]
7+
#[cfg_attr(
8+
feature = "serialization",
9+
derive(serde::Serialize, serde::Deserialize)
10+
)]
811
pub struct Node {
912
/// The vector data (L2-normalized at insert time).
1013
pub vector: Vec<f32>,
@@ -16,7 +19,10 @@ pub struct Node {
1619
}
1720

1821
/// The HNSW layered graph.
19-
#[cfg_attr(feature = "serialization", derive(serde::Serialize, serde::Deserialize))]
22+
#[cfg_attr(
23+
feature = "serialization",
24+
derive(serde::Serialize, serde::Deserialize)
25+
)]
2026
pub struct Graph {
2127
/// All nodes in insertion order. Node ID = index.
2228
pub nodes: Vec<Node>,

0 commit comments

Comments
 (0)