1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
//! 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
//!
//! ```rust,ignore
//! 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.
// autumn-panic-gate: request-path crate — production code path must be panic-free.
// See CONTRIBUTING.md "Request-path panic gate". Justify exceptions with
// #[allow(clippy::<lint>, reason = "…")] at the narrowest scope.
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use MemorySearchBackend;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
// Re-exported so an app can name the derived vocabulary without also depending
// on `autumn_web::search` directly.
pub use ;
// Results are `Page`-shaped and queries are `PageRequest`/`ListQuery`-shaped,
// so re-export those too rather than making every call site reach into
// `autumn_web::pagination`.
pub use ;