@lde/search 0.8.0 → 0.9.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
@@ -29,11 +29,11 @@ It provides four things:
29
29
  out of it, so the two cannot drift;
30
30
  - **engine port** – `SearchEngine` and the logical result types
31
31
  (`SearchResult` / `SearchHit` / `ResultDocument` / `Reference` / …);
32
- - **streaming projection** – `projectGraph`, RDF `CONSTRUCT` quads → flat
33
- search documents.
32
+ - **streaming projection** – `projectRoots`, RDF `CONSTRUCT` quads → flat
33
+ search documents, one root type at a time.
34
34
 
35
35
  ```
36
- SearchSchema ─┬─► projection (projectGraph → flat documents) [here]
36
+ SearchSchema ─┬─► projection (projectRoots → flat documents) [here]
37
37
  ├─► engine adapter (collection definition + query compiler) e.g. @lde/search-typesense
38
38
  ├─► query semantics (SearchQuery, filter/sort/facet) [here]
39
39
  └─► API surface (GraphQL / REST) e.g. @lde/search-api-graphql
@@ -58,7 +58,7 @@ a typed, deterministic function – easy to test, and swappable per deployment.
58
58
  Exports are stratified by audience:
59
59
 
60
60
  - **`@lde/search`** – the authoring surface: `defineSearchType`,
61
- `searchSchema`, `projectGraph` (+ the IR readers `derive` functions use),
61
+ `searchSchema`, `projectRoots` (+ the IR readers `derive` functions use),
62
62
  validation, and every model/query/result type.
63
63
  - **`@lde/search/adapter`** – plumbing for engine adapters and API surfaces:
64
64
  `physicalFields`, the field selectors, `assertValidQuery`, the filter
@@ -78,8 +78,9 @@ and GraphQL (one of the surfaces):
78
78
  | `SearchType` | One root type’s complete declaration: its logical API `name`, its `class` IRI and its fields (incl. derived) | NodeShape | object type |
79
79
  | `SearchSchema` | The whole search declaration: every `SearchType`, keyed by `class` IRI – build one with `searchSchema(...types)` | shapes graph | schema |
80
80
 
81
- `projectGraph` and the GraphQL surface consume a `SearchSchema` (projecting
82
- every type in one pass); the engine port executes one `SearchType` at a time.
81
+ `projectRoots` and the engine port each execute one `SearchType` at a time –
82
+ projection over the roots the pipeline selector supplied for that type; the
83
+ GraphQL surface consumes the whole `SearchSchema`.
83
84
 
84
85
  ### API conventions
85
86
 
@@ -110,7 +111,7 @@ query compiler all share.
110
111
  ```ts
111
112
  import {
112
113
  defineSearchType,
113
- projectGraph,
114
+ projectRoots,
114
115
  irisOf,
115
116
  searchSchema,
116
117
  } from '@lde/search';
@@ -150,14 +151,12 @@ const DATASET = defineSearchType({
150
151
  ],
151
152
  });
152
153
 
153
- for await (const { searchType, document } of projectGraph(
154
- quads,
155
- searchSchema(DATASET),
156
- )) {
157
- // one flat search document per matching subject, streamed and tagged with the
158
- // SearchType it was framed from — so a multi-collection writer can route each
159
- // document to the collection for its type. A single-collection consumer just
160
- // reads `document`.
154
+ const schema = searchSchema(DATASET);
155
+ for await (const document of projectRoots(quads, roots, schema, DATASET)) {
156
+ // one flat search document per given root subject, streamed. The caller (the
157
+ // pipeline selector) supplies `roots`; pairing a document with its type for a
158
+ // multi-collection writer is the pipeline glue’s job (see
159
+ // `@lde/search-pipeline`), not the projection’s.
161
160
  }
162
161
  ```
163
162
 
@@ -219,31 +218,38 @@ source fails at startup. A reference without a `labelSource` stays id-only.
219
218
 
220
219
  ## Projection
221
220
 
222
- `projectGraph` is fully streaming: subjects are grouped and framed one at a time
223
- and documents are yielded as produced, so beyond a subject index memory stays
224
- flat at scale (framing the whole graph at once is roughly O(N²)). Duplicate
225
- triples are collapsed first, because some SPARQL engines (e.g. QLever) do not
226
- deduplicate `CONSTRUCT` output. The IR carries no `@context`, so a `derive` function
227
- reading it sees full predicate IRIs with language tags preserved.
228
-
229
- Each yielded value is a `{ searchType, document }` pair: the whole schema
230
- projects into **one** mixed stream, and every document is tagged with the
231
- `SearchType` it was framed from. That tag is what lets the write side fan the
232
- stream out to per-type collections without re-deriving the type from the
233
- document – see `@lde/search-pipeline`’s multi-collection writer.
234
-
235
- `projectGraph` **consumes the quads once** – a single scan builds the subject
236
- index every type frames off – and so accepts any `Iterable<Quad>`, not just a
237
- materialized array. A caller merging several sources can pass a chained
221
+ `projectRoots` projects **one root type** over the roots the caller supplies –
222
+ the pipeline selector already holds them, so nothing is discovered from
223
+ `rdf:type` and a `CONSTRUCT` need emit no type triple. It is fully streaming:
224
+ each root’s subgraph is framed one at a time and its document yielded as
225
+ produced, so beyond a subject index memory stays flat at scale (framing the
226
+ whole graph at once is roughly O(N²)). Duplicate triples are collapsed first,
227
+ because some SPARQL engines (e.g. QLever) do not deduplicate `CONSTRUCT` output.
228
+ The IR carries no `@context`, so a `derive` function reading it sees full
229
+ predicate IRIs with language tags preserved. `assertTypeInSchema` guards that
230
+ the passed `SearchType` is a member of the schema – the port’s own membership
231
+ check – so no schema is ever forged to scope a projection to one type.
232
+
233
+ `projectRoots` yields a **bare** `SearchDocument`. Pairing each document with the
234
+ `SearchType` it belongs to, so the write side can fan a mixed stream out to
235
+ per-type collections, is a routing concern owned by the pipeline glue – see
236
+ `@lde/search-pipeline`’s `searchStages` and multi-collection writer – not the
237
+ projection. One stage per root type keeps `@lde/search` pipeline-free.
238
+
239
+ `projectRoots` **consumes the quads once** – a single scan builds the subject
240
+ index the roots frame off – and so accepts any `Iterable<Quad>`, not just a
241
+ materialized array. A caller merging several readers can pass a chained
238
242
  generator instead of building a third full array at the projection peak:
239
243
 
240
244
  ```ts
241
- projectGraph(
245
+ projectRoots(
242
246
  (function* () {
243
247
  yield* registerQuads;
244
248
  yield* dkgQuads;
245
249
  })(),
250
+ roots,
246
251
  schema,
252
+ DATASET,
247
253
  );
248
254
  ```
249
255
 
@@ -307,7 +313,7 @@ unrepresentable; the port enforces them for everyone else (deployment
307
313
  ## Typed results
308
314
 
309
315
  An engine is **bound to the whole `SearchSchema` at construction** – like
310
- every other schema consumer (`projectGraph(quads, schema)`,
316
+ every other schema consumer (`projectRoots(quads, roots, schema, type)`,
311
317
  `buildGraphQLSchema(schema)`): the adapter factory takes the deployment’s
312
318
  declaration, so a query can never meet the wrong index, and deployment-level
313
319
  concerns (the label cache, cross-type search, facet batching) have one home.
package/dist/engine.d.ts CHANGED
@@ -3,10 +3,10 @@ import type { SearchSchema, SearchType } from './schema.js';
3
3
  /**
4
4
  * The engine port: the boundary a concrete engine adapter (e.g. the engine
5
5
  * `@lde/search-typesense`’s `createTypesenseSearchEngine` returns) implements.
6
- * An engine is **bound to the whole {@link SearchSchema} at construction** —
6
+ * An engine is **bound to the whole {@link SearchSchema} at construction** –
7
7
  * the adapter factory takes the deployment’s declaration together with the
8
8
  * physical location of every type (its collections map), mirroring the other
9
- * schema consumers (`projectGraph(quads, schema)`,
9
+ * schema consumers (`projectRoots(quads, roots, schema, type)`,
10
10
  * `buildGraphQLSchema(schema)`): everything is a function of the schema. A
11
11
  * query can never meet the wrong index, deployment-level concerns (label
12
12
  * cache, cross-type search, facet batching) have one home, and a search names
@@ -19,8 +19,8 @@ import type { SearchSchema, SearchType } from './schema.js';
19
19
  *
20
20
  * Port contract: an adapter ALWAYS rejects a `searchType` that is not in its
21
21
  * bound schema, and ALWAYS validates the incoming query against it
22
- * (`assertValidQuery`) — unknown or non-filterable fields, mismatched
23
- * operators, unknown facets — rather than passing garbage to its engine.
22
+ * (`assertValidQuery`) – unknown or non-filterable fields, mismatched
23
+ * operators, unknown facets – rather than passing garbage to its engine.
24
24
  * Validation is not the caller’s job: it must hold for every surface and for
25
25
  * injected deployment policy.
26
26
  *
@@ -31,7 +31,7 @@ import type { SearchSchema, SearchType } from './schema.js';
31
31
  * gracefully to string keys ({@link FacetFieldsOf}).
32
32
  */
33
33
  export interface SearchEngine<Types extends readonly SearchType[] = readonly SearchType[]> {
34
- /** The declaration this engine serves — exposed so a surface can route and
34
+ /** The declaration this engine serves – exposed so a surface can route and
35
35
  * a caller can enumerate the searchable types. */
36
36
  readonly schema: SearchSchema<Types>;
37
37
  search<T extends Types[number]>(searchType: T, query: SearchQuery): Promise<SearchResult<FacetFieldsOf<T>, OutputFieldsOf<T>>>;
@@ -78,7 +78,7 @@ export interface SearchResult<FacetField extends string = string, OutputField ex
78
78
  */
79
79
  export type FacetMap<FacetField extends string = string> = Readonly<Partial<Record<FacetField, readonly FacetBucket[]>>>;
80
80
  /**
81
- * The facet-field-name union of a search type — the keys a {@link SearchResult}’s
81
+ * The facet-field-name union of a search type – the keys a {@link SearchResult}’s
82
82
  * `facets` can hold. Narrow for a declaration captured as a literal (via
83
83
  * `defineSearchType` or `as const satisfies SearchType`); a plain
84
84
  * `: SearchType` annotation degrades to `string` (its field names are not
@@ -88,7 +88,7 @@ export type FacetFieldsOf<Type extends SearchType> = SearchType extends Type ? s
88
88
  readonly facetable: true;
89
89
  }>['name'];
90
90
  /**
91
- * The output-field-name union of a search type — the keys a {@link ResultDocument}
91
+ * The output-field-name union of a search type – the keys a {@link ResultDocument}
92
92
  * can hold. Like {@link FacetFieldsOf}, degrades to `string` for a widened
93
93
  * declaration.
94
94
  */
@@ -98,7 +98,7 @@ export type OutputFieldsOf<Type extends SearchType> = SearchType extends Type ?
98
98
  /**
99
99
  * One result row. `id` (the stable document key, an IRI) is kept *out* of
100
100
  * {@link ResultDocument}: it is always present and is the hit’s identity, a
101
- * different contract from the optional, typed logical field values — and it maps
101
+ * different contract from the optional, typed logical field values – and it maps
102
102
  * straight onto the GraphQL output’s guaranteed `id: String!`. The document
103
103
  * holds only the selectable fields.
104
104
  */
@@ -107,7 +107,7 @@ export interface SearchHit<OutputField extends string = string> {
107
107
  readonly document: ResultDocument<OutputField>;
108
108
  }
109
109
  /**
110
- * The logical result document at the query seam — engine- and RDF-neutral.
110
+ * The logical result document at the query seam – engine- and RDF-neutral.
111
111
  * Distinct from the flat, fanned-out projection `SearchDocument` that lives
112
112
  * index-side: this carries logical fields with language maps and references,
113
113
  * ready for a surface to shape. Keyed by output field name; `Partial` because a
@@ -2,45 +2,40 @@ 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
- * 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.
5
+ * A one-pass index of a quad source: every subject’s (deduplicated) triples.
6
+ * Built by {@link buildSubjectIndex} from a single iteration of the source, so a
7
+ * caller can pass a chained generator (`function* () { yield* a; yield* b; }`)
8
+ * rather than materialize a merged array, then frame any of its subjects off
9
+ * this one index.
11
10
  */
12
11
  export interface SubjectIndex {
13
12
  /** Each subject IRI → its own deduplicated triples, in first-seen order. */
14
13
  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
14
  }
18
15
  /**
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
16
+ * Iterate `quads` once, grouping each subject’s triples. Duplicate triples are
17
+ * collapsed here because some SPARQL engines (e.g. QLever) do not deduplicate
18
+ * CONSTRUCT output. The source is consumed a single time, so it may be a
19
+ * one-shot iterable (a generator chaining several sources); the whole subject
20
+ * index is held, but never more than that plus one framed subgraph at a time
25
21
  * downstream.
22
+ *
23
+ * Roots are never discovered here – the caller ({@link projectRoots}, from the
24
+ * pipeline selector) already holds them and passes them to {@link frameSubjects}
25
+ * – so `quads` need carry no `rdf:type` triple.
26
26
  */
27
- export declare function buildSubjectIndex(quads: Iterable<Quad>, rootTypes: Iterable<string>): SubjectIndex;
27
+ export declare function buildSubjectIndex(quads: Iterable<Quad>): SubjectIndex;
28
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
31
- * root subject’s own triples plus the one-hop nodes it references (e.g. nested
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.
29
+ * Frame each of the given `roots` from a prebuilt {@link SubjectIndex} into one
30
+ * JSON-LD node. Each root subject’s own triples plus the one-hop nodes it
31
+ * references (e.g. nested publisher/distribution resources) are framed one
32
+ * subject at a time, so beyond the shared subject index only a single subgraph
33
+ * is held (whole-graph `jsonld.frame()` is ~O(N²)). The frame carries no
34
+ * `@context`, so framed keys are full predicate IRIs.
35
+ *
36
+ * The roots are supplied explicitly rather than discovered from `rdf:type`: the
37
+ * caller ({@link projectRoots}, from the pipeline selector) already holds them
38
+ * and passes them directly. A root absent from the index simply frames nothing.
36
39
  */
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.
44
- */
45
- export declare function frameByType(quads: Iterable<Quad>, rootType: string): AsyncIterable<FramedNode>;
40
+ export declare function frameSubjects(index: SubjectIndex, roots: readonly string[]): AsyncIterable<FramedNode>;
46
41
  //# 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;;;;;;;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"}
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;AAGzC,4EAA4E;AAC5E,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAIjD;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,IAAI,EAAE,CAAC,CAAC;CAC1D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,YAAY,CAkBrE;AAED;;;;;;;;;;;GAWG;AACH,wBAAuB,aAAa,CAClC,KAAK,EAAE,YAAY,EACnB,KAAK,EAAE,SAAS,MAAM,EAAE,GACvB,aAAa,CAAC,UAAU,CAAC,CA6B3B"}
@@ -1,22 +1,19 @@
1
1
  import jsonld from 'jsonld';
2
- import { rdf } from '@tpluscode/rdf-ns-builders';
3
- const RDF_TYPE = rdf.type.value;
4
2
  const FRAME_OPTIONS = { omitGraph: false, embed: '@always' };
5
3
  /**
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
4
+ * Iterate `quads` once, grouping each subject’s triples. Duplicate triples are
5
+ * collapsed here because some SPARQL engines (e.g. QLever) do not deduplicate
6
+ * CONSTRUCT output. The source is consumed a single time, so it may be a
7
+ * one-shot iterable (a generator chaining several sources); the whole subject
8
+ * index is held, but never more than that plus one framed subgraph at a time
12
9
  * downstream.
10
+ *
11
+ * Roots are never discovered here – the caller ({@link projectRoots}, from the
12
+ * pipeline selector) already holds them and passes them to {@link frameSubjects}
13
+ * – so `quads` need carry no `rdf:type` triple.
13
14
  */
14
- export function buildSubjectIndex(quads, rootTypes) {
15
+ export function buildSubjectIndex(quads) {
15
16
  const bySubject = new Map();
16
- const rootsByType = new Map();
17
- for (const type of rootTypes) {
18
- rootsByType.set(type, []);
19
- }
20
17
  const seen = new Set();
21
18
  for (const quad of quads) {
22
19
  const key = `${quad.subject.value} ${quad.predicate.value} ${quad.object.value} ${quad.object.termType === 'Literal' ? quad.object.language || quad.object.datatype.value : ''}`;
@@ -32,31 +29,23 @@ export function buildSubjectIndex(quads, rootTypes) {
32
29
  else {
33
30
  owned.push(quad);
34
31
  }
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
- }
42
- }
43
32
  }
44
- return { bySubject, rootsByType };
33
+ return { bySubject };
45
34
  }
46
35
  /**
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.
36
+ * Frame each of the given `roots` from a prebuilt {@link SubjectIndex} into one
37
+ * JSON-LD node. Each root subject’s own triples plus the one-hop nodes it
38
+ * references (e.g. nested publisher/distribution resources) are framed one
39
+ * subject at a time, so beyond the shared subject index only a single subgraph
40
+ * is held (whole-graph `jsonld.frame()` is ~O(N²)). The frame carries no
41
+ * `@context`, so framed keys are full predicate IRIs.
42
+ *
43
+ * The roots are supplied explicitly rather than discovered from `rdf:type`: the
44
+ * caller ({@link projectRoots}, from the pipeline selector) already holds them
45
+ * and passes them directly. A root absent from the index simply frames nothing.
54
46
  */
55
- export async function* frameSubjects(index, rootType) {
47
+ export async function* frameSubjects(index, roots) {
56
48
  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
49
  for (const rootIri of roots) {
61
50
  const owned = bySubject.get(rootIri) ?? [];
62
51
  const referenced = owned
@@ -65,11 +54,12 @@ export async function* frameSubjects(index, rootType) {
65
54
  .flatMap((quad) => bySubject.get(quad.object.value) ?? []);
66
55
  const subgraph = [...owned, ...referenced];
67
56
  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.
57
+ // Frame for THIS specific root subject by its `@id`. The subgraph embeds the
58
+ // root’s one-hop references, and such a referent can itself be one of the
59
+ // requested `roots` (e.g. a terminology source that is also a separately
60
+ // registered dataset). Framing by `{ '@id': rootIri }` pins the output to
61
+ // this subject, so the referent is framed once in its own turn rather than
62
+ // surfacing here as a second `@graph` node that `[0]` might pick instead.
73
63
  const framed = await jsonld.frame(expanded, { '@id': rootIri }, FRAME_OPTIONS);
74
64
  const node = framed['@graph']?.[0];
75
65
  if (node !== undefined) {
@@ -77,13 +67,3 @@ export async function* frameSubjects(index, rootType) {
77
67
  }
78
68
  }
79
69
  }
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/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { projectGraph, irisOf, literalsOf, firstLiteralOf } from './project.js';
2
- export type { SearchDocument, TypedSearchDocument } from './project.js';
1
+ export { projectRoots, irisOf, literalsOf, firstLiteralOf } from './project.js';
2
+ export type { SearchDocument } from './project.js';
3
3
  export { defineSearchType, searchSchema, validateSearchType, assertValidSearchType, } from './schema.js';
4
4
  export type { FieldKind, SearchField, SearchFieldBase, TextField, KeywordField, ReferenceField, NumericField, BooleanField, SearchType, SearchTypeIssue, SearchSchema, FacetRange, } from './schema.js';
5
5
  export type { SearchQuery, Filter, Sort } from './query.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAChF,YAAY,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAKxE,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,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,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAChF,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,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
@@ -6,8 +6,8 @@
6
6
  // Projection: RDF CONSTRUCT quads → flat search documents, driven by the
7
7
  // unified SearchField/SearchType model. The IR readers (irisOf, …) are here
8
8
  // because `derive` functions are written against them.
9
- export { projectGraph, irisOf, literalsOf, firstLiteralOf } from './project.js';
9
+ export { projectRoots, irisOf, literalsOf, firstLiteralOf } from './project.js';
10
10
  // Unified field model: one declaration drives projection, engine collection
11
- // schema, query semantics and the GraphQL surface — a discriminated union by
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
13
  export { defineSearchType, searchSchema, validateSearchType, assertValidSearchType, } from './schema.js';
package/dist/project.d.ts CHANGED
@@ -5,16 +5,6 @@ import { type SearchSchema, type SearchType } from './schema.js';
5
5
  export type SearchDocument = {
6
6
  id: string;
7
7
  } & Record<string, unknown>;
8
- /**
9
- * A projected document tagged with the {@link SearchType} it was projected
10
- * from – one element of {@link projectGraph}’s mixed, whole-schema stream. The
11
- * tag is what lets a multi-collection writer route each document to the
12
- * collection for its type without re-deriving the type from the document.
13
- */
14
- export interface TypedSearchDocument {
15
- readonly searchType: SearchType;
16
- readonly document: SearchDocument;
17
- }
18
8
  /**
19
9
  * Project one framed JSON-LD node into a flat search document: apply each field
20
10
  * of the type in declaration order. A field with a `derive` function computes
@@ -25,20 +15,19 @@ export interface TypedSearchDocument {
25
15
  */
26
16
  export declare function projectDocument(node: FramedNode, searchType: SearchType): SearchDocument;
27
17
  /**
28
- * Frame `quads` for every root type in the schema and project each node with its
29
- * type’s declaration – the multi-shape pipeline. Yields each document tagged
30
- * with its {@link SearchType} ({@link TypedSearchDocument}), so a downstream
31
- * multi-collection writer can route it to that type’s collection; a
32
- * single-collection consumer just reads `.document`. Streams one document at a
33
- * time so memory stays flat. The IR maps to a declaration by type, so adding a
34
- * shape is adding a `SearchType` to the schema (no engine change).
18
+ * Project a single type over a known set of `roots` – the per-type, roots-given
19
+ * projection. The roots are supplied by the caller (the pipeline selector)
20
+ * rather than discovered from `rdf:type`, so `quads` need carry no type triples
21
+ * and the projection frames each distinct subject once. {@link assertTypeInSchema}
22
+ * guards that `searchType` belongs to `schema` – the port’s own membership check
23
+ * – so no schema is ever forged to scope a projection to one type. Yields a bare
24
+ * {@link SearchDocument}: pairing a document with its type is a routing concern,
25
+ * owned by the pipeline glue, not the projection.
35
26
  *
36
- * Consumes `quads` once (a single scan builds the shared subject index that
37
- * every type frames off), so it accepts any `Iterable` – a materialized array or
38
- * a chained generator merging several sources (`function* () { yield* a; yield* b; }`)
39
- * with no intermediate copy at the projection peak.
27
+ * Consumes `quads` once, so it accepts any `Iterable` – a batch’s materialized
28
+ * array or a chained generator merging several readers.
40
29
  */
41
- export declare function projectGraph(quads: Iterable<Quad>, schema: SearchSchema): AsyncIterable<TypedSearchDocument>;
30
+ export declare function projectRoots(quads: Iterable<Quad>, roots: readonly string[], schema: SearchSchema, searchType: SearchType): AsyncIterable<SearchDocument>;
42
31
  export declare function literalsOf(node: FramedNode, path: string): string[];
43
32
  export declare function firstLiteralOf(node: FramedNode, path: string): string | undefined;
44
33
  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,EAGL,KAAK,UAAU,EAChB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAOL,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;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;CACnC;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,UAAU,EAChB,UAAU,EAAE,UAAU,GACrB,cAAc,CAYhB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAuB,YAAY,CACjC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,EACrB,MAAM,EAAE,YAAY,GACnB,aAAa,CAAC,mBAAmB,CAAC,CAWpC;AAqJD,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,EAQL,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;;;;;;;;;;;;GAYG;AACH,wBAAuB,YAAY,CACjC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,EACrB,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,MAAM,EAAE,YAAY,EACpB,UAAU,EAAE,UAAU,GACrB,aAAa,CAAC,cAAc,CAAC,CAU/B;AAqJD,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,6 +1,6 @@
1
1
  import { fold } from '@lde/text-normalization';
2
2
  import { buildSubjectIndex, frameSubjects, } from './frame-by-type.js';
3
- import { displayFieldName, isoToUnixSeconds, physicalFields, } from './schema.js';
3
+ import { assertTypeInSchema, displayFieldName, isoToUnixSeconds, physicalFields, } from './schema.js';
4
4
  /**
5
5
  * Project one framed JSON-LD node into a flat search document: apply each field
6
6
  * of the type in declaration order. A field with a `derive` function computes
@@ -21,26 +21,27 @@ export function projectDocument(node, searchType) {
21
21
  return document;
22
22
  }
23
23
  /**
24
- * Frame `quads` for every root type in the schema and project each node with its
25
- * type’s declaration – the multi-shape pipeline. Yields each document tagged
26
- * with its {@link SearchType} ({@link TypedSearchDocument}), so a downstream
27
- * multi-collection writer can route it to that type’s collection; a
28
- * single-collection consumer just reads `.document`. Streams one document at a
29
- * time so memory stays flat. The IR maps to a declaration by type, so adding a
30
- * shape is adding a `SearchType` to the schema (no engine change).
24
+ * Project a single type over a known set of `roots` – the per-type, roots-given
25
+ * projection. The roots are supplied by the caller (the pipeline selector)
26
+ * rather than discovered from `rdf:type`, so `quads` need carry no type triples
27
+ * and the projection frames each distinct subject once. {@link assertTypeInSchema}
28
+ * guards that `searchType` belongs to `schema` – the port’s own membership check
29
+ * – so no schema is ever forged to scope a projection to one type. Yields a bare
30
+ * {@link SearchDocument}: pairing a document with its type is a routing concern,
31
+ * owned by the pipeline glue, not the projection.
31
32
  *
32
- * Consumes `quads` once (a single scan builds the shared subject index that
33
- * every type frames off), so it accepts any `Iterable` – a materialized array or
34
- * a chained generator merging several sources (`function* () { yield* a; yield* b; }`)
35
- * with no intermediate copy at the projection peak.
33
+ * Consumes `quads` once, so it accepts any `Iterable` – a batch’s materialized
34
+ * array or a chained generator merging several readers.
36
35
  */
37
- export async function* projectGraph(quads, schema) {
38
- const types = [...schema.values()];
39
- const index = buildSubjectIndex(quads, types.map((searchType) => searchType.class));
40
- for (const searchType of types) {
41
- for await (const node of frameSubjects(index, searchType.class)) {
42
- yield { searchType, document: projectDocument(node, searchType) };
43
- }
36
+ export async function* projectRoots(quads, roots, schema, searchType) {
37
+ assertTypeInSchema(schema, searchType);
38
+ const index = buildSubjectIndex(quads);
39
+ // Distinct roots only. A selector may return an IRI more than once – a
40
+ // non-`DISTINCT` `SELECT` over a one-to-many join yields the same subject per
41
+ // matched row – and a repeated root would otherwise frame and emit a
42
+ // duplicate document under the same `id`.
43
+ for await (const node of frameSubjects(index, [...new Set(roots)])) {
44
+ yield projectDocument(node, searchType);
44
45
  }
45
46
  }
46
47
  function applyField(document, node, field) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lde/search",
3
- "version": "0.8.0",
3
+ "version": "0.9.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",
@@ -39,12 +39,12 @@
39
39
  "dependencies": {
40
40
  "@lde/text-normalization": "^0.1.1",
41
41
  "@rdfjs/types": "^2.0.1",
42
- "@tpluscode/rdf-ns-builders": "^5.0.0",
43
42
  "inflection": "^3.0.2",
44
43
  "jsonld": "^9.0.0",
45
44
  "tslib": "^2.3.0"
46
45
  },
47
46
  "devDependencies": {
47
+ "@tpluscode/rdf-ns-builders": "^5.0.0",
48
48
  "@types/jsonld": "^1.5.15",
49
49
  "n3": "^2.1.1"
50
50
  },