@lde/search 0.3.1 → 0.4.1

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
@@ -203,6 +203,12 @@ A `reference` carries `labelOnly` today (id + display label); the `idOnly` and
203
203
  day one so that `inline` can later **add** fields to a reference type without
204
204
  breaking clients.
205
205
 
206
+ A reference resolves its label from a **label source**: `labelSource` names
207
+ the `SearchType` whose collection holds the referenced entities. The named
208
+ type must declare an `output`, `searchable` text field called `label` –
209
+ `searchSchema` validates this schema-wide, so a dangling or unsuitable label
210
+ source fails at startup. A reference without a `labelSource` stays id-only.
211
+
206
212
  ## Projection
207
213
 
208
214
  `projectGraph` is fully streaming: subjects are grouped and framed one at a time
@@ -212,6 +218,21 @@ triples are collapsed first, because some SPARQL engines (e.g. QLever) do not
212
218
  deduplicate `CONSTRUCT` output. The IR carries no `@context`, so a `derive` function
213
219
  reading it sees full predicate IRIs with language tags preserved.
214
220
 
221
+ `projectGraph` **consumes the quads once** – a single scan builds the subject
222
+ index every type frames off – and so accepts any `Iterable<Quad>`, not just a
223
+ materialized array. A caller merging several sources can pass a chained
224
+ generator instead of building a third full array at the projection peak:
225
+
226
+ ```ts
227
+ projectGraph(
228
+ (function* () {
229
+ yield* registerQuads;
230
+ yield* dkgQuads;
231
+ })(),
232
+ schema,
233
+ );
234
+ ```
235
+
215
236
  ## Locales
216
237
 
217
238
  `locales` is the **single** list of locales a `text` field projects (`und` =
@@ -286,7 +307,7 @@ await engine.search(OTHER_TYPE, query); // compile error: not in this schema
286
307
  ```
287
308
 
288
309
  `searchFacets(type, queries)` is the port’s **batch entry point**: several
289
- facet-only queries – e.g. a faceted sidebar’s skip-own-filter variants –
310
+ facet-only queries – e.g. a faceted listing’s skip-own-filter variants –
290
311
  answered in one engine round-trip (Typesense: a single `multi_search`), one
291
312
  outcome per query, positionally aligned – its facet map, or an in-place error,
292
313
  so one failed query never discards its siblings’ facets. The same schema
package/dist/adapter.d.ts CHANGED
@@ -6,7 +6,7 @@
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, searchableFields, facetableFields, filterableFields, sortableFields, outputFields, referenceFields, fieldNamed, isRangeFacet, isoToUnixSeconds, unixSecondsToIso, } from './schema.js';
9
+ export { assertTypeInSchema, physicalFields, searchableFields, facetableFields, filterableFields, sortableFields, outputFields, referenceFields, fieldNamed, labelFieldOf, isRangeFacet, isoToUnixSeconds, unixSecondsToIso, } from './schema.js';
10
10
  export type { PhysicalFields } from './schema.js';
11
11
  export { filterOperatorFor, filterOperator, validateQuery, assertValidQuery, pageForOffset, } from './query.js';
12
12
  export type { FilterOperator, QueryIssue } from './query.js';
@@ -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,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,eAAe,EACf,UAAU,EACV,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,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,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,eAAe,EACf,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,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"}
package/dist/adapter.js CHANGED
@@ -6,5 +6,5 @@
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, searchableFields, facetableFields, filterableFields, sortableFields, outputFields, referenceFields, fieldNamed, isRangeFacet, isoToUnixSeconds, unixSecondsToIso, } from './schema.js';
9
+ export { assertTypeInSchema, physicalFields, searchableFields, facetableFields, filterableFields, sortableFields, outputFields, referenceFields, fieldNamed, labelFieldOf, isRangeFacet, isoToUnixSeconds, unixSecondsToIso, } from './schema.js';
10
10
  export { filterOperatorFor, filterOperator, validateQuery, assertValidQuery, pageForOffset, } from './query.js';
package/dist/engine.d.ts CHANGED
@@ -40,7 +40,7 @@ export interface SearchEngine<Types extends readonly SearchType[] = readonly Sea
40
40
  * in a single engine round-trip (where the engine supports one, e.g.
41
41
  * Typesense `multi_search`), returning one {@link FacetsOutcome} per query,
42
42
  * positionally aligned with `queries`. This is what keeps a faceted
43
- * sidebar – one skip-own-filter query variant per filtered facet – from
43
+ * listing – one skip-own-filter query variant per filtered facet – from
44
44
  * fanning out into per-facet engine calls. Only facet buckets come back: an
45
45
  * engine answers every query in the batch facet-only (as if `limit: 0` and
46
46
  * without ordering, whatever the query carries), so hits are never
@@ -2,15 +2,45 @@ import type { Quad } from '@rdfjs/types';
2
2
  /** A framed JSON-LD node (full-IRI keys); the engine-agnostic search IR. */
3
3
  export type FramedNode = Record<string, unknown>;
4
4
  /**
5
- * Frame CONSTRUCT quads into one JSON-LD node per subject of `rootType`. Each
5
+ * A one-pass index of a quad source: every subject’s (deduplicated) triples,
6
+ * plus the root subjects of each requested type in appearance order. Built by
7
+ * {@link buildSubjectIndex} from a single iteration of the source, so a caller
8
+ * can pass a chained generator (`function* () { yield* a; yield* b; }`) rather
9
+ * than materialize a merged array; a multi-type projection then frames every
10
+ * type off this one index instead of re-scanning per type.
11
+ */
12
+ export interface SubjectIndex {
13
+ /** Each subject IRI → its own deduplicated triples, in first-seen order. */
14
+ readonly bySubject: ReadonlyMap<string, readonly Quad[]>;
15
+ /** Each requested root type IRI → its root subjects, in appearance order. */
16
+ readonly rootsByType: ReadonlyMap<string, readonly string[]>;
17
+ }
18
+ /**
19
+ * Iterate `quads` once, grouping each subject’s triples and collecting the root
20
+ * subjects of every type in `rootTypes`. Duplicate triples are collapsed here
21
+ * because some SPARQL engines (e.g. QLever) do not deduplicate CONSTRUCT
22
+ * output. The source is consumed a single time, so it may be a one-shot
23
+ * iterable (a generator chaining several sources); the whole subject index is
24
+ * held, but never more than that plus one framed subgraph at a time
25
+ * downstream.
26
+ */
27
+ export declare function buildSubjectIndex(quads: Iterable<Quad>, rootTypes: Iterable<string>): SubjectIndex;
28
+ /**
29
+ * Frame every root subject of `rootType` from a prebuilt {@link SubjectIndex}
30
+ * into one JSON-LD node – the reusable core behind {@link frameByType}. Each
6
31
  * root subject’s own triples plus the one-hop nodes it references (e.g. nested
7
- * publisher/distribution resources) are grouped lazily and framed one at a
8
- * time, so beyond the subject index only a single subgraph is held — whole-graph
9
- * `jsonld.frame()` is ~O(N²). Duplicate triples are collapsed first because some
10
- * SPARQL engines
11
- * (e.g. QLever) do not dedupe CONSTRUCT output. The caller supplies the root
12
- * type, keeping the framing domain-agnostic; the frame carries no `@context`, so
13
- * framed keys are full predicate IRIs.
32
+ * publisher/distribution resources) are framed one subject at a time, so beyond
33
+ * the shared subject index only a single subgraph is held (whole-graph
34
+ * `jsonld.frame()` is ~O(N²)). The frame carries no `@context`, so framed keys
35
+ * are full predicate IRIs.
36
+ */
37
+ export declare function frameSubjects(index: SubjectIndex, rootType: string): AsyncIterable<FramedNode>;
38
+ /**
39
+ * Frame CONSTRUCT quads into one JSON-LD node per subject of `rootType`, for a
40
+ * single root type. Consumes `quads` once (see {@link buildSubjectIndex}), so
41
+ * it accepts any `Iterable` – a materialized array or a chained generator. For
42
+ * a multi-type schema, {@link buildSubjectIndex} + {@link frameSubjects} share
43
+ * one index across types rather than re-scanning per type.
14
44
  */
15
- export declare function frameByType(quads: readonly Quad[], rootType: string): AsyncIterable<FramedNode>;
45
+ export declare function frameByType(quads: Iterable<Quad>, rootType: string): AsyncIterable<FramedNode>;
16
46
  //# sourceMappingURL=frame-by-type.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"frame-by-type.d.ts","sourceRoot":"","sources":["../src/frame-by-type.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAMzC,4EAA4E;AAC5E,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAIjD;;;;;;;;;;GAUG;AACH,wBAAuB,WAAW,CAChC,KAAK,EAAE,SAAS,IAAI,EAAE,EACtB,QAAQ,EAAE,MAAM,GACf,aAAa,CAAC,UAAU,CAAC,CAkB3B"}
1
+ {"version":3,"file":"frame-by-type.d.ts","sourceRoot":"","sources":["../src/frame-by-type.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAMzC,4EAA4E;AAC5E,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAIjD;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,IAAI,EAAE,CAAC,CAAC;IACzD,6EAA6E;IAC7E,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC;CAC9D;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,EACrB,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,GAC1B,YAAY,CA8Bd;AAED;;;;;;;;GAQG;AACH,wBAAuB,aAAa,CAClC,KAAK,EAAE,YAAY,EACnB,QAAQ,EAAE,MAAM,GACf,aAAa,CAAC,UAAU,CAAC,CA+B3B;AAED;;;;;;GAMG;AACH,wBAAuB,WAAW,CAChC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,EACrB,QAAQ,EAAE,MAAM,GACf,aAAa,CAAC,UAAU,CAAC,CAE3B"}
@@ -3,40 +3,20 @@ import { rdf } from '@tpluscode/rdf-ns-builders';
3
3
  const RDF_TYPE = rdf.type.value;
4
4
  const FRAME_OPTIONS = { omitGraph: false, embed: '@always' };
5
5
  /**
6
- * Frame CONSTRUCT quads into one JSON-LD node per subject of `rootType`. Each
7
- * root subject’s own triples plus the one-hop nodes it references (e.g. nested
8
- * publisher/distribution resources) are grouped lazily and framed one at a
9
- * time, so beyond the subject index only a single subgraph is held — whole-graph
10
- * `jsonld.frame()` is ~O(N²). Duplicate triples are collapsed first because some
11
- * SPARQL engines
12
- * (e.g. QLever) do not dedupe CONSTRUCT output. The caller supplies the root
13
- * type, keeping the framing domain-agnostic; the frame carries no `@context`, so
14
- * framed keys are full predicate IRIs.
6
+ * Iterate `quads` once, grouping each subject’s triples and collecting the root
7
+ * subjects of every type in `rootTypes`. Duplicate triples are collapsed here
8
+ * because some SPARQL engines (e.g. QLever) do not deduplicate CONSTRUCT
9
+ * output. The source is consumed a single time, so it may be a one-shot
10
+ * iterable (a generator chaining several sources); the whole subject index is
11
+ * held, but never more than that plus one framed subgraph at a time
12
+ * downstream.
15
13
  */
16
- export async function* frameByType(quads, rootType) {
17
- for (const { rootIri, subgraph } of groupByRoot(quads, rootType)) {
18
- const expanded = await jsonld.fromRDF(subgraph);
19
- // Frame for THIS specific root subject by `@id`, not just by root type. A
20
- // one-hop reference can itself be of `rootType` (e.g. a terminology source
21
- // that is also a separately registered dataset), so framing by type alone
22
- // returns several root nodes and `[0]` could be the referenced one — which
23
- // would emit it twice and drop this subject entirely.
24
- const framed = await jsonld.frame(expanded, { '@id': rootIri }, FRAME_OPTIONS);
25
- const node = framed['@graph']?.[0];
26
- if (node !== undefined) {
27
- yield node;
28
- }
29
- }
30
- }
31
- /**
32
- * Yield one self-contained quad subgraph per root subject – its own (deduped)
33
- * triples plus the triples of the one-hop IRI or blank nodes it references –
34
- * lazily, so only the subject index and the current subgraph are held at once
35
- * (never the whole materialized list of subgraphs).
36
- */
37
- function* groupByRoot(quads, rootType) {
14
+ export function buildSubjectIndex(quads, rootTypes) {
38
15
  const bySubject = new Map();
39
- const rootIris = [];
16
+ const rootsByType = new Map();
17
+ for (const type of rootTypes) {
18
+ rootsByType.set(type, []);
19
+ }
40
20
  const seen = new Set();
41
21
  for (const quad of quads) {
42
22
  const key = `${quad.subject.value} ${quad.predicate.value} ${quad.object.value} ${quad.object.termType === 'Literal' ? quad.object.language || quad.object.datatype.value : ''}`;
@@ -52,16 +32,58 @@ function* groupByRoot(quads, rootType) {
52
32
  else {
53
33
  owned.push(quad);
54
34
  }
55
- if (quad.predicate.value === RDF_TYPE && quad.object.value === rootType) {
56
- rootIris.push(subject);
35
+ // `rootsByType` is keyed by exactly the requested root types, so the lookup
36
+ // doubles as the membership test: a hit means this subject is a root.
37
+ if (quad.predicate.value === RDF_TYPE) {
38
+ const roots = rootsByType.get(quad.object.value);
39
+ if (roots !== undefined) {
40
+ roots.push(subject);
41
+ }
57
42
  }
58
43
  }
59
- for (const iri of rootIris) {
60
- const owned = bySubject.get(iri) ?? [];
44
+ return { bySubject, rootsByType };
45
+ }
46
+ /**
47
+ * Frame every root subject of `rootType` from a prebuilt {@link SubjectIndex}
48
+ * into one JSON-LD node – the reusable core behind {@link frameByType}. Each
49
+ * root subject’s own triples plus the one-hop nodes it references (e.g. nested
50
+ * publisher/distribution resources) are framed one subject at a time, so beyond
51
+ * the shared subject index only a single subgraph is held (whole-graph
52
+ * `jsonld.frame()` is ~O(N²)). The frame carries no `@context`, so framed keys
53
+ * are full predicate IRIs.
54
+ */
55
+ export async function* frameSubjects(index, rootType) {
56
+ const { bySubject } = index;
57
+ // A type the index was not built for has no roots, so it frames nothing; the
58
+ // `projectType`/`projectGraph` callers only pass types they registered.
59
+ const roots = index.rootsByType.get(rootType) ?? [];
60
+ for (const rootIri of roots) {
61
+ const owned = bySubject.get(rootIri) ?? [];
61
62
  const referenced = owned
62
63
  .filter((quad) => quad.object.termType === 'NamedNode' ||
63
64
  quad.object.termType === 'BlankNode')
64
65
  .flatMap((quad) => bySubject.get(quad.object.value) ?? []);
65
- yield { rootIri: iri, subgraph: [...owned, ...referenced] };
66
+ const subgraph = [...owned, ...referenced];
67
+ const expanded = await jsonld.fromRDF(subgraph);
68
+ // Frame for THIS specific root subject by `@id`, not just by root type. A
69
+ // one-hop reference can itself be of `rootType` (e.g. a terminology source
70
+ // that is also a separately registered dataset), so framing by type alone
71
+ // returns several root nodes and `[0]` could be the referenced one — which
72
+ // would emit it twice and drop this subject entirely.
73
+ const framed = await jsonld.frame(expanded, { '@id': rootIri }, FRAME_OPTIONS);
74
+ const node = framed['@graph']?.[0];
75
+ if (node !== undefined) {
76
+ yield node;
77
+ }
66
78
  }
67
79
  }
80
+ /**
81
+ * Frame CONSTRUCT quads into one JSON-LD node per subject of `rootType`, for a
82
+ * single root type. Consumes `quads` once (see {@link buildSubjectIndex}), so
83
+ * it accepts any `Iterable` – a materialized array or a chained generator. For
84
+ * a multi-type schema, {@link buildSubjectIndex} + {@link frameSubjects} share
85
+ * one index across types rather than re-scanning per type.
86
+ */
87
+ export async function* frameByType(quads, rootType) {
88
+ yield* frameSubjects(buildSubjectIndex(quads, [rootType]), rootType);
89
+ }
package/dist/project.d.ts CHANGED
@@ -19,8 +19,13 @@ export declare function projectDocument(node: FramedNode, searchType: SearchType
19
19
  * type’s declaration — the multi-shape pipeline. Streams one document at a time
20
20
  * so memory stays flat. The IR maps to a declaration by type, so adding a shape
21
21
  * is adding a `SearchType` to the schema (no engine change).
22
+ *
23
+ * Consumes `quads` once (a single scan builds the shared subject index that
24
+ * every type frames off), so it accepts any `Iterable` – a materialized array or
25
+ * a chained generator merging several sources (`function* () { yield* a; yield* b; }`)
26
+ * with no intermediate copy at the projection peak.
22
27
  */
23
- export declare function projectGraph(quads: readonly Quad[], schema: SearchSchema): AsyncIterable<SearchDocument>;
28
+ export declare function projectGraph(quads: Iterable<Quad>, schema: SearchSchema): AsyncIterable<SearchDocument>;
24
29
  export declare function literalsOf(node: FramedNode, path: string): string[];
25
30
  export declare function firstLiteralOf(node: FramedNode, path: string): string | undefined;
26
31
  export declare function irisOf(node: FramedNode, path: string): string[];
@@ -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,EAAe,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAClE,OAAO,EAML,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,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEtE;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,UAAU,EAChB,UAAU,EAAE,UAAU,GACrB,cAAc,CAYhB;AAED;;;;;GAKG;AACH,wBAAuB,YAAY,CACjC,KAAK,EAAE,SAAS,IAAI,EAAE,EACtB,MAAM,EAAE,YAAY,GACnB,aAAa,CAAC,cAAc,CAAC,CAM/B;AAsID,wBAAgB,UAAU,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAInE;AAED,wBAAgB,cAAc,CAC5B,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,MAAM,GACX,MAAM,GAAG,SAAS,CAEpB;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAI/D"}
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,EAML,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,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEtE;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,UAAU,EAChB,UAAU,EAAE,UAAU,GACrB,cAAc,CAYhB;AAED;;;;;;;;;;GAUG;AACH,wBAAuB,YAAY,CACjC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,EACrB,MAAM,EAAE,YAAY,GACnB,aAAa,CAAC,cAAc,CAAC,CAW/B;AAsID,wBAAgB,UAAU,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAInE;AAED,wBAAgB,cAAc,CAC5B,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,MAAM,GACX,MAAM,GAAG,SAAS,CAEpB;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAI/D"}
package/dist/project.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { fold } from '@lde/text-normalization';
2
- import { frameByType } from './frame-by-type.js';
2
+ import { buildSubjectIndex, frameSubjects, } from './frame-by-type.js';
3
3
  import { isoToUnixSeconds, physicalFields, } from './schema.js';
4
4
  /**
5
5
  * Project one framed JSON-LD node into a flat search document: apply each field
@@ -25,10 +25,17 @@ export function projectDocument(node, searchType) {
25
25
  * type’s declaration — the multi-shape pipeline. Streams one document at a time
26
26
  * so memory stays flat. The IR maps to a declaration by type, so adding a shape
27
27
  * is adding a `SearchType` to the schema (no engine change).
28
+ *
29
+ * Consumes `quads` once (a single scan builds the shared subject index that
30
+ * every type frames off), so it accepts any `Iterable` – a materialized array or
31
+ * a chained generator merging several sources (`function* () { yield* a; yield* b; }`)
32
+ * with no intermediate copy at the projection peak.
28
33
  */
29
34
  export async function* projectGraph(quads, schema) {
30
- for (const searchType of schema.values()) {
31
- for await (const node of frameByType(quads, searchType.type)) {
35
+ const types = [...schema.values()];
36
+ const index = buildSubjectIndex(quads, types.map((searchType) => searchType.type));
37
+ for (const searchType of types) {
38
+ for await (const node of frameSubjects(index, searchType.type)) {
32
39
  yield projectDocument(node, searchType);
33
40
  }
34
41
  }
package/dist/schema.d.ts CHANGED
@@ -125,6 +125,15 @@ export interface ReferenceField extends SearchFieldBase, Searchable {
125
125
  /** Projection-time value transform. */
126
126
  readonly transform?: (value: string) => string;
127
127
  readonly facetRanges?: never;
128
+ /**
129
+ * The `name` of the {@link SearchType} whose collection resolves this
130
+ * reference’s labels – its ‘label source’. The named type must declare an
131
+ * `output`, `searchable` text field called `label` (validated by
132
+ * {@link searchSchema}), so an engine can both reconstruct the label and
133
+ * search it (typeahead). Omit for an id-only reference: no label
134
+ * resolution.
135
+ */
136
+ readonly labelSource?: string;
128
137
  /** The referenced entity’s shape and how much of it to carry. Required when
129
138
  * the field is `output` (the API surfaces need the reference type name);
130
139
  * optional for a facet- or filter-only reference. */
@@ -234,6 +243,15 @@ export interface SearchSchema<Types extends readonly SearchType[] = readonly Sea
234
243
  * schema fails at startup, not per document at index time or per query.
235
244
  */
236
245
  export declare function searchSchema<const Types extends readonly SearchType[]>(...types: Types): SearchSchema<Types>;
246
+ /**
247
+ * The text field a label source serves labels from – the ‘label’ convention
248
+ * in one place: an `output` (something to reconstruct a label from),
249
+ * `searchable` (something to type ahead against) text field called `label`.
250
+ * Returns `undefined` when the type declares no such field; a schema built by
251
+ * {@link searchSchema} guarantees it for every type named as a
252
+ * {@link ReferenceField.labelSource}.
253
+ */
254
+ export declare function labelFieldOf(searchType: SearchType): TextField | undefined;
237
255
  /**
238
256
  * One structural problem {@link validateSearchType} found: a field declares a
239
257
  * capability or property its `kind` cannot honour, or the declaration is
@@ -315,7 +333,7 @@ export declare function sortableFields(searchType: SearchType): readonly SearchF
315
333
  /** Fields that appear in the API output type, in declaration order. */
316
334
  export declare function outputFields(searchType: SearchType): readonly SearchField[];
317
335
  /** Fields of kind `reference` (IRI-valued, label-resolved), in declaration order. */
318
- export declare function referenceFields(searchType: SearchType): readonly SearchField[];
336
+ export declare function referenceFields(searchType: SearchType): readonly ReferenceField[];
319
337
  /** Look up a field by its logical name. */
320
338
  export declare function fieldNamed(searchType: SearchType, name: string): SearchField | undefined;
321
339
  /**
@@ -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
@@ -51,9 +51,54 @@ export function searchSchema(...types) {
51
51
  typeIris.add(searchType.type);
52
52
  names.add(searchType.name);
53
53
  }
54
+ assertResolvableLabelSources(types);
54
55
  // The one blessed cast: only this validated constructor mints the brand.
55
56
  return new Map(types.map((searchType) => [searchType.type, searchType]));
56
57
  }
58
+ /**
59
+ * The text field a label source serves labels from – the ‘label’ convention
60
+ * in one place: an `output` (something to reconstruct a label from),
61
+ * `searchable` (something to type ahead against) text field called `label`.
62
+ * Returns `undefined` when the type declares no such field; a schema built by
63
+ * {@link searchSchema} guarantees it for every type named as a
64
+ * {@link ReferenceField.labelSource}.
65
+ */
66
+ export function labelFieldOf(searchType) {
67
+ const field = fieldNamed(searchType, 'label');
68
+ return field !== undefined &&
69
+ field.kind === 'text' &&
70
+ field.output === true &&
71
+ field.searchable !== undefined
72
+ ? field
73
+ : undefined;
74
+ }
75
+ /**
76
+ * Every {@link ReferenceField.labelSource} must name a declared type that can
77
+ * actually serve labels ({@link labelFieldOf}). Checked schema-wide, because
78
+ * a single declaration cannot see its siblings.
79
+ */
80
+ function assertResolvableLabelSources(types) {
81
+ const byName = new Map(types.map((searchType) => [searchType.name, searchType]));
82
+ for (const searchType of types) {
83
+ for (const field of searchType.fields) {
84
+ const labelSource = field
85
+ .labelSource;
86
+ if (labelSource === undefined) {
87
+ continue;
88
+ }
89
+ if (field.kind !== 'reference') {
90
+ throw new Error(`Field “${searchType.name}.${field.name}” declares a label source but is a ${field.kind} field; only reference fields resolve labels from a source.`);
91
+ }
92
+ const source = byName.get(labelSource);
93
+ if (source === undefined) {
94
+ throw new Error(`Reference “${searchType.name}.${field.name}” names unknown label source “${field.labelSource}”; declare a SearchType with that name.`);
95
+ }
96
+ if (labelFieldOf(source) === undefined) {
97
+ throw new Error(`Reference “${searchType.name}.${field.name}” uses label source “${field.labelSource}”, which must declare an output, searchable text field “label”.`);
98
+ }
99
+ }
100
+ }
101
+ }
57
102
  /** Kinds that can feed full-text search (project a folded search field). */
58
103
  const SEARCHABLE_KINDS = ['text', 'keyword', 'reference'];
59
104
  /** Kinds whose projection applies the {@link KeywordField.transform}. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lde/search",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
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",
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/jsonld": "^1.5.15",
48
- "n3": "^2.1.0"
48
+ "n3": "^2.1.1"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "vitest": "^4.0.0"