@lde/search 0.1.2 → 0.2.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/README.md +249 -119
- package/dist/adapter.d.ts +13 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +10 -0
- package/dist/engine.d.ts +124 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -0
- package/dist/project.d.ts +12 -84
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +91 -53
- package/dist/query.d.ts +87 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +89 -0
- package/dist/schema.d.ts +343 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +250 -0
- package/dist/testing.d.ts +19 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +106 -0
- package/package.json +22 -2
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import type { FramedNode } from './frame-by-type.js';
|
|
2
|
+
import type { SearchDocument } from './project.js';
|
|
3
|
+
/**
|
|
4
|
+
* The engine-neutral kind of a queryable field. It drives every downstream
|
|
5
|
+
* behavior: which physical fields the projection emits, the engine
|
|
6
|
+
* collection-schema type, the `where`/facet/sort semantics, and the GraphQL
|
|
7
|
+
* output/input type. The Typesense-vocabulary types (`string`, `int32`, …) are
|
|
8
|
+
* *derived* from this by the engine adapter, never declared here.
|
|
9
|
+
*/
|
|
10
|
+
export type FieldKind = 'text' | 'keyword' | 'integer' | 'number' | 'boolean' | 'date' | 'reference';
|
|
11
|
+
/** The `where` operator a kind accepts, or `undefined` when it is not filterable
|
|
12
|
+
* through `where` (`text` feeds the free-text `query` instead). */
|
|
13
|
+
export type FilterOperator = 'in' | 'range' | 'is';
|
|
14
|
+
/**
|
|
15
|
+
* The `where` operator a field of this kind accepts (per the ADR filter-semantics
|
|
16
|
+
* table), or `undefined` for `text` — which feeds the free-text `query` rather
|
|
17
|
+
* than `where`. The ONE source for the surface’s `where` input type, the
|
|
18
|
+
* adapter’s filter compiler and declaration validation, so they cannot drift.
|
|
19
|
+
*/
|
|
20
|
+
export declare function filterOperatorFor(kind: FieldKind): FilterOperator | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* One queryable field — the single declarative source that drives all four
|
|
23
|
+
* consumers (projection, engine collection schema, query semantics, and the
|
|
24
|
+
* GraphQL surface).
|
|
25
|
+
*
|
|
26
|
+
* A **discriminated union by `kind`**: each kind declares exactly the
|
|
27
|
+
* properties it can honour (`ref` on references, `locales` on text,
|
|
28
|
+
* `facetRanges` on numerics), so an illegal declaration fails to compile.
|
|
29
|
+
* {@link validateSearchType} enforces the same rules at runtime for
|
|
30
|
+
* declarations built outside TypeScript (a SHACL generator, plain JS).
|
|
31
|
+
*
|
|
32
|
+
* Capability flags (`searchable`/`filterable`/`facetable`/`sortable`/`output`)
|
|
33
|
+
* are independent opt-ins: a field exposes exactly the roles it declares. A
|
|
34
|
+
* field with a {@link SearchFieldBase.derive `derive`} function instead of a
|
|
35
|
+
* `path` is a **derived field** — computed from the framed node rather than
|
|
36
|
+
* projected from the IR — yet it still carries full query/schema/output
|
|
37
|
+
* behavior (e.g. `status`, the compatibility booleans).
|
|
38
|
+
*
|
|
39
|
+
* The physical field names a declaration fans out to (per-locale search/sort
|
|
40
|
+
* keys) follow one convention, owned by
|
|
41
|
+
* {@link physicalFields} so projection, collection-schema and query compiler
|
|
42
|
+
* cannot disagree.
|
|
43
|
+
*
|
|
44
|
+
* SHACL is one possible *source*, not a dependency: a generator can emit a
|
|
45
|
+
* declaration from a NodeShape + `search:` annotations
|
|
46
|
+
* (`kind`←`sh:datatype`/`sh:nodeKind`, `path`←`sh:path`, `array`←`sh:maxCount`,
|
|
47
|
+
* `locales`←`sh:languageIn` (plus `und` where plain strings are allowed),
|
|
48
|
+
* `ref`←`sh:node`/`sh:class`),
|
|
49
|
+
* and a hand-written declaration is just as valid.
|
|
50
|
+
*/
|
|
51
|
+
export type SearchField = TextField | KeywordField | ReferenceField | NumericField | BooleanField;
|
|
52
|
+
/** The declaration members every {@link SearchField} kind shares. */
|
|
53
|
+
export interface SearchFieldBase {
|
|
54
|
+
/** Logical API name; the physical fanout derives from it. Declare camelCase
|
|
55
|
+
* where it surfaces in GraphQL. */
|
|
56
|
+
readonly name: string;
|
|
57
|
+
/** Framed-IR predicate IRI to project from. Omit for a field populated by
|
|
58
|
+
* {@link SearchFieldBase.derive} (or outside the projection entirely). */
|
|
59
|
+
readonly path?: string;
|
|
60
|
+
/** Multi-valued. */
|
|
61
|
+
readonly array?: boolean;
|
|
62
|
+
/** Always present: a non-null scalar in the API output and
|
|
63
|
+
* a non-optional field in the engine index. Moot for arrays/booleans/`id`,
|
|
64
|
+
* which are non-null regardless. */
|
|
65
|
+
readonly required?: boolean;
|
|
66
|
+
/** Appears in the API output type / carries a display label. */
|
|
67
|
+
readonly output?: boolean;
|
|
68
|
+
/** Usable in `where`. */
|
|
69
|
+
readonly filterable?: boolean;
|
|
70
|
+
/** Returned as facet buckets. */
|
|
71
|
+
readonly facetable?: boolean;
|
|
72
|
+
/** Publicly selectable in `orderBy`; localized text also emits a folded sort key. */
|
|
73
|
+
readonly sortable?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Compute this field’s value instead of projecting it from a `path` — a
|
|
76
|
+
* status token, a compatibility boolean, a count over the framed node.
|
|
77
|
+
* Mutually exclusive with `path`. Runs in declaration order during
|
|
78
|
+
* projection, receiving the framed node and the document as populated so
|
|
79
|
+
* far, so a derived field may read fields declared before it (e.g. a
|
|
80
|
+
* `statusRank` reading the derived `status`). Return `undefined` to leave
|
|
81
|
+
* the field absent. The field still carries full query/schema/output
|
|
82
|
+
* behaviour like any other.
|
|
83
|
+
*/
|
|
84
|
+
readonly derive?: (node: FramedNode, document: SearchDocument) => unknown;
|
|
85
|
+
}
|
|
86
|
+
/** Full-text inclusion with a `query_by` weight (folded; per-locale for
|
|
87
|
+
* localized text). Presence is what makes a field searchable. */
|
|
88
|
+
export interface Searchable {
|
|
89
|
+
readonly searchable?: {
|
|
90
|
+
readonly weight: number;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Free-running text (prose), always multilingual in shape: projected per
|
|
95
|
+
* locale into display/search/sort companions. `locales` lists the language
|
|
96
|
+
* tags to emit; the reserved locale **`und`** (JSON-LD `@none`, RDF `und`)
|
|
97
|
+
* buckets untagged literals, so a monolingual or untagged corpus declares
|
|
98
|
+
* `locales: ['und']` and mixed data `['nl', 'und']` — one mechanism, and
|
|
99
|
+
* adding a language later is additive (the API output shape never changes).
|
|
100
|
+
* Declaring a real language is RECOMMENDED where the data has one: it drives
|
|
101
|
+
* the engine’s per-locale stemming; `und` is folded but unstemmed (unless the
|
|
102
|
+
* deployment’s `defaultLocale` opts in). Feeds the free-text query rather
|
|
103
|
+
* than `where`/facets, so it is deliberately not filterable or facetable. Use
|
|
104
|
+
* {@link KeywordField} only for exact-match tokens, never for prose.
|
|
105
|
+
*/
|
|
106
|
+
export interface TextField extends SearchFieldBase, Searchable {
|
|
107
|
+
readonly kind: 'text';
|
|
108
|
+
/** The locales to emit (per-locale fanout); at least one. `und` = untagged. */
|
|
109
|
+
readonly locales: readonly string[];
|
|
110
|
+
readonly filterable?: never;
|
|
111
|
+
readonly facetable?: never;
|
|
112
|
+
readonly facetRanges?: never;
|
|
113
|
+
}
|
|
114
|
+
/** An exact-match token or free string: filtered by membership, faceted per
|
|
115
|
+
* value, searchable folded. */
|
|
116
|
+
export interface KeywordField extends SearchFieldBase, Searchable {
|
|
117
|
+
readonly kind: 'keyword';
|
|
118
|
+
/** Projection-time value transform (e.g. strip a media-type prefix). */
|
|
119
|
+
readonly transform?: (value: string) => string;
|
|
120
|
+
readonly facetRanges?: never;
|
|
121
|
+
}
|
|
122
|
+
/** An IRI-valued reference to another entity, label-resolved at the surface. */
|
|
123
|
+
export interface ReferenceField extends SearchFieldBase, Searchable {
|
|
124
|
+
readonly kind: 'reference';
|
|
125
|
+
/** Projection-time value transform. */
|
|
126
|
+
readonly transform?: (value: string) => string;
|
|
127
|
+
readonly facetRanges?: never;
|
|
128
|
+
/** The referenced entity’s shape and how much of it to carry. Required when
|
|
129
|
+
* the field is `output` (the API surfaces need the reference type name);
|
|
130
|
+
* optional for a facet- or filter-only reference. */
|
|
131
|
+
readonly ref?: {
|
|
132
|
+
/** Logical API type name of the referenced entity (PascalCase, e.g.
|
|
133
|
+
* `Organization`) — names the reference’s type in the API surfaces, the
|
|
134
|
+
* way {@link SearchType.name} names a root type; fields sharing it share
|
|
135
|
+
* one emitted type. A name, not a key: it need not correspond to any
|
|
136
|
+
* indexed root type (and until cross-collection references exist, it must
|
|
137
|
+
* not collide with one). */
|
|
138
|
+
readonly typeName: string;
|
|
139
|
+
/** How much of the referenced entity the reference carries. Only
|
|
140
|
+
* `labelOnly` (id + display label) is implemented; `idOnly` and `inline`
|
|
141
|
+
* are forward declarations, so that declarations (and the SHACL
|
|
142
|
+
* `search:nestedStrategy` mapping) keep their shape when those land, and
|
|
143
|
+
* `inline` can then add fields to the reference type additively. */
|
|
144
|
+
readonly strategy: 'labelOnly' | 'idOnly' | 'inline';
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Range-facet bins for a numeric (`integer`/`number`/`date`) facetable field.
|
|
149
|
+
* When set, the field facets into these fixed half-open `[min, max)` ranges (a
|
|
150
|
+
* histogram) rather than one bucket per distinct value — the per-bucket counts
|
|
151
|
+
* a UI slider needs. Bins are query-time only (no index impact) and
|
|
152
|
+
* engine-neutral: the Typesense adapter emits a `facet_by` range, an
|
|
153
|
+
* OpenSearch adapter a `range` aggregation. See {@link FacetRange}.
|
|
154
|
+
*/
|
|
155
|
+
export interface RangeFacetable {
|
|
156
|
+
readonly facetRanges?: readonly FacetRange[];
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* A numeric value: range-filtered, range- or value-faceted, sortable.
|
|
160
|
+
* `integer` is a whole number, `number` a float, `date` a point in time
|
|
161
|
+
* (ISO 8601 at the edges, Unix seconds in the index) — identical capabilities,
|
|
162
|
+
* so one interface serves all three kinds; `field.kind` still narrows.
|
|
163
|
+
*/
|
|
164
|
+
export interface NumericField extends SearchFieldBase, RangeFacetable {
|
|
165
|
+
readonly kind: 'integer' | 'number' | 'date';
|
|
166
|
+
readonly searchable?: never;
|
|
167
|
+
}
|
|
168
|
+
/** A boolean flag; absent in a document means `false`. */
|
|
169
|
+
export interface BooleanField extends SearchFieldBase {
|
|
170
|
+
readonly kind: 'boolean';
|
|
171
|
+
readonly searchable?: never;
|
|
172
|
+
readonly facetRanges?: never;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* One half-open `[min, max)` range-facet bin: `min` inclusive, `max` exclusive,
|
|
176
|
+
* so contiguous bins partition cleanly with no boundary double-counting. Omit
|
|
177
|
+
* `min` (or `max`) for an open-ended bin (`< max`, resp. `≥ min`). `key` is the
|
|
178
|
+
* bucket’s stable label, echoed back as the {@link FacetBucket} `value`.
|
|
179
|
+
*/
|
|
180
|
+
export interface FacetRange {
|
|
181
|
+
readonly key: string;
|
|
182
|
+
readonly min?: number;
|
|
183
|
+
readonly max?: number;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* One root type’s complete search declaration: its logical API `name`, the
|
|
187
|
+
* `type` IRI its documents are instances of, and the queryable `fields`
|
|
188
|
+
* (including {@link SearchField.derive derived} ones). A SHACL generator can
|
|
189
|
+
* emit one per NodeShape (`name`←`sh:name`/local name, `type`←`sh:targetClass`,
|
|
190
|
+
* `fields`←its property shapes), but that is a source, not a requirement.
|
|
191
|
+
*/
|
|
192
|
+
export interface SearchType {
|
|
193
|
+
/** Logical API name (PascalCase, e.g. `Dataset`) — names the type in the API
|
|
194
|
+
* surfaces (GraphQL type names, a REST path), the way each field’s
|
|
195
|
+
* {@link SearchField.name} names that field. Deliberately declared rather
|
|
196
|
+
* than derived from the `type` IRI, so re-modelling the vocabulary cannot
|
|
197
|
+
* silently rename the public contract. */
|
|
198
|
+
readonly name: string;
|
|
199
|
+
readonly type: string;
|
|
200
|
+
readonly fields: readonly SearchField[];
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Declare a {@link SearchType}, capturing it as a literal: the `const` type
|
|
204
|
+
* parameter preserves the field names and capability flags that the type-level
|
|
205
|
+
* helpers (`FacetFieldsOf`, `OutputFieldsOf`) read off the type —
|
|
206
|
+
* with none of the widening a plain `: SearchType` annotation causes and
|
|
207
|
+
* without having to remember `as const satisfies SearchType`. Identity at
|
|
208
|
+
* runtime.
|
|
209
|
+
*/
|
|
210
|
+
export declare function defineSearchType<const Type extends SearchType>(searchType: Type): Type;
|
|
211
|
+
/**
|
|
212
|
+
* The complete search declaration of a deployment: every root {@link SearchType},
|
|
213
|
+
* keyed by its `type` IRI. Build one with {@link searchSchema}, which captures
|
|
214
|
+
* the declared types as a literal tuple (`Types`), so schema-bound consumers
|
|
215
|
+
* (the engine port) can type their per-type behaviour off it. A plain
|
|
216
|
+
* `: SearchSchema` annotation widens gracefully to `SearchType`.
|
|
217
|
+
*/
|
|
218
|
+
/** Brand for {@link SearchSchema}: type-only, no runtime existence. Makes the
|
|
219
|
+
* schema NOMINAL — a hand-built `Map` is not assignable, so `searchSchema()`
|
|
220
|
+
* (which validates) is the only way to obtain one and downstream consumers
|
|
221
|
+
* need no defensive re-validation. */
|
|
222
|
+
export declare const validSearchSchema: unique symbol;
|
|
223
|
+
export interface SearchSchema<Types extends readonly SearchType[] = readonly SearchType[]> extends ReadonlyMap<string, Types[number]> {
|
|
224
|
+
readonly [validSearchSchema]: true;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Build a {@link SearchSchema} from root-type declarations, keyed by `type`.
|
|
228
|
+
*
|
|
229
|
+
* Every declaration is validated ({@link assertValidSearchType}) — the
|
|
230
|
+
* declaration-time counterpart of the port’s `assertValidQuery` — and the
|
|
231
|
+
* schema-wide invariants are enforced: no two types may share a `type` IRI
|
|
232
|
+
* (they would silently overwrite each other in the map) or a `name` (names
|
|
233
|
+
* key the API surfaces). Throws on the first invalid declaration, so a bad
|
|
234
|
+
* schema fails at startup, not per document at index time or per query.
|
|
235
|
+
*/
|
|
236
|
+
export declare function searchSchema<const Types extends readonly SearchType[]>(...types: Types): SearchSchema<Types>;
|
|
237
|
+
/**
|
|
238
|
+
* One structural problem {@link validateSearchType} found: a field declares a
|
|
239
|
+
* capability or property its `kind` cannot honour, or the declaration is
|
|
240
|
+
* internally inconsistent. Each reason names the field-level rule it violates;
|
|
241
|
+
* the rules mirror the per-kind semantics table in the README.
|
|
242
|
+
*/
|
|
243
|
+
export interface SearchTypeIssue {
|
|
244
|
+
readonly field: string;
|
|
245
|
+
readonly reason: 'duplicate-field-name' | 'missing-ref' | 'ref-not-allowed' | 'text-requires-locales' | 'locales-not-allowed' | 'facet-ranges-not-allowed' | 'searchable-not-allowed' | 'transform-not-allowed' | 'derive-with-path' | 'text-not-filterable' | 'text-not-facetable';
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Structurally validate one {@link SearchType} declaration — the
|
|
249
|
+
* declaration-time counterpart of `validateQuery`. Rules:
|
|
250
|
+
*
|
|
251
|
+
* - field names are unique (a duplicate would silently shadow in every
|
|
252
|
+
* consumer, each picking a different winner);
|
|
253
|
+
* - a `reference` field that is `output` declares `ref` (the API surfaces
|
|
254
|
+
* need the reference type name); `ref` on any other kind is meaningless;
|
|
255
|
+
* - a `text` field declares at least one locale (`und` = untagged; projection and
|
|
256
|
+
* result reconstruction have no representation for unlocalized text — use
|
|
257
|
+
* `keyword` for untagged strings); `locales` on any other kind is
|
|
258
|
+
* meaningless;
|
|
259
|
+
* - a kind without a `where` operator (`text` — it feeds the free-text query)
|
|
260
|
+
* is neither `filterable` nor `facetable`;
|
|
261
|
+
* - `facetRanges` only on the `range`-operator kinds (`integer`/`number`/`date`);
|
|
262
|
+
* - `searchable` only on `text`/`keyword`/`reference` (projection emits no
|
|
263
|
+
* folded search field for the other kinds);
|
|
264
|
+
* - `transform` only on `keyword`/`reference` (the only kinds whose
|
|
265
|
+
* projection applies it);
|
|
266
|
+
* - `derive` and `path` are mutually exclusive (a field is projected or
|
|
267
|
+
* computed, never both).
|
|
268
|
+
*
|
|
269
|
+
* Pure and total: returns every issue rather than throwing;
|
|
270
|
+
* {@link assertValidSearchType} is the throwing entry point.
|
|
271
|
+
*/
|
|
272
|
+
export declare function validateSearchType(searchType: SearchType): readonly SearchTypeIssue[];
|
|
273
|
+
/**
|
|
274
|
+
* Throw when `searchType` is not a member of `schema` — the port membership
|
|
275
|
+
* guard every engine adapter applies before searching, so a query can never
|
|
276
|
+
* meet an index the deployment did not declare. Identity-based: the exact
|
|
277
|
+
* declaration object must be in the schema, not a lookalike.
|
|
278
|
+
*/
|
|
279
|
+
export declare function assertTypeInSchema(schema: SearchSchema, searchType: SearchType): void;
|
|
280
|
+
/** Throw on a structurally invalid {@link SearchType} ({@link validateSearchType}),
|
|
281
|
+
* naming every issue. Called by {@link searchSchema} for each declaration. */
|
|
282
|
+
export declare function assertValidSearchType(searchType: SearchType): void;
|
|
283
|
+
/**
|
|
284
|
+
* The physical engine fields one {@link SearchField} fans out into, grouped by
|
|
285
|
+
* the role each plays. The single source of truth for the naming convention, so
|
|
286
|
+
* the projection (writes them), the collection schema (declares them) and the
|
|
287
|
+
* query compiler (reads them) cannot disagree.
|
|
288
|
+
*/
|
|
289
|
+
export interface PhysicalFields {
|
|
290
|
+
/** Per-locale output labels `${name}_${locale}` (localized text, `output`). */
|
|
291
|
+
readonly display: readonly string[];
|
|
292
|
+
/** Folded match fields: `${name}_search_${locale}` per locale (localized) or a
|
|
293
|
+
* single `${name}_search` (non-localized), when `searchable`. */
|
|
294
|
+
readonly search: readonly string[];
|
|
295
|
+
/** Per-locale folded sort keys `${name}_sort_${locale}` (localized text,
|
|
296
|
+
* `sortable`); a non-localized field sorts on its own `name` field. */
|
|
297
|
+
readonly sort: readonly string[];
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Full-text searchable fields, highest `query_by` weight first — the order the
|
|
301
|
+
* engine adapter weights `query_by` in. A field is searchable iff it carries a
|
|
302
|
+
* `searchable` weight.
|
|
303
|
+
*/
|
|
304
|
+
export declare function searchableFields(searchType: SearchType): readonly (SearchField & {
|
|
305
|
+
readonly searchable: {
|
|
306
|
+
readonly weight: number;
|
|
307
|
+
};
|
|
308
|
+
})[];
|
|
309
|
+
/** Fields returned as facet buckets, in declaration order. */
|
|
310
|
+
export declare function facetableFields(searchType: SearchType): readonly SearchField[];
|
|
311
|
+
/** Fields usable in `where`, in declaration order. */
|
|
312
|
+
export declare function filterableFields(searchType: SearchType): readonly SearchField[];
|
|
313
|
+
/** Fields publicly selectable in `orderBy`, in declaration order. */
|
|
314
|
+
export declare function sortableFields(searchType: SearchType): readonly SearchField[];
|
|
315
|
+
/** Fields that appear in the API output type, in declaration order. */
|
|
316
|
+
export declare function outputFields(searchType: SearchType): readonly SearchField[];
|
|
317
|
+
/** Fields of kind `reference` (IRI-valued, label-resolved), in declaration order. */
|
|
318
|
+
export declare function referenceFields(searchType: SearchType): readonly SearchField[];
|
|
319
|
+
/** Look up a field by its logical name. */
|
|
320
|
+
export declare function fieldNamed(searchType: SearchType, name: string): SearchField | undefined;
|
|
321
|
+
/**
|
|
322
|
+
* Whether a facet on this field returns fixed range bins (a histogram) rather
|
|
323
|
+
* than one bucket per distinct value: it declares non-empty
|
|
324
|
+
* {@link RangeFacetable.facetRanges}. One predicate for the surface’s facet
|
|
325
|
+
* type, the adapter’s facet clause and the bucket reconstruction, so they
|
|
326
|
+
* cannot disagree.
|
|
327
|
+
*/
|
|
328
|
+
export declare function isRangeFacet(field: SearchField): field is NumericField & {
|
|
329
|
+
readonly facetRanges: readonly FacetRange[];
|
|
330
|
+
};
|
|
331
|
+
/**
|
|
332
|
+
* The engine storage codec for `date` fields: stored as Unix seconds (a
|
|
333
|
+
* sortable, range-filterable int64), ISO 8601 at the API edges. One pair for
|
|
334
|
+
* the projection (writes), the query compiler (filter bounds) and the surface
|
|
335
|
+
* (output), so the three cannot disagree. Returns `undefined` for an
|
|
336
|
+
* unparseable value.
|
|
337
|
+
*/
|
|
338
|
+
export declare function isoToUnixSeconds(iso: string): number | undefined;
|
|
339
|
+
/** The inverse of {@link isoToUnixSeconds}: stored Unix seconds → ISO 8601. */
|
|
340
|
+
export declare function unixSecondsToIso(seconds: number): string;
|
|
341
|
+
/** Derive the physical engine field names a declaration produces. */
|
|
342
|
+
export declare function physicalFields(field: SearchField): PhysicalFields;
|
|
343
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
const OPERATOR_BY_KIND = {
|
|
2
|
+
text: undefined,
|
|
3
|
+
keyword: 'in',
|
|
4
|
+
reference: 'in',
|
|
5
|
+
integer: 'range',
|
|
6
|
+
number: 'range',
|
|
7
|
+
date: 'range',
|
|
8
|
+
boolean: 'is',
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* The `where` operator a field of this kind accepts (per the ADR filter-semantics
|
|
12
|
+
* table), or `undefined` for `text` — which feeds the free-text `query` rather
|
|
13
|
+
* than `where`. The ONE source for the surface’s `where` input type, the
|
|
14
|
+
* adapter’s filter compiler and declaration validation, so they cannot drift.
|
|
15
|
+
*/
|
|
16
|
+
export function filterOperatorFor(kind) {
|
|
17
|
+
return OPERATOR_BY_KIND[kind];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Declare a {@link SearchType}, capturing it as a literal: the `const` type
|
|
21
|
+
* parameter preserves the field names and capability flags that the type-level
|
|
22
|
+
* helpers (`FacetFieldsOf`, `OutputFieldsOf`) read off the type —
|
|
23
|
+
* with none of the widening a plain `: SearchType` annotation causes and
|
|
24
|
+
* without having to remember `as const satisfies SearchType`. Identity at
|
|
25
|
+
* runtime.
|
|
26
|
+
*/
|
|
27
|
+
export function defineSearchType(searchType) {
|
|
28
|
+
return searchType;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Build a {@link SearchSchema} from root-type declarations, keyed by `type`.
|
|
32
|
+
*
|
|
33
|
+
* Every declaration is validated ({@link assertValidSearchType}) — the
|
|
34
|
+
* declaration-time counterpart of the port’s `assertValidQuery` — and the
|
|
35
|
+
* schema-wide invariants are enforced: no two types may share a `type` IRI
|
|
36
|
+
* (they would silently overwrite each other in the map) or a `name` (names
|
|
37
|
+
* key the API surfaces). Throws on the first invalid declaration, so a bad
|
|
38
|
+
* schema fails at startup, not per document at index time or per query.
|
|
39
|
+
*/
|
|
40
|
+
export function searchSchema(...types) {
|
|
41
|
+
const typeIris = new Set();
|
|
42
|
+
const names = new Set();
|
|
43
|
+
for (const searchType of types) {
|
|
44
|
+
assertValidSearchType(searchType);
|
|
45
|
+
if (typeIris.has(searchType.type)) {
|
|
46
|
+
throw new Error(`Duplicate search type IRI “${searchType.type}”; each SearchType must declare a distinct type.`);
|
|
47
|
+
}
|
|
48
|
+
if (names.has(searchType.name)) {
|
|
49
|
+
throw new Error(`Duplicate search type name “${searchType.name}”; each SearchType must declare a distinct name.`);
|
|
50
|
+
}
|
|
51
|
+
typeIris.add(searchType.type);
|
|
52
|
+
names.add(searchType.name);
|
|
53
|
+
}
|
|
54
|
+
// The one blessed cast: only this validated constructor mints the brand.
|
|
55
|
+
return new Map(types.map((searchType) => [searchType.type, searchType]));
|
|
56
|
+
}
|
|
57
|
+
/** Kinds that can feed full-text search (project a folded search field). */
|
|
58
|
+
const SEARCHABLE_KINDS = ['text', 'keyword', 'reference'];
|
|
59
|
+
/** Kinds whose projection applies the {@link KeywordField.transform}. */
|
|
60
|
+
const TRANSFORMABLE_KINDS = ['keyword', 'reference'];
|
|
61
|
+
/**
|
|
62
|
+
* Structurally validate one {@link SearchType} declaration — the
|
|
63
|
+
* declaration-time counterpart of `validateQuery`. Rules:
|
|
64
|
+
*
|
|
65
|
+
* - field names are unique (a duplicate would silently shadow in every
|
|
66
|
+
* consumer, each picking a different winner);
|
|
67
|
+
* - a `reference` field that is `output` declares `ref` (the API surfaces
|
|
68
|
+
* need the reference type name); `ref` on any other kind is meaningless;
|
|
69
|
+
* - a `text` field declares at least one locale (`und` = untagged; projection and
|
|
70
|
+
* result reconstruction have no representation for unlocalized text — use
|
|
71
|
+
* `keyword` for untagged strings); `locales` on any other kind is
|
|
72
|
+
* meaningless;
|
|
73
|
+
* - a kind without a `where` operator (`text` — it feeds the free-text query)
|
|
74
|
+
* is neither `filterable` nor `facetable`;
|
|
75
|
+
* - `facetRanges` only on the `range`-operator kinds (`integer`/`number`/`date`);
|
|
76
|
+
* - `searchable` only on `text`/`keyword`/`reference` (projection emits no
|
|
77
|
+
* folded search field for the other kinds);
|
|
78
|
+
* - `transform` only on `keyword`/`reference` (the only kinds whose
|
|
79
|
+
* projection applies it);
|
|
80
|
+
* - `derive` and `path` are mutually exclusive (a field is projected or
|
|
81
|
+
* computed, never both).
|
|
82
|
+
*
|
|
83
|
+
* Pure and total: returns every issue rather than throwing;
|
|
84
|
+
* {@link assertValidSearchType} is the throwing entry point.
|
|
85
|
+
*/
|
|
86
|
+
export function validateSearchType(searchType) {
|
|
87
|
+
const issues = [];
|
|
88
|
+
const seen = new Set();
|
|
89
|
+
for (const declared of searchType.fields) {
|
|
90
|
+
// Validation guards declarations built OUTSIDE TypeScript (a SHACL
|
|
91
|
+
// generator, plain JS), so it inspects the uniform flat shape rather
|
|
92
|
+
// than trusting the discriminated union.
|
|
93
|
+
const field = declared;
|
|
94
|
+
const issue = (reason) => issues.push({ field: field.name, reason });
|
|
95
|
+
if (seen.has(field.name)) {
|
|
96
|
+
issue('duplicate-field-name');
|
|
97
|
+
}
|
|
98
|
+
seen.add(field.name);
|
|
99
|
+
if (field.kind === 'reference') {
|
|
100
|
+
if (field.output === true && field.ref === undefined) {
|
|
101
|
+
issue('missing-ref');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else if (field.ref !== undefined) {
|
|
105
|
+
issue('ref-not-allowed');
|
|
106
|
+
}
|
|
107
|
+
if (field.kind === 'text') {
|
|
108
|
+
if ((field.locales ?? []).length === 0) {
|
|
109
|
+
issue('text-requires-locales');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else if (field.locales !== undefined) {
|
|
113
|
+
issue('locales-not-allowed');
|
|
114
|
+
}
|
|
115
|
+
// Derived from the kind→operator table, so validation, the surfaces and
|
|
116
|
+
// the compilers cannot disagree: a kind without a `where` operator (text)
|
|
117
|
+
// is neither filterable nor facetable, and only `range` kinds bin.
|
|
118
|
+
if (field.filterable === true &&
|
|
119
|
+
filterOperatorFor(field.kind) === undefined) {
|
|
120
|
+
issue('text-not-filterable');
|
|
121
|
+
}
|
|
122
|
+
if (field.facetable === true &&
|
|
123
|
+
filterOperatorFor(field.kind) === undefined) {
|
|
124
|
+
issue('text-not-facetable');
|
|
125
|
+
}
|
|
126
|
+
if (field.facetRanges !== undefined &&
|
|
127
|
+
filterOperatorFor(field.kind) !== 'range') {
|
|
128
|
+
issue('facet-ranges-not-allowed');
|
|
129
|
+
}
|
|
130
|
+
if (field.searchable !== undefined &&
|
|
131
|
+
!SEARCHABLE_KINDS.includes(field.kind)) {
|
|
132
|
+
issue('searchable-not-allowed');
|
|
133
|
+
}
|
|
134
|
+
if (field.transform !== undefined &&
|
|
135
|
+
!TRANSFORMABLE_KINDS.includes(field.kind)) {
|
|
136
|
+
issue('transform-not-allowed');
|
|
137
|
+
}
|
|
138
|
+
if (field.derive !== undefined && field.path !== undefined) {
|
|
139
|
+
issue('derive-with-path');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return issues;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Throw when `searchType` is not a member of `schema` — the port membership
|
|
146
|
+
* guard every engine adapter applies before searching, so a query can never
|
|
147
|
+
* meet an index the deployment did not declare. Identity-based: the exact
|
|
148
|
+
* declaration object must be in the schema, not a lookalike.
|
|
149
|
+
*/
|
|
150
|
+
export function assertTypeInSchema(schema, searchType) {
|
|
151
|
+
if (schema.get(searchType.type) !== searchType) {
|
|
152
|
+
throw new Error(`Search type “${searchType.name}” is not in this engine’s schema; it serves ${[
|
|
153
|
+
...schema.values(),
|
|
154
|
+
]
|
|
155
|
+
.map((declared) => `“${declared.name}”`)
|
|
156
|
+
.join(', ')}.`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** Throw on a structurally invalid {@link SearchType} ({@link validateSearchType}),
|
|
160
|
+
* naming every issue. Called by {@link searchSchema} for each declaration. */
|
|
161
|
+
export function assertValidSearchType(searchType) {
|
|
162
|
+
const issues = validateSearchType(searchType);
|
|
163
|
+
if (issues.length > 0) {
|
|
164
|
+
const detail = issues
|
|
165
|
+
.map((issue) => `“${issue.field}” (${issue.reason})`)
|
|
166
|
+
.join(', ');
|
|
167
|
+
throw new Error(`Invalid search type “${searchType.name}”: ${detail}.`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Full-text searchable fields, highest `query_by` weight first — the order the
|
|
172
|
+
* engine adapter weights `query_by` in. A field is searchable iff it carries a
|
|
173
|
+
* `searchable` weight.
|
|
174
|
+
*/
|
|
175
|
+
export function searchableFields(searchType) {
|
|
176
|
+
return searchType.fields
|
|
177
|
+
.filter((field) => field.searchable !== undefined)
|
|
178
|
+
.sort((left, right) => right.searchable.weight - left.searchable.weight);
|
|
179
|
+
}
|
|
180
|
+
/** Fields returned as facet buckets, in declaration order. */
|
|
181
|
+
export function facetableFields(searchType) {
|
|
182
|
+
return searchType.fields.filter((field) => field.facetable === true);
|
|
183
|
+
}
|
|
184
|
+
/** Fields usable in `where`, in declaration order. */
|
|
185
|
+
export function filterableFields(searchType) {
|
|
186
|
+
return searchType.fields.filter((field) => field.filterable === true);
|
|
187
|
+
}
|
|
188
|
+
/** Fields publicly selectable in `orderBy`, in declaration order. */
|
|
189
|
+
export function sortableFields(searchType) {
|
|
190
|
+
return searchType.fields.filter((field) => field.sortable === true);
|
|
191
|
+
}
|
|
192
|
+
/** Fields that appear in the API output type, in declaration order. */
|
|
193
|
+
export function outputFields(searchType) {
|
|
194
|
+
return searchType.fields.filter((field) => field.output === true);
|
|
195
|
+
}
|
|
196
|
+
/** Fields of kind `reference` (IRI-valued, label-resolved), in declaration order. */
|
|
197
|
+
export function referenceFields(searchType) {
|
|
198
|
+
return searchType.fields.filter((field) => field.kind === 'reference');
|
|
199
|
+
}
|
|
200
|
+
/** Look up a field by its logical name. */
|
|
201
|
+
export function fieldNamed(searchType, name) {
|
|
202
|
+
return searchType.fields.find((field) => field.name === name);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Whether a facet on this field returns fixed range bins (a histogram) rather
|
|
206
|
+
* than one bucket per distinct value: it declares non-empty
|
|
207
|
+
* {@link RangeFacetable.facetRanges}. One predicate for the surface’s facet
|
|
208
|
+
* type, the adapter’s facet clause and the bucket reconstruction, so they
|
|
209
|
+
* cannot disagree.
|
|
210
|
+
*/
|
|
211
|
+
export function isRangeFacet(field) {
|
|
212
|
+
return field.facetRanges !== undefined && field.facetRanges.length > 0;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* The engine storage codec for `date` fields: stored as Unix seconds (a
|
|
216
|
+
* sortable, range-filterable int64), ISO 8601 at the API edges. One pair for
|
|
217
|
+
* the projection (writes), the query compiler (filter bounds) and the surface
|
|
218
|
+
* (output), so the three cannot disagree. Returns `undefined` for an
|
|
219
|
+
* unparseable value.
|
|
220
|
+
*/
|
|
221
|
+
export function isoToUnixSeconds(iso) {
|
|
222
|
+
const millis = new Date(iso).getTime();
|
|
223
|
+
return Number.isNaN(millis) ? undefined : Math.trunc(millis / 1000);
|
|
224
|
+
}
|
|
225
|
+
/** The inverse of {@link isoToUnixSeconds}: stored Unix seconds → ISO 8601. */
|
|
226
|
+
export function unixSecondsToIso(seconds) {
|
|
227
|
+
return new Date(seconds * 1000).toISOString();
|
|
228
|
+
}
|
|
229
|
+
/** Derive the physical engine field names a declaration produces. */
|
|
230
|
+
export function physicalFields(field) {
|
|
231
|
+
if (field.kind === 'text') {
|
|
232
|
+
const locales = field.locales;
|
|
233
|
+
return {
|
|
234
|
+
display: field.output
|
|
235
|
+
? locales.map((locale) => `${field.name}_${locale}`)
|
|
236
|
+
: [],
|
|
237
|
+
search: field.searchable
|
|
238
|
+
? locales.map((locale) => `${field.name}_search_${locale}`)
|
|
239
|
+
: [],
|
|
240
|
+
sort: field.sortable
|
|
241
|
+
? locales.map((locale) => `${field.name}_sort_${locale}`)
|
|
242
|
+
: [],
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
display: [],
|
|
247
|
+
search: field.searchable !== undefined ? [`${field.name}_search`] : [],
|
|
248
|
+
sort: [],
|
|
249
|
+
};
|
|
250
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { SearchEngine } from './engine.js';
|
|
2
|
+
/**
|
|
3
|
+
* The executable {@link SearchEngine} port contract (import from
|
|
4
|
+
* `@lde/search/testing`): a vitest suite every engine adapter runs against a
|
|
5
|
+
* live instance of itself, so the port rules hold by test rather than by
|
|
6
|
+
* prose. Covers schema binding (a type outside the bound schema is rejected),
|
|
7
|
+
* the always-on query validation (a structurally invalid query is rejected
|
|
8
|
+
* before it reaches the engine) and the result shape of a browse query — for
|
|
9
|
+
* every type in the schema.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* describeSearchEngineContract('TypesenseSearchEngine', () => engine);
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* `engine` is a thunk so the instance may be created in a `beforeAll` (e.g.
|
|
16
|
+
* against a testcontainer); it is called inside each test.
|
|
17
|
+
*/
|
|
18
|
+
export declare function describeSearchEngineContract(name: string, engine: () => SearchEngine): void;
|
|
19
|
+
//# sourceMappingURL=testing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAShD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,YAAY,GACzB,IAAI,CAqFN"}
|