<div align="center">
<img src="https://srotasspace.s3.ap-south-1.amazonaws.com/srotas.svg" alt="Searchez" width="100" />
</div>
# searchez
[](https://crates.io/crates/searchez)
[](https://docs.rs/searchez)
[](LICENSE)
> **A searchable-model layer for Rust.** Make a type searchable, keep its index
> in sync as records change, and search with real relevance ranking — over a
> pluggable backend, with a batteries-included in-memory engine that needs no
> external service.
Rust has excellent search **engines** (tantivy, meilisearch, opensearch) and,
until now, nothing above them. Every project rewrites the same three things: the
`to_document()` mapping, the "call the search client after every save", and a
one-off backfill binary. **searchez is that missing layer** — the Rust analog of
Ruby's Searchkick.
---
## Install
```toml
[dependencies]
searchez = "1.0.0"
```
## Quick start
```rust
use searchez::{Searchable, SearchEngine, MemoryBackend, Document, Query};
struct Product { id: u64, name: String, in_stock: bool }
impl Searchable for Product {
fn index_name() -> &'static str { "products" }
fn search_id(&self) -> String { self.id.to_string() }
fn to_document(&self) -> Document {
Document::new()
.field("name", self.name.clone())
.field("in_stock", self.in_stock)
}
}
# async fn demo() -> searchez::Result<()> {
let engine = SearchEngine::new(MemoryBackend::new());
engine.index(&Product { id: 1, name: "Dark Roast Coffee".into(), in_stock: true }).await?;
// full-text + filter, ranked by BM25
let hits = engine
.search::<Product>(Query::text("coffee").filter("in_stock", true))
.await?;
assert_eq!(hits[0].id, "1");
# Ok(()) }
```
## The three pieces
| **`Searchable`** | your model | declares its index, id, and `to_document()` |
| **`SearchEngine`** | the spine | `index` · `remove` · `reindex` · `search` |
| **`Backend`** | the engine | pluggable; ships with a BM25 `MemoryBackend` |
## Keeping the index in sync
Rust has no universal ORM callback, so sync points are **explicit** — and in
exchange the boilerplate is done for you:
```rust,ignore
// after creating or updating a record
engine.index(&product).await?;
// after deleting one
engine.remove(&product).await?; // or remove_id::<Product>("42")
// the backfill job: rebuild an index from every record
let n = engine.reindex(&all_products).await?;
```
## Search
```rust,ignore
// a bare string is a full-text query with sensible defaults
engine.search::<Product>("dark roast").await?;
// or build it up
let q = Query::text("dark roast")
.filter("in_stock", true) // exact-match filters, ANDed
.limit(10)
.offset(0);
engine.search::<Product>(q).await?;
// a pure filter query — everything matching, no text
engine.search::<Product>(Query::all().filter("in_stock", true)).await?;
```
Each [`Hit`] carries `id`, a BM25 `score`, and the stored `document`.
## Relevance is real
The in-memory backend ranks with **BM25** — the same algorithm Lucene,
Elasticsearch and Tantivy use — not a naive term count. A short, dense match
outranks a passing mention in a long body, and rare terms count for more than
common ones. Ties break by id, so paging is stable.
## Hydration — search → database records
A hit carries the indexed document, which is enough to display results. When you
need the **full** database record (columns or associations you didn't index),
search for the ids and load the rows, preserving rank order:
```rust,ignore
let ids = engine.search_ids::<Product>("dark roast").await?; // ranked
let mut rows = db.load_products(&ids).await?; // your DB call
// keep the search order (databases return rows in their own order)
let rank: std::collections::HashMap<_, _> =
ids.iter().enumerate().map(|(i, id)| (id.clone(), i)).collect();
## Backends
`MemoryBackend` (default) is in-memory and non-persistent — perfect for tests,
development, and small single-process datasets.
### Meilisearch (feature `meilisearch`)
A production backend backed by a [Meilisearch](https://www.meilisearch.com/)
server:
```toml
searchez = { version = "1.0.0", features = ["meilisearch"] }
```
```rust,ignore
use searchez::{SearchEngine, MeilisearchBackend};
let backend = MeilisearchBackend::new("http://localhost:7700", Some("masterKey"))?;
// Meilisearch only filters on fields declared filterable — do this once at setup:
backend.set_filterable_attributes("products", &["in_stock"]).await?;
let engine = SearchEngine::new(backend);
// ...everything else is identical to the in-memory example.
```
Two Meilisearch specifics searchez handles or surfaces:
- **Primary key** — searchez injects its id into each document under
`searchez_id` and declares it the primary key. Ids should therefore match
Meilisearch's `^[a-zA-Z0-9_-]+$` (avoid `/`, `.`, spaces).
- **Filterable fields** — call `set_filterable_attributes` once per index for any
field you filter on, or filtered searches error (a Meilisearch requirement).
> **Verification status:** the Meilisearch backend compiles against
> `meilisearch-sdk` 0.28 and is written to its documented API, but has **not yet
> been exercised against a live Meilisearch server** in this repo's CI. Treat it
> as beta until you've run it against your instance. The in-memory backend is
> fully tested.
### Bring your own
Implement [`Backend`] (four async methods: `upsert` · `delete` · `search` ·
`clear`, plus `count`) over OpenSearch, Tantivy, or anything else. **Nothing
above the trait changes** — your `Searchable` models, index/search calls, and
hydration code all stay put.
## Status & roadmap
`1.0.0` — the abstraction, a BM25-ranked in-memory backend (fully tested), and a
Meilisearch backend (compile-verified, live-verification pending).
Not yet built (contributions welcome):
- **More server backends** — OpenSearch / Tantivy behind feature flags.
- **Zero-downtime reindex** — alias-swap, for backends that support it (today's
`reindex` is clear-then-fill).
- **Aggregations / facets**, misspelling tolerance, field boosting.
- **Framework glue** — e.g. an adminx integration mirroring how `adminx-audit`
hooks resource writes, so indexing stays in sync automatically.
## License
MIT
---
Made with ❤️ by [Srotas Space](https://open-source.srotas.space)
---
## 👥 Contributors
- **[Sandeep Maurya](https://github.com/srotas-space)** - Creator & Lead Developer
<img src="https://srotasspace.s3.ap-south-1.amazonaws.com/snm.png" alt="Sandeep Maurya" width="80" height="80" style="border-radius: 50%;">
[LinkedIn](https://www.linkedin.com/in/snmmaurya/)
---
[](https://github.com/srotas-space/searchez)