-
Notifications
You must be signed in to change notification settings - Fork 0
docs: phase 2 -- MkDocs site, API reference, executable examples #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
5dff819
docs: add executable examples + CI smoke test
d3eba45
docs: add MkDocs site with Material theme + API reference
03a1a08
docs: trim README to hero + quickstart + docs links
f0f26cf
docs: address PR #49 bot review feedback
5974ac8
docs: address Copilot review on PR #49
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| name: Docs | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| pull_request: | ||
| branches: [main] | ||
| workflow_dispatch: | ||
|
|
||
| concurrency: | ||
| group: docs-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| build: | ||
| name: Build docs | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: "3.12" | ||
| cache: pip | ||
|
|
||
| - name: Install package + docs dependencies | ||
| run: | | ||
| python -m pip install --upgrade pip | ||
| pip install -e ".[docs]" | ||
|
|
||
| - name: Build site (strict) | ||
| run: mkdocs build --strict | ||
|
|
||
| - name: Upload Pages artifact | ||
| if: github.ref == 'refs/heads/main' | ||
| uses: actions/upload-pages-artifact@v3 | ||
| with: | ||
| path: site | ||
|
|
||
| deploy: | ||
| name: Deploy to GitHub Pages | ||
| needs: build | ||
| if: github.ref == 'refs/heads/main' | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| pages: write | ||
| id-token: write | ||
| environment: | ||
| name: github-pages | ||
| url: ${{ steps.deployment.outputs.page_url }} | ||
| steps: | ||
| - name: Deploy | ||
| id: deployment | ||
| uses: actions/deploy-pages@v4 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # Helpers | ||
|
|
||
| Lower-level building blocks exposed for advanced use. Most users should | ||
| reach for one of the index classes. | ||
|
|
||
| ## `rht` | ||
|
|
||
| ::: snapvec.rht | ||
|
|
||
| ## `padded_dim` | ||
|
|
||
| ::: snapvec.padded_dim | ||
|
|
||
| ## `get_codebook` | ||
|
|
||
| ::: snapvec.get_codebook |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| # IVFPQSnapIndex | ||
|
|
||
| ::: snapvec.IVFPQSnapIndex |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| # PQSnapIndex | ||
|
|
||
| ::: snapvec.PQSnapIndex |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| # ResidualSnapIndex | ||
|
|
||
| ::: snapvec.ResidualSnapIndex |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| # SnapIndex | ||
|
|
||
| ::: snapvec.SnapIndex |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| # Architecture | ||
|
|
||
| How `snapvec` compresses vectors without destroying inner-product recall. | ||
|
|
||
| ## Motivation | ||
|
|
||
| Dense embeddings from modern encoders (384-dim BGE, 768-dim E5, 1024-dim+ | ||
| OpenAI, etc.) are expensive to store and scan at scale. At float32, | ||
| 1M vectors of dim=768 take 3 GB. Float16 halves that but still scales | ||
| linearly with dim and only halves latency because memory bandwidth is | ||
| the bottleneck. | ||
|
|
||
| `snapvec` targets the compression / recall frontier: give up a small | ||
| amount of recall (typically 1-5 percentage points) for 6-96x less disk | ||
| and RAM, and run search kernels that fit in L2 cache. | ||
|
|
||
| ## Pipeline (SnapIndex) | ||
|
|
||
| 1. **Normalize** (optional): unit-length every vector. Turns inner | ||
| product into cosine similarity and removes one nuisance dimension. | ||
| 2. **Randomized Hadamard Transform (RHT)**: multiply by a signed random | ||
| Hadamard matrix. Decorrelates coordinates so their marginal | ||
| distribution approaches iid Gaussian, even when the input is not. | ||
| 3. **Lloyd-Max scalar quantization**: quantize each coordinate to `bits` | ||
| levels using the Lloyd-Max boundaries for a unit Gaussian. This is | ||
| the optimal scalar quantizer for a Gaussian source under MSE. | ||
| 4. **Bit-pack** the quantized codes so storage is exactly `bits * pdim / 8` | ||
| bytes per vector (`pdim` is the Hadamard-padded dim). | ||
|
|
||
| At query time, the query goes through the same normalize + RHT + quantize | ||
| path, then a vectorized inner-product sum over the unpacked codes scores | ||
| the whole corpus. | ||
|
|
||
| ## Why RHT? | ||
|
|
||
| Scalar quantization is optimal for a Gaussian source. Real embeddings are | ||
| not Gaussian along each coordinate. RHT is an isometry that makes | ||
| coordinates look Gaussian without changing inner products, so Lloyd-Max | ||
| becomes near-optimal. It also distributes signal energy across | ||
| coordinates, so no single quantization error dominates. | ||
|
|
||
| The RHT is implemented as a reshape-view + in-place xor across `log2(pdim)` | ||
| levels, so it costs `O(pdim * log(pdim))` time but `O(log(pdim))` Python | ||
| dispatch overhead. | ||
|
|
||
| ## Lloyd-Max codebooks | ||
|
|
||
| Pre-computed per bit-depth: | ||
|
|
||
| - 2-bit: 4 reconstruction levels at Gaussian-optimal boundaries | ||
| - 3-bit: 8 levels | ||
| - 4-bit: 16 levels | ||
|
|
||
| Stored as constants in [`snapvec/_codebooks.py`](https://github.com/stffns/snapvec/blob/main/snapvec/_codebooks.py). | ||
|
|
||
| ## Product Quantization (PQSnapIndex) | ||
|
|
||
| For higher recall at the same bytes/vec, `PQSnapIndex` replaces scalar | ||
| quantization with product quantization: | ||
|
|
||
| 1. Split each vector into `M` contiguous sub-vectors. | ||
| 2. For each subspace, learn `K=256` k-means centroids over a training | ||
| sample. | ||
| 3. Encode each vector as `M` bytes (one centroid index per subspace). | ||
|
|
||
| Query scoring uses an ADC (asymmetric distance computation) lookup table: | ||
| pre-compute `M * K` partial inner products against the query, then sum | ||
| `M` table lookups per corpus vector. The Cython+OpenMP kernel | ||
| (`snapvec._fast.adc_colmajor`) runs this in parallel with scores as a | ||
| stripe of ~4-8 MB that fits in L2. | ||
|
|
||
| ## Inverted File (IVFPQSnapIndex) | ||
|
|
||
| For sub-linear search, layer an inverted file (IVF) on top of PQ: | ||
|
|
||
| 1. Learn `nlist` coarse centroids from a training sample. | ||
| 2. Assign each corpus vector to its nearest coarse centroid. | ||
| 3. Store vectors contiguous per cluster, with an `offsets` array for | ||
| random access. | ||
| 4. At query time, rank coarse centroids by query-centroid distance and | ||
| visit only the top `nprobe` clusters. | ||
|
|
||
| Each cluster stores PQ codes of the **residual** (vector minus its coarse | ||
| centroid), which is easier to quantize than the raw vector. | ||
|
|
||
| ### Float16 rerank | ||
|
|
||
| PQ has a recall ceiling set by codebook granularity. To break it, | ||
| `IVFPQSnapIndex` with `keep_full_precision=True` stores each vector in | ||
| float16 as well. At query time, the top `rerank_candidates` returned by | ||
| the PQ pass are rescored against their stored fp16 vectors, and the | ||
| final top-k comes from the rerank scores. | ||
|
|
||
| This recovers 4-7 percentage points of recall at the cost of one | ||
| `(rerank_candidates, dim) @ (dim,)` matmul per query, typically under | ||
| 1 ms even at large `nprobe`. | ||
|
|
||
| ## References | ||
|
|
||
| - Zandieh et al. (2025). *TurboQuant: Online Vector Quantization with | ||
| Near-optimal Distortion Rate.* [arXiv:2504.19874](https://arxiv.org/abs/2504.19874) | ||
| - Jegou et al. (2011). *Product Quantization for Nearest Neighbor Search.* | ||
| IEEE TPAMI. | ||
| - Andre et al. (2015). *Cache Locality is Not Enough: High-performance | ||
| Nearest Neighbor Search with Product Quantization Fast Scan.* |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| # Benchmarks | ||
|
|
||
| All numbers measured on the same hardware with a fixed random seed. Raw | ||
| scripts live in [`experiments/`](https://github.com/stffns/snapvec/tree/main/experiments); | ||
| a reproducible CI-runnable suite is planned. | ||
|
|
||
| ## Headline: IVFPQSnapIndex on FIQA | ||
|
|
||
| BEIR FIQA, N = 57,638, dim = 384 (BGE-small), nlist = 512, M = 192, K = 256. | ||
| `keep_full_precision=True`, `rerank_candidates=100`. | ||
|
|
||
| | `nprobe` | Configuration | recall@10 | Latency (us/query) | | ||
| |---------|---------------|-----------|--------------------| | ||
| | 8 | PQ only | 0.85 | 180 | | ||
| | 32 | PQ only | 0.92 | 340 | | ||
| | 64 | PQ + fp16 rerank | **0.977** | **441** | | ||
| | 256 | PQ + fp16 rerank | **0.998** | **1021** | | ||
|
|
||
| 5.8x speedup vs v0.6 at identical recall, past the PQ-only 0.929 ceiling. | ||
|
|
||
| ## snapvec vs sqlite-vec (measured) | ||
|
|
||
| Same corpus, same hardware. `snapvec` at `nprobe=64` + rerank. | ||
|
|
||
| | N | sqlite-vec | snapvec | Speedup | Recall tradeoff | | ||
| |---|------------|---------|---------|-----------------| | ||
| | 10k | 2.3 ms | 0.44 ms | 5x | 0.997 | | ||
| | 57k | 15.1 ms | 0.44 ms | 34x | 0.977 | | ||
| | 100k | 23.8 ms | 1.04 ms | 23x | 0.994 | | ||
| | 500k | ~110 ms | 0.9 ms | 125x | ~0.97 | | ||
| | 1M | brute-force infeasible | 1.1 ms | -- | -- | | ||
|
|
||
| Disk footprint is 2-8x smaller across the range. | ||
|
|
||
| ## Compression ratios | ||
|
|
||
| For BGE-small (dim=384, float32 baseline = 1536 B/vec): | ||
|
|
||
| | Index | Config | B/vec | Compression | | ||
| |-------|--------|-------|-------------| | ||
| | `SnapIndex` | bits=2 | 132 | 11.6x | | ||
| | `SnapIndex` | bits=3 | 196 | 7.8x | | ||
| | `SnapIndex` | bits=4 | 260 | 5.9x | | ||
| | `PQSnapIndex` | M=16 | 16 | 96x | | ||
| | `PQSnapIndex` | M=32 | 32 | 48x | | ||
| | `IVFPQSnapIndex` | M=192 + fp16 rerank | ~960 | 1.6x (rerank cache dominates) | | ||
| | `IVFPQSnapIndex` | M=192, no rerank | ~192 | 8x | | ||
|
|
||
| ## Reproduction | ||
|
|
||
| ```bash | ||
| pip install -e ".[dev]" | ||
| python experiments/bench_v090_fiqa.py # FIQA recall / latency | ||
| python experiments/bench_sqlite_vec_baseline.py # sqlite-vec comparison | ||
| ``` | ||
|
|
||
| The `experiments/` folder is WIP; expect rough edges. A first-class | ||
| `bench/` suite that runs in CI and emits machine-readable results is | ||
| tracked on the roadmap. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| --8<-- "CHANGELOG.md" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| # Installation | ||
|
|
||
| ## From PyPI | ||
|
|
||
| ```bash | ||
| pip install snapvec | ||
| ``` | ||
|
|
||
| Pre-compiled wheels are published for CPython 3.10-3.13 on Linux (x86_64, | ||
| aarch64), macOS (x86_64 and arm64, macOS 13+), and Windows (AMD64). NumPy | ||
| >= 1.24 is the only runtime dependency. | ||
|
|
||
| ## From source | ||
|
|
||
| ```bash | ||
| git clone https://github.com/stffns/snapvec.git | ||
| cd snapvec | ||
| pip install -e ".[dev]" | ||
| ``` | ||
|
|
||
| On macOS you need OpenMP from Homebrew before building: | ||
|
|
||
| ```bash | ||
| brew install libomp | ||
| ``` | ||
|
|
||
| Without `libomp`, the Cython kernels compile in serial mode. The library | ||
| still works but the parallel search paths do not. | ||
|
|
||
| ## Input dtype | ||
|
|
||
| `snapvec` expects `np.float32` inputs everywhere. Internally, `add_batch` | ||
| and `search` pass your arrays through `np.asarray(..., dtype=np.float32)`, | ||
| so a `float64` input will be cast silently. That cast allocates a full | ||
| copy of the batch on every call, which is easy to miss in a hot path. | ||
| Prefer `float32` at the source. | ||
|
|
||
| ```python | ||
| # Models that return float32 directly (most modern embeddings) | ||
| vecs = model.encode(texts) # already float32 | ||
|
|
||
| # Arrays from np.array([...]) default to float64 -- cast explicitly | ||
| vecs = np.array(vecs_list, dtype=np.float32) | ||
|
|
||
| # Loading from disk -- respect the original dtype or force float32 | ||
| vecs = np.load("embeddings.npy").astype(np.float32, copy=False) | ||
| ``` | ||
|
|
||
| ## Verify install | ||
|
|
||
| ```python | ||
| import snapvec | ||
| print(snapvec.__version__) | ||
| ``` |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The docs workflow sets
pages: writeandid-token: writeat the workflow level, which also applies topull_requestruns. Consider scoping elevated permissions to thedeployjob only (and keepingbuildat read-only) to follow least-privilege and reduce the impact of a compromised build step.