@lde/search 0.12.3 → 0.14.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/dist/adapter.d.ts +2 -2
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/project.d.ts +9 -1
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +30 -9
- package/dist/query.d.ts +23 -5
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +37 -4
- package/dist/schema.d.ts +21 -5
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +20 -1
- package/dist/testing.d.ts +3 -2
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +20 -3
- package/package.json +1 -1
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
|
package/dist/adapter.d.ts.map
CHANGED
|
@@ -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
|
-
export type { SearchDocument } from './project.js';
|
|
3
|
-
export { defineSearchType, searchSchema, validateSearchType, assertValidSearchType, } from './schema.js';
|
|
2
|
+
export type { ProjectedNode, SearchDocument } from './project.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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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,aAAa,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAKlE,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/project.d.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import type { Quad } from '@rdfjs/types';
|
|
2
2
|
import { type FramedNode } from './frame-by-type.js';
|
|
3
3
|
import { type RootType, type SearchSchema, type SearchType } from './schema.js';
|
|
4
|
+
/**
|
|
5
|
+
* A projected node: the fields of one {@link SearchType}, flat. A root always
|
|
6
|
+
* carries the `id` that keys it – it is a {@link SearchDocument} – but an inline
|
|
7
|
+
* referent carries one only when the referent is a named node: a nested document
|
|
8
|
+
* is not a document key, so nesting needs the referent’s fields, not its
|
|
9
|
+
* identity.
|
|
10
|
+
*/
|
|
11
|
+
export type ProjectedNode = Record<string, unknown>;
|
|
4
12
|
/** A flat search document. `id` is the engine document key. */
|
|
5
13
|
export type SearchDocument = {
|
|
6
14
|
id: string;
|
|
7
|
-
} &
|
|
15
|
+
} & ProjectedNode;
|
|
8
16
|
/**
|
|
9
17
|
* Project one framed JSON-LD node into a flat search document: apply each field
|
|
10
18
|
* of the type in declaration order. A field with a `derive` function computes
|
package/dist/project.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAEzC,OAAO,EAGL,KAAK,UAAU,EAChB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAYL,KAAK,QAAQ,EAEb,KAAK,YAAY,EACjB,KAAK,UAAU,EAEhB,MAAM,aAAa,CAAC;AAErB,+DAA+D;AAC/D,MAAM,MAAM,cAAc,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,
|
|
1
|
+
{"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAEzC,OAAO,EAGL,KAAK,UAAU,EAChB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAYL,KAAK,QAAQ,EAEb,KAAK,YAAY,EACjB,KAAK,UAAU,EAEhB,MAAM,aAAa,CAAC;AAErB;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEpD,+DAA+D;AAC/D,MAAM,MAAM,cAAc,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,aAAa,CAAC;AAE5D;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,UAAU,EAChB,UAAU,EAAE,UAAU,EACtB,MAAM,CAAC,EAAE,YAAY,GACpB,cAAc,CAWhB;AAwED;;;;;;;;;;;;GAYG;AACH,wBAAuB,YAAY,CACjC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,EACrB,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,MAAM,EAAE,YAAY,EACpB,UAAU,EAAE,QAAQ,GACnB,aAAa,CAAC,cAAc,CAAC,CAW/B"}
|
package/dist/project.js
CHANGED
|
@@ -16,6 +16,11 @@ import { assertTypeInSchema, displayFieldName, inlineFramingDepth, irAlias, isIn
|
|
|
16
16
|
* the engine collection definition and the query compiler.
|
|
17
17
|
*/
|
|
18
18
|
export function projectDocument(node, searchType, schema) {
|
|
19
|
+
if (documentKey(node) === undefined) {
|
|
20
|
+
throw new Error(`Cannot project a “${searchType.name}” node without an @id (a blank node label is not one): every search document needs a stable key, and an empty one would collide with other keyless nodes.`);
|
|
21
|
+
}
|
|
22
|
+
// The guard above is what makes this a SearchDocument: projectFields sets `id`
|
|
23
|
+
// for exactly the nodes documentKey resolves.
|
|
19
24
|
const document = projectFields(node, searchType, schema);
|
|
20
25
|
pruneInternalFields(document, searchType, schema);
|
|
21
26
|
return document;
|
|
@@ -36,7 +41,7 @@ function pruneInternalFields(document, searchType, schema) {
|
|
|
36
41
|
delete document[field.name];
|
|
37
42
|
continue;
|
|
38
43
|
}
|
|
39
|
-
// A surfaced inline reference nests its referent(s) as
|
|
44
|
+
// A surfaced inline reference nests its referent(s) as projected node(s);
|
|
40
45
|
// prune those too, by their reference type.
|
|
41
46
|
if (schema === undefined || field.kind !== 'reference') {
|
|
42
47
|
continue;
|
|
@@ -61,16 +66,24 @@ function pruneInternalFields(document, searchType, schema) {
|
|
|
61
66
|
* once the whole nested structure is projected, so a derive at any depth can
|
|
62
67
|
* still read an internal field before it is removed. */
|
|
63
68
|
function projectFields(node, searchType, schema) {
|
|
64
|
-
const id = node
|
|
65
|
-
|
|
66
|
-
throw new Error(`Cannot project a “${searchType.name}” node without an @id: every search document needs a stable key, and an empty one would collide with other keyless nodes.`);
|
|
67
|
-
}
|
|
68
|
-
const document = { id };
|
|
69
|
+
const id = documentKey(node);
|
|
70
|
+
const document = id === undefined ? {} : { id };
|
|
69
71
|
for (const field of searchType.fields) {
|
|
70
72
|
applyField(document, node, field, searchType, schema);
|
|
71
73
|
}
|
|
72
74
|
return document;
|
|
73
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* The document key a framed node carries, if any. A blank node label (`_:b0`) is
|
|
78
|
+
* not one: framing mints it per call, so it recurs across documents and can
|
|
79
|
+
* change when unrelated triples do. A node bearing one is therefore projected as
|
|
80
|
+
* if framing had pruned its `@id` – which it does whenever the label occurs only
|
|
81
|
+
* once in the framing results.
|
|
82
|
+
*/
|
|
83
|
+
function documentKey(node) {
|
|
84
|
+
const id = node['@id'];
|
|
85
|
+
return typeof id === 'string' && !id.startsWith('_:') ? id : undefined;
|
|
86
|
+
}
|
|
74
87
|
/**
|
|
75
88
|
* Project a single type over a known set of `roots` – the per-type, roots-given
|
|
76
89
|
* projection. The roots are supplied by the caller (the pipeline selector)
|
|
@@ -214,7 +227,10 @@ function applyFacet(document, raw, field) {
|
|
|
214
227
|
* {@link irAlias IR Alias} are each projected through the reference’s
|
|
215
228
|
* {@link ReferenceType} (whose own fields read their own aliases, minted against
|
|
216
229
|
* the reference type) and attached under the field’s name – a nested
|
|
217
|
-
* {@link
|
|
230
|
+
* {@link ProjectedNode} for a single reference, an array for an `array` one.
|
|
231
|
+
* A referent needs **no identity**: nesting carries its fields, not a document
|
|
232
|
+
* key, so a blank-node referent – whose `@id` JSON-LD 1.1 framing prunes when its
|
|
233
|
+
* label occurs once – nests exactly like a named one, minus the `id`.
|
|
218
234
|
* The referent is projected in full – internal fields included – so the
|
|
219
235
|
* declaring type’s (or the reference type’s own) derives can read them;
|
|
220
236
|
* {@link pruneInternalFields} then removes the internal fields from a *surfaced*
|
|
@@ -233,8 +249,13 @@ function applyInlineReference(document, node, alias, field, schema) {
|
|
|
233
249
|
}
|
|
234
250
|
const referents = valuesOf(node, alias)
|
|
235
251
|
.filter(isObject)
|
|
236
|
-
.
|
|
237
|
-
|
|
252
|
+
.map((referent) => projectFields(referent, referenceType, schema))
|
|
253
|
+
// Fields, not identity, are what makes something a referent: a literal
|
|
254
|
+
// value object under the alias (dirty source data), or a node this
|
|
255
|
+
// reference type reads nothing from, projects nothing and is no referent.
|
|
256
|
+
// Nesting it would hand the writer a content-free document – and, for a
|
|
257
|
+
// single-valued reference, let it win the slot over a real referent.
|
|
258
|
+
.filter((referent) => Object.keys(referent).length > 0);
|
|
238
259
|
if (referents.length === 0) {
|
|
239
260
|
return;
|
|
240
261
|
}
|
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
|
|
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
|
|
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})
|
|
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
|
|
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
|
|
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.
|
package/dist/query.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
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})
|
|
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
|
|
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
|
|
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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ProjectedNode } from './project.js';
|
|
2
2
|
/**
|
|
3
3
|
* The engine-neutral kind of a queryable field. It drives every downstream
|
|
4
4
|
* behavior: which physical fields the projection emits, the engine
|
|
@@ -82,9 +82,12 @@ export interface SearchFieldBase {
|
|
|
82
82
|
* {@link isInternalField internal} field). Return `undefined` to leave the
|
|
83
83
|
* field absent. The field still carries full query/schema/output behaviour
|
|
84
84
|
* like any other. Reading only the document is what keeps `path` the complete
|
|
85
|
-
* statement of what the projection reads from the graph.
|
|
85
|
+
* statement of what the projection reads from the graph. The node it receives
|
|
86
|
+
* is the search document for a root type, and the referent’s own
|
|
87
|
+
* projected fields for a Reference Type – which carry an `id` only when the
|
|
88
|
+
* referent is a named node, never for a blank-node one.
|
|
86
89
|
*/
|
|
87
|
-
readonly derive?: (document:
|
|
90
|
+
readonly derive?: (document: ProjectedNode) => unknown;
|
|
88
91
|
}
|
|
89
92
|
/** Full-text inclusion with a `query_by` weight (folded; per-locale for
|
|
90
93
|
* localized text). Presence is what makes a field searchable. */
|
|
@@ -341,8 +344,21 @@ export declare function labelFieldOf(searchType: SearchType): TextField | undefi
|
|
|
341
344
|
*/
|
|
342
345
|
export interface SearchTypeIssue {
|
|
343
346
|
readonly field: string;
|
|
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';
|
|
347
|
+
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';
|
|
345
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* The reserved logical name of a document’s IRI – the one field every indexed
|
|
351
|
+
* thing carries, and the only one no {@link SearchType} declares. It is the
|
|
352
|
+
* hit’s identity ({@link SearchHit.id}), not a value in its
|
|
353
|
+
* {@link ResultDocument}, so it is surfaced and filtered by name alone.
|
|
354
|
+
*
|
|
355
|
+
* `id` answers *what the thing is*; a domain field like `schema:identifier`
|
|
356
|
+
* answers *what a source system calls it* (a catalogue or record number). Those
|
|
357
|
+
* are different questions, so a declaration may still carry an `identifier`
|
|
358
|
+
* field of its own – but never an `id` one, which
|
|
359
|
+
* {@link validateSearchType} rejects as `reserved-field-name`.
|
|
360
|
+
*/
|
|
361
|
+
export declare const ID_FIELD = "id";
|
|
346
362
|
/**
|
|
347
363
|
* Structurally validate one {@link SearchType} declaration – the
|
|
348
364
|
* declaration-time counterpart of `validateQuery`. Rules:
|
|
@@ -350,7 +366,7 @@ export interface SearchTypeIssue {
|
|
|
350
366
|
* - field names are unique (a duplicate would silently shadow in every
|
|
351
367
|
* consumer, each picking a different winner) and a metacharacter-free
|
|
352
368
|
* identifier (the name is interpolated into physical field names and the
|
|
353
|
-
* display RE2 pattern);
|
|
369
|
+
* display RE2 pattern), and is not the reserved {@link ID_FIELD};
|
|
354
370
|
* - a `text` field’s declared locales are BCP-47-shaped (no `_`, which is the
|
|
355
371
|
* reserved name↔locale separator);
|
|
356
372
|
* - a `reference` field that is `output` declares `ref` (the API surfaces
|
package/dist/schema.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,
|
|
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)
|
|
9
|
-
* `searchFacets` batch – for every type in the
|
|
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);
|
package/dist/testing.d.ts.map
CHANGED
|
@@ -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;
|
|
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)
|
|
11
|
-
* `searchFacets` batch – for every type in the
|
|
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.
|
|
3
|
+
"version": "0.14.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",
|