Expand description
Keyword and vector search for autumn-web applications (issue #1191).
Autumn already had full-text search primitives in core (#842): a
#[searchable] model attribute, a tsvector column, and a
#[repository(searchable)] search() method. What it did not have was a
search subsystem — a way to declare a model searchable, keep an index
in sync as records change, and query it through one engine-agnostic API,
let alone semantic / vector search for “find similar” and RAG retrieval.
This crate is that subsystem: the autumn-shaped answer to Laravel Scout, with vectors first-class rather than bolted on.
§Quick start
use std::sync::Arc;
use autumn_search::{SearchPlugin, SearchSyncHooks};
// 1. Mark the model searchable. `embed` nominates the field to embed.
#[autumn_web::model]
#[searchable(language = "english")]
pub struct Article {
#[id] pub id: i64,
#[searchable(weight = "A")] pub title: String,
#[searchable(weight = "B", embed)] pub body: String,
}
// 2. Keep the index in sync from the record lifecycle. `#[repository]`
// takes a plain type NAME for `hooks`, so alias the generic first.
type ArticleSearchHooks = SearchSyncHooks<Article, NewArticle, UpdateArticle>;
#[autumn_web::repository(Article, hooks = ArticleSearchHooks, commit_hooks = true)]
pub trait ArticleRepository {}
// 3. Mount the plugin.
autumn_web::app()
.plugin(
SearchPlugin::new()
.postgres()
.embedder(Arc::new(MyEmbedder))
.index::<Article>(),
)
.run()
.await;
// 4. Query it. The plugin installs the client as an `AppState` extension.
let search = state.extension::<SearchClient>().expect("SearchPlugin installed");
let page = search.search::<Article>("rust web framework", &page_req).await?;
let similar = search.similar::<Article>("how do I add auth?", 5).await?;Configuration comes from [search], which the plugin resolves itself
(base autumn.toml → [profile.<name>.search] → autumn-<profile>.toml
→ AUTUMN_SEARCH__*, the same layering core applies) unless the app passes
SearchPlugin::config(...).
§How the pieces fit
| Concern | Type | Notes |
|---|---|---|
| Index shape | autumn_web::search::IndexDefinition | derived from #[searchable], never hand-written |
| Record extraction | autumn_web::search::SearchDocument | derived from #[searchable] |
| Engine | SearchBackend | MemorySearchBackend, PostgresSearchStore, or yours |
| Embeddings | Embedder | no model or vendor ships here, by design |
| Index sync | SearchSyncHooks → ReindexArgs → #[job] | off-request and durable |
| Reading records | DocumentSource | drives both reindex and backfill |
| Authorization | SearchVisibility → SearchFilter | pushed into the engine query |
| Querying | SearchClient | Page/ListQuery-shaped results |
§Design notes worth knowing
Reindex re-reads the row. A reindex job carries (index, id), not the
record. So create, update, delete, a replayed job, a lost event, and a row
changed by direct SQL all converge to the same index state, and
at-least-once delivery is safe by construction.
Filters are arguments, not afterthoughts. SearchFilter is a
required parameter of the backend query methods, so an engine cannot
quietly return rows the caller may not see; filters intersect, so a
caller can only ever narrow what authorization allowed.
Query text is a bag of words. Every token must match, and operator
syntax (OR, field:, *) is never interpreted. That keeps results
consistent across engines and makes a hostile query structurally incapable
of widening the result set.
Structs§
- Backend
Capabilities - What a backend can do, so callers can degrade deliberately.
- Backfill
Args - Payload of the
BACKFILL_JOB. - Backfill
Options - Knobs for a full reindex.
- Backfill
Report - What a backfill did.
- DenyAll
- A
SearchVisibilitythat denies everything. - Hashing
Embedder - A deterministic, dependency-free hashing vectorizer.
- Index
Definition - The engine-agnostic description of one model’s search index.
- Indexed
Document - A
SearchDocumentpaired with its (optional) embedding. - Keyword
Query - A ranked keyword query.
- List
Query - Sort/filter parameters parsed from the query string, composing with
PageRequestto drive an allowlisted list query. - Memory
Document Source - An in-memory
DocumentSourcefor tests and for apps that already hold their corpus in process. - Memory
Search Backend - An in-memory
SearchBackend. - NoEmbedder
- The default embedder: refuses to produce vectors.
- Page
- Paginated response wrapper with navigation metadata.
- Page
Request - Pagination parameters parsed from the query string.
- Postgres
Search Store - Postgres-backed search storage.
- Reindex
Args - Payload of the
REINDEX_JOB. - Search
Client - The application-facing search API.
- Search
Client Builder - Builder for
SearchClient. - Search
Config - Configuration for the search subsystem.
- Search
Document - A single record, extracted into the shape every backend indexes.
- Search
Field Value - One extracted field value of a
SearchDocument. - Search
Filter - Restrictions applied inside the backend query.
- Search
Hit - One ranked result.
- Search
Index Field - One indexed field of a searchable model.
- Search
Plugin - Keyword + vector search for
autumn-webapplications. - Search
Sync Hooks MutationHooksthat keep a searchable model’s index in sync.- Tenant
Visibility - Restricts every search to the ambient tenant.
- Vector
Query - A vector / k-NN similarity query.
Enums§
- Backfill
Target - What a one-shot backfill boot was asked to rebuild.
- Reindex
Op - What a reindex instruction should do.
- Search
Config Error - Why a
[search]section was rejected. - Search
Error - Everything that can go wrong indexing or querying.
- Vector
Mode - How embeddings are physically stored.
Constants§
- BACKFILL_
ENV - Environment variable that turns a normal boot into a one-shot backfill.
- BACKFILL_
JOB - Job name for the full backfill.
- BACKFILL_
PURGE_ ENV - Environment variable that makes the one-shot backfill purge first.
- DOCUMENTS_
TABLE - Physical table every index’s documents live in.
- REINDEX_
JOB - Job name for the per-record reindex. A wire contract: in-flight payloads outlive a deploy, so this string must not change.
- TENANT_
FILTER_ KEY - The reserved
SearchFilter::equalskey that maps ontoSearchDocument::tenant_idrather than an indexed field.
Traits§
- Document
Source - Reads records out of the system of record and extracts them into
SearchDocuments. - Embedder
- Turns text into vectors for semantic search.
- Search
Backend - A search engine
autumn-searchcan drive. - Search
Indexed - A model that
#[model] #[searchable]has given a search index. - Search
Text Value - Converts a model field into the text a search backend indexes.
- Search
Visibility - Turns a request context into the restriction a search must run under.
Functions§
- ambient_
tenant_ filter - The ambient-tenant restriction for
definition. - backfill_
request_ from_ env - The backfill requested through the environment, if any.
- client_
from_ state - Pull the installed
SearchClientoff application state. - cosine_
similarity - Cosine similarity of two vectors, in
[-1.0, 1.0]. - current_
tenant_ filter - The ambient-tenant restriction, or an empty filter outside a tenant scope.
- empty_
page - Build an empty page for
request— the fail-closed result every backend returns for a blank query or an impossible filter. - enqueue_
reindex - Enqueue a reindex for one record.
- enqueue_
reindex_ for - Enqueue an upsert reindex for
record. - enqueue_
unindex_ for - Enqueue a delete reindex for
record. - normalize
- Scale
vectorto unit length in place. A zero vector is left alone (it has no direction), which keepscosine_similarityfree ofNaN. - query_
tokens - Tokenize a user query, returning
Nonewhen nothing usable remains. - search_
job_ infos - The
JobInfoset for the search jobs, routed toqueue. - tokenize
- Split
textinto lowercase alphanumeric tokens.
Type Aliases§
- BoxFuture
- Boxed future returned by the object-safe search traits.
- Search
Result - Result alias used throughout
autumn-search.