@voltro/plugin-search 0.1.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 +52 -0
- package/LICENSE +57 -0
- package/README.md +26 -0
- package/SECURITY.md +56 -0
- package/THIRD-PARTY-NOTICES.md +1716 -0
- package/dist/index.d.ts +222 -0
- package/dist/index.js +623 -0
- package/dist/rpc.d.ts +47 -0
- package/dist/rpc.js +56 -0
- package/dist/web.d.ts +52 -0
- package/dist/web.js +18 -0
- package/package.json +60 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { ColumnDefinition } from '@voltro/database';
|
|
2
|
+
import { DataStore } from '@voltro/database';
|
|
3
|
+
import { PluginChangeEvent } from '@voltro/protocol';
|
|
4
|
+
import { TableIndex } from '@voltro/database';
|
|
5
|
+
import { TableLike } from '@voltro/database';
|
|
6
|
+
import { VoidIfEmpty } from 'effect/Types';
|
|
7
|
+
import { VoltroPlugin } from '@voltro/protocol';
|
|
8
|
+
import { YieldableError } from 'effect/Cause';
|
|
9
|
+
|
|
10
|
+
/** Algolia. Native: filters (facetFilters + numericFilters), facets
|
|
11
|
+
* (`facets`→`facets` response), highlighting (`_highlightResult`), typo
|
|
12
|
+
* tolerance (`typoTolerance`), engine-param passthrough.
|
|
13
|
+
* DEGRADED: like Meili, no numeric edit-distance knob — `fuzziness:0` maps to
|
|
14
|
+
* `typoTolerance:false`; other values keep Algolia's built-in tolerance. */
|
|
15
|
+
export declare const algoliaBackend: (cfg: VendorConfig) => SearchBackend;
|
|
16
|
+
|
|
17
|
+
/** Apply one ChangeEvent to the backend (pure-ish; exported for tests). */
|
|
18
|
+
export declare const applyChange: (backend: SearchBackend, spec: IndexSpec, event: PluginChangeEvent) => Promise<void>;
|
|
19
|
+
|
|
20
|
+
/** Seed an index from existing rows (call from a `*.startup.tsx` or a CLI). */
|
|
21
|
+
export declare const backfillIndex: (backend: SearchBackend, spec: IndexSpec, rows: ReadonlyArray<Record<string, unknown>>) => Promise<number>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* DataStore-backed stats. Each replica owns one row per index (keyed by
|
|
25
|
+
* `replicaId`). A counter bump is a compare-and-set loop on THAT row (mirror of
|
|
26
|
+
* `plugin-billing`'s usage-store CAS): read `synced`/`removed`, conditionally
|
|
27
|
+
* `updateMany` gated on the read value, `insertIgnore` the first time. Reads
|
|
28
|
+
* aggregate every replica's row for the index.
|
|
29
|
+
*/
|
|
30
|
+
export declare const dataStoreStatsStore: (store: DataStore, replicaId?: string) => StatsStore;
|
|
31
|
+
|
|
32
|
+
/** field → (value → count). Present only for the requested `facets`. */
|
|
33
|
+
export declare type FacetCounts = Record<string, Record<string, number>>;
|
|
34
|
+
|
|
35
|
+
/** Typo-tolerance knob: a max edit distance (0 = exact), or `'auto'` to let the
|
|
36
|
+
* engine pick per-term length. Memory approximates `'auto'` as distance 1–2. */
|
|
37
|
+
export declare type Fuzziness = number | 'auto';
|
|
38
|
+
|
|
39
|
+
export declare interface HighlightSpec {
|
|
40
|
+
/** Fields to highlight. Omit / empty ⇒ every text field the engine indexes. */
|
|
41
|
+
readonly fields?: ReadonlyArray<string>;
|
|
42
|
+
/** Wrapping tags around a matched term. Default `<mark>`/`</mark>`. */
|
|
43
|
+
readonly preTag?: string;
|
|
44
|
+
readonly postTag?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export declare interface IndexSpec {
|
|
48
|
+
/** Target index/collection name in the backend. */
|
|
49
|
+
readonly index: string;
|
|
50
|
+
/** Map a table row → a flat search doc (must include `id`). */
|
|
51
|
+
readonly map: (row: Record<string, unknown>) => SearchDoc;
|
|
52
|
+
/** Doc field carrying the tenant id — when set, `search.query` auto-filters
|
|
53
|
+
* to the caller's tenant (no cross-tenant leakage). */
|
|
54
|
+
readonly tenantField?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Aggregated, cross-replica stats for one index — the exact wire shape the
|
|
58
|
+
* `/indexes` inspect endpoint + the dashboard panel already consume. */
|
|
59
|
+
export declare interface IndexStats {
|
|
60
|
+
readonly synced: number;
|
|
61
|
+
readonly removed: number;
|
|
62
|
+
/** The MOST RECENT reindex across all replicas (max of each replica's). */
|
|
63
|
+
readonly lastReindexAt: string | null;
|
|
64
|
+
/** Docs seeded by that most-recent reindex. */
|
|
65
|
+
readonly lastReindexCount: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Meilisearch. Native: filters (range+negation), facets
|
|
69
|
+
* (`facets`→`facetDistribution`), highlighting (`attributesToHighlight`), typo
|
|
70
|
+
* tolerance (on by default; `fuzziness:0` disables), engine-param passthrough.
|
|
71
|
+
* DEGRADED: Meilisearch has no numeric edit-distance knob — a `fuzziness`
|
|
72
|
+
* number other than 0 falls back to its built-in typo tolerance (documented). */
|
|
73
|
+
export declare const meilisearchBackend: (cfg: VendorConfig) => SearchBackend;
|
|
74
|
+
|
|
75
|
+
/** In-process inverted-ish search: substring (or fuzzy) match over stringified
|
|
76
|
+
* doc values + structured filters. Computes facets + highlights ITSELF (it's
|
|
77
|
+
* the honest-degradation reference — every feature works, just not at scale).
|
|
78
|
+
* Not for scale — but correct + dependency-free, and the right default for
|
|
79
|
+
* dev/tests/single-process apps. */
|
|
80
|
+
export declare const memoryBackend: () => SearchBackend;
|
|
81
|
+
|
|
82
|
+
export declare const memoryStatsStore: () => StatsStore;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Compute the backend filters for a query, FAIL-CLOSED on tenant scoping.
|
|
86
|
+
* Returns `null` when the caller must see NOTHING: a tenant-scoped index
|
|
87
|
+
* (`tenantField` set) queried by a caller with no tenant (`tenantId == null`).
|
|
88
|
+
* A tenant-scoped index gets an `eq(tenantField, tenantId)` clause ANDed into
|
|
89
|
+
* whatever filters the caller passed; anon on a tenant-scoped index gets an
|
|
90
|
+
* empty result (not an unfiltered all-tenant query — the cross-tenant leak).
|
|
91
|
+
*/
|
|
92
|
+
export declare const scopeSearchFilters: (tenantField: string | undefined, tenantId: string | null, filters: ReadonlyArray<SearchFilter> | undefined) => ReadonlyArray<SearchFilter> | null;
|
|
93
|
+
|
|
94
|
+
export declare const SEARCH_STATS_TABLE = "_voltro_search_stats";
|
|
95
|
+
|
|
96
|
+
export declare interface SearchBackend {
|
|
97
|
+
readonly upsert: (index: string, docs: ReadonlyArray<SearchDoc>) => Promise<void>;
|
|
98
|
+
readonly remove: (index: string, ids: ReadonlyArray<string>) => Promise<void>;
|
|
99
|
+
readonly query: (index: string, query: SearchQuery) => Promise<SearchResult>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export declare type SearchBackendConfig = 'memory' | SearchBackend | ({
|
|
103
|
+
readonly engine: 'typesense' | 'meilisearch' | 'algolia';
|
|
104
|
+
} & VendorConfig);
|
|
105
|
+
|
|
106
|
+
/** Typed failure surfaced by a backend op. Tagged so a caller can discriminate
|
|
107
|
+
* a backend outage / bad query from a generic rpc error. `transient` marks a
|
|
108
|
+
* retry-worthy failure (network / engine unavailable) vs. a permanent one
|
|
109
|
+
* (unsupported query shape). */
|
|
110
|
+
export declare class SearchBackendError extends SearchBackendError_base<{
|
|
111
|
+
readonly engine: string;
|
|
112
|
+
readonly op: 'upsert' | 'remove' | 'query';
|
|
113
|
+
readonly message: string;
|
|
114
|
+
readonly transient: boolean;
|
|
115
|
+
}> {
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
declare const SearchBackendError_base: new <A extends Record<string, any> = {}>(args: VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => YieldableError & {
|
|
119
|
+
readonly _tag: "SearchBackendError";
|
|
120
|
+
} & Readonly<A>;
|
|
121
|
+
|
|
122
|
+
export declare interface SearchDoc {
|
|
123
|
+
readonly id: string;
|
|
124
|
+
readonly [field: string]: unknown;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export declare interface SearchFilter {
|
|
128
|
+
readonly field: string;
|
|
129
|
+
readonly op: SearchFilterOp;
|
|
130
|
+
/** Scalar for eq/neq/gt/gte/lt/lte; an array for in/nin. */
|
|
131
|
+
readonly value: SearchFilterValue | ReadonlyArray<SearchFilterValue>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** A single filter clause. `op` widens the old equality-only model to range
|
|
135
|
+
* (`gt`/`gte`/`lt`/`lte`) and negation (`neq`/`nin`). `in`/`nin` take an
|
|
136
|
+
* array value; the scalar ops take a string|number|boolean. */
|
|
137
|
+
export declare type SearchFilterOp = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'nin';
|
|
138
|
+
|
|
139
|
+
declare type SearchFilterValue = string | number | boolean;
|
|
140
|
+
|
|
141
|
+
/** One hit — the doc plus, when `highlight` was requested and the engine
|
|
142
|
+
* supports it, per-field snippet HTML. */
|
|
143
|
+
export declare interface SearchHit {
|
|
144
|
+
readonly doc: SearchDoc;
|
|
145
|
+
/** field → snippet with matched terms wrapped. Absent when highlight wasn't
|
|
146
|
+
* requested OR the engine doesn't support it (degraded — see per backend). */
|
|
147
|
+
readonly highlights?: Record<string, string>;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export declare const searchPlugin: (options: SearchPluginOptions) => VoltroPlugin;
|
|
151
|
+
|
|
152
|
+
export declare interface SearchPluginOptions {
|
|
153
|
+
readonly backend?: SearchBackendConfig;
|
|
154
|
+
/** Map of source table → index spec. */
|
|
155
|
+
readonly indexes: Record<string, IndexSpec>;
|
|
156
|
+
readonly name?: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export declare interface SearchQuery {
|
|
160
|
+
readonly q: string;
|
|
161
|
+
readonly limit?: number;
|
|
162
|
+
/** Skip this many leading hits before returning `limit` — offset paging. */
|
|
163
|
+
readonly offset?: number;
|
|
164
|
+
/** Filter clauses ANDed together — equality, range (`gt`/`lte`/…) and
|
|
165
|
+
* negation (`neq`/`nin`). Replaces the old equality-only `Record`. */
|
|
166
|
+
readonly filters?: ReadonlyArray<SearchFilter>;
|
|
167
|
+
/** Request facet counts for these fields (returned in `SearchResult.facets`). */
|
|
168
|
+
readonly facets?: ReadonlyArray<string>;
|
|
169
|
+
/** Request matched-term snippets (returned per-hit in `SearchResult.hits`). */
|
|
170
|
+
readonly highlight?: HighlightSpec;
|
|
171
|
+
/** Typo tolerance. Omit ⇒ the engine default (memory: exact substring). */
|
|
172
|
+
readonly fuzziness?: Fuzziness;
|
|
173
|
+
/** Escape hatch: forwarded VERBATIM into the engine's search-params object,
|
|
174
|
+
* shallow-merged AFTER the mapped params (so it can override them). Ignored
|
|
175
|
+
* by the memory backend (nothing to forward to). */
|
|
176
|
+
readonly engineParams?: Record<string, unknown>;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export declare interface SearchResult {
|
|
180
|
+
readonly hits: ReadonlyArray<SearchHit>;
|
|
181
|
+
/** Facet counts for the requested fields. `{}` when none requested or the
|
|
182
|
+
* engine can't facet natively (degraded — memory computes them itself). */
|
|
183
|
+
readonly facets: FacetCounts;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** `_voltro_search_stats` — one row per (indexName, replicaId). UNIQUE over the
|
|
187
|
+
* pair so each replica's create-path `insertIgnore` is idempotent and the CAS
|
|
188
|
+
* increment targets exactly its own row (no cross-replica contention). */
|
|
189
|
+
export declare const searchStatsTable: StatsTable;
|
|
190
|
+
|
|
191
|
+
/** All tables for `extendSchema.tables`. */
|
|
192
|
+
export declare const searchTables: () => ReadonlyArray<StatsTable>;
|
|
193
|
+
|
|
194
|
+
/** The stats store. All ops key on the index name; the durable impl fans out to
|
|
195
|
+
* per-replica rows internally and aggregates on read. */
|
|
196
|
+
export declare interface StatsStore {
|
|
197
|
+
/** +1 synced (an insert/update mirrored) or +1 removed (a delete). */
|
|
198
|
+
readonly bump: (index: string, kind: 'synced' | 'removed') => Promise<void>;
|
|
199
|
+
/** Record a reindex: +count synced AND set lastReindex{At,Count}. */
|
|
200
|
+
readonly recordReindex: (index: string, count: number, at: string) => Promise<void>;
|
|
201
|
+
/** Aggregated stats for one index (summed across replicas). */
|
|
202
|
+
readonly get: (index: string) => Promise<IndexStats>;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
declare interface StatsTable extends TableLike {
|
|
206
|
+
readonly fields: Record<string, ColumnDefinition<unknown>>;
|
|
207
|
+
readonly appliedIndexes: ReadonlyArray<TableIndex>;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Typesense (self-host / cloud). Native: filters (range+negation), facets,
|
|
211
|
+
* highlighting, typo tolerance (`num_typos`), engine-param passthrough. */
|
|
212
|
+
export declare const typesenseBackend: (cfg: VendorConfig) => SearchBackend;
|
|
213
|
+
|
|
214
|
+
export declare interface VendorConfig {
|
|
215
|
+
readonly url?: string;
|
|
216
|
+
readonly host?: string;
|
|
217
|
+
readonly apiKey: string;
|
|
218
|
+
/** Algolia application id. */
|
|
219
|
+
readonly appId?: string;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export { }
|