@lde/search 0.12.2 → 0.13.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 CHANGED
@@ -6,400 +6,12 @@ a running search engine needs is derived from it: the document projection, the
6
6
  engine collection definition, the query semantics, and the API surface. All these
7
7
  are kept in sync automatically rather than handwritten per deployment.
8
8
 
9
- The core itself is **engine-, API- and domain-agnostic**: it bakes in no search
10
- engine, no API protocol, and no domain vocabulary. The engine- and API-specific
11
- halves are adapters that plug into the ports defined here:
9
+ ## Installation
12
10
 
13
- - **engine adapters** implement the `SearchEngine` port:
14
- [`@lde/search-typesense`](../search-typesense), with OpenSearch to follow;
15
- - **API surfaces** drive it, parsing client input into `search(SearchQuery)`:
16
- [`@lde/search-api-graphql`](../search-api-graphql), with a REST
17
- surface to follow.
18
-
19
- The library never names your domain: the same core drives a `Dataset`,
20
- `Person`, or `CreativeWork` search.
21
-
22
- It provides four things:
23
-
24
- - **unified field model** – `SearchField` / `SearchType` / `SearchSchema`:
25
- one declaration per field that drives all four consumers below, so they
26
- cannot drift;
27
- - **neutral query IR** – `SearchQuery` / `Filter` / `Sort` + filter
28
- semantics: every API surface compiles into it, every engine adapter compiles
29
- out of it, so the two cannot drift;
30
- - **engine port** – `SearchEngine` and the logical result types
31
- (`SearchResult` / `SearchHit` / `ResultDocument` / `Reference` / …);
32
- - **streaming projection** – `projectRoots`, RDF `CONSTRUCT` quads → flat
33
- search documents, one root type at a time.
34
-
35
- ```
36
- SearchSchema ─┬─► projection (projectRoots → flat documents) [here]
37
- ├─► engine adapter (collection definition + query compiler) e.g. @lde/search-typesense
38
- ├─► query semantics (SearchQuery, filter/sort/facet) [here]
39
- └─► API surface (GraphQL / REST) e.g. @lde/search-api-graphql
40
- ```
41
-
42
- At runtime, everything those consumers do is a **pure transformation between
43
- data shapes**, each one parameterised by the schema – three chains, meeting at
44
- the engine:
45
-
46
- ```
47
- indexing: RDF quads ──frame──► FramedNode ──project──► SearchDocument ──import──► engine
48
- querying: client input ──parse──► SearchQuery ──compile──► engine query
49
- results: engine response ──parse──► SearchResult ──shape──► API output
50
- ```
51
-
52
- Validation happens before the first arrow (SHACL over the RDF) and inside the
53
- last (the engine enforces its collection definition); between them every stage is
54
- a typed, deterministic function – easy to test, and swappable per deployment.
55
-
56
- ## Entry points
57
-
58
- Exports are stratified by audience:
59
-
60
- - **`@lde/search`** – the authoring surface: `defineSearchType`,
61
- `searchSchema`, `projectRoots`, validation, and every model/query/result type.
62
- - **`@lde/search/adapter`** – plumbing for engine adapters and API surfaces:
63
- `physicalFields`, the field selectors, `assertValidQuery`, the filter
64
- operators and storage codecs.
65
- - **`@lde/search/module`** – `loadSchemaModule`, the Node-only loader for a
66
- mounted schema-declaration module (an `.mjs` default-exporting declarations
67
- as plain data). The one loader both the indexer image and the served-API
68
- image boot from, so the write and the read side cannot disagree about the
69
- schema.
70
- - **`@lde/search/testing`** – `describeSearchEngineContract`, the executable
71
- port contract every engine adapter runs against a live instance of itself
72
- (vitest; optional peer).
73
-
74
- ## Terminology
75
-
76
- The model has three levels, with analogues in SHACL ([one possible source](#why-a-declarative-model))
77
- and GraphQL (one of the surfaces):
78
-
79
- | Term | What it is | SHACL | GraphQL |
80
- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ----------- |
81
- | `SearchField` | One queryable field: a `kind`, the IR `path` it projects from, and the capability flags it opts into | property shape | field |
82
- | `SearchType` | One type’s complete declaration: its logical API `name` and fields (incl. derived). A **Root Type** declares a `class` (indexed, keys the schema); a **Reference Type** declares none (reached only through an inline reference) | NodeShape | object type |
83
- | `SearchSchema` | The whole search declaration: every Root Type, keyed by `class` IRI, plus the Reference Types – build one with `searchSchema(...types)` | shapes graph | schema |
84
-
85
- A `SearchType` is a **Root Type** or a **Reference Type**, told apart by one
86
- absence: a Root Type declares a `class`, so roots are selected for it and a
87
- writer opens a collection for it; a Reference Type declares none, so it is
88
- never selected, framed by type or indexed – its identity is its name, and its
89
- type comes from the edge that points at it. `searchSchema` partitions its
90
- arguments accordingly: Root Types key the class map (`schema.values()` yields
91
- only them, so no writer ever opens a collection for a Reference Type), Reference
92
- Types go into a name index an inline `ref.typeName` resolves against. The
93
- absence is load-bearing, and enforced at the type level – an indexed Reference
94
- Type fails to compile ([ADR 11](../../docs/decisions/0011-decouple-rdf-depth-from-the-api-surface.md)).
95
-
96
- `projectRoots` and the engine port each execute one `SearchType` at a time –
97
- projection over the roots the pipeline selector supplied for that type; the
98
- GraphQL surface consumes the whole `SearchSchema`.
99
-
100
- ### API conventions
101
-
102
- Two conventions hold across the whole family:
103
-
104
- - **Parameter order** – a function takes the value it operates on first and
105
- the declaration right after it: `projectDocument(node, type)`,
106
- `buildSearchParams(query, type)`,
107
- `createTypesenseSearchEngine(client, schema, options)`,
108
- `engine.search(type, query)`.
109
- - **Factory verbs** – the verb tells you what kind of thing comes back.
110
- `define*` captures a declaration as a literal (`defineSearchType`);
111
- `build*` is a pure data-to-data constructor (`buildCollectionDefinition`,
112
- `buildSearchParams`, `buildGraphQLSchema`); `create*` makes a stateful
113
- instance (`createTypesenseSearchEngine`). A bare noun (`searchSchema`)
114
- constructs the trivial container it names.
115
-
116
- ## Field model
117
-
118
- The mapping is data, not code. Each field declares its `kind`, the IR `path` to
119
- read (or a `derive` function for a **derived** field, computed from the document
120
- in declaration order – so it may read fields declared before it, never the
121
- graph), and the capabilities (**roles**) it opts into. `path` is therefore the
122
- complete statement of what the projection reads from the graph. A field that
123
- declares **no** role is an **internal field**: projected so a later `derive` can
124
- read it, then pruned before the writer and absent from the collection definition
125
- – not stored, not indexed, no RAM. The physical field names a declaration fans
126
- out to (per-locale search/sort keys) come from `physicalFields`, the single
127
- convention projection, the collection definition and the query compiler all
128
- share.
129
-
130
- ```ts
131
- import { defineSearchType, projectRoots, searchSchema } from '@lde/search';
132
-
133
- const DATASET = defineSearchType({
134
- name: 'Dataset', // logical API name: names the GraphQL type, a REST path, …
135
- class: 'http://www.w3.org/ns/dcat#Dataset',
136
- fields: [
137
- // → title_<lang> (display, every present language), title_search_nl/_en, title_sort_nl/_en
138
- {
139
- name: 'title',
140
- path: 'http://purl.org/dc/terms/title',
141
- kind: 'text',
142
- locales: ['nl', 'en'],
143
- output: true,
144
- searchable: { weight: 5 },
145
- sortable: true,
146
- },
147
- // → publisher (IRI facet, resolved to a labelled reference at the surface)
148
- {
149
- name: 'publisher',
150
- path: 'http://purl.org/dc/terms/publisher',
151
- kind: 'reference',
152
- facetable: true,
153
- output: true,
154
- ref: { typeName: 'Organization', strategy: 'labelOnly' },
155
- },
156
- // → size (int)
157
- { name: 'size', path: 'urn:dr:size', kind: 'integer', sortable: true },
158
- // internal field (no role): projected as a reading device for the derive
159
- // below, then pruned before the writer – absent from the collection too
160
- { name: 'classes', path: 'urn:dr:class', kind: 'reference' },
161
- // derived field (no path): computed from the document in declaration order,
162
- // never from the graph – so `path` stays the whole statement of what is read
163
- {
164
- name: 'classCount',
165
- kind: 'integer',
166
- sortable: true,
167
- derive: (document) =>
168
- (document.classes as string[] | undefined)?.length ?? 0,
169
- },
170
- ],
171
- });
172
-
173
- const schema = searchSchema(DATASET);
174
- for await (const document of projectRoots(quads, roots, schema, DATASET)) {
175
- // one flat search document per given root subject, streamed. The caller (the
176
- // pipeline selector) supplies `roots`; pairing a document with its type for a
177
- // multi-collection writer is the pipeline glue’s job (see
178
- // `@lde/search-pipeline`), not the projection’s.
179
- }
11
+ ```sh
12
+ npm install @lde/search
180
13
  ```
181
14
 
182
- `defineSearchType` captures the declaration as a literal (what
183
- `as const satisfies SearchType` would do manually, with nothing to remember),
184
- so typed facet/output keys can be derived from it – see
185
- [Typed results](#typed-results) and `@lde/search-api-graphql`.
186
-
187
- **Kinds** (`FieldKind`): `text`, `keyword`, `integer`, `number`, `boolean`,
188
- `date`, `reference`. The Typesense/engine vocabulary and the GraphQL types are
189
- _derived_ from the kind by the adapter and the surface – never declared here.
190
-
191
- `SearchField` is a **discriminated union by `kind`** (`TextField`,
192
- `KeywordField`, `ReferenceField`, `NumericField`, `BooleanField`): each kind
193
- declares exactly the properties it can honour – `locales` on text, `ref` on
194
- references, `facetRanges` on numerics – so an illegal declaration fails to
195
- compile. Text is **always multilingual in shape**: `locales` lists the
196
- language tags to **index** (search/sort), and the reserved **`und`** locale
197
- (JSON-LD `@none`, RDF `und`) buckets untagged literals – a monolingual or
198
- untagged corpus declares `locales: ['und']`, mixed data `['nl', 'und']`.
199
- Display, by contrast, keeps every language present, not only the listed ones
200
- (see [Locales](#locales)). Declaring a real language is recommended (it drives
201
- per-locale stemming); `und` is folded but unstemmed unless `defaultLocale` opts
202
- in, is never demoted in search weighting, and adding a language later is
203
- additive – the API output shape never changes. Use `keyword` for exact-match
204
- tokens, never for prose.
205
-
206
- **Declarations are also validated at runtime** (for declarations built
207
- outside TypeScript – a SHACL generator, plain JS): `searchSchema()`
208
- rejects a structurally invalid declaration (duplicate field names, an `output`
209
- reference without `ref`, `text` without locales, `locales` on a non-text kind,
210
- `facetRanges` on a non-numeric kind, `searchable`/`transform` on a kind whose
211
- projection cannot honour it, `filterable`/`facetable` on `text`, two types
212
- sharing a `class` IRI or `name`) – the declaration-time counterpart of the
213
- port’s always-on query validation, so a bad schema fails at startup rather
214
- than per document at index time. `validateSearchType` /
215
- `assertValidSearchType` are exported for validating a single declaration
216
- directly.
217
-
218
- | kind | `where` | facet | sort | output |
219
- | -------------------- | -------------------- | ----- | ---------------- | ------------------------------- |
220
- | `text` | – (feeds free text) | – | yes (per-locale) | best-first language list |
221
- | `keyword` | `in` (membership) | yes | – | string / `string[]` |
222
- | `reference` | `in` (membership) | yes | – | labelled reference (id + label) |
223
- | `integer` / `number` | `range { min, max }` | yes | yes | number |
224
- | `date` | `range` (inclusive) | yes | yes | ISO 8601 string (surface) |
225
- | `boolean` | `is` | yes | – | boolean (absent = false) |
226
-
227
- A `reference` carries one of two strategies today: `labelOnly` (id + display
228
- label, resolved at query time from a label source) and `inline` (the referent’s
229
- own projected fields, carried inline). `idOnly` stays a forward declaration.
230
-
231
- An **inline reference** resolves `ref.typeName` to a declared **Reference Type**
232
- and projects the referent through it – a nested `SearchDocument`, or an array
233
- for an `array` reference. Roles decide whether the nesting surfaces, so the same
234
- construct serves two jobs (see
235
- [ADR 11](../../docs/decisions/0011-decouple-rdf-depth-from-the-api-surface.md)):
236
-
237
- - a **reading device** declares no role, so it is an internal field: projected
238
- so a later `derive` can select and flatten a value a `path` cannot address (a
239
- qualified hop), then pruned before the writer – nothing nested reaches the
240
- engine or the API;
241
- - an **API device** declares `output`, deliberately surfacing the nested
242
- Reference Type.
243
-
244
- So RDF depth and API shape stay independent: inline as deep as the source
245
- demands, expose exactly the flat fields you want. Framing follows the inline
246
- reference graph to the depth the schema declares (`Dataset → Subset →
247
- Measurement` is two hops), and `searchSchema` rejects inline cycles – the one
248
- way that depth could be unbounded – so it stays a bounded property of the
249
- declaration.
250
-
251
- A reference resolves its label from a **label source**: `labelSource` names
252
- the `SearchType` whose collection holds the referenced entities. The named
253
- type must declare an `output`, `searchable` text field called `label` –
254
- `searchSchema` validates this schema-wide, so a dangling or unsuitable label
255
- source fails at startup. A reference without a `labelSource` stays id-only.
256
-
257
- ## Projection
258
-
259
- `projectRoots` projects **one root type** over the roots the caller supplies –
260
- the pipeline selector already holds them, so nothing is discovered from
261
- `rdf:type` and a `CONSTRUCT` need emit no type triple. It is fully streaming:
262
- each root’s subgraph is framed one at a time and its document yielded as
263
- produced, so beyond a subject index memory stays flat at scale (framing the
264
- whole graph at once is roughly O(N²)). Duplicate triples are collapsed first,
265
- because some SPARQL engines (e.g. QLever) do not deduplicate `CONSTRUCT` output.
266
- Every predicate a value comes from is read through a field’s `path`; a `derive`
267
- computes only from the document projected so far, so `path` is the whole
268
- statement of what the projection reads. `assertTypeInSchema` guards that
269
- the passed `SearchType` is a member of the schema – the port’s own membership
270
- check – so no schema is ever forged to scope a projection to one type.
271
-
272
- **Blank-node roots are not indexable**: a blank node has no stable document
273
- key, so framing skips a blank-node root (and any root absent from the index)
274
- rather than crash. Blank-node subjects still embed fine when _referenced_ from
275
- a root; they just cannot _be_ one – select roots accordingly
276
- (`selectByClass` in `@lde/search-pipeline` already excludes them).
277
-
278
- `projectRoots` yields a **bare** `SearchDocument`. Pairing each document with the
279
- `SearchType` it belongs to, so the write side can fan a mixed stream out to
280
- per-type collections, is a routing concern owned by the pipeline glue – see
281
- `@lde/search-pipeline`’s `searchStages` and multi-collection writer – not the
282
- projection. One stage per root type keeps `@lde/search` pipeline-free.
283
-
284
- `projectRoots` **consumes the quads once** – a single scan builds the subject
285
- index the roots frame off – and so accepts any `Iterable<Quad>`, not just a
286
- materialized array. A caller merging several readers can pass a chained
287
- generator instead of building a third full array at the projection peak:
288
-
289
- ```ts
290
- projectRoots(
291
- (function* () {
292
- yield* registerQuads;
293
- yield* dkgQuads;
294
- })(),
295
- roots,
296
- schema,
297
- DATASET,
298
- );
299
- ```
300
-
301
- ## Locales
302
-
303
- `locales` declares the languages a `text` field wants **indexed** (`und` =
304
- untagged literals). It drives the two per-locale, in-memory fanouts –
305
- `searchable` and `sortable` – but **not** display, which preserves every
306
- language present:
307
-
308
- - `output` → `title_<lang>` for **every** language the data carries (`title_nl`,
309
- `title_en`, `title_fr`, `title_und`, …), accents preserved. Display is stored
310
- `index: false` (on disk, off RAM), so extra languages cost nothing – a value
311
- in a language outside `locales`, or an untagged one, still renders rather than
312
- collapsing to a bare IRI. One un-indexed regex field (`${name}_<lang>`, see
313
- `displayFieldPattern`) captures them all;
314
- - `searchable` → `title_search_nl`/`title_search_en` (folded; one field per
315
- locale lets a query `query_by` them and rank the user’s language higher, and
316
- lets a language that needs a dedicated tokenizer set its own stemming `locale`
317
- in the engine schema);
318
- - `sortable` → `title_sort_nl`/`title_sort_en` (folded, so a locale-switching UI
319
- sorts on the active language).
320
-
321
- A field with `searchable` but no `output` is **search-only** – folded and stemmed
322
- for retrieval but never rendered (e.g. a creator searched here but shown via a
323
- separate label). **Only listed locales are indexed** (searched and sorted); a
324
- literal whose language tag is not in `locales` is still **displayed** but not
325
- matched or sorted on. Display fields are **omitted, never empty**, when a document
326
- lacks that language, and the per-locale search/sort fields likewise, so declare
327
- them optional in the engine schema and sort with `missing_values: last`. A
328
- deployment that wants to bound the displayed languages narrows them upstream
329
- (e.g. selecting a language subset in its CONSTRUCT query), since preservation is
330
- the default.
331
-
332
- Folding the search fields is what lets diacritic-insensitive matching and
333
- stemming coexist. A search engine on its **default** locale typically folds case
334
- and diacritics for you; enabling a language’s **stemming** switches it to ICU
335
- tokenization, which **preserves** diacritics – at which point `fold()` (from
336
- [`@lde/text-normalization`](../text-normalization)) is what keeps matching
337
- diacritic-insensitive. Stemming is rules-based and can mangle proper nouns (the
338
- Dutch stemmer reduces the city `Bergen` to `berg`), so enable it on free-text
339
- fields and keep proper-noun facets on a separate, unstemmed field.
340
-
341
- ## Querying
342
-
343
- The search fields are stored already case- and diacritic-folded, so **the query
344
- must be folded the same way** with the same `fold()` before it reaches the engine,
345
- or index and query normalize differently and matches silently miss. This contract
346
- holds for **any** consumer, including an API built on this package – which is why
347
- engine adapters and surfaces compile through the shared `SearchQuery` IR and the
348
- `physicalFields` convention rather than re-deriving field names.
349
-
350
- Queries are **always validated**: the port contract requires every engine
351
- adapter to reject a structurally invalid `SearchQuery` (`assertValidQuery`) –
352
- unknown or non-`filterable` fields in `where`, an operator not matching the
353
- field’s kind, non-`facetable` facet requests – no matter which surface or
354
- policy produced it. A typed surface like GraphQL makes most of these
355
- unrepresentable; the port enforces them for everyone else (deployment
356
- `queryDefaults`, in-process callers, weaker-typed surfaces).
357
-
358
- ## Typed results
359
-
360
- An engine is **bound to the whole `SearchSchema` at construction** – like
361
- every other schema consumer (`projectRoots(quads, roots, schema, type)`,
362
- `buildGraphQLSchema(schema)`): the adapter factory takes the deployment’s
363
- declaration, so a query can never meet the wrong index, and deployment-level
364
- concerns (the label cache, cross-type search, facet batching) have one home.
365
- Where each type physically lives is the **adapter’s** to decide – it derives a
366
- collection/index name from the type, by its own engine’s naming conventions,
367
- and a deployment only overrides that where it must. A search names its type per
368
- call. Because `searchSchema()` captures the declared types as a literal
369
- tuple, `search()` accepts **only the deployment’s own types** (a foreign type
370
- is a compile error) and returns facet/document keys typed by the type passed
371
- – no caller-side generics:
372
-
373
- ```ts
374
- // No `collections`: each type reads the collection the adapter names it,
375
- // which is the one its writer builds. Pass `collections` only to override.
376
- const engine = createTypesenseSearchEngine(client, schema);
377
-
378
- const result = await engine.search(DATASET, query);
379
- result.facets.publisher; // typed: only DATASET’s facetable fields are keys
380
- result.facets.publsher; // compile error (typo)
381
- result.hits[0].document.title; // typed: only DATASET’s output fields are keys
382
- await engine.search(OTHER_TYPE, query); // compile error: not in this schema
383
- ```
384
-
385
- `searchFacets(type, queries)` is the port’s **batch entry point**: several
386
- facet-only queries – e.g. a faceted listing’s skip-own-filter variants –
387
- answered in one engine round-trip (Typesense: a single `multi_search`), one
388
- outcome per query, positionally aligned – its facet map, or an in-place error,
389
- so one failed query never discards its siblings’ facets. The same schema
390
- binding, per-query validation and typed facet keys apply to every query in
391
- the batch.
392
-
393
- This only works when the types were declared with `defineSearchType` (or
394
- captured `as const satisfies SearchType`) and composed with `searchSchema()`;
395
- a plain `: SearchSchema` annotation widens gracefully to string keys.
396
- `FacetFieldsOf`/`OutputFieldsOf` are exported for annotating your own
397
- signatures, and `engine.schema` exposes the bound declaration for routing.
398
-
399
- ## Why a declarative model
15
+ ## Documentation
400
16
 
401
- The vocabulary mirrors SHACL on purpose: `path` is `sh:path`, `array` is
402
- `sh:maxCount`, `required` is `sh:minCount`, `locales` is `sh:languageIn`, `ref`
403
- is `sh:class`/`sh:node`. So the same core that runs a hand-written `SearchSchema`
404
- today will run a **SHACL-generated** one tomorrow – the model, the ports and the
405
- IR stay; only schema-authoring gets automated.
17
+ See the [full documentation](https://ldelements.org/reference/search).
package/dist/adapter.d.ts CHANGED
@@ -6,9 +6,9 @@
6
6
  * deployment author declaring a schema and running searches needs none of
7
7
  * this; the main entry point carries that (small) authoring surface.
8
8
  */
9
- export { assertTypeInSchema, physicalFields, irAlias, displayFieldName, displayFieldPattern, displayLangOf, searchableFields, facetableFields, filterableFields, sortableFields, outputFields, isInternalField, isInlineReference, referenceFields, referenceTypeNamed, inlineFramingDepth, fieldNamed, labelFieldOf, isRangeFacet, isoToUnixSeconds, unixSecondsToIso, } from './schema.js';
9
+ export { assertTypeInSchema, physicalFields, irAlias, displayFieldName, displayFieldPattern, displayLangOf, searchableFields, facetableFields, filterableFields, sortableFields, outputFields, isInternalField, isInlineReference, referenceFields, referenceTypeNamed, inlineFramingDepth, fieldNamed, ID_FIELD, labelFieldOf, isRangeFacet, isoToUnixSeconds, unixSecondsToIso, } from './schema.js';
10
10
  export type { PhysicalFields } from './schema.js';
11
11
  export { physicalNameTokens } from './physical-name.js';
12
- export { filterOperatorFor, filterOperator, validateQuery, assertValidQuery, pageForOffset, } from './query.js';
12
+ export { filterOperatorFor, filterOperator, isUnsatisfiable, validateQuery, assertValidQuery, pageForOffset, } from './query.js';
13
13
  export type { FilterOperator, QueryIssue } from './query.js';
14
14
  //# sourceMappingURL=adapter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,OAAO,EACP,gBAAgB,EAChB,mBAAmB,EACnB,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,OAAO,EACP,gBAAgB,EAChB,mBAAmB,EACnB,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,UAAU,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,aAAa,EACb,gBAAgB,EAChB,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
package/dist/adapter.js CHANGED
@@ -6,6 +6,6 @@
6
6
  * deployment author declaring a schema and running searches needs none of
7
7
  * this; the main entry point carries that (small) authoring surface.
8
8
  */
9
- export { assertTypeInSchema, physicalFields, irAlias, displayFieldName, displayFieldPattern, displayLangOf, searchableFields, facetableFields, filterableFields, sortableFields, outputFields, isInternalField, isInlineReference, referenceFields, referenceTypeNamed, inlineFramingDepth, fieldNamed, labelFieldOf, isRangeFacet, isoToUnixSeconds, unixSecondsToIso, } from './schema.js';
9
+ export { assertTypeInSchema, physicalFields, irAlias, displayFieldName, displayFieldPattern, displayLangOf, searchableFields, facetableFields, filterableFields, sortableFields, outputFields, isInternalField, isInlineReference, referenceFields, referenceTypeNamed, inlineFramingDepth, fieldNamed, ID_FIELD, labelFieldOf, isRangeFacet, isoToUnixSeconds, unixSecondsToIso, } from './schema.js';
10
10
  export { physicalNameTokens } from './physical-name.js';
11
- export { filterOperatorFor, filterOperator, validateQuery, assertValidQuery, pageForOffset, } from './query.js';
11
+ export { filterOperatorFor, filterOperator, isUnsatisfiable, validateQuery, assertValidQuery, pageForOffset, } from './query.js';
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { projectRoots } from './project.js';
2
2
  export type { SearchDocument } from './project.js';
3
- export { defineSearchType, searchSchema, validateSearchType, assertValidSearchType, } from './schema.js';
3
+ export { defineSearchType, searchSchema, validateSearchType, assertValidSearchType, ID_FIELD, } from './schema.js';
4
4
  export type { FieldKind, SearchField, SearchFieldBase, TextField, KeywordField, ReferenceField, NumericField, BooleanField, SearchType, SearchTypeBase, RootType, ReferenceType, RootTypeOf, SearchTypeIssue, SearchSchema, FacetRange, } from './schema.js';
5
5
  export type { SearchQuery, Filter, Sort } from './query.js';
6
6
  export type { SearchEngine, SearchResult, SearchHit, ResultDocument, SearchValue, LocalizedValue, Reference, FacetBucket, FacetMap, FacetsOutcome, FacetFieldsOf, OutputFieldsOf, } from './engine.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAKnD,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,EAClB,qBAAqB,GACtB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,SAAS,EACT,WAAW,EACX,eAAe,EACf,SAAS,EACT,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,cAAc,EACd,QAAQ,EACR,aAAa,EACb,UAAU,EACV,eAAe,EACf,YAAY,EACZ,UAAU,GACX,MAAM,aAAa,CAAC;AAIrB,YAAY,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAI5D,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,cAAc,EACd,WAAW,EACX,cAAc,EACd,SAAS,EACT,WAAW,EACX,QAAQ,EACR,aAAa,EACb,aAAa,EACb,cAAc,GACf,MAAM,aAAa,CAAC;AAErB,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAKnD,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,EAClB,qBAAqB,EACrB,QAAQ,GACT,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,SAAS,EACT,WAAW,EACX,eAAe,EACf,SAAS,EACT,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,cAAc,EACd,QAAQ,EACR,aAAa,EACb,UAAU,EACV,eAAe,EACf,YAAY,EACZ,UAAU,GACX,MAAM,aAAa,CAAC;AAIrB,YAAY,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAI5D,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,cAAc,EACd,WAAW,EACX,cAAc,EACd,SAAS,EACT,WAAW,EACX,QAAQ,EACR,aAAa,EACb,aAAa,EACb,cAAc,GACf,MAAM,aAAa,CAAC;AAErB,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC"}
package/dist/index.js CHANGED
@@ -10,4 +10,4 @@ export { projectRoots } from './project.js';
10
10
  // Unified field model: one declaration drives projection, engine collection
11
11
  // schema, query semantics and the GraphQL surface – a discriminated union by
12
12
  // `kind`, validated again at runtime when the schema is built.
13
- export { defineSearchType, searchSchema, validateSearchType, assertValidSearchType, } from './schema.js';
13
+ export { defineSearchType, searchSchema, validateSearchType, assertValidSearchType, ID_FIELD, } from './schema.js';
package/dist/query.d.ts CHANGED
@@ -24,7 +24,7 @@ export interface SearchQuery {
24
24
  * One `where` clause. The operator is fixed by the target field’s {@link FieldKind}
25
25
  * ({@link filterOperatorFor}): keyword/reference use `in` (OR within the field),
26
26
  * the numeric/date kinds use an inclusive `range`, boolean uses `is`. Bounds are
27
- * inclusive only — no `gt`/`gte`/`lt`/`lte`.
27
+ * inclusive only – no `gt`/`gte`/`lt`/`lte`.
28
28
  */
29
29
  export type Filter = {
30
30
  readonly field: string;
@@ -49,11 +49,28 @@ export type { FilterOperator } from './schema.js';
49
49
  import type { FilterOperator } from './schema.js';
50
50
  /** The operator a {@link Filter} value carries, from its discriminating key. */
51
51
  export declare function filterOperator(filter: Filter): FilterOperator;
52
+ /**
53
+ * Whether the query asks for nothing, so no engine can answer it with a hit.
54
+ * True for an empty `in` on {@link ID_FIELD} – and only there, deliberately:
55
+ *
56
+ * An **identity** filter enumerates the documents wanted, so the empty set
57
+ * wants none. A **value** filter constrains a dimension, so an empty set
58
+ * constrains nothing – which is why every other vacuous clause is a no-op the
59
+ * compilers skip (a facet UI with nothing selected sends exactly that). The
60
+ * readings differ because the fields differ in kind, and only for `id` would
61
+ * “no constraint” hand back the whole collection to a caller who asked for
62
+ * specific things – the likely shape being a client mapping a possibly-empty
63
+ * reference array (`id: { in: doc.publisher }`) into a batch lookup.
64
+ *
65
+ * An adapter MUST answer an unsatisfiable query with an empty result rather
66
+ * than dispatching it.
67
+ */
68
+ export declare function isUnsatisfiable(query: SearchQuery): boolean;
52
69
  /**
53
70
  * One structural problem {@link validateQuery} found: the query references a
54
71
  * field the search type does not declare, or uses it in a role it does not
55
72
  * opt into. Vacuous-but-valid clauses (an empty `in` list, a `range` with no
56
- * bound) are NOT issues — a compiler skips those as no-ops.
73
+ * bound) are NOT issues – a compiler skips those as no-ops.
57
74
  */
58
75
  export interface QueryIssue {
59
76
  readonly part: 'where' | 'facets' | 'orderBy';
@@ -63,14 +80,15 @@ export interface QueryIssue {
63
80
  /**
64
81
  * Structurally validate a query against its search type: every `where` clause
65
82
  * targets a declared, `filterable` field with the operator its kind accepts
66
- * ({@link filterOperatorFor}); every requested facet is a declared, `facetable`
83
+ * ({@link filterOperatorFor}) – or the undeclared, always-filterable
84
+ * {@link ID_FIELD}, which takes `in`; every requested facet is a declared, `facetable`
67
85
  * field; every sort is `relevance` or a declared field. Sorting deliberately
68
86
  * checks declaration only, not the `sortable` flag: that flag means *publicly
69
87
  * selectable*, and a deployment policy may sort on a private tie-break field.
70
88
  *
71
89
  * This is the port’s always-on guard: every {@link SearchEngine} adapter MUST
72
90
  * reject a query with issues ({@link assertValidQuery}) instead of passing
73
- * garbage to its engine, so validation holds for every caller — including
91
+ * garbage to its engine, so validation holds for every caller – including
74
92
  * `queryDefaults` policies and surfaces weaker than GraphQL.
75
93
  */
76
94
  export declare function validateQuery(query: SearchQuery, searchType: SearchType): readonly QueryIssue[];
@@ -78,7 +96,7 @@ export declare function validateQuery(query: SearchQuery, searchType: SearchType
78
96
  * naming every issue. The always-on entry point for engine adapters. */
79
97
  export declare function assertValidQuery(query: SearchQuery, searchType: SearchType): void;
80
98
  /**
81
- * The 1-based page an `offset` falls on — the numbered-pagination presentation
99
+ * The 1-based page an `offset` falls on – the numbered-pagination presentation
82
100
  * of the IR, shared by the surfaces and the adapters. `limit: 0` (a facet-only
83
101
  * query) fetches no hits and has no meaningful page, so it pins to 1 rather
84
102
  * than dividing by zero.
@@ -1 +1 @@
1
- {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiC,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAE7E;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,wEAAwE;IACxE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,yBAAyB;IACzB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,EAAE,SAAS,IAAI,EAAE,CAAC;IAClC,2BAA2B;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,uDAAuD;IACvD,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,MAAM,MAAM,MAAM,GACd;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GAC1D;IACE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE;QACd,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QAC/B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;KAChC,CAAC;CACH,GACD;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAA;CAAE,CAAC;AAErD,+BAA+B;AAC/B,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,KAAK,GAAG,MAAM,CAAC;CACpC;AAKD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,gFAAgF;AAChF,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,CAE7D;AAED;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAC;IAC9C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EACX,eAAe,GACf,gBAAgB,GAChB,mBAAmB,GACnB,eAAe,CAAC;CACrB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,WAAW,EAClB,UAAU,EAAE,UAAU,GACrB,SAAS,UAAU,EAAE,CA6CvB;AAED;yEACyE;AACzE,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,WAAW,EAClB,UAAU,EAAE,UAAU,GACrB,IAAI,CAUN;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnE"}
1
+ {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,UAAU,EAChB,MAAM,aAAa,CAAC;AAErB;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,wEAAwE;IACxE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,yBAAyB;IACzB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,EAAE,SAAS,IAAI,EAAE,CAAC;IAClC,2BAA2B;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,uDAAuD;IACvD,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,MAAM,MAAM,MAAM,GACd;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GAC1D;IACE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE;QACd,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QAC/B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;KAChC,CAAC;CACH,GACD;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAA;CAAE,CAAC;AAErD,+BAA+B;AAC/B,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,KAAK,GAAG,MAAM,CAAC;CACpC;AAKD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,gFAAgF;AAChF,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,CAE7D;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAK3D;AAED;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAC;IAC9C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EACb,eAAe,GAAG,gBAAgB,GAAG,mBAAmB,GAAG,eAAe,CAAC;CAC9E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,WAAW,EAClB,UAAU,EAAE,UAAU,GACrB,SAAS,UAAU,EAAE,CA0DvB;AAED;yEACyE;AACzE,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,WAAW,EAClB,UAAU,EAAE,UAAU,GACrB,IAAI,CAUN;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnE"}
package/dist/query.js CHANGED
@@ -1,4 +1,4 @@
1
- import { fieldNamed, filterOperatorFor } from './schema.js';
1
+ import { fieldNamed, filterOperatorFor, ID_FIELD, } from './schema.js';
2
2
  // The kind→operator table lives with the field model in schema.ts (one source
3
3
  // for the query IR, the surfaces, the adapters AND declaration validation);
4
4
  // re-exported here so the query-IR module stays the natural import site.
@@ -7,22 +7,55 @@ export { filterOperatorFor } from './schema.js';
7
7
  export function filterOperator(filter) {
8
8
  return 'in' in filter ? 'in' : 'range' in filter ? 'range' : 'is';
9
9
  }
10
+ /**
11
+ * Whether the query asks for nothing, so no engine can answer it with a hit.
12
+ * True for an empty `in` on {@link ID_FIELD} – and only there, deliberately:
13
+ *
14
+ * An **identity** filter enumerates the documents wanted, so the empty set
15
+ * wants none. A **value** filter constrains a dimension, so an empty set
16
+ * constrains nothing – which is why every other vacuous clause is a no-op the
17
+ * compilers skip (a facet UI with nothing selected sends exactly that). The
18
+ * readings differ because the fields differ in kind, and only for `id` would
19
+ * “no constraint” hand back the whole collection to a caller who asked for
20
+ * specific things – the likely shape being a client mapping a possibly-empty
21
+ * reference array (`id: { in: doc.publisher }`) into a batch lookup.
22
+ *
23
+ * An adapter MUST answer an unsatisfiable query with an empty result rather
24
+ * than dispatching it.
25
+ */
26
+ export function isUnsatisfiable(query) {
27
+ return query.where.some((filter) => filter.field === ID_FIELD && 'in' in filter && filter.in.length === 0);
28
+ }
10
29
  /**
11
30
  * Structurally validate a query against its search type: every `where` clause
12
31
  * targets a declared, `filterable` field with the operator its kind accepts
13
- * ({@link filterOperatorFor}); every requested facet is a declared, `facetable`
32
+ * ({@link filterOperatorFor}) – or the undeclared, always-filterable
33
+ * {@link ID_FIELD}, which takes `in`; every requested facet is a declared, `facetable`
14
34
  * field; every sort is `relevance` or a declared field. Sorting deliberately
15
35
  * checks declaration only, not the `sortable` flag: that flag means *publicly
16
36
  * selectable*, and a deployment policy may sort on a private tie-break field.
17
37
  *
18
38
  * This is the port’s always-on guard: every {@link SearchEngine} adapter MUST
19
39
  * reject a query with issues ({@link assertValidQuery}) instead of passing
20
- * garbage to its engine, so validation holds for every caller — including
40
+ * garbage to its engine, so validation holds for every caller – including
21
41
  * `queryDefaults` policies and surfaces weaker than GraphQL.
22
42
  */
23
43
  export function validateQuery(query, searchType) {
24
44
  const issues = [];
25
45
  for (const filter of query.where) {
46
+ // `id` is filterable on every type without being declared by any: it is the
47
+ // document’s IRI, so the lookup exists wherever documents do. Membership
48
+ // only – an IRI has no range and no truth value.
49
+ if (filter.field === ID_FIELD) {
50
+ if (filterOperator(filter) !== 'in') {
51
+ issues.push({
52
+ part: 'where',
53
+ field: filter.field,
54
+ reason: 'operator-mismatch',
55
+ });
56
+ }
57
+ continue;
58
+ }
26
59
  const field = fieldNamed(searchType, filter.field);
27
60
  if (field === undefined) {
28
61
  issues.push({
@@ -79,7 +112,7 @@ export function assertValidQuery(query, searchType) {
79
112
  }
80
113
  }
81
114
  /**
82
- * The 1-based page an `offset` falls on — the numbered-pagination presentation
115
+ * The 1-based page an `offset` falls on – the numbered-pagination presentation
83
116
  * of the IR, shared by the surfaces and the adapters. `limit: 0` (a facet-only
84
117
  * query) fetches no hits and has no meaningful page, so it pins to 1 rather
85
118
  * than dividing by zero.
package/dist/schema.d.ts CHANGED
@@ -144,9 +144,13 @@ export interface ReferenceField extends SearchFieldBase, Searchable {
144
144
  /** Logical API type name of the referenced entity (PascalCase, e.g.
145
145
  * `Organization`) – names the reference’s type in the API surfaces, the
146
146
  * way {@link SearchType.name} names a root type; fields sharing it share
147
- * one emitted type. A name, not a key: it need not correspond to any
148
- * indexed root type (and until cross-collection references exist, it must
149
- * not collide with one). */
147
+ * one emitted type. For a `labelOnly`/`idOnly` reference it is a name,
148
+ * not a key: it need not correspond to any indexed root type, and it may
149
+ * name one (`creator` → `Person`, with the labels resolved from that
150
+ * root’s collection via `labelSource`) – an API surface then serves the
151
+ * reference under a derived name (GraphQL: `‹Name›Reference`) to keep
152
+ * type names unique. An `inline` reference’s typeName instead resolves to
153
+ * a declared {@link ReferenceType}, so it can never name a root type. */
150
154
  readonly typeName: string;
151
155
  /** How much of the referenced entity the reference carries. `labelOnly`
152
156
  * (id + display label, resolved from a label source) and `inline` (the
@@ -337,8 +341,21 @@ export declare function labelFieldOf(searchType: SearchType): TextField | undefi
337
341
  */
338
342
  export interface SearchTypeIssue {
339
343
  readonly field: string;
340
- readonly reason: 'duplicate-field-name' | 'invalid-field-name' | 'unknown-kind' | 'invalid-locale' | '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';
344
+ readonly reason: 'duplicate-field-name' | 'invalid-field-name' | 'unknown-kind' | 'invalid-locale' | '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' | 'reserved-field-name';
341
345
  }
346
+ /**
347
+ * The reserved logical name of a document’s IRI – the one field every indexed
348
+ * thing carries, and the only one no {@link SearchType} declares. It is the
349
+ * hit’s identity ({@link SearchHit.id}), not a value in its
350
+ * {@link ResultDocument}, so it is surfaced and filtered by name alone.
351
+ *
352
+ * `id` answers *what the thing is*; a domain field like `schema:identifier`
353
+ * answers *what a source system calls it* (a catalogue or record number). Those
354
+ * are different questions, so a declaration may still carry an `identifier`
355
+ * field of its own – but never an `id` one, which
356
+ * {@link validateSearchType} rejects as `reserved-field-name`.
357
+ */
358
+ export declare const ID_FIELD = "id";
342
359
  /**
343
360
  * Structurally validate one {@link SearchType} declaration – the
344
361
  * declaration-time counterpart of `validateQuery`. Rules:
@@ -346,7 +363,7 @@ export interface SearchTypeIssue {
346
363
  * - field names are unique (a duplicate would silently shadow in every
347
364
  * consumer, each picking a different winner) and a metacharacter-free
348
365
  * identifier (the name is interpolated into physical field names and the
349
- * display RE2 pattern);
366
+ * display RE2 pattern), and is not the reserved {@link ID_FIELD};
350
367
  * - a `text` field’s declared locales are BCP-47-shaped (no `_`, which is the
351
368
  * reserved name↔locale separator);
352
369
  * - a `reference` field that is `output` declares `ref` (the API surfaces
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
package/dist/schema.js CHANGED
@@ -210,6 +210,19 @@ function assertResolvableLabelSources(types) {
210
210
  }
211
211
  }
212
212
  }
213
+ /**
214
+ * The reserved logical name of a document’s IRI – the one field every indexed
215
+ * thing carries, and the only one no {@link SearchType} declares. It is the
216
+ * hit’s identity ({@link SearchHit.id}), not a value in its
217
+ * {@link ResultDocument}, so it is surfaced and filtered by name alone.
218
+ *
219
+ * `id` answers *what the thing is*; a domain field like `schema:identifier`
220
+ * answers *what a source system calls it* (a catalogue or record number). Those
221
+ * are different questions, so a declaration may still carry an `identifier`
222
+ * field of its own – but never an `id` one, which
223
+ * {@link validateSearchType} rejects as `reserved-field-name`.
224
+ */
225
+ export const ID_FIELD = 'id';
213
226
  /** Kinds that can feed full-text search (project a folded search field). */
214
227
  const SEARCHABLE_KINDS = ['text', 'keyword', 'reference'];
215
228
  /** Kinds whose projection applies the {@link KeywordField.transform}. */
@@ -237,7 +250,7 @@ const LOCALE_PATTERN = /^[A-Za-z0-9]+(-[A-Za-z0-9]+)*$/;
237
250
  * - field names are unique (a duplicate would silently shadow in every
238
251
  * consumer, each picking a different winner) and a metacharacter-free
239
252
  * identifier (the name is interpolated into physical field names and the
240
- * display RE2 pattern);
253
+ * display RE2 pattern), and is not the reserved {@link ID_FIELD};
241
254
  * - a `text` field’s declared locales are BCP-47-shaped (no `_`, which is the
242
255
  * reserved name↔locale separator);
243
256
  * - a `reference` field that is `output` declares `ref` (the API surfaces
@@ -277,6 +290,12 @@ export function validateSearchType(searchType) {
277
290
  if (!FIELD_NAME_PATTERN.test(field.name)) {
278
291
  issue('invalid-field-name');
279
292
  }
293
+ // `id` is the document’s IRI, surfaced and filtered for every type; a
294
+ // declared field of that name would shadow it in the API output and in
295
+ // `where`, silently answering a different question.
296
+ if (field.name === ID_FIELD) {
297
+ issue('reserved-field-name');
298
+ }
280
299
  // Every kind-dependent rule below would silently pass for a kind outside
281
300
  // the union, so a typo’d kind in a plain-JS declaration must fail here.
282
301
  if (!Object.hasOwn(OPERATOR_BY_KIND, field.kind)) {
package/dist/testing.d.ts CHANGED
@@ -5,8 +5,9 @@ import type { SearchEngine } from './engine.js';
5
5
  * live instance of itself, so the port rules hold by test rather than by
6
6
  * prose. Covers schema binding (a type outside the bound schema is rejected),
7
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 and a
9
- * `searchFacets` batch – for every type in the schema.
8
+ * before it reaches the engine), the undeclared `id` lookup, and the result
9
+ * shape of a browse query and a `searchFacets` batch – for every type in the
10
+ * schema.
10
11
  *
11
12
  * ```ts
12
13
  * describeSearchEngineContract('TypesenseSearchEngine', () => engine);
@@ -1 +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,CAsJN"}
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAchD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,YAAY,GACzB,IAAI,CAuKN"}
package/dist/testing.js CHANGED
@@ -1,14 +1,15 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { filterOperatorFor, } from './query.js';
3
- import { facetableFields, filterableFields } from './schema.js';
3
+ import { facetableFields, filterableFields, ID_FIELD, } from './schema.js';
4
4
  /**
5
5
  * The executable {@link SearchEngine} port contract (import from
6
6
  * `@lde/search/testing`): a vitest suite every engine adapter runs against a
7
7
  * live instance of itself, so the port rules hold by test rather than by
8
8
  * prose. Covers schema binding (a type outside the bound schema is rejected),
9
9
  * the always-on query validation (a structurally invalid query is rejected
10
- * before it reaches the engine) and the result shape of a browse query and a
11
- * `searchFacets` batch – for every type in the schema.
10
+ * before it reaches the engine), the undeclared `id` lookup, and the result
11
+ * shape of a browse query and a `searchFacets` batch – for every type in the
12
+ * schema.
12
13
  *
13
14
  * ```ts
14
15
  * describeSearchEngineContract('TypesenseSearchEngine', () => engine);
@@ -52,6 +53,22 @@ export function describeSearchEngineContract(name, engine) {
52
53
  await expect(engine().search(searchType, query)).rejects.toThrow(/unknown-field/);
53
54
  }
54
55
  });
56
+ it('applies an `id` lookup rather than ignoring the clause', async () => {
57
+ // `id` is filterable on every type without being declared by any, so an
58
+ // adapter that resolves `where` fields through its declaration alone
59
+ // takes the unknown-field path and drops the clause – returning EVERY
60
+ // document where the caller asked for one. Pinned here because that
61
+ // failure is silent: no error, just the wrong result set.
62
+ for (const searchType of types()) {
63
+ const query = {
64
+ ...browse(searchType),
65
+ where: [{ field: ID_FIELD, in: ['urn:test:no-such-document'] }],
66
+ };
67
+ const result = await engine().search(searchType, query);
68
+ expect(result.total).toBe(0);
69
+ expect(result.hits).toEqual([]);
70
+ }
71
+ });
55
72
  it('rejects a facet request for an unknown field', async () => {
56
73
  for (const searchType of types()) {
57
74
  const query = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lde/search",
3
- "version": "0.12.2",
3
+ "version": "0.13.0",
4
4
  "description": "Engine- and domain-agnostic core for RDF-backed search: a unified declarative field model (SearchField/SearchType/SearchSchema), a neutral query IR, the SearchEngine port with logical result types, and a streaming CONSTRUCT-to-document projection. Bakes in no engine, protocol, or domain.",
5
5
  "repository": {
6
6
  "url": "git+https://github.com/ldelements/lde.git",