@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 +39 -33
- package/dist/engine.d.ts +9 -9
- package/dist/frame-by-type.d.ts +26 -31
- package/dist/frame-by-type.d.ts.map +1 -1
- package/dist/frame-by-type.js +28 -48
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/project.d.ts +11 -22
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +20 -19
- package/package.json +2 -2
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** – `
|
|
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 (
|
|
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`, `
|
|
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
|
-
`
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
)
|
|
157
|
-
//
|
|
158
|
-
//
|
|
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
|
-
`
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
`SearchType`
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
`
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
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 (`
|
|
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 (`
|
|
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`)
|
|
23
|
-
* operators, unknown facets
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
package/dist/frame-by-type.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
|
27
|
+
export declare function buildSubjectIndex(quads: Iterable<Quad>): SubjectIndex;
|
|
28
28
|
/**
|
|
29
|
-
* Frame
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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,
|
|
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;
|
|
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"}
|
package/dist/frame-by-type.js
CHANGED
|
@@ -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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
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
|
|
33
|
+
return { bySubject };
|
|
45
34
|
}
|
|
46
35
|
/**
|
|
47
|
-
* Frame
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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,
|
|
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
|
|
69
|
-
// one-hop
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
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 {
|
|
2
|
-
export type { SearchDocument
|
|
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAChF,YAAY,EAAE,cAAc,EAAE,
|
|
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 {
|
|
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
|
|
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
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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
|
|
37
|
-
*
|
|
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
|
|
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[];
|
package/dist/project.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAEzC,OAAO,EAGL,KAAK,UAAU,EAChB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,
|
|
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
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
|
33
|
-
*
|
|
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*
|
|
38
|
-
|
|
39
|
-
const index = buildSubjectIndex(quads
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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.
|
|
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
|
},
|