searchez
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
[]
= "1.0.0"
Quick start
use ;
# async
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:
// after creating or updating a record
engine.index.await?;
// after deleting one
engine.remove.await?; // or remove_id::<Product>("42")
// the backfill job: rebuild an index from every record
let n = engine.reindex.await?;
Search
// a bare string is a full-text query with sensible defaults
engine..await?;
// or build it up
let q = text
.filter // exact-match filters, ANDed
.limit
.offset;
engine..await?;
// a pure filter query — everything matching, no text
engine..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:
let ids = engine..await?; // ranked
let mut rows = db.load_products.await?; // your DB call
// keep the search order (databases return rows in their own order)
let rank: HashMap =
ids.iter.enumerate.map.collect;
rows.sort_by_key;
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 server:
= { = "1.0.0", = ["meilisearch"] }
use ;
let backend = new?;
// Meilisearch only filters on fields declared filterable — do this once at setup:
backend.set_filterable_attributes.await?;
let engine = new;
// ...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_idand declares it the primary key. Ids should therefore match Meilisearch's^[a-zA-Z0-9_-]+$(avoid/,., spaces). - Filterable fields — call
set_filterable_attributesonce per index for any field you filter on, or filtered searches error (a Meilisearch requirement).
Verification status: the Meilisearch backend compiles against
meilisearch-sdk0.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
reindexis clear-then-fill). - Aggregations / facets, misspelling tolerance, field boosting.
- Framework glue — e.g. an adminx integration mirroring how
adminx-audithooks resource writes, so indexing stays in sync automatically.
License
MIT
Made with ❤️ by Srotas Space
👥 Contributors
- Sandeep Maurya - Creator & Lead Developer LinkedIn