@voltro/plugin-search 0.32.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2006 -0
- package/dist/index.d.ts +417 -12
- package/dist/index.js +759 -273
- package/dist/rpc-BQBNr0OU.js +61 -0
- package/dist/rpc.d.ts +58 -2
- package/dist/rpc.js +2 -56
- package/dist/web.d.ts +10 -4
- package/package.json +6 -5
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { ColumnDefinition } from '@voltro/database';
|
|
2
2
|
import { DataStore } from '@voltro/database';
|
|
3
3
|
import { PluginChangeEvent } from '@voltro/protocol';
|
|
4
|
+
import { QueryDescriptor } from '@voltro/database';
|
|
5
|
+
import { Schedule } from 'effect';
|
|
6
|
+
import { Schema } from 'effect';
|
|
4
7
|
import { TableIndex } from '@voltro/database';
|
|
5
8
|
import { TableLike } from '@voltro/database';
|
|
6
9
|
import { VoidIfEmpty } from 'effect/Types';
|
|
@@ -12,14 +15,56 @@ import { YieldableError } from 'effect/Cause';
|
|
|
12
15
|
* tolerance (`typoTolerance`), engine-param passthrough.
|
|
13
16
|
* DEGRADED: like Meili, no numeric edit-distance knob — `fuzziness:0` maps to
|
|
14
17
|
* `typoTolerance:false`; other values keep Algolia's built-in tolerance. */
|
|
15
|
-
export declare const algoliaBackend: (cfg: VendorConfig) => SearchBackend;
|
|
18
|
+
export declare const algoliaBackend: (cfg: VendorConfig, hooks?: BackendHooks) => SearchBackend;
|
|
19
|
+
|
|
20
|
+
/** The engineParams keys this engine accepts from a wire caller — exported so
|
|
21
|
+
* the docs and tests read the same list the runtime enforces. */
|
|
22
|
+
export declare const allowedEngineParams: (engine: SearchEngine) => ReadonlyArray<string>;
|
|
16
23
|
|
|
17
24
|
/** Apply one ChangeEvent to the backend (pure-ish; exported for tests). */
|
|
18
25
|
export declare const applyChange: (backend: SearchBackend, spec: IndexSpec, event: PluginChangeEvent) => Promise<void>;
|
|
19
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Coerce whatever the sync path threw into the typed error the retry policy
|
|
29
|
+
* reads. Anything that is not a `SearchBackendError` — a `map(row)` that threw
|
|
30
|
+
* a TypeError on one row's shape — is by definition NOT transient: retrying a
|
|
31
|
+
* deterministic throw spends the whole budget and cannot succeed.
|
|
32
|
+
*/
|
|
33
|
+
export declare const asBackendError: (e: unknown) => SearchBackendError;
|
|
34
|
+
|
|
35
|
+
export declare const auditMemoryBackend: (input: MemoryBackendAuditInput) => MemoryBackendVerdict;
|
|
36
|
+
|
|
37
|
+
/** Anything the `backend` option accepts, narrowed to what the guard reads. */
|
|
38
|
+
export declare type BackendConfigLike = undefined | 'memory' | (SearchBackend & {
|
|
39
|
+
readonly engineKind?: string;
|
|
40
|
+
}) | {
|
|
41
|
+
readonly engine?: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Server-side knobs a vendor backend takes from whoever CONSTRUCTS it (the
|
|
46
|
+
* plugin, from `app.config.ts`). Deliberately not part of `VendorConfig` and
|
|
47
|
+
* never reachable from the wire — these decide how much of a wire caller's
|
|
48
|
+
* `engineParams` is honoured, so a caller must not be able to set them.
|
|
49
|
+
*/
|
|
50
|
+
export declare interface BackendHooks {
|
|
51
|
+
/** Extra `engineParams` keys THIS app trusts, on top of the built-in
|
|
52
|
+
* presentation-only allowlist. Keys that select the document set
|
|
53
|
+
* (`filter_by`, `facetFilters`, `restrictSearchableAttributes`, …) stay
|
|
54
|
+
* refused regardless — see `NEVER_ALLOWED_ENGINE_PARAMS`. */
|
|
55
|
+
readonly allowEngineParams?: ReadonlyArray<string>;
|
|
56
|
+
/** Called when a query is stripped of `engineParams` keys, so a refusal that
|
|
57
|
+
* changes the answer is visible in the log rather than silent. */
|
|
58
|
+
readonly onWarn?: (message: string, fields?: Record<string, unknown>) => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export declare const backendKindOf: (cfg: BackendConfigLike) => SearchBackendKind;
|
|
62
|
+
|
|
20
63
|
/** Seed an index from existing rows (call from a `*.startup.tsx` or a CLI). */
|
|
21
64
|
export declare const backfillIndex: (backend: SearchBackend, spec: IndexSpec, rows: ReadonlyArray<Record<string, unknown>>) => Promise<number>;
|
|
22
65
|
|
|
66
|
+
export declare const dataStoreDriftLedger: (store: DataStore) => DriftLedger;
|
|
67
|
+
|
|
23
68
|
/**
|
|
24
69
|
* DataStore-backed stats. Each replica owns one row per index (keyed by
|
|
25
70
|
* `replicaId`). A counter bump is a compare-and-set loop on THAT row (mirror of
|
|
@@ -29,9 +74,76 @@ export declare const backfillIndex: (backend: SearchBackend, spec: IndexSpec, ro
|
|
|
29
74
|
*/
|
|
30
75
|
export declare const dataStoreStatsStore: (store: DataStore, replicaId?: string) => StatsStore;
|
|
31
76
|
|
|
77
|
+
/** One un-applied change, as the ledger stores it. */
|
|
78
|
+
export declare interface DriftEntry {
|
|
79
|
+
readonly indexName: string;
|
|
80
|
+
/** Source table the row lives in — repair re-reads it from here. */
|
|
81
|
+
readonly sourceTable: string;
|
|
82
|
+
/** Primary key of the source ROW (what repair re-reads). */
|
|
83
|
+
readonly rowId: string;
|
|
84
|
+
/** Id of the search DOC (`map(row).id`) — what repair removes when the row
|
|
85
|
+
* is gone. Not always the row id: `map` may re-key. */
|
|
86
|
+
readonly docId: string;
|
|
87
|
+
readonly op: DriftOp;
|
|
88
|
+
/** How many times applying this row has failed (first failure = 1). */
|
|
89
|
+
readonly attempts: number;
|
|
90
|
+
readonly lastError: string;
|
|
91
|
+
readonly firstFailedAt: string;
|
|
92
|
+
readonly lastFailedAt: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export declare interface DriftLedger {
|
|
96
|
+
/** Record (or re-record) a row whose index update did not land. */
|
|
97
|
+
readonly record: (entry: DriftRecord) => Promise<void>;
|
|
98
|
+
/** Oldest-first batch to repair. */
|
|
99
|
+
readonly list: (limit: number) => Promise<ReadonlyArray<DriftEntry>>;
|
|
100
|
+
readonly summary: (indexName: string) => Promise<DriftSummary>;
|
|
101
|
+
/** Drop the entry — the row is in sync again. */
|
|
102
|
+
readonly clear: (indexName: string, rowId: string) => Promise<void>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** What the repair has to do for this row: put the doc back, or take it out. */
|
|
106
|
+
export declare type DriftOp = 'upsert' | 'remove';
|
|
107
|
+
|
|
108
|
+
/** The write side of `DriftEntry` — the counters/timestamps are the ledger's. */
|
|
109
|
+
export declare interface DriftRecord {
|
|
110
|
+
readonly indexName: string;
|
|
111
|
+
readonly sourceTable: string;
|
|
112
|
+
readonly rowId: string;
|
|
113
|
+
readonly docId: string;
|
|
114
|
+
readonly op: DriftOp;
|
|
115
|
+
readonly error: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Re-read ONE source row by primary key. Exported so `index.ts` can drive the
|
|
119
|
+
* repair through the store handle it already holds, without importing the
|
|
120
|
+
* query builder itself. */
|
|
121
|
+
export declare const driftSourceQuery: (sourceTable: string, rowId: string) => QueryDescriptor;
|
|
122
|
+
|
|
123
|
+
/** Per-index drift, as `/indexes` reports it. `pending > 0` means the index is
|
|
124
|
+
* KNOWN to disagree with the database right now. */
|
|
125
|
+
export declare interface DriftSummary {
|
|
126
|
+
readonly pending: number;
|
|
127
|
+
readonly lastFailedAt: string | null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
declare interface DriftTable extends TableLike {
|
|
131
|
+
readonly fields: Record<string, ColumnDefinition<unknown>>;
|
|
132
|
+
readonly appliedIndexes: ReadonlyArray<TableIndex>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** All tables for `extendSchema.tables`. */
|
|
136
|
+
export declare const driftTables: () => ReadonlyArray<DriftTable>;
|
|
137
|
+
|
|
32
138
|
/** field → (value → count). Present only for the requested `facets`. */
|
|
33
139
|
export declare type FacetCounts = Record<string, Record<string, number>>;
|
|
34
140
|
|
|
141
|
+
/** A caller-supplied field name must be a plain field path: an identifier,
|
|
142
|
+
* optionally dotted for a nested attribute. No spaces, quotes, brackets,
|
|
143
|
+
* operators or boolean keywords — i.e. nothing that means anything to a
|
|
144
|
+
* filter DSL. */
|
|
145
|
+
export declare const FIELD_NAME_PATTERN: RegExp;
|
|
146
|
+
|
|
35
147
|
/** Typo-tolerance knob: a max edit distance (0 = exact), or `'auto'` to let the
|
|
36
148
|
* engine pick per-term length. Memory approximates `'auto'` as distance 1–2. */
|
|
37
149
|
export declare type Fuzziness = number | 'auto';
|
|
@@ -52,6 +164,19 @@ export declare interface IndexSpec {
|
|
|
52
164
|
/** Doc field carrying the tenant id — when set, `search.query` auto-filters
|
|
53
165
|
* to the caller's tenant (no cross-tenant leakage). */
|
|
54
166
|
readonly tenantField?: string;
|
|
167
|
+
/**
|
|
168
|
+
* Doc fields a WIRE caller may name — in `filters[].field`, `facets[]` and
|
|
169
|
+
* `highlight.fields[]`. When set, anything else is refused with a typed
|
|
170
|
+
* `SearchFieldRejected`; when omitted, any plain field path
|
|
171
|
+
* (`^[A-Za-z_][A-Za-z0-9_.]*$`) is accepted.
|
|
172
|
+
*
|
|
173
|
+
* Opt-in because it cannot be derived: `map(row)` is a FUNCTION, so the doc's
|
|
174
|
+
* field set only exists at runtime. The pattern is the floor every index gets
|
|
175
|
+
* without declaring anything; this narrows it to what your UI actually needs.
|
|
176
|
+
* `tenantField` does not belong here — the tenant clause is injected AFTER
|
|
177
|
+
* this check, by the server, and is never a caller's to name.
|
|
178
|
+
*/
|
|
179
|
+
readonly queryableFields?: ReadonlyArray<string>;
|
|
55
180
|
}
|
|
56
181
|
|
|
57
182
|
/** Aggregated, cross-replica stats for one index — the exact wire shape the
|
|
@@ -59,28 +184,117 @@ export declare interface IndexSpec {
|
|
|
59
184
|
export declare interface IndexStats {
|
|
60
185
|
readonly synced: number;
|
|
61
186
|
readonly removed: number;
|
|
187
|
+
/** Changes that exhausted the sync retry and were written to the drift
|
|
188
|
+
* ledger instead of reaching the engine. Cumulative; `> 0` means the index
|
|
189
|
+
* HAS drifted at some point (see `DriftSummary.pending` for now). */
|
|
190
|
+
readonly dropped: number;
|
|
62
191
|
/** The MOST RECENT reindex across all replicas (max of each replica's). */
|
|
63
192
|
readonly lastReindexAt: string | null;
|
|
64
193
|
/** Docs seeded by that most-recent reindex. */
|
|
65
194
|
readonly lastReindexCount: number;
|
|
66
195
|
}
|
|
67
196
|
|
|
197
|
+
export declare const isSafeFieldName: (field: string) => boolean;
|
|
198
|
+
|
|
68
199
|
/** Meilisearch. Native: filters (range+negation), facets
|
|
69
200
|
* (`facets`→`facetDistribution`), highlighting (`attributesToHighlight`), typo
|
|
70
201
|
* tolerance (on by default; `fuzziness:0` disables), engine-param passthrough.
|
|
71
202
|
* DEGRADED: Meilisearch has no numeric edit-distance knob — a `fuzziness`
|
|
72
203
|
* number other than 0 falls back to its built-in typo tolerance (documented). */
|
|
73
|
-
export declare const meilisearchBackend: (cfg: VendorConfig) => SearchBackend;
|
|
204
|
+
export declare const meilisearchBackend: (cfg: VendorConfig, hooks?: BackendHooks) => SearchBackend;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* A backend that is BOTH per-process and non-durable, tagged so the plugin can
|
|
208
|
+
* recognise it however it arrived — as the `'memory'` string, as the
|
|
209
|
+
* zero-config default, or handed in through the `SearchBackend` slot.
|
|
210
|
+
*
|
|
211
|
+
* The tag exists because the boot refusal (`./memoryGuard`) would otherwise be
|
|
212
|
+
* decorative: `backend: memoryBackend()` is the same deployment as
|
|
213
|
+
* `backend: 'memory'` and must read the same to the guard.
|
|
214
|
+
*/
|
|
215
|
+
export declare interface MemoryBackend extends SearchBackend {
|
|
216
|
+
/** Discriminator read by `backendKindOf` — see `./memoryGuard`. */
|
|
217
|
+
readonly engineKind: 'memory';
|
|
218
|
+
}
|
|
74
219
|
|
|
75
220
|
/** In-process inverted-ish search: substring (or fuzzy) match over stringified
|
|
76
221
|
* doc values + structured filters. Computes facets + highlights ITSELF (it's
|
|
77
222
|
* the honest-degradation reference — every feature works, just not at scale).
|
|
78
223
|
* Not for scale — but correct + dependency-free, and the right default for
|
|
79
|
-
* dev/tests/single-process apps.
|
|
80
|
-
|
|
224
|
+
* dev/tests/single-process apps.
|
|
225
|
+
*
|
|
226
|
+
* It holds every doc in THIS process's heap: a second replica has its own
|
|
227
|
+
* copy, and a restart starts empty. `./memoryGuard` is what keeps that from
|
|
228
|
+
* being a production surprise. */
|
|
229
|
+
export declare const memoryBackend: () => MemoryBackend;
|
|
230
|
+
|
|
231
|
+
export declare interface MemoryBackendAuditInput {
|
|
232
|
+
readonly backendKind: SearchBackendKind;
|
|
233
|
+
/** `NODE_ENV === 'production'`. */
|
|
234
|
+
readonly production: boolean;
|
|
235
|
+
/** The app asserted the single-process + re-seed contract — see
|
|
236
|
+
* `SearchPluginOptions.singleProcessMemoryIndex`. */
|
|
237
|
+
readonly singleProcessDeclared: boolean;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* `'quiet'` says nothing at all (the common, correct cases). `'note'` logs at
|
|
242
|
+
* info — the app declared the memory contract and we restate what it signed up
|
|
243
|
+
* for, once, at boot. `'refuse'` aborts the boot.
|
|
244
|
+
*/
|
|
245
|
+
export declare type MemoryBackendVerdict = {
|
|
246
|
+
readonly level: 'quiet';
|
|
247
|
+
} | {
|
|
248
|
+
readonly level: 'note';
|
|
249
|
+
readonly message: string;
|
|
250
|
+
} | {
|
|
251
|
+
readonly level: 'refuse';
|
|
252
|
+
readonly message: string;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/** In-process ledger. The default until `bindDataStore` swaps in the durable
|
|
256
|
+
* one — it still makes drift OBSERVABLE (`/indexes` reports it) and still
|
|
257
|
+
* self-heals in a running dev process; it just does not survive a restart. */
|
|
258
|
+
export declare const memoryDriftLedger: () => DriftLedger;
|
|
81
259
|
|
|
82
260
|
export declare const memoryStatsStore: () => StatsStore;
|
|
83
261
|
|
|
262
|
+
/**
|
|
263
|
+
* The message logged when a peer replica actually announces itself while this
|
|
264
|
+
* process is serving search from its own heap. Fires once per boot, in ANY
|
|
265
|
+
* environment — this is an observation, so it does not care what `NODE_ENV`
|
|
266
|
+
* says.
|
|
267
|
+
*/
|
|
268
|
+
export declare const peerObservedWarning: (peerInstanceId: string, singleProcessDeclared: boolean) => string;
|
|
269
|
+
|
|
270
|
+
export declare type ResolvedSyncOptions = {
|
|
271
|
+
readonly [K in keyof typeof SYNC_DEFAULTS]: number;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
export declare const resolveSyncOptions: (o: SearchSyncOptions | undefined) => ResolvedSyncOptions;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* The surviving keys, split by HOW they were allowed — the two halves merge on
|
|
278
|
+
* opposite sides of the adapter's own mapped params:
|
|
279
|
+
*
|
|
280
|
+
* - `params` (built-in list) merges FIRST, so a wire caller can fill a gap
|
|
281
|
+
* the adapter leaves but never overwrite something it mapped. That is the
|
|
282
|
+
* hedge against the allowlist itself being wrong about a key.
|
|
283
|
+
* - `appAllowed` (this app's `allowedEngineParams`) merges LAST, because the
|
|
284
|
+
* app owner widened it on purpose — usually to reach a key the adapter
|
|
285
|
+
* defaults (Typesense `query_by: '*'`), which merging first would make a
|
|
286
|
+
* silent no-op.
|
|
287
|
+
*/
|
|
288
|
+
export declare interface SanitizedEngineParams {
|
|
289
|
+
readonly params: Record<string, unknown>;
|
|
290
|
+
readonly appAllowed: Record<string, unknown>;
|
|
291
|
+
/** The keys that survived neither, in input order. Never silent: the caller
|
|
292
|
+
* asked for something the server refused, and that changes the answer. */
|
|
293
|
+
readonly dropped: ReadonlyArray<string>;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export declare const sanitizeEngineParams: (engine: SearchEngine, params: Record<string, unknown> | undefined, extraAllowed: ReadonlyArray<string> | undefined) => SanitizedEngineParams;
|
|
297
|
+
|
|
84
298
|
/**
|
|
85
299
|
* Compute the backend filters for a query, FAIL-CLOSED on tenant scoping.
|
|
86
300
|
* Returns `null` when the caller must see NOTHING: a tenant-scoped index
|
|
@@ -91,6 +305,8 @@ export declare const memoryStatsStore: () => StatsStore;
|
|
|
91
305
|
*/
|
|
92
306
|
export declare const scopeSearchFilters: (tenantField: string | undefined, tenantId: string | null, filters: ReadonlyArray<SearchFilter> | undefined) => ReadonlyArray<SearchFilter> | null;
|
|
93
307
|
|
|
308
|
+
export declare const SEARCH_DRIFT_TABLE = "_voltro_search_drift";
|
|
309
|
+
|
|
94
310
|
export declare const SEARCH_STATS_TABLE = "_voltro_search_stats";
|
|
95
311
|
|
|
96
312
|
export declare interface SearchBackend {
|
|
@@ -119,11 +335,65 @@ declare const SearchBackendError_base: new <A extends Record<string, any> = {}>(
|
|
|
119
335
|
readonly _tag: "SearchBackendError";
|
|
120
336
|
} & Readonly<A>;
|
|
121
337
|
|
|
338
|
+
/**
|
|
339
|
+
* How the resolved backend answers queries — not which vendor it is.
|
|
340
|
+
*
|
|
341
|
+
* `'custom'` is an app-supplied `SearchBackend` the framework knows nothing
|
|
342
|
+
* about; the app owns its durability. `memoryBackend()` is NOT custom however
|
|
343
|
+
* it was passed (it carries `engineKind: 'memory'`), because it is the same
|
|
344
|
+
* deployment either way.
|
|
345
|
+
*/
|
|
346
|
+
export declare type SearchBackendKind = 'memory' | 'vendor' | 'custom';
|
|
347
|
+
|
|
348
|
+
/** Thrown from `onActivate` to abort the boot. */
|
|
349
|
+
export declare class SearchBackendNotDurable extends Error {
|
|
350
|
+
readonly _tag = "SearchBackendNotDurable";
|
|
351
|
+
constructor(message: string);
|
|
352
|
+
}
|
|
353
|
+
|
|
122
354
|
export declare interface SearchDoc {
|
|
123
355
|
readonly id: string;
|
|
124
356
|
readonly [field: string]: unknown;
|
|
125
357
|
}
|
|
126
358
|
|
|
359
|
+
/**
|
|
360
|
+
* `_voltro_search_drift` — one row per (indexName, rowId) that is known to be
|
|
361
|
+
* missing from / stale in the index. UNIQUE over the pair so every replica's
|
|
362
|
+
* `insertIgnore` is idempotent and a repeatedly-failing row stays ONE entry.
|
|
363
|
+
*
|
|
364
|
+
* A `_voltro_*` table needs an explicit `id({ prefix })` (a typeid prefix
|
|
365
|
+
* cannot be derived from a name starting with `_`), and needs no codemod: the
|
|
366
|
+
* declarative differ reconciles it on `voltro db apply` and on a `voltro dev`
|
|
367
|
+
* boot, on every dialect.
|
|
368
|
+
*/
|
|
369
|
+
export declare const searchDriftTable: DriftTable;
|
|
370
|
+
|
|
371
|
+
export declare type SearchEngine = 'typesense' | 'meilisearch' | 'algolia';
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* A field name the caller supplied — in `filters[].field`, `facets[]` or
|
|
375
|
+
* `highlight.fields[]` — was refused before it could reach the engine.
|
|
376
|
+
*
|
|
377
|
+
* `reason: 'not-an-identifier'` — the name is not a plain field path
|
|
378
|
+
* (`^[A-Za-z_][A-Za-z0-9_.]*$`). Every vendor adapter interpolates the name
|
|
379
|
+
* into that engine's filter DSL, so anything else is a control-plane injection
|
|
380
|
+
* (a Typesense `filter_by` is ONE flat string with `||`, so a crafted name
|
|
381
|
+
* re-groups the boolean tree around the appended tenant clause).
|
|
382
|
+
*
|
|
383
|
+
* `reason: 'not-queryable'` — the name is well-formed but not in the index's
|
|
384
|
+
* declared `queryableFields` allowlist.
|
|
385
|
+
*/
|
|
386
|
+
export declare class SearchFieldRejected extends SearchFieldRejected_base {
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
declare const SearchFieldRejected_base: Schema.TaggedErrorClass<SearchFieldRejected, "SearchFieldRejected", {
|
|
390
|
+
readonly _tag: Schema.tag<"SearchFieldRejected">;
|
|
391
|
+
} & {
|
|
392
|
+
field: typeof Schema.String;
|
|
393
|
+
where: Schema.Literal<["filter", "facet", "highlight"]>;
|
|
394
|
+
reason: Schema.Literal<["not-an-identifier", "not-queryable"]>;
|
|
395
|
+
}>;
|
|
396
|
+
|
|
127
397
|
export declare interface SearchFilter {
|
|
128
398
|
readonly field: string;
|
|
129
399
|
readonly op: SearchFilterOp;
|
|
@@ -147,13 +417,80 @@ export declare interface SearchHit {
|
|
|
147
417
|
readonly highlights?: Record<string, string>;
|
|
148
418
|
}
|
|
149
419
|
|
|
420
|
+
/**
|
|
421
|
+
* The queried index is not one the app declared in `searchPlugin({ indexes })`.
|
|
422
|
+
*
|
|
423
|
+
* This is a REFUSAL, not an empty result, and the distinction is the whole
|
|
424
|
+
* point: an unknown index has no `IndexSpec`, therefore no `tenantField`,
|
|
425
|
+
* therefore no tenant clause — so answering it would run an unfiltered query
|
|
426
|
+
* against whatever collection of that name exists on the (usually shared)
|
|
427
|
+
* engine. Fail instead of querying something we cannot scope.
|
|
428
|
+
*/
|
|
429
|
+
export declare class SearchIndexNotFound extends SearchIndexNotFound_base {
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
declare const SearchIndexNotFound_base: Schema.TaggedErrorClass<SearchIndexNotFound, "SearchIndexNotFound", {
|
|
433
|
+
readonly _tag: Schema.tag<"SearchIndexNotFound">;
|
|
434
|
+
} & {
|
|
435
|
+
index: typeof Schema.String;
|
|
436
|
+
}>;
|
|
437
|
+
|
|
150
438
|
export declare const searchPlugin: (options: SearchPluginOptions) => VoltroPlugin;
|
|
151
439
|
|
|
152
440
|
export declare interface SearchPluginOptions {
|
|
153
441
|
readonly backend?: SearchBackendConfig;
|
|
154
442
|
/** Map of source table → index spec. */
|
|
155
443
|
readonly indexes: Record<string, IndexSpec>;
|
|
444
|
+
/** Retry / drift-repair tunables — see {@link SearchSyncOptions}. */
|
|
445
|
+
readonly sync?: SearchSyncOptions;
|
|
446
|
+
/**
|
|
447
|
+
* Namespace for this plugin's rpc tags + inspect endpoints. Default `search`.
|
|
448
|
+
*
|
|
449
|
+
* Set it when your app already publishes under that name — an exact tag
|
|
450
|
+
* collision is fatal at codegen, and this is the way out. Orthogonal to
|
|
451
|
+
* `name` below: `alias` REPLACES the namespace, `name` distinguishes two
|
|
452
|
+
* installations within it.
|
|
453
|
+
*
|
|
454
|
+
* The cost, stated because nothing else states it: the local and cloud
|
|
455
|
+
* dashboards fetch this plugin's panel at the DEFAULT slug, so an aliased
|
|
456
|
+
* install keeps working while its dashboard panel 404s. Alias to escape a
|
|
457
|
+
* collision, not for taste.
|
|
458
|
+
*/
|
|
459
|
+
readonly alias?: string;
|
|
460
|
+
/**
|
|
461
|
+
* Discriminator for a SECOND installation of this plugin, when one app runs
|
|
462
|
+
* two (`@voltro/plugin-search#analytics`). Not a rename — for that use
|
|
463
|
+
* `alias`.
|
|
464
|
+
*/
|
|
156
465
|
readonly name?: string;
|
|
466
|
+
/**
|
|
467
|
+
* Extra `engineParams` keys a wire caller may set, on top of the built-in
|
|
468
|
+
* per-engine allowlist of presentation-only keys (paging / ordering / typo
|
|
469
|
+
* tolerance / highlight shaping).
|
|
470
|
+
*
|
|
471
|
+
* This is the deliberate, server-side widening of the escape hatch — an app
|
|
472
|
+
* that needs, say, Typesense's `query_by` says so in `app.config.ts` rather
|
|
473
|
+
* than every caller getting it. Keys that select the DOCUMENT SET
|
|
474
|
+
* (`filter_by`, `filter`, `facetFilters`, `restrictSearchableAttributes`,
|
|
475
|
+
* `preset`, `pinned_hits`, …) stay refused even when listed here: they are
|
|
476
|
+
* the authority the injected tenant clause holds, and a filter belongs in the
|
|
477
|
+
* validated `filters[]` list, which is ANDed with that clause.
|
|
478
|
+
*/
|
|
479
|
+
readonly allowedEngineParams?: ReadonlyArray<string>;
|
|
480
|
+
/**
|
|
481
|
+
* Assert that this deployment runs the in-memory backend the only way it can
|
|
482
|
+
* be correct outside dev: **exactly one process**, which **re-seeds every
|
|
483
|
+
* index at startup** (a `*.startup.tsx` calling `backfillIndex`).
|
|
484
|
+
*
|
|
485
|
+
* Without it, `NODE_ENV=production` + the memory backend REFUSES to boot —
|
|
486
|
+
* see `./memoryGuard` for why a single replica is not on its own enough (the
|
|
487
|
+
* heap-resident index is empty after every deploy).
|
|
488
|
+
*
|
|
489
|
+
* This is not a mute switch. It is a claim the plugin holds you to: if the
|
|
490
|
+
* membership registry reports a peer replica, the plugin logs that the claim
|
|
491
|
+
* has been contradicted, in any environment.
|
|
492
|
+
*/
|
|
493
|
+
readonly singleProcessMemoryIndex?: boolean;
|
|
157
494
|
}
|
|
158
495
|
|
|
159
496
|
export declare interface SearchQuery {
|
|
@@ -170,9 +507,15 @@ export declare interface SearchQuery {
|
|
|
170
507
|
readonly highlight?: HighlightSpec;
|
|
171
508
|
/** Typo tolerance. Omit ⇒ the engine default (memory: exact substring). */
|
|
172
509
|
readonly fuzziness?: Fuzziness;
|
|
173
|
-
/** Escape hatch:
|
|
174
|
-
*
|
|
175
|
-
*
|
|
510
|
+
/** Escape hatch, ALLOWLISTED per engine: keys on that engine's
|
|
511
|
+
* presentation-only list (paging / ordering / typo / highlight — see
|
|
512
|
+
* `sanitize.ts`) are shallow-merged BEFORE the mapped params, so they can
|
|
513
|
+
* never out-vote the injected tenant filter; keys the APP widened to via
|
|
514
|
+
* `BackendHooks.allowEngineParams` merge after (that widening is
|
|
515
|
+
* server-side config, and its point is to reach a key the adapter
|
|
516
|
+
* defaults). Every other key is dropped and reported through
|
|
517
|
+
* `BackendHooks.onWarn`. Ignored by the memory backend (nothing to forward
|
|
518
|
+
* to). */
|
|
176
519
|
readonly engineParams?: Record<string, unknown>;
|
|
177
520
|
}
|
|
178
521
|
|
|
@@ -188,17 +531,59 @@ export declare interface SearchResult {
|
|
|
188
531
|
* increment targets exactly its own row (no cross-replica contention). */
|
|
189
532
|
export declare const searchStatsTable: StatsTable;
|
|
190
533
|
|
|
534
|
+
/**
|
|
535
|
+
* Durability knobs for the change→index sync. Every number the plugin would
|
|
536
|
+
* otherwise pick on your behalf lives here, with a default.
|
|
537
|
+
*
|
|
538
|
+
* The retry only fires for a failure the backend marked `transient` (network /
|
|
539
|
+
* engine unavailable / 5xx / rate-limited). A permanent failure — an
|
|
540
|
+
* unsupported query shape, a `map(row)` that throws on this row — is NOT
|
|
541
|
+
* retried: repeating it costs latency and changes nothing. Both kinds end in
|
|
542
|
+
* the drift ledger, which is what makes the distinction safe to draw.
|
|
543
|
+
*/
|
|
544
|
+
export declare interface SearchSyncOptions {
|
|
545
|
+
/** Retries AFTER the first attempt, for a `transient` failure. Default 5
|
|
546
|
+
* (≈ 200 ms → 3.2 s of backoff before giving up). `0` disables retry. */
|
|
547
|
+
readonly retries?: number;
|
|
548
|
+
/** First backoff delay; doubles per attempt. Default 200 ms. */
|
|
549
|
+
readonly retryBaseDelayMs?: number;
|
|
550
|
+
/** Backoff ceiling — no single wait exceeds this. Default 10_000 ms. */
|
|
551
|
+
readonly retryMaxDelayMs?: number;
|
|
552
|
+
/**
|
|
553
|
+
* How often the cluster-coordinated sweep repairs drift-ledger entries.
|
|
554
|
+
* Default 60_000 ms. `0` turns the automatic sweep off entirely, leaving the
|
|
555
|
+
* `POST /resync` inspect endpoint as the (manual) repair path — the ledger
|
|
556
|
+
* still records, so nothing is lost, it just waits for an operator.
|
|
557
|
+
*/
|
|
558
|
+
readonly resyncIntervalMs?: number;
|
|
559
|
+
/** Max ledger entries repaired per sweep. Default 200. */
|
|
560
|
+
readonly resyncBatchSize?: number;
|
|
561
|
+
}
|
|
562
|
+
|
|
191
563
|
/** All tables for `extendSchema.tables`. */
|
|
192
564
|
export declare const searchTables: () => ReadonlyArray<StatsTable>;
|
|
193
565
|
|
|
566
|
+
/** What a bump counts. `dropped` is the drift counter — the one an operator
|
|
567
|
+
* must be able to see WITHOUT reading logs. */
|
|
568
|
+
export declare type StatsKind = 'synced' | 'removed' | 'dropped';
|
|
569
|
+
|
|
570
|
+
/** The store's change visibility, as `PluginChangeEvent.changeScope` reports
|
|
571
|
+
* it — it decides SUM vs MAX on read (see the header). */
|
|
572
|
+
export declare type StatsScope = 'local' | 'fleet';
|
|
573
|
+
|
|
194
574
|
/** The stats store. All ops key on the index name; the durable impl fans out to
|
|
195
575
|
* per-replica rows internally and aggregates on read. */
|
|
196
576
|
export declare interface StatsStore {
|
|
197
|
-
/** +1 synced (an insert/update mirrored)
|
|
198
|
-
|
|
199
|
-
|
|
577
|
+
/** +1 synced (an insert/update mirrored), +1 removed (a delete), or +1
|
|
578
|
+
* dropped (retries exhausted → drift). `scope` is the event's changeScope;
|
|
579
|
+
* it decides how this replica's row aggregates with its peers'. */
|
|
580
|
+
readonly bump: (index: string, kind: StatsKind, scope: StatsScope) => Promise<void>;
|
|
581
|
+
/** Record a reindex: +count synced AND set lastReindex{At,Count}. Takes no
|
|
582
|
+
* scope — a reindex is one deliberate act on one replica, and stamping the
|
|
583
|
+
* row's `scope` from it would overwrite what the change STREAM reported. */
|
|
200
584
|
readonly recordReindex: (index: string, count: number, at: string) => Promise<void>;
|
|
201
|
-
/** Aggregated stats for one index (summed across replicas
|
|
585
|
+
/** Aggregated stats for one index (summed or maxed across replicas — see the
|
|
586
|
+
* file header). */
|
|
202
587
|
readonly get: (index: string) => Promise<IndexStats>;
|
|
203
588
|
}
|
|
204
589
|
|
|
@@ -207,9 +592,29 @@ declare interface StatsTable extends TableLike {
|
|
|
207
592
|
readonly appliedIndexes: ReadonlyArray<TableIndex>;
|
|
208
593
|
}
|
|
209
594
|
|
|
595
|
+
/** Defaults for every {@link SearchSyncOptions} knob, in ONE place — the
|
|
596
|
+
* `/indexes` panel reports the policy actually in force from here, so a
|
|
597
|
+
* default can never be a number only the source knows. */
|
|
598
|
+
export declare const SYNC_DEFAULTS: {
|
|
599
|
+
readonly retries: 5;
|
|
600
|
+
readonly retryBaseDelayMs: 200;
|
|
601
|
+
readonly retryMaxDelayMs: 10000;
|
|
602
|
+
readonly resyncIntervalMs: 60000;
|
|
603
|
+
readonly resyncBatchSize: 200;
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Bounded, capped exponential backoff — and the first READER of
|
|
608
|
+
* `SearchBackendError.transient`, which every vendor adapter has been setting
|
|
609
|
+
* into the void. `whileInput` exits the retry the moment the failure is
|
|
610
|
+
* permanent; `intersect(recurs)` bounds the attempts; `either(spaced)` caps a
|
|
611
|
+
* single wait at `retryMaxDelayMs`.
|
|
612
|
+
*/
|
|
613
|
+
export declare const syncRetrySchedule: (o: ResolvedSyncOptions) => Schedule.Schedule<unknown, SearchBackendError>;
|
|
614
|
+
|
|
210
615
|
/** Typesense (self-host / cloud). Native: filters (range+negation), facets,
|
|
211
616
|
* highlighting, typo tolerance (`num_typos`), engine-param passthrough. */
|
|
212
|
-
export declare const typesenseBackend: (cfg: VendorConfig) => SearchBackend;
|
|
617
|
+
export declare const typesenseBackend: (cfg: VendorConfig, hooks?: BackendHooks) => SearchBackend;
|
|
213
618
|
|
|
214
619
|
export declare interface VendorConfig {
|
|
215
620
|
readonly url?: string;
|