<h1 align="center">semstore</h1>
<p align="center">
Local semantic search for Rust applications — store text, search by meaning, no cloud required.
</p>
<p align="center">
<a href="https://github.com/rustkit-ai/semstore/actions/workflows/ci.yml"><img src="https://github.com/rustkit-ai/semstore/actions/workflows/ci.yml/badge.svg" alt="CI"/></a>
<a href="https://crates.io/crates/semstore"><img src="https://img.shields.io/crates/v/semstore.svg" alt="crates.io"/></a>
<a href="https://docs.rs/semstore"><img src="https://docs.rs/semstore/badge.svg" alt="docs.rs"/></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT"/></a>
</p>
---
**Semantic search in Rust applications is harder than it should be.** You either wire up a cloud embedding API (latency, cost, data leaving your machine), run a separate vector database process, or write the plumbing yourself. None of those are reasonable for a library dependency.
`semstore` is a self-contained semantic index you embed directly in your Rust binary. One struct, four methods, zero infrastructure.
```rust
let mut idx = SemanticIndex::open("./index.db")?;
idx.insert("Rust ownership prevents data races at compile time", json!({ "lang": "rust" }))?;
idx.insert("Python uses reference counting for memory management", json!({ "lang": "python" }))?;
let results = idx.search("memory safety", 5)?;
// [0.87] Rust ownership prevents data races at compile time
// [0.74] Python uses reference counting for memory management
```
**No API key. No server. No Python.** The BGE-Small model (~23 MB) runs on CPU via ONNX and is cached locally after the first use.
---
## Install
```toml
[dependencies]
semstore = "0.1"
serde_json = "1"
```
The BGE-Small model (~23 MB) is downloaded from HuggingFace on first use and cached locally.
### Feature flags
| `default-embedder` | ✓ | Bundles BGE-Small-EN-v1.5 via fastembed |
| `bundled-sqlite` | ✓ | Statically links SQLite (no system library required) |
Bring your own embedder by disabling `default-embedder`:
```toml
semstore = { version = "0.1", default-features = false, features = ["bundled-sqlite"] }
```
---
## What's inside
- **BGE-Small-EN-v1.5** — 23 MB ONNX embedding model, runs on CPU
- **HNSW** — approximate nearest-neighbour search via [usearch](https://github.com/unum-cloud/usearch)
- **SQLite** — persistent storage for entries and embeddings (survives restarts)
| **RAG** | Retrieve relevant context before calling an LLM |
| **Semantic cache** | Avoid redundant LLM calls for similar questions |
| **Knowledge base** | Search docs, notes, code by meaning |
| **Deduplication** | Find near-duplicate entries in a dataset |
---
## Quick start
```rust
use semstore::SemanticIndex;
use serde_json::json;
fn main() -> semstore::Result<()> {
let mut idx = SemanticIndex::open("./index.db")?;
idx.insert("Rust ownership prevents data races at compile time",
json!({ "lang": "rust", "topic": "memory" }))?;
idx.insert("Python uses reference counting for memory management",
json!({ "lang": "python", "topic": "memory" }))?;
for r in idx.search("memory safety", 5)? {
println!("[{:.2}] {}", r.score, r.content);
}
Ok(())
}
```
```
[0.87] Rust ownership prevents data races at compile time
[0.74] Python uses reference counting for memory management
```
---
## RAG pattern
```rust
fn build_prompt(question: &str, idx: &SemanticIndex) -> semstore::Result<String> {
let context = idx.search(question, 3)?
.iter()
.map(|r| format!("- {}", r.content))
.collect::<Vec<_>>()
.join("\n");
Ok(format!("Context:\n{context}\n\nQuestion: {question}\nAnswer:"))
}
```
---
## Custom embedder
```rust
use semstore::{Embedder, Error, SemanticIndex};
struct OpenAiEmbedder { /* your HTTP client */ }
impl Embedder for OpenAiEmbedder {
fn embed(&self, text: &str) -> Result<Vec<f32>, Error> {
// POST to https://api.openai.com/v1/embeddings
todo!()
}
fn dimensions(&self) -> usize { 1536 } // text-embedding-3-small
}
let mut idx = SemanticIndex::builder()
.embedder(OpenAiEmbedder { /* … */ })
.path("./index.db")
.threshold(0.80)
.build()?;
```
---
## API
```rust
// Constructors
SemanticIndex::open("./index.db")? // persistent
SemanticIndex::in_memory()? // ephemeral (tests)
SemanticIndex::builder() // full configuration
.path("./index.db")
.threshold(0.75)
.embedder(my_embedder)
.build()?
// Write
idx.insert("content", json!({ "key": "value" }))? // → u64
idx.insert_batch([("a", json!({})), ("b", json!({}))])? // → Vec<u64>
idx.remove(id)? // → bool
// Read
idx.search("query", limit)? // → Vec<SearchResult> sorted by score
idx.len() // → usize
idx.stats()? // → Stats { total: u64 }
// SearchResult
r.id // u64
r.content // String
r.metadata // serde_json::Value
r.score // f32 in [0.0, 1.0]
```
---
## Performance
| First load (model init) | ~1–2 s |
| `embed()` single text | ~5 ms |
| `insert()` | ~6 ms |
| `search()` 10k entries | ~6 ms |
---
## Examples
```bash
cargo run --example basic # in-memory index
cargo run --example rag # RAG context building
cargo run --example custom_embedder # plug in your own model
```
---
## License
MIT — see [LICENSE](LICENSE).