├── src ├── example_index.bin ├── prelude.rs ├── lib.rs ├── test.py ├── errors.rs ├── metrics.rs ├── storage.rs ├── concurrency.rs └── index.rs ├── pyproject.toml ├── Cargo.toml ├── scripts ├── batch_ben.py └── benchmark.py ├── .gitignore ├── CHANGELOG.md ├── .github └── workflows │ └── CI.yml ├── README.md └── Cargo.lock /src/example_index.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxprogrammer007/Annie/main/src/example_index.bin -------------------------------------------------------------------------------- /src/prelude.rs: -------------------------------------------------------------------------------- 1 | // src/prelude.rs 2 | 3 | //! A “prelude” to import the most common types and traits in one go. 4 | 5 | pub use crate::index::AnnIndex; 6 | pub use crate::metrics::Distance; 7 | pub use crate::storage::{save_index, load_index}; 8 | pub use crate::concurrency::ThreadSafeIndex; 9 | pub use crate::errors::RustAnnError; 10 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=1.8,<2.0"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | name = "rust_annie" 7 | requires-python = ">=3.8" 8 | classifiers = [ 9 | "Programming Language :: Rust", 10 | "Programming Language :: Python :: Implementation :: CPython", 11 | "Programming Language :: Python :: Implementation :: PyPy", 12 | ] 13 | dynamic = ["version"] 14 | [tool.maturin] 15 | features = ["pyo3/extension-module"] 16 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod index; 2 | mod storage; 3 | mod metrics; 4 | mod errors; 5 | mod concurrency; 6 | 7 | use pyo3::prelude::*; 8 | use index::AnnIndex; 9 | use metrics::Distance; 10 | use concurrency::ThreadSafeAnnIndex; 11 | 12 | /// The Python module declaration. 13 | #[pymodule] 14 | fn rust_annie(_py: Python, m: &PyModule) -> PyResult<()> { 15 | m.add_class::()?; 16 | m.add_class::()?; 17 | m.add_class::()?; 18 | Ok(()) 19 | } 20 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust_annie" 3 | version = "0.1.2" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | [dependencies] 10 | # PyO3 for Python bindings 11 | pyo3 = { version = "0.18.3", features = ["extension-module"] } 12 | 13 | # Rust–NumPy interoperability 14 | numpy = "0.18.0" 15 | 16 | # ndarray for constructing 2D arrays 17 | ndarray = "0.15" 18 | 19 | # Serialization 20 | serde = { version = "1.0.188", features = ["derive"] } 21 | bincode = "1.3.3" 22 | 23 | rayon = "1.7" 24 | -------------------------------------------------------------------------------- /src/test.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from rust_annie import AnnIndex, Distance 3 | 4 | def main(): 5 | dim = 4 6 | 7 | # Initialize the index 8 | idx = AnnIndex(dim, Distance.COSINE) 9 | 10 | # Create some data 11 | data = np.random.rand(10, dim).astype(np.float32) 12 | ids = np.arange(10, dtype=np.int64) 13 | 14 | # Add data to the index 15 | idx.add(data, ids) 16 | 17 | # Query the first vector 18 | q = data[0] 19 | neigh_ids, distances = idx.search(q, k=3) 20 | print("Query:", q) 21 | print("Neighbors:", neigh_ids) 22 | print("Distances:", distances) 23 | 24 | # Save & reload 25 | idx.save("example_index.bin") 26 | idx2 = AnnIndex.load("example_index.bin") 27 | neigh2, _ = idx2.search(q, k=3) 28 | print("Reloaded neighbors:", neigh2) 29 | 30 | if __name__ == "__main__": 31 | main() 32 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | // src/errors.rs 2 | 3 | use pyo3::exceptions::{PyException, PyIOError}; 4 | use pyo3::PyErr; 5 | 6 | /// A simple error type for the ANN library, used to convert Rust errors into Python exceptions. 7 | #[derive(Debug)] 8 | pub struct RustAnnError(pub String); 9 | 10 | impl RustAnnError { 11 | /// Create a generic Python exception (`Exception`) with the given message. 12 | pub fn py_err(msg: impl Into) -> PyErr { 13 | PyException::new_err(msg.into()) 14 | } 15 | 16 | /// Create a RustAnnError wrapping an I/O error message. 17 | /// This is used internally in save/load to signal I/O or serialization failures. 18 | pub fn io_err(msg: impl Into) -> RustAnnError { 19 | RustAnnError(msg.into()) 20 | } 21 | 22 | /// Convert this RustAnnError into a Python `IOError` (`OSError`) exception. 23 | pub fn into_pyerr(self) -> PyErr { 24 | PyIOError::new_err(self.0) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /scripts/batch_ben.py: -------------------------------------------------------------------------------- 1 | # batch_benchmark.py 2 | 3 | import time 4 | import numpy as np 5 | from rust_annie import AnnIndex, Distance 6 | 7 | def benchmark_batch(N=10000, D=64, k=10, batch_size=64, repeats=20): 8 | # 1. Prepare random data 9 | data = np.random.rand(N, D).astype(np.float32) 10 | ids = np.arange(N, dtype=np.int64) 11 | idx = AnnIndex(D, Distance.EUCLIDEAN) 12 | idx.add(data, ids) 13 | 14 | # 2. Prepare query batch 15 | queries = data[:batch_size] 16 | 17 | # Warm-up 18 | idx.search_batch(queries, k) 19 | 20 | # 3. Benchmark Rust batch search 21 | t0 = time.perf_counter() 22 | for _ in range(repeats): 23 | idx.search_batch(queries, k) 24 | t_batch = (time.perf_counter() - t0) / repeats 25 | 26 | print(f"Rust batch search time ({batch_size} queries): {t_batch*1e3:8.3f} ms") 27 | print(f"Per-query time: {t_batch/batch_size*1e3:8.3f} ms") 28 | 29 | if __name__ == "__main__": 30 | benchmark_batch() 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | .pytest_cache/ 6 | *.py[cod] 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | .venv/ 14 | env/ 15 | bin/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | include/ 26 | man/ 27 | venv/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | pip-selfcheck.json 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | 45 | # Translations 46 | *.mo 47 | 48 | # Mr Developer 49 | .mr.developer.cfg 50 | .project 51 | .pydevproject 52 | 53 | # Rope 54 | .ropeproject 55 | 56 | # Django stuff: 57 | *.log 58 | *.pot 59 | 60 | .DS_Store 61 | 62 | # Sphinx documentation 63 | docs/_build/ 64 | 65 | # PyCharm 66 | .idea/ 67 | 68 | # VSCode 69 | .vscode/ 70 | 71 | # Pyenv 72 | .python-version 73 | -------------------------------------------------------------------------------- /src/metrics.rs: -------------------------------------------------------------------------------- 1 | use pyo3::prelude::*; 2 | use serde::{Serialize, Deserialize}; 3 | 4 | /// Unit‐only Distance enum for simple metrics. 5 | #[pyclass] 6 | #[derive(Clone, Copy, Serialize, Deserialize, Debug)] 7 | pub enum Distance { 8 | /// Euclidean (L2) 9 | Euclidean, 10 | /// Cosine 11 | Cosine, 12 | /// Manhattan (L1) 13 | Manhattan, 14 | /// Chebyshev (L∞) 15 | Chebyshev, 16 | } 17 | 18 | #[pymethods] 19 | impl Distance { 20 | #[classattr] pub const EUCLIDEAN: Distance = Distance::Euclidean; 21 | #[classattr] pub const COSINE: Distance = Distance::Cosine; 22 | #[classattr] pub const MANHATTAN: Distance = Distance::Manhattan; 23 | #[classattr] pub const CHEBYSHEV: Distance = Distance::Chebyshev; 24 | 25 | fn __repr__(&self) -> &'static str { 26 | match self { 27 | Distance::Euclidean => "Distance.EUCLIDEAN", 28 | Distance::Cosine => "Distance.COSINE", 29 | Distance::Manhattan => "Distance.MANHATTAN", 30 | Distance::Chebyshev => "Distance.CHEBYSHEV", 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /scripts/benchmark.py: -------------------------------------------------------------------------------- 1 | # benchmark.py 2 | import time 3 | import numpy as np 4 | from rust_annie import AnnIndex, Distance 5 | 6 | def pure_python_search(data, ids, q, k): 7 | # data: (N,D), q: (D,) 8 | # compute L2 distances and return top‐k 9 | dists = np.linalg.norm(data - q, axis=1) 10 | idx = np.argsort(dists)[:k] 11 | return ids[idx], dists[idx] 12 | 13 | def benchmark(N=10000, D=64, k=10, repeats=50): 14 | # 1. Prepare random data 15 | data = np.random.rand(N, D).astype(np.float32) 16 | ids = np.arange(N, dtype=np.int64) 17 | q = data[0] 18 | 19 | # 2. Build Rust index 20 | idx = AnnIndex(D, Distance.EUCLIDEAN) 21 | idx.add(data, ids) 22 | # warm-up 23 | idx.search(q, k) 24 | 25 | # 3. Benchmark Rust search 26 | t0 = time.perf_counter() 27 | for _ in range(repeats): 28 | idx.search(q, k) 29 | t_rust = (time.perf_counter() - t0) / repeats 30 | 31 | # 4. Benchmark pure-Python search 32 | t0 = time.perf_counter() 33 | for _ in range(repeats): 34 | pure_python_search(data, ids, q, k) 35 | t_py = (time.perf_counter() - t0) / repeats 36 | 37 | print(f"Rust avg search time: {t_rust*1e3:8.3f} ms") 38 | print(f"Pure-Python avg time: {t_py*1e3:8.3f} ms") 39 | print(f"Speedup (Python / Rust): {t_py / t_rust:6.2f}×") 40 | 41 | if __name__ == "__main__": 42 | benchmark() 43 | -------------------------------------------------------------------------------- /src/storage.rs: -------------------------------------------------------------------------------- 1 | // src/storage.rs 2 | 3 | use std::{ 4 | fs::File, 5 | io::{BufReader, BufWriter}, 6 | path::Path, 7 | }; 8 | 9 | use bincode; 10 | use serde::{Serialize, de::DeserializeOwned}; 11 | 12 | use pyo3::exceptions::PyIOError; 13 | 14 | use crate::errors::RustAnnError; 15 | use crate::index::AnnIndex; 16 | 17 | /// Serialize and write the given index to `path` using bincode. 18 | /// 19 | /// Returns a Python IOError on failure. 20 | pub fn save_index(idx: &AnnIndex, path: &str) -> Result<(), RustAnnError> { 21 | let path = Path::new(path); 22 | let file = File::create(path) 23 | .map_err(|e| RustAnnError::io_err(format!("Failed to create file {}: {}", path.display(), e)))?; 24 | let writer = BufWriter::new(file); 25 | bincode::serialize_into(writer, idx) 26 | .map_err(|e| RustAnnError::io_err(format!("Serialization error: {}", e)))?; 27 | Ok(()) 28 | } 29 | 30 | /// Read and deserialize an `AnnIndex` from `path` using bincode. 31 | /// 32 | /// Returns a Python IOError on failure. 33 | pub fn load_index(path: &str) -> Result { 34 | let path = Path::new(path); 35 | let file = File::open(path) 36 | .map_err(|e| RustAnnError::io_err(format!("Failed to open file {}: {}", path.display(), e)))?; 37 | let reader = BufReader::new(file); 38 | let idx: AnnIndex = bincode::deserialize_from(reader) 39 | .map_err(|e| RustAnnError::io_err(format!("Deserialization error: {}", e)))?; 40 | Ok(idx) 41 | } 42 | -------------------------------------------------------------------------------- /src/concurrency.rs: -------------------------------------------------------------------------------- 1 | // src/concurrency.rs 2 | 3 | use std::sync::{Arc, RwLock}; 4 | use pyo3::prelude::*; 5 | use numpy::{PyReadonlyArray1, PyReadonlyArray2}; 6 | use crate::index::AnnIndex; 7 | use crate::metrics::Distance; 8 | 9 | /// Python‐visible thread‐safe ANN index. 10 | #[pyclass] 11 | pub struct ThreadSafeAnnIndex { 12 | inner: Arc>, 13 | } 14 | 15 | #[pymethods] 16 | impl ThreadSafeAnnIndex { 17 | #[new] 18 | pub fn new(dim: usize, metric: Distance) -> PyResult { 19 | let idx = AnnIndex::new(dim, metric)?; 20 | Ok(ThreadSafeAnnIndex { inner: Arc::new(RwLock::new(idx)) }) 21 | } 22 | 23 | pub fn add(&self, py: Python, data: PyReadonlyArray2, ids: PyReadonlyArray1) 24 | -> PyResult<()> 25 | { 26 | // Acquire write lock then call under GIL 27 | let mut guard = self.inner.write().unwrap(); 28 | guard.add(py, data, ids) 29 | } 30 | 31 | pub fn remove(&self, _py: Python, ids: Vec) -> PyResult<()> { 32 | let mut guard = self.inner.write().unwrap(); 33 | guard.remove(ids) 34 | } 35 | 36 | pub fn search(&self, py: Python, query: PyReadonlyArray1, k: usize) 37 | -> PyResult<(PyObject, PyObject)> 38 | { 39 | let guard = self.inner.read().unwrap(); 40 | guard.search(py, query, k) 41 | } 42 | 43 | pub fn search_batch(&self, py: Python, data: PyReadonlyArray2, k: usize) 44 | -> PyResult<(PyObject, PyObject)> 45 | { 46 | let guard = self.inner.read().unwrap(); 47 | guard.search_batch(py, data, k) 48 | } 49 | 50 | pub fn save(&self, _py: Python, path: &str) -> PyResult<()> { 51 | let guard = self.inner.read().unwrap(); 52 | guard.save(path) 53 | } 54 | 55 | #[staticmethod] 56 | pub fn load(_py: Python, path: &str) -> PyResult { 57 | let idx = AnnIndex::load(path)?; 58 | Ok(ThreadSafeAnnIndex { inner: Arc::new(RwLock::new(idx)) }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## 📋 Changelog Structure 4 | 5 | 6 | Under each version, group entries into categories: 7 | 8 | * **Added** for new features 9 | * **Changed** for updates to existing behavior 10 | * **Deprecated** for soon-to-be-removed features 11 | * **Removed** for now-removed features 12 | * **Fixed** for bug fixes 13 | * **Security** for vulnerability patches 14 | 15 | --- 16 | 17 | 18 | 19 | 20 | # Changelog 21 | 22 | All notable changes to this project will be documented in this file. 23 | 24 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 25 | and this project adheres to [Semantic Versioning](https://semver.org). 26 | 27 | ## [Unreleased] 28 | 29 | ### Added 30 | - Support for Manhattan (L1) distance in `Distance.MANHATTAN`. 31 | - New `remove(ids: List[int])` method to delete entries by ID. 32 | - GIL-release in `search()` and `search_batch()` for lower Python-side latency. 33 | 34 | ### Changed 35 | - Bumped `rust_annie` version to **0.1.1**. 36 | 37 | ## [0.1.1] – 2025-05-20 38 | 39 | ### Added 40 | - Manhattan (L1) distance support: 41 | ```python 42 | from rust_annie import Distance 43 | idx = AnnIndex(16, Distance.MANHATTAN) 44 | 45 | 46 | * `Distance.MANHATTAN` class attribute and `__repr__` value. 47 | 48 | ### Fixed 49 | 50 | * Correctly annotate `.collect::>()` in batch search. 51 | * Removed `into_pyerr()` misuse in `search()`. 52 | 53 | ## \[0.1.0] – 2025-05-16 54 | 55 | ### Added 56 | 57 | * Initial release with Euclidean (L2) and Cosine distances. 58 | * `AnnIndex`, `search()`, `search_batch()`, `add()`, `save()`, `load()` APIs. 59 | * SIMD‐free brute-force search accelerated by **Rayon**. 60 | * Thread-safe wrapper `ThreadSafeAnnIndex` with GIL release. 61 | 62 | ### Changed 63 | 64 | * Logging improvements in CI workflow. 65 | * Performance optimizations: cached norms, GIL release, parallel loops. 66 | 67 | ### Fixed 68 | 69 | * Various build errors on Windows and macOS. 70 | 71 | ## \[0.0.1] – 2025-05-10 72 | 73 | ### Added 74 | 75 | * Prototype implementation of brute-force k-NN index in Rust. 76 | this in place, anyone browsing your repo or reading release notes on PyPI will immediately see what changed in each version. 77 | 78 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | # This file is autogenerated by maturin v1.8.4 2 | # To update, run 3 | # 4 | # maturin generate-ci github 5 | # 6 | name: CI 7 | 8 | on: 9 | push: 10 | branches: 11 | - main 12 | - master 13 | tags: 14 | - '*' 15 | pull_request: 16 | workflow_dispatch: 17 | 18 | permissions: 19 | contents: read 20 | 21 | jobs: 22 | linux: 23 | runs-on: ${{ matrix.platform.runner }} 24 | strategy: 25 | matrix: 26 | platform: 27 | - runner: ubuntu-22.04 28 | target: x86_64 29 | - runner: ubuntu-22.04 30 | target: x86 31 | - runner: ubuntu-22.04 32 | target: aarch64 33 | - runner: ubuntu-22.04 34 | target: armv7 35 | - runner: ubuntu-22.04 36 | target: s390x 37 | - runner: ubuntu-22.04 38 | target: ppc64le 39 | steps: 40 | - uses: actions/checkout@v4 41 | - uses: actions/setup-python@v5 42 | with: 43 | python-version: 3.x 44 | - name: Build wheels 45 | uses: PyO3/maturin-action@v1 46 | with: 47 | target: ${{ matrix.platform.target }} 48 | args: --release --out dist --find-interpreter 49 | sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} 50 | manylinux: auto 51 | - name: Upload wheels 52 | uses: actions/upload-artifact@v4 53 | with: 54 | name: wheels-linux-${{ matrix.platform.target }} 55 | path: dist 56 | 57 | musllinux: 58 | runs-on: ${{ matrix.platform.runner }} 59 | strategy: 60 | matrix: 61 | platform: 62 | - runner: ubuntu-22.04 63 | target: x86_64 64 | - runner: ubuntu-22.04 65 | target: x86 66 | - runner: ubuntu-22.04 67 | target: aarch64 68 | - runner: ubuntu-22.04 69 | target: armv7 70 | steps: 71 | - uses: actions/checkout@v4 72 | - uses: actions/setup-python@v5 73 | with: 74 | python-version: 3.x 75 | - name: Build wheels 76 | uses: PyO3/maturin-action@v1 77 | with: 78 | target: ${{ matrix.platform.target }} 79 | args: --release --out dist --find-interpreter 80 | sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} 81 | manylinux: musllinux_1_2 82 | - name: Upload wheels 83 | uses: actions/upload-artifact@v4 84 | with: 85 | name: wheels-musllinux-${{ matrix.platform.target }} 86 | path: dist 87 | 88 | windows: 89 | runs-on: ${{ matrix.platform.runner }} 90 | strategy: 91 | matrix: 92 | platform: 93 | - runner: windows-latest 94 | target: x64 95 | - runner: windows-latest 96 | target: x86 97 | steps: 98 | - uses: actions/checkout@v4 99 | - uses: actions/setup-python@v5 100 | with: 101 | python-version: 3.x 102 | architecture: ${{ matrix.platform.target }} 103 | - name: Build wheels 104 | uses: PyO3/maturin-action@v1 105 | with: 106 | target: ${{ matrix.platform.target }} 107 | args: --release --out dist --find-interpreter 108 | sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} 109 | - name: Upload wheels 110 | uses: actions/upload-artifact@v4 111 | with: 112 | name: wheels-windows-${{ matrix.platform.target }} 113 | path: dist 114 | 115 | macos: 116 | runs-on: ${{ matrix.platform.runner }} 117 | strategy: 118 | matrix: 119 | platform: 120 | - runner: macos-13 121 | target: x86_64 122 | - runner: macos-14 123 | target: aarch64 124 | steps: 125 | - uses: actions/checkout@v4 126 | - uses: actions/setup-python@v5 127 | with: 128 | python-version: 3.x 129 | - name: Build wheels 130 | uses: PyO3/maturin-action@v1 131 | with: 132 | target: ${{ matrix.platform.target }} 133 | args: --release --out dist --find-interpreter 134 | sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} 135 | - name: Upload wheels 136 | uses: actions/upload-artifact@v4 137 | with: 138 | name: wheels-macos-${{ matrix.platform.target }} 139 | path: dist 140 | 141 | sdist: 142 | runs-on: ubuntu-latest 143 | steps: 144 | - uses: actions/checkout@v4 145 | - name: Build sdist 146 | uses: PyO3/maturin-action@v1 147 | with: 148 | command: sdist 149 | args: --out dist 150 | - name: Upload sdist 151 | uses: actions/upload-artifact@v4 152 | with: 153 | name: wheels-sdist 154 | path: dist 155 | 156 | release: 157 | name: Release 158 | runs-on: ubuntu-latest 159 | if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }} 160 | needs: [linux, musllinux, windows, macos, sdist] 161 | permissions: 162 | # Use to sign the release artifacts 163 | id-token: write 164 | # Used to upload release artifacts 165 | contents: write 166 | # Used to generate artifact attestation 167 | attestations: write 168 | steps: 169 | - uses: actions/download-artifact@v4 170 | - name: Generate artifact attestation 171 | uses: actions/attest-build-provenance@v2 172 | with: 173 | subject-path: 'wheels-*/*' 174 | - name: Publish to PyPI 175 | if: ${{ startsWith(github.ref, 'refs/tags/') }} 176 | uses: PyO3/maturin-action@v1 177 | env: 178 | MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} 179 | with: 180 | command: upload 181 | args: --non-interactive --skip-existing wheels-*/* 182 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rust-annie 2 | 3 | ![Annie](https://github.com/Programmers-Paradise/.github/blob/main/ChatGPT%20Image%20May%2015,%202025,%2003_58_16%20PM.png?raw=true) 4 | 5 | [![PyPI](https://img.shields.io/pypi/v/rust-annie.svg)](https://pypi.org/project/rust-annie) 6 | 7 | 8 | A lightning-fast, Rust-powered brute-force k-NN library for Python, with optional batch queries, thread-safety, and on-disk persistence. 9 | 10 | --- 11 | 12 | ## 📝 Table of Contents 13 | 14 | 1. [Features](#features) 15 | 2. [Installation](#installation) 16 | 3. [Quick Start](#quick-start) 17 | 4. [Examples](#examples) 18 | - [Single Query](#single-query) 19 | - [Batch Query](#batch-query) 20 | - [Thread-Safe Usage](#thread-safe-usage) 21 | 5. [Benchmark Results](#benchmark-results) 22 | 6. [API Reference](#api-reference) 23 | 7. [Development & CI](#development--ci) 24 | 8. [Roadmap](#roadmap) 25 | 9. [Contributing](#contributing) 26 | 10. [License](#license) 27 | 28 | --- 29 | 30 | ## 🚀 Features 31 | 32 | - **Ultra-fast brute-force** k-NN search (Euclidean , Cosine, Manhattan) 33 | - **Batch** queries over multiple vectors 34 | - **Thread-safe** wrapper with GIL release for true concurrency 35 | - **Zero-copy** NumPy integration (via PyO3 & rust-numpy) 36 | - **On-disk** persistence with bincode + serde 37 | - **Multi-platform** wheels (manylinux, musllinux, Windows, macOS) 38 | - **Automated CI** with correctness & performance checks 39 | 40 | --- 41 | 42 | ## ⚙️ Installation 43 | 44 | ```bash 45 | # Stable release from PyPI: 46 | pip install rust-annie 47 | 48 | # Or install from source (requires Rust toolchain + maturin): 49 | git clone https://github.com/yourusername/rust_annie.git 50 | cd rust_annie 51 | pip install maturin 52 | maturin develop --release 53 | ``` 54 | 55 | 56 | 57 | 58 | ## 🎉 Quick Start 59 | 60 | ```python 61 | import numpy as np 62 | from rust_annie import AnnIndex, Distance 63 | 64 | # Create an 8-dim Euclidean index 65 | idx = AnnIndex(8, Distance.EUCLIDEAN) 66 | 67 | # Add 100 random vectors 68 | data = np.random.rand(100, 8).astype(np.float32) 69 | ids = np.arange(100, dtype=np.int64) 70 | idx.add(data, ids) 71 | 72 | # Query one vector 73 | labels, dists = idx.search(data[0], k=5) 74 | print("Nearest IDs:", labels) 75 | print("Distances :", dists) 76 | ``` 77 | 78 | --- 79 | 80 | ## 📚 Examples 81 | 82 | ### Single Query 83 | 84 | ```python 85 | from rust_annie import AnnIndex, Distance 86 | import numpy as np 87 | 88 | idx = AnnIndex(4, Distance.COSINE) 89 | data = np.random.rand(50, 4).astype(np.float32) 90 | ids = np.arange(50, dtype=np.int64) 91 | idx.add(data, ids) 92 | 93 | labels, dists = idx.search(data[10], k=3) 94 | print(labels, dists) 95 | ``` 96 | 97 | ### Batch Query 98 | 99 | ```python 100 | from rust_annie import AnnIndex, Distance 101 | import numpy as np 102 | 103 | idx = AnnIndex(16, Distance.EUCLIDEAN) 104 | data = np.random.rand(1000, 16).astype(np.float32) 105 | ids = np.arange(1000, dtype=np.int64) 106 | idx.add(data, ids) 107 | 108 | # Query 32 vectors at once: 109 | queries = data[:32] 110 | labels_batch, dists_batch = idx.search_batch(queries, k=10) 111 | print(labels_batch.shape) # (32, 10) 112 | ``` 113 | 114 | ### Thread-Safe Usage 115 | 116 | ```python 117 | from rust_annie import ThreadSafeAnnIndex, Distance 118 | import numpy as np 119 | from concurrent.futures import ThreadPoolExecutor 120 | 121 | idx = ThreadSafeAnnIndex(32, Distance.EUCLIDEAN) 122 | data = np.random.rand(500, 32).astype(np.float32) 123 | ids = np.arange(500, dtype=np.int64) 124 | idx.add(data, ids) 125 | 126 | def task(q): 127 | return idx.search(q, k=5) 128 | 129 | with ThreadPoolExecutor(max_workers=8) as executor: 130 | futures = [executor.submit(task, data[i]) for i in range(8)] 131 | for f in futures: 132 | print(f.result()) 133 | ``` 134 | 135 | --- 136 | 137 | ## 📈 Benchmark Results 138 | 139 | Measured on a 6-core CPU: 140 | 141 | | Mode | Per-query Time | 142 | | -------------------------------- | -------------: | 143 | | Pure-Python (NumPy - 𝑙2) | \~2.8 ms | 144 | | Rust AnnIndex single query | \~0.7 ms | 145 | | Rust AnnIndex batch (64 queries) | \~0.23 ms | 146 | 147 | That’s a \~4× speedup vs. NumPy! 148 | 149 | --- 150 | 151 | ## 📖 API Reference 152 | 153 | ### `rust_annie.AnnIndex(dim: int, metric: Distance)` 154 | 155 | Create a new brute-force index. 156 | 157 | ### Methods 158 | 159 | * `add(data: np.ndarray[N×D], ids: np.ndarray[N]) -> None` 160 | * `search(query: np.ndarray[D], k: int) -> (ids: np.ndarray[k], dists: np.ndarray[k])` 161 | * `search_batch(data: np.ndarray[N×D], k: int) -> (ids: np.ndarray[N×k], dists: np.ndarray[N×k])` 162 | * `remove(ids: Sequence[int]) -> None` 163 | * `save(path: str) -> None` 164 | * `load(path: str) -> AnnIndex` (static) 165 | 166 | ### `rust_annie.Distance` 167 | 168 | Enum: `Distance.EUCLIDEAN`, `Distance.COSINE`.`Distance.MANHATTAN` 169 | 170 | ### `rust_annie.ThreadSafeAnnIndex` 171 | 172 | Same API as `AnnIndex`, safe for concurrent use. 173 | 174 | --- 175 | 176 | ## 🔧 Development & CI 177 | 178 | **CI** runs on GitHub Actions, building wheels on Linux, Windows, macOS, plus: 179 | 180 | * `cargo test` 181 | * `pytest` 182 | * `benchmark.py` & `batch_benchmark.py` 183 | 184 | ```bash 185 | # Locally run tests & benchmarks 186 | cargo test 187 | pytest 188 | python benchmark.py 189 | python batch_benchmark.py 190 | ``` 191 | 192 | --- 193 | 194 | ## 🚧 Roadmap 195 | 196 | * [x] SIMD-accelerated dot products 197 | * [x] Rayon parallelism & GIL release 198 | * [ ] Integrate HNSW/FAISS for sub-ms ANN at scale 199 | * [ ] GPU‐backed search (CUDA/ROCm) 200 | * [ ] Richer Python docs & type hints 201 | 202 | --- 203 | 204 | ## 🤝 Contributing 205 | 206 | Contributions are welcome! Please: 207 | 208 | 1. Fork the repo 209 | 2. Create a feature branch 210 | 3. Add tests & docs 211 | 4. Submit a Pull Request 212 | 213 | See [CONTRIBUTING.md](./CONTRIBUTING.md) for details. 214 | 215 | --- 216 | 217 | ## 📜 License 218 | 219 | This project is licensed under the **MIT License**. See [LICENSE](./LICENSE) for details. 220 | 221 | ``` 222 | ``` 223 | -------------------------------------------------------------------------------- /src/index.rs: -------------------------------------------------------------------------------- 1 | // src/index.rs 2 | 3 | use pyo3::prelude::*; 4 | use numpy::{PyReadonlyArray1, PyReadonlyArray2, IntoPyArray}; 5 | use ndarray::Array2; 6 | use rayon::prelude::*; 7 | use serde::{Serialize, Deserialize}; 8 | 9 | use crate::storage::{save_index, load_index}; 10 | use crate::metrics::Distance; 11 | use crate::errors::RustAnnError; 12 | 13 | /// A brute-force k-NN index with cached norms, Rayon parallelism, 14 | /// and support for L1, L2, Cosine, Chebyshev, and Minkowski-p distances. 15 | #[pyclass] 16 | #[derive(Serialize, Deserialize)] 17 | pub struct AnnIndex { 18 | dim: usize, 19 | metric: Distance, 20 | /// If Some(p), use Minkowski-p distance instead of `metric`. 21 | minkowski_p: Option, 22 | /// Stored entries as (id, vector, squared_norm) tuples. 23 | entries: Vec<(i64, Vec, f32)>, 24 | } 25 | 26 | #[pymethods] 27 | impl AnnIndex { 28 | /// Create a new index for unit-variant metrics (Euclidean, Cosine, Manhattan, Chebyshev). 29 | #[new] 30 | pub fn new(dim: usize, metric: Distance) -> PyResult { 31 | if dim == 0 { 32 | return Err(RustAnnError::py_err("Dimension must be > 0")); 33 | } 34 | Ok(AnnIndex { 35 | dim, 36 | metric, 37 | minkowski_p: None, 38 | entries: Vec::new(), 39 | }) 40 | } 41 | 42 | /// Create a new index using Minkowski-p distance (p > 0). 43 | #[staticmethod] 44 | pub fn new_minkowski(dim: usize, p: f32) -> PyResult { 45 | if dim == 0 { 46 | return Err(RustAnnError::py_err("Dimension must be > 0")); 47 | } 48 | if p <= 0.0 { 49 | return Err(RustAnnError::py_err("`p` must be > 0 for Minkowski distance")); 50 | } 51 | Ok(AnnIndex { 52 | dim, 53 | metric: Distance::Euclidean, // placeholder 54 | minkowski_p: Some(p), 55 | entries: Vec::new(), 56 | }) 57 | } 58 | 59 | /// Add a batch of vectors (shape: N×dim) with integer IDs. 60 | pub fn add( 61 | &mut self, 62 | _py: Python, 63 | data: PyReadonlyArray2, 64 | ids: PyReadonlyArray1, 65 | ) -> PyResult<()> { 66 | let view = data.as_array(); 67 | let ids = ids.as_slice()?; 68 | if view.nrows() != ids.len() { 69 | return Err(RustAnnError::py_err("`data` and `ids` must have same length")); 70 | } 71 | for (row, &id) in view.outer_iter().zip(ids) { 72 | let v = row.to_vec(); 73 | if v.len() != self.dim { 74 | return Err(RustAnnError::py_err(format!( 75 | "Expected dimension {}, got {}", self.dim, v.len() 76 | ))); 77 | } 78 | let sq_norm = v.iter().map(|x| x * x).sum::(); 79 | self.entries.push((id, v, sq_norm)); 80 | } 81 | Ok(()) 82 | } 83 | 84 | /// Remove entries whose IDs appear in `ids`. 85 | pub fn remove(&mut self, ids: Vec) -> PyResult<()> { 86 | if !ids.is_empty() { 87 | let to_rm: std::collections::HashSet = ids.into_iter().collect(); 88 | self.entries.retain(|(id, _, _)| !to_rm.contains(id)); 89 | } 90 | Ok(()) 91 | } 92 | 93 | /// Search the k nearest neighbors for a single query vector. 94 | pub fn search( 95 | &self, 96 | py: Python, 97 | query: PyReadonlyArray1, 98 | k: usize, 99 | ) -> PyResult<(PyObject, PyObject)> { 100 | let q = query.as_slice()?; 101 | let q_sq = q.iter().map(|x| x * x).sum::(); 102 | 103 | // Release the GIL for the heavy compute: 104 | let result: PyResult<(Vec, Vec)> = py.allow_threads(|| { 105 | self.inner_search(q, q_sq, k) 106 | }); 107 | let (ids, dists) = result?; 108 | 109 | Ok(( 110 | ids.into_pyarray(py).to_object(py), 111 | dists.into_pyarray(py).to_object(py), 112 | )) 113 | } 114 | 115 | /// Batch-search k nearest neighbors for each row in an (N×dim) array. 116 | pub fn search_batch( 117 | &self, 118 | py: Python, 119 | data: PyReadonlyArray2, 120 | k: usize, 121 | ) -> PyResult<(PyObject, PyObject)> { 122 | let arr = data.as_array(); 123 | let n = arr.nrows(); 124 | 125 | // Release the GIL around the parallel batch: 126 | let results: Vec<(Vec, Vec)> = py.allow_threads(|| { 127 | (0..n) 128 | .into_par_iter() 129 | .map(|i| { 130 | let row = arr.row(i); 131 | let q: Vec = row.to_vec(); 132 | let q_sq = q.iter().map(|x| x * x).sum::(); 133 | // safe unwrap: dims validated 134 | self.inner_search(&q, q_sq, k).unwrap() 135 | }) 136 | .collect::>() 137 | }); 138 | 139 | // Flatten the results 140 | let mut all_ids = Vec::with_capacity(n * k); 141 | let mut all_dists = Vec::with_capacity(n * k); 142 | for (ids, dists) in results { 143 | all_ids.extend(ids); 144 | all_dists.extend(dists); 145 | } 146 | 147 | // Build (n × k) ndarrays 148 | let ids_arr: Array2 = Array2::from_shape_vec((n, k), all_ids) 149 | .map_err(|e| RustAnnError::py_err(format!("Reshape ids failed: {}", e)))?; 150 | let dists_arr: Array2 = Array2::from_shape_vec((n, k), all_dists) 151 | .map_err(|e| RustAnnError::py_err(format!("Reshape dists failed: {}", e)))?; 152 | 153 | Ok(( 154 | ids_arr.into_pyarray(py).to_object(py), 155 | dists_arr.into_pyarray(py).to_object(py), 156 | )) 157 | } 158 | 159 | /// Save index to `.bin`. 160 | pub fn save(&self, path: &str) -> PyResult<()> { 161 | let full = format!("{}.bin", path); 162 | save_index(self, &full).map_err(|e| e.into_pyerr()) 163 | } 164 | 165 | /// Load index from `.bin`. 166 | #[staticmethod] 167 | pub fn load(path: &str) -> PyResult { 168 | let full = format!("{}.bin", path); 169 | load_index(&full).map_err(|e| e.into_pyerr()) 170 | } 171 | } 172 | 173 | impl AnnIndex { 174 | /// Core search logic covering L2, Cosine, L1 (Manhattan), L∞ (Chebyshev), and Lₚ. 175 | fn inner_search(&self, q: &[f32], q_sq: f32, k: usize) -> PyResult<(Vec, Vec)> { 176 | if q.len() != self.dim { 177 | return Err(RustAnnError::py_err(format!( 178 | "Expected dimension {}, got {}", self.dim, q.len() 179 | ))); 180 | } 181 | 182 | let p_opt = self.minkowski_p; 183 | let mut results: Vec<(i64, f32)> = self.entries 184 | .par_iter() 185 | .map(|(id, vec, vec_sq)| { 186 | // dot only used by L2/Cosine 187 | let dot = vec.iter().zip(q.iter()).map(|(x, y)| x * y).sum::(); 188 | 189 | let dist = if let Some(p) = p_opt { 190 | // Minkowski-p: (∑ |x-y|^p)^(1/p) 191 | let sum_p = vec.iter().zip(q.iter()) 192 | .map(|(x, y)| (x - y).abs().powf(p)) 193 | .sum::(); 194 | sum_p.powf(1.0 / p) 195 | } else { 196 | match self.metric { 197 | Distance::Euclidean => ((vec_sq + q_sq - 2.0 * dot).max(0.0)).sqrt(), 198 | Distance::Cosine => { 199 | let denom = vec_sq.sqrt().max(1e-12) * q_sq.sqrt().max(1e-12); 200 | (1.0 - (dot / denom)).max(0.0) 201 | } 202 | Distance::Manhattan => vec.iter().zip(q.iter()) 203 | .map(|(x, y)| (x - y).abs()) 204 | .sum::(), 205 | Distance::Chebyshev => vec.iter().zip(q.iter()) 206 | .map(|(x, y)| (x - y).abs()) 207 | .fold(0.0, f32::max), 208 | } 209 | }; 210 | 211 | (*id, dist) 212 | }) 213 | .collect(); 214 | 215 | // Sort ascending by distance and keep top-k 216 | results.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap()); 217 | results.truncate(k); 218 | 219 | // Split into IDs and distances 220 | let ids = results.iter().map(|(i, _)| *i).collect(); 221 | let dists = results.iter().map(|(_, d)| *d).collect(); 222 | Ok((ids, dists)) 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "autocfg" 7 | version = "1.4.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 10 | 11 | [[package]] 12 | name = "bincode" 13 | version = "1.3.3" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" 16 | dependencies = [ 17 | "serde", 18 | ] 19 | 20 | [[package]] 21 | name = "bitflags" 22 | version = "2.9.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 25 | 26 | [[package]] 27 | name = "cfg-if" 28 | version = "1.0.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 31 | 32 | [[package]] 33 | name = "crossbeam-deque" 34 | version = "0.8.6" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 37 | dependencies = [ 38 | "crossbeam-epoch", 39 | "crossbeam-utils", 40 | ] 41 | 42 | [[package]] 43 | name = "crossbeam-epoch" 44 | version = "0.9.18" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 47 | dependencies = [ 48 | "crossbeam-utils", 49 | ] 50 | 51 | [[package]] 52 | name = "crossbeam-utils" 53 | version = "0.8.21" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 56 | 57 | [[package]] 58 | name = "either" 59 | version = "1.15.0" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 62 | 63 | [[package]] 64 | name = "indoc" 65 | version = "1.0.9" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" 68 | 69 | [[package]] 70 | name = "libc" 71 | version = "0.2.172" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 74 | 75 | [[package]] 76 | name = "lock_api" 77 | version = "0.4.12" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 80 | dependencies = [ 81 | "autocfg", 82 | "scopeguard", 83 | ] 84 | 85 | [[package]] 86 | name = "matrixmultiply" 87 | version = "0.3.10" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" 90 | dependencies = [ 91 | "autocfg", 92 | "rawpointer", 93 | ] 94 | 95 | [[package]] 96 | name = "memoffset" 97 | version = "0.8.0" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" 100 | dependencies = [ 101 | "autocfg", 102 | ] 103 | 104 | [[package]] 105 | name = "ndarray" 106 | version = "0.15.6" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" 109 | dependencies = [ 110 | "matrixmultiply", 111 | "num-complex", 112 | "num-integer", 113 | "num-traits", 114 | "rawpointer", 115 | ] 116 | 117 | [[package]] 118 | name = "num-complex" 119 | version = "0.4.6" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" 122 | dependencies = [ 123 | "num-traits", 124 | ] 125 | 126 | [[package]] 127 | name = "num-integer" 128 | version = "0.1.46" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 131 | dependencies = [ 132 | "num-traits", 133 | ] 134 | 135 | [[package]] 136 | name = "num-traits" 137 | version = "0.2.19" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 140 | dependencies = [ 141 | "autocfg", 142 | ] 143 | 144 | [[package]] 145 | name = "numpy" 146 | version = "0.18.0" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "96b0fee4571867d318651c24f4a570c3f18408cf95f16ccb576b3ce85496a46e" 149 | dependencies = [ 150 | "libc", 151 | "ndarray", 152 | "num-complex", 153 | "num-integer", 154 | "num-traits", 155 | "pyo3", 156 | "rustc-hash", 157 | ] 158 | 159 | [[package]] 160 | name = "once_cell" 161 | version = "1.21.3" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 164 | 165 | [[package]] 166 | name = "parking_lot" 167 | version = "0.12.3" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 170 | dependencies = [ 171 | "lock_api", 172 | "parking_lot_core", 173 | ] 174 | 175 | [[package]] 176 | name = "parking_lot_core" 177 | version = "0.9.10" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 180 | dependencies = [ 181 | "cfg-if", 182 | "libc", 183 | "redox_syscall", 184 | "smallvec", 185 | "windows-targets", 186 | ] 187 | 188 | [[package]] 189 | name = "proc-macro2" 190 | version = "1.0.95" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 193 | dependencies = [ 194 | "unicode-ident", 195 | ] 196 | 197 | [[package]] 198 | name = "pyo3" 199 | version = "0.18.3" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "e3b1ac5b3731ba34fdaa9785f8d74d17448cd18f30cf19e0c7e7b1fdb5272109" 202 | dependencies = [ 203 | "cfg-if", 204 | "indoc", 205 | "libc", 206 | "memoffset", 207 | "parking_lot", 208 | "pyo3-build-config", 209 | "pyo3-ffi", 210 | "pyo3-macros", 211 | "unindent", 212 | ] 213 | 214 | [[package]] 215 | name = "pyo3-build-config" 216 | version = "0.18.3" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "9cb946f5ac61bb61a5014924910d936ebd2b23b705f7a4a3c40b05c720b079a3" 219 | dependencies = [ 220 | "once_cell", 221 | "target-lexicon", 222 | ] 223 | 224 | [[package]] 225 | name = "pyo3-ffi" 226 | version = "0.18.3" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "fd4d7c5337821916ea2a1d21d1092e8443cf34879e53a0ac653fbb98f44ff65c" 229 | dependencies = [ 230 | "libc", 231 | "pyo3-build-config", 232 | ] 233 | 234 | [[package]] 235 | name = "pyo3-macros" 236 | version = "0.18.3" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "a9d39c55dab3fc5a4b25bbd1ac10a2da452c4aca13bb450f22818a002e29648d" 239 | dependencies = [ 240 | "proc-macro2", 241 | "pyo3-macros-backend", 242 | "quote", 243 | "syn 1.0.109", 244 | ] 245 | 246 | [[package]] 247 | name = "pyo3-macros-backend" 248 | version = "0.18.3" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "97daff08a4c48320587b5224cc98d609e3c27b6d437315bd40b605c98eeb5918" 251 | dependencies = [ 252 | "proc-macro2", 253 | "quote", 254 | "syn 1.0.109", 255 | ] 256 | 257 | [[package]] 258 | name = "quote" 259 | version = "1.0.40" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 262 | dependencies = [ 263 | "proc-macro2", 264 | ] 265 | 266 | [[package]] 267 | name = "rawpointer" 268 | version = "0.2.1" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" 271 | 272 | [[package]] 273 | name = "rayon" 274 | version = "1.10.0" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 277 | dependencies = [ 278 | "either", 279 | "rayon-core", 280 | ] 281 | 282 | [[package]] 283 | name = "rayon-core" 284 | version = "1.12.1" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 287 | dependencies = [ 288 | "crossbeam-deque", 289 | "crossbeam-utils", 290 | ] 291 | 292 | [[package]] 293 | name = "redox_syscall" 294 | version = "0.5.12" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" 297 | dependencies = [ 298 | "bitflags", 299 | ] 300 | 301 | [[package]] 302 | name = "rust_annie" 303 | version = "0.1.2" 304 | dependencies = [ 305 | "bincode", 306 | "ndarray", 307 | "numpy", 308 | "pyo3", 309 | "rayon", 310 | "serde", 311 | ] 312 | 313 | [[package]] 314 | name = "rustc-hash" 315 | version = "1.1.0" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 318 | 319 | [[package]] 320 | name = "scopeguard" 321 | version = "1.2.0" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 324 | 325 | [[package]] 326 | name = "serde" 327 | version = "1.0.219" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 330 | dependencies = [ 331 | "serde_derive", 332 | ] 333 | 334 | [[package]] 335 | name = "serde_derive" 336 | version = "1.0.219" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 339 | dependencies = [ 340 | "proc-macro2", 341 | "quote", 342 | "syn 2.0.101", 343 | ] 344 | 345 | [[package]] 346 | name = "smallvec" 347 | version = "1.15.0" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 350 | 351 | [[package]] 352 | name = "syn" 353 | version = "1.0.109" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 356 | dependencies = [ 357 | "proc-macro2", 358 | "quote", 359 | "unicode-ident", 360 | ] 361 | 362 | [[package]] 363 | name = "syn" 364 | version = "2.0.101" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 367 | dependencies = [ 368 | "proc-macro2", 369 | "quote", 370 | "unicode-ident", 371 | ] 372 | 373 | [[package]] 374 | name = "target-lexicon" 375 | version = "0.12.16" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" 378 | 379 | [[package]] 380 | name = "unicode-ident" 381 | version = "1.0.18" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 384 | 385 | [[package]] 386 | name = "unindent" 387 | version = "0.1.11" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c" 390 | 391 | [[package]] 392 | name = "windows-targets" 393 | version = "0.52.6" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 396 | dependencies = [ 397 | "windows_aarch64_gnullvm", 398 | "windows_aarch64_msvc", 399 | "windows_i686_gnu", 400 | "windows_i686_gnullvm", 401 | "windows_i686_msvc", 402 | "windows_x86_64_gnu", 403 | "windows_x86_64_gnullvm", 404 | "windows_x86_64_msvc", 405 | ] 406 | 407 | [[package]] 408 | name = "windows_aarch64_gnullvm" 409 | version = "0.52.6" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 412 | 413 | [[package]] 414 | name = "windows_aarch64_msvc" 415 | version = "0.52.6" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 418 | 419 | [[package]] 420 | name = "windows_i686_gnu" 421 | version = "0.52.6" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 424 | 425 | [[package]] 426 | name = "windows_i686_gnullvm" 427 | version = "0.52.6" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 430 | 431 | [[package]] 432 | name = "windows_i686_msvc" 433 | version = "0.52.6" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 436 | 437 | [[package]] 438 | name = "windows_x86_64_gnu" 439 | version = "0.52.6" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 442 | 443 | [[package]] 444 | name = "windows_x86_64_gnullvm" 445 | version = "0.52.6" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 448 | 449 | [[package]] 450 | name = "windows_x86_64_msvc" 451 | version = "0.52.6" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 454 | --------------------------------------------------------------------------------