@lde/search 0.4.0 → 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
@@ -218,6 +218,21 @@ triples are collapsed first, because some SPARQL engines (e.g. QLever) do not
218
218
  deduplicate `CONSTRUCT` output. The IR carries no `@context`, so a `derive` function
219
219
  reading it sees full predicate IRIs with language tags preserved.
220
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
+
221
236
  ## Locales
222
237
 
223
238
  `locales` is the **single** list of locales a `text` field projects (`und` =
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lde/search",
3
- "version": "0.4.0",
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"