@lde/search 0.7.1 → 0.8.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.
@@ -26,15 +26,20 @@ export interface SubjectIndex {
26
26
  */
27
27
  export declare function buildSubjectIndex(quads: Iterable<Quad>, rootTypes: Iterable<string>): 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`: a
37
+ * caller with the roots already in hand ({@link projectRoots}, from the pipeline
38
+ * selector) passes them directly; a whole-graph caller ({@link frameByType},
39
+ * {@link projectGraph}) passes {@link SubjectIndex.rootsByType}. A root absent
40
+ * from the index simply frames nothing.
36
41
  */
37
- export declare function frameSubjects(index: SubjectIndex, rootType: string): AsyncIterable<FramedNode>;
42
+ export declare function frameSubjects(index: SubjectIndex, roots: readonly string[]): AsyncIterable<FramedNode>;
38
43
  /**
39
44
  * Frame CONSTRUCT quads into one JSON-LD node per subject of `rootType`, for a
40
45
  * single root type. Consumes `quads` once (see {@link buildSubjectIndex}), so
@@ -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;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;;;;;;;;;;;;;GAaG;AACH,wBAAuB,aAAa,CAClC,KAAK,EAAE,YAAY,EACnB,KAAK,EAAE,SAAS,MAAM,EAAE,GACvB,aAAa,CAAC,UAAU,CAAC,CA4B3B;AAED;;;;;;GAMG;AACH,wBAAuB,WAAW,CAChC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,EACrB,QAAQ,EAAE,MAAM,GACf,aAAa,CAAC,UAAU,CAAC,CAG3B"}
@@ -44,19 +44,21 @@ export function buildSubjectIndex(quads, rootTypes) {
44
44
  return { bySubject, rootsByType };
45
45
  }
46
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.
47
+ * Frame each of the given `roots` from a prebuilt {@link SubjectIndex} into one
48
+ * JSON-LD node. Each root subject’s own triples plus the one-hop nodes it
49
+ * references (e.g. nested publisher/distribution resources) are framed one
50
+ * subject at a time, so beyond the shared subject index only a single subgraph
51
+ * is held (whole-graph `jsonld.frame()` is ~O(N²)). The frame carries no
52
+ * `@context`, so framed keys are full predicate IRIs.
53
+ *
54
+ * The roots are supplied explicitly rather than discovered from `rdf:type`: a
55
+ * caller with the roots already in hand ({@link projectRoots}, from the pipeline
56
+ * selector) passes them directly; a whole-graph caller ({@link frameByType},
57
+ * {@link projectGraph}) passes {@link SubjectIndex.rootsByType}. A root absent
58
+ * from the index simply frames nothing.
54
59
  */
55
- export async function* frameSubjects(index, rootType) {
60
+ export async function* frameSubjects(index, roots) {
56
61
  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
62
  for (const rootIri of roots) {
61
63
  const owned = bySubject.get(rootIri) ?? [];
62
64
  const referenced = owned
@@ -68,7 +70,7 @@ export async function* frameSubjects(index, rootType) {
68
70
  // Frame for THIS specific root subject by `@id`, not just by root type. A
69
71
  // one-hop reference can itself be of `rootType` (e.g. a terminology source
70
72
  // 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
73
+ // returns several root nodes and `[0]` could be the referenced one – which
72
74
  // would emit it twice and drop this subject entirely.
73
75
  const framed = await jsonld.frame(expanded, { '@id': rootIri }, FRAME_OPTIONS);
74
76
  const node = framed['@graph']?.[0];
@@ -85,5 +87,6 @@ export async function* frameSubjects(index, rootType) {
85
87
  * one index across types rather than re-scanning per type.
86
88
  */
87
89
  export async function* frameByType(quads, rootType) {
88
- yield* frameSubjects(buildSubjectIndex(quads, [rootType]), rootType);
90
+ const index = buildSubjectIndex(quads, [rootType]);
91
+ yield* frameSubjects(index, index.rootsByType.get(rootType) ?? []);
89
92
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { projectGraph, irisOf, literalsOf, firstLiteralOf } from './project.js';
1
+ export { projectGraph, projectRoots, irisOf, literalsOf, firstLiteralOf, } from './project.js';
2
2
  export type { SearchDocument, TypedSearchDocument } 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';
@@ -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,EACL,YAAY,EACZ,YAAY,EACZ,MAAM,EACN,UAAU,EACV,cAAc,GACf,MAAM,cAAc,CAAC;AACtB,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"}
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 { projectGraph, 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';
@@ -24,8 +24,17 @@ import type { SearchType } from './schema.js';
24
24
  * rules produce (`serieses`, `analysises`). An already-plural name is
25
25
  * idempotent (`People` → `people`).
26
26
  *
27
- * Returns an empty array when `name` carries no alphanumerics at all; the
28
- * adapter decides what that means, since only it knows its engine’s rules.
27
+ * **Throws rather than name the wrong container.** Dropping what it cannot
28
+ * match is what lets `creative_work` tokenize like `CreativeWork`, but it would
29
+ * just as quietly turn `Café` into `['cafs']` and `Musée` into `['mus', 'es']`
30
+ * – a perfectly legal name for a container nobody meant. So a name is spellable
31
+ * only when it is ASCII words and word separators, and only when it leaves at
32
+ * least one word to name anything after; anything else throws here. This guard
33
+ * lives with the split it protects, not in each adapter: an adapter cannot
34
+ * derive a name without going through this function, so it cannot forget to
35
+ * check – and `SearchType.name` is otherwise unvalidated
36
+ * ({@link validateSearchType} rules on field names only). A deployment whose
37
+ * type name this cannot spell passes its adapter an explicit name instead.
29
38
  */
30
39
  export declare function physicalNameTokens(searchType: SearchType): readonly string[];
31
40
  //# sourceMappingURL=physical-name.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"physical-name.d.ts","sourceRoot":"","sources":["../src/physical-name.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,UAAU,GAAG,SAAS,MAAM,EAAE,CAK5E"}
1
+ {"version":3,"file":"physical-name.d.ts","sourceRoot":"","sources":["../src/physical-name.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,UAAU,GAAG,SAAS,MAAM,EAAE,CAiB5E"}
@@ -24,15 +24,43 @@ import { pluralize } from 'inflection';
24
24
  * rules produce (`serieses`, `analysises`). An already-plural name is
25
25
  * idempotent (`People` → `people`).
26
26
  *
27
- * Returns an empty array when `name` carries no alphanumerics at all; the
28
- * adapter decides what that means, since only it knows its engine’s rules.
27
+ * **Throws rather than name the wrong container.** Dropping what it cannot
28
+ * match is what lets `creative_work` tokenize like `CreativeWork`, but it would
29
+ * just as quietly turn `Café` into `['cafs']` and `Musée` into `['mus', 'es']`
30
+ * – a perfectly legal name for a container nobody meant. So a name is spellable
31
+ * only when it is ASCII words and word separators, and only when it leaves at
32
+ * least one word to name anything after; anything else throws here. This guard
33
+ * lives with the split it protects, not in each adapter: an adapter cannot
34
+ * derive a name without going through this function, so it cannot forget to
35
+ * check – and `SearchType.name` is otherwise unvalidated
36
+ * ({@link validateSearchType} rules on field names only). A deployment whose
37
+ * type name this cannot spell passes its adapter an explicit name instead.
29
38
  */
30
39
  export function physicalNameTokens(searchType) {
31
- const words = searchType.name.match(NAME_WORD_PATTERN) ?? [];
32
- const tokens = words.map((word) => word.toLowerCase());
40
+ const { name } = searchType;
41
+ if (!SPELLABLE_NAME.test(name)) {
42
+ throw new Error(`Cannot name search type “${name}”: a physical name is derived from the type’s name, and this one carries characters outside ASCII words and word separators – deriving would silently drop them and name the wrong container. Rename the type, or give the adapter an explicit name.`);
43
+ }
44
+ const tokens = (name.match(NAME_WORD_PATTERN) ?? []).map((word) => word.toLowerCase());
33
45
  const last = tokens.at(-1);
34
- return last === undefined ? [] : [...tokens.slice(0, -1), pluralize(last)];
46
+ if (last === undefined) {
47
+ throw new Error(`Cannot name search type “${name}”: its name carries no word to name a container after. Rename the type, or give the adapter an explicit name.`);
48
+ }
49
+ return [...tokens.slice(0, -1), pluralize(last)];
35
50
  }
51
+ /**
52
+ * A name the split can spell without losing part of it: ASCII letters and
53
+ * digits, plus the separators a name may already be written with (`creative
54
+ * work`, `creative_work`, `creative-work` all tokenize like `CreativeWork`).
55
+ * Deliberately narrower than what {@link NAME_WORD_PATTERN} would tolerate –
56
+ * that one skips anything it does not match, which is precisely the silent
57
+ * mangling this rejects.
58
+ *
59
+ * Matches the empty name too, on purpose: nothing about it is *unspellable*,
60
+ * there is simply nothing to spell, which the no-word check below reports far
61
+ * better than a complaint about characters it does not carry.
62
+ */
63
+ const SPELLABLE_NAME = /^[A-Za-z0-9 _-]*$/;
36
64
  /**
37
65
  * One word of a PascalCase/camelCase name, in precedence order: a run of
38
66
  * capitals not followed by a lowercase letter (an acronym – `DCAT` in
package/dist/project.d.ts CHANGED
@@ -39,6 +39,21 @@ export declare function projectDocument(node: FramedNode, searchType: SearchType
39
39
  * with no intermediate copy at the projection peak.
40
40
  */
41
41
  export declare function projectGraph(quads: Iterable<Quad>, schema: SearchSchema): AsyncIterable<TypedSearchDocument>;
42
+ /**
43
+ * Project a single type over a known set of `roots` – the per-type, roots-given
44
+ * projection. Unlike {@link projectGraph}, the roots are supplied by the caller
45
+ * (the pipeline selector) rather than discovered from `rdf:type`, so `quads`
46
+ * need carry no type triples and the projection frames each distinct subject
47
+ * once. {@link assertTypeInSchema} guards that `searchType` belongs to `schema`
48
+ * – the port’s own membership check – so no schema is ever forged to scope a
49
+ * projection to one type. Yields a bare {@link SearchDocument}: tagging a
50
+ * document with its type is a routing concern, owned by the pipeline glue, not
51
+ * the projection.
52
+ *
53
+ * Consumes `quads` once, so it accepts any `Iterable` – a batch’s materialized
54
+ * array or a chained generator merging several readers.
55
+ */
56
+ export declare function projectRoots(quads: Iterable<Quad>, roots: readonly string[], schema: SearchSchema, searchType: SearchType): AsyncIterable<SearchDocument>;
42
57
  export declare function literalsOf(node: FramedNode, path: string): string[];
43
58
  export declare function firstLiteralOf(node: FramedNode, path: string): string | undefined;
44
59
  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;;;;;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,CAYpC;AAED;;;;;;;;;;;;;GAaG;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,CAW/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
@@ -38,11 +38,38 @@ export async function* projectGraph(quads, schema) {
38
38
  const types = [...schema.values()];
39
39
  const index = buildSubjectIndex(quads, types.map((searchType) => searchType.class));
40
40
  for (const searchType of types) {
41
- for await (const node of frameSubjects(index, searchType.class)) {
41
+ const roots = index.rootsByType.get(searchType.class) ?? [];
42
+ for await (const node of frameSubjects(index, roots)) {
42
43
  yield { searchType, document: projectDocument(node, searchType) };
43
44
  }
44
45
  }
45
46
  }
47
+ /**
48
+ * Project a single type over a known set of `roots` – the per-type, roots-given
49
+ * projection. Unlike {@link projectGraph}, the roots are supplied by the caller
50
+ * (the pipeline selector) rather than discovered from `rdf:type`, so `quads`
51
+ * need carry no type triples and the projection frames each distinct subject
52
+ * once. {@link assertTypeInSchema} guards that `searchType` belongs to `schema`
53
+ * – the port’s own membership check – so no schema is ever forged to scope a
54
+ * projection to one type. Yields a bare {@link SearchDocument}: tagging a
55
+ * document with its type is a routing concern, owned by the pipeline glue, not
56
+ * the projection.
57
+ *
58
+ * Consumes `quads` once, so it accepts any `Iterable` – a batch’s materialized
59
+ * array or a chained generator merging several readers.
60
+ */
61
+ export async function* projectRoots(quads, roots, schema, searchType) {
62
+ assertTypeInSchema(schema, searchType);
63
+ const index = buildSubjectIndex(quads, []);
64
+ // Distinct roots only. A selector may return an IRI more than once – a
65
+ // non-`DISTINCT` `SELECT` over a one-to-many join yields the same subject per
66
+ // matched row – and a repeated root would otherwise frame and emit a
67
+ // duplicate document under the same `id`. (`projectGraph`’s roots come from
68
+ // `rootsByType`, which is inherently unique, so it never needs this.)
69
+ for await (const node of frameSubjects(index, [...new Set(roots)])) {
70
+ yield projectDocument(node, searchType);
71
+ }
72
+ }
46
73
  function applyField(document, node, field) {
47
74
  if (field.derive !== undefined) {
48
75
  const value = field.derive(node, document);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lde/search",
3
- "version": "0.7.1",
3
+ "version": "0.8.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",