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
154
155
156
157
158
159
160
161
162
163
164
// SPDX-FileCopyrightText: Szilárd Hajba
// SPDX-License-Identifier: LGPL-3.0-or-later
//! Full-text search for Cloudillo.
//!
//! # Where the index lives, and why
//!
//! Everything searchable — files, deep document parts, actions, profiles —
//! lands in one `search_docs` table in the meta SQLite database, mirrored into
//! an FTS5 virtual table. One table means one query surface and one ABAC
//! predicate shape; FTS5 supplies `bm25()` ranking and `snippet()` for free;
//! and RTDB, CRDT and blob content can all funnel through the same
//! `serde_json::Value` → text extractor.
//!
//! # Rule-driven indexing
//!
//! No indexing-specific Rust is needed to index new content. What text an object
//! carries is *declared* next to the thing it describes, in one extraction
//! language ([`rules`], [`extract`]) shared by both sources of rules:
//!
//! - **Documents** — an app registers a *document-format manifest* ([`format`])
//! naming which RTDB/CRDT collections and fields carry text and what the
//! deep-link key is. [`indexer`] applies it to an exported document, so a hit
//! points at a subpage rather than at the whole file.
//! - **Actions** — an action type's DSL definition carries a `search` block.
//! [`objects`] applies it. A type without one is not indexed; that absence is
//! the whole allowlist.
//!
//! Files and profiles keep a fixed mapping in [`objects`] rather than a
//! manifest: their schema is server-owned and not app-extensible, so there is
//! nothing for an app to declare.
//!
//! # Who writes which rows
//!
//! Every row is written from Rust, on the scheduler, after a debounce:
//! [`objects`] owns the whole-object `'F'`/`'P'`/`'A'` rows and [`indexer`] the
//! deep `'D'` parts. Neither decides who may see a hit: the meta adapter derives
//! every row's ACL columns from its source table in SQL, in the same transaction
//! as the write, overwriting whatever this crate passed in. The ACL values this
//! crate supplies are advisory — SQL is the authority, so a visibility flip
//! landing mid-debounce cannot be undone by the stale value that run carries.
//!
//! # Module map
//!
//! | Module | Responsibility |
//! |---|---|
//! | [`rules`] | Manifest JSON → validated `IndexRules` / `ActionSearchRules` |
//! | [`extract`] | `serde_json::Value` → plain text |
//! | [`prune`] | Deleting manifest-named nodes from a document before extraction |
//! | [`objects`] | File / profile / action → its one index row; the debounced per-object task |
//! | [`indexer`] | Stored document → deep index rows; the debounced per-document task |
//! | [`crdt`] | Yjs document → the same JSON shape RTDB exports |
//! | [`reindex`] | Bulk sweeps: startup backfill, weekly cron, rules-changed |
//! | [`format`] | `/api/doc-formats` handlers |
//! | [`admin`] | `POST /api/search/reindex` |
//! | [`handler`] | `GET /api/search` |
pub use register_settings;
use App;
use ;
/// Revision of this build's extraction semantics.
///
/// Bump it by hand whenever what the extractor produces changes — a new extract
/// mode applied to an existing manifest, a changed file/profile mapping, a new
/// `search` block on an action type. A tenant whose stored revision differs gets
/// one full sweep on the next startup and then stops re-extracting; see
/// [`reindex`].
pub const INDEX_REV: u32 = 3;
/// Serialises document materialisation across the whole process.
///
/// Materialising a document is memory-unbounded: `export_all` hands back the
/// whole document set owned, before any of `indexer::build_parts`' budgets
/// apply. One at a time is affordable; several at once puts the node out of
/// memory. The [`reindex`] sweep is already sequential — this bounds the
/// scheduler-run `IndexDocumentTask`s, which run concurrently.
///
/// Held across the export → prune → `build_parts` span only, never across the
/// meta-adapter write that follows.
pub static MATERIALIZE_PERMIT: Semaphore = const_new;
/// Whether this tenant keeps the extracted plain text alongside its index.
///
/// `true` (the default) routes its rows to the external-content `search_fts`,
/// where `snippet()` works; `false` routes them to the contentless
/// `search_fts_cl`, which indexes the same text but stores none of it.
///
/// Read on every index run and every query — cheap, since the settings service
/// caches. A read failure falls back to the default: a tenant whose setting
/// cannot be read is better indexed the ordinary way than not at all.
///
/// A flip migrates nothing by itself. Objects move as they are re-indexed, and
/// the whole tenant on the next `reindex`, which [`reindex::index_stamp`] makes
/// stale as soon as this value changes.
pub async
/// Register the search subsystem's scheduler tasks.
///
/// Must run during app initialization, before the scheduler loads persisted
/// tasks — an unregistered task kind cannot be rebuilt from its stored row.
/// Schedule the recurring index maintenance.
///
/// Separate from [`init`] because it writes to the task store, so it belongs
/// with the other `schedule_recurring` calls rather than with registration.
///
/// Two sweeps, not one with `run_on_startup`, because they answer different
/// questions and a single task cannot tell which of its triggers fired. The
/// weekly one always rebuilds — it is the safety net for a write path that
/// forgot to ask for an index update. The startup one rebuilds only a tenant
/// whose stored [`INDEX_REV`] is behind this build, so an ordinary restart costs
/// one reaping DELETE per tenant instead of re-extracting everything.
pub async
// vim: ts=4