@lde/search 0.8.1 → 0.10.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 +57 -47
- package/dist/adapter.d.ts +1 -1
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +1 -1
- package/dist/engine.d.ts +9 -9
- package/dist/frame-by-type.d.ts +18 -28
- package/dist/frame-by-type.d.ts.map +1 -1
- package/dist/frame-by-type.js +20 -43
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/project.d.ts +15 -40
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +29 -43
- package/dist/schema.d.ts +27 -11
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +19 -0
- 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,8 +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`, `
|
|
62
|
-
validation, and every model/query/result type.
|
|
61
|
+
`searchSchema`, `projectRoots`, validation, and every model/query/result type.
|
|
63
62
|
- **`@lde/search/adapter`** – plumbing for engine adapters and API surfaces:
|
|
64
63
|
`physicalFields`, the field selectors, `assertValidQuery`, the filter
|
|
65
64
|
operators and storage codecs.
|
|
@@ -78,8 +77,9 @@ and GraphQL (one of the surfaces):
|
|
|
78
77
|
| `SearchType` | One root type’s complete declaration: its logical API `name`, its `class` IRI and its fields (incl. derived) | NodeShape | object type |
|
|
79
78
|
| `SearchSchema` | The whole search declaration: every `SearchType`, keyed by `class` IRI – build one with `searchSchema(...types)` | shapes graph | schema |
|
|
80
79
|
|
|
81
|
-
`
|
|
82
|
-
|
|
80
|
+
`projectRoots` and the engine port each execute one `SearchType` at a time –
|
|
81
|
+
projection over the roots the pipeline selector supplied for that type; the
|
|
82
|
+
GraphQL surface consumes the whole `SearchSchema`.
|
|
83
83
|
|
|
84
84
|
### API conventions
|
|
85
85
|
|
|
@@ -100,20 +100,19 @@ Two conventions hold across the whole family:
|
|
|
100
100
|
## Field model
|
|
101
101
|
|
|
102
102
|
The mapping is data, not code. Each field declares its `kind`, the IR `path` to
|
|
103
|
-
read (or a `derive` function for a **derived** field, computed from the
|
|
104
|
-
|
|
105
|
-
capabilities it opts into.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
103
|
+
read (or a `derive` function for a **derived** field, computed from the document
|
|
104
|
+
in declaration order – so it may read fields declared before it, never the
|
|
105
|
+
graph), and the capabilities (**roles**) it opts into. `path` is therefore the
|
|
106
|
+
complete statement of what the projection reads from the graph. A field that
|
|
107
|
+
declares **no** role is an **internal field**: projected so a later `derive` can
|
|
108
|
+
read it, then pruned before the writer and absent from the collection definition
|
|
109
|
+
– not stored, not indexed, no RAM. The physical field names a declaration fans
|
|
110
|
+
out to (per-locale search/sort keys) come from `physicalFields`, the single
|
|
111
|
+
convention projection, the collection definition and the query compiler all
|
|
112
|
+
share.
|
|
109
113
|
|
|
110
114
|
```ts
|
|
111
|
-
import {
|
|
112
|
-
defineSearchType,
|
|
113
|
-
projectGraph,
|
|
114
|
-
irisOf,
|
|
115
|
-
searchSchema,
|
|
116
|
-
} from '@lde/search';
|
|
115
|
+
import { defineSearchType, projectRoots, searchSchema } from '@lde/search';
|
|
117
116
|
|
|
118
117
|
const DATASET = defineSearchType({
|
|
119
118
|
name: 'Dataset', // logical API name: names the GraphQL type, a REST path, …
|
|
@@ -140,24 +139,27 @@ const DATASET = defineSearchType({
|
|
|
140
139
|
},
|
|
141
140
|
// → size (int)
|
|
142
141
|
{ name: 'size', path: 'urn:dr:size', kind: 'integer', sortable: true },
|
|
143
|
-
//
|
|
142
|
+
// internal field (no role): projected as a reading device for the derive
|
|
143
|
+
// below, then pruned before the writer – absent from the collection too
|
|
144
|
+
{ name: 'classes', path: 'urn:dr:class', kind: 'reference' },
|
|
145
|
+
// derived field (no path): computed from the document in declaration order,
|
|
146
|
+
// never from the graph – so `path` stays the whole statement of what is read
|
|
144
147
|
{
|
|
145
148
|
name: 'classCount',
|
|
146
149
|
kind: 'integer',
|
|
147
150
|
sortable: true,
|
|
148
|
-
derive: (
|
|
151
|
+
derive: (document) =>
|
|
152
|
+
(document.classes as string[] | undefined)?.length ?? 0,
|
|
149
153
|
},
|
|
150
154
|
],
|
|
151
155
|
});
|
|
152
156
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
)
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
// document to the collection for its type. A single-collection consumer just
|
|
160
|
-
// reads `document`.
|
|
157
|
+
const schema = searchSchema(DATASET);
|
|
158
|
+
for await (const document of projectRoots(quads, roots, schema, DATASET)) {
|
|
159
|
+
// one flat search document per given root subject, streamed. The caller (the
|
|
160
|
+
// pipeline selector) supplies `roots`; pairing a document with its type for a
|
|
161
|
+
// multi-collection writer is the pipeline glue’s job (see
|
|
162
|
+
// `@lde/search-pipeline`), not the projection’s.
|
|
161
163
|
}
|
|
162
164
|
```
|
|
163
165
|
|
|
@@ -219,31 +221,39 @@ source fails at startup. A reference without a `labelSource` stays id-only.
|
|
|
219
221
|
|
|
220
222
|
## Projection
|
|
221
223
|
|
|
222
|
-
`
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
`
|
|
236
|
-
|
|
237
|
-
|
|
224
|
+
`projectRoots` projects **one root type** over the roots the caller supplies –
|
|
225
|
+
the pipeline selector already holds them, so nothing is discovered from
|
|
226
|
+
`rdf:type` and a `CONSTRUCT` need emit no type triple. It is fully streaming:
|
|
227
|
+
each root’s subgraph is framed one at a time and its document yielded as
|
|
228
|
+
produced, so beyond a subject index memory stays flat at scale (framing the
|
|
229
|
+
whole graph at once is roughly O(N²)). Duplicate triples are collapsed first,
|
|
230
|
+
because some SPARQL engines (e.g. QLever) do not deduplicate `CONSTRUCT` output.
|
|
231
|
+
Every predicate a value comes from is read through a field’s `path`; a `derive`
|
|
232
|
+
computes only from the document projected so far, so `path` is the whole
|
|
233
|
+
statement of what the projection reads. `assertTypeInSchema` guards that
|
|
234
|
+
the passed `SearchType` is a member of the schema – the port’s own membership
|
|
235
|
+
check – so no schema is ever forged to scope a projection to one type.
|
|
236
|
+
|
|
237
|
+
`projectRoots` yields a **bare** `SearchDocument`. Pairing each document with the
|
|
238
|
+
`SearchType` it belongs to, so the write side can fan a mixed stream out to
|
|
239
|
+
per-type collections, is a routing concern owned by the pipeline glue – see
|
|
240
|
+
`@lde/search-pipeline`’s `searchStages` and multi-collection writer – not the
|
|
241
|
+
projection. One stage per root type keeps `@lde/search` pipeline-free.
|
|
242
|
+
|
|
243
|
+
`projectRoots` **consumes the quads once** – a single scan builds the subject
|
|
244
|
+
index the roots frame off – and so accepts any `Iterable<Quad>`, not just a
|
|
245
|
+
materialized array. A caller merging several readers can pass a chained
|
|
238
246
|
generator instead of building a third full array at the projection peak:
|
|
239
247
|
|
|
240
248
|
```ts
|
|
241
|
-
|
|
249
|
+
projectRoots(
|
|
242
250
|
(function* () {
|
|
243
251
|
yield* registerQuads;
|
|
244
252
|
yield* dkgQuads;
|
|
245
253
|
})(),
|
|
254
|
+
roots,
|
|
246
255
|
schema,
|
|
256
|
+
DATASET,
|
|
247
257
|
);
|
|
248
258
|
```
|
|
249
259
|
|
|
@@ -307,7 +317,7 @@ unrepresentable; the port enforces them for everyone else (deployment
|
|
|
307
317
|
## Typed results
|
|
308
318
|
|
|
309
319
|
An engine is **bound to the whole `SearchSchema` at construction** – like
|
|
310
|
-
every other schema consumer (`
|
|
320
|
+
every other schema consumer (`projectRoots(quads, roots, schema, type)`,
|
|
311
321
|
`buildGraphQLSchema(schema)`): the adapter factory takes the deployment’s
|
|
312
322
|
declaration, so a query can never meet the wrong index, and deployment-level
|
|
313
323
|
concerns (the label cache, cross-type search, facet batching) have one home.
|
package/dist/adapter.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* deployment author declaring a schema and running searches needs none of
|
|
7
7
|
* this; the main entry point carries that (small) authoring surface.
|
|
8
8
|
*/
|
|
9
|
-
export { assertTypeInSchema, physicalFields, displayFieldName, displayFieldPattern, displayLangOf, searchableFields, facetableFields, filterableFields, sortableFields, outputFields, referenceFields, fieldNamed, labelFieldOf, isRangeFacet, isoToUnixSeconds, unixSecondsToIso, } from './schema.js';
|
|
9
|
+
export { assertTypeInSchema, physicalFields, displayFieldName, displayFieldPattern, displayLangOf, searchableFields, facetableFields, filterableFields, sortableFields, outputFields, isInternalField, referenceFields, fieldNamed, labelFieldOf, isRangeFacet, isoToUnixSeconds, unixSecondsToIso, } from './schema.js';
|
|
10
10
|
export type { PhysicalFields } from './schema.js';
|
|
11
11
|
export { physicalNameTokens } from './physical-name.js';
|
|
12
12
|
export { filterOperatorFor, filterOperator, validateQuery, assertValidQuery, pageForOffset, } from './query.js';
|
package/dist/adapter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,mBAAmB,EACnB,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,eAAe,EACf,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,mBAAmB,EACnB,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,eAAe,EACf,eAAe,EACf,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/adapter.js
CHANGED
|
@@ -6,6 +6,6 @@
|
|
|
6
6
|
* deployment author declaring a schema and running searches needs none of
|
|
7
7
|
* this; the main entry point carries that (small) authoring surface.
|
|
8
8
|
*/
|
|
9
|
-
export { assertTypeInSchema, physicalFields, displayFieldName, displayFieldPattern, displayLangOf, searchableFields, facetableFields, filterableFields, sortableFields, outputFields, referenceFields, fieldNamed, labelFieldOf, isRangeFacet, isoToUnixSeconds, unixSecondsToIso, } from './schema.js';
|
|
9
|
+
export { assertTypeInSchema, physicalFields, displayFieldName, displayFieldPattern, displayLangOf, searchableFields, facetableFields, filterableFields, sortableFields, outputFields, isInternalField, referenceFields, fieldNamed, labelFieldOf, isRangeFacet, isoToUnixSeconds, unixSecondsToIso, } from './schema.js';
|
|
10
10
|
export { physicalNameTokens } from './physical-name.js';
|
|
11
11
|
export { filterOperatorFor, filterOperator, validateQuery, assertValidQuery, pageForOffset, } from './query.js';
|
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,29 +2,29 @@ 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
29
|
* Frame each of the given `roots` from a prebuilt {@link SubjectIndex} into one
|
|
30
30
|
* JSON-LD node. Each root subject’s own triples plus the one-hop nodes it
|
|
@@ -33,19 +33,9 @@ export declare function buildSubjectIndex(quads: Iterable<Quad>, rootTypes: Iter
|
|
|
33
33
|
* is held (whole-graph `jsonld.frame()` is ~O(N²)). The frame carries no
|
|
34
34
|
* `@context`, so framed keys are full predicate IRIs.
|
|
35
35
|
*
|
|
36
|
-
* The roots are supplied explicitly rather than discovered from `rdf:type`:
|
|
37
|
-
* caller
|
|
38
|
-
*
|
|
39
|
-
* {@link projectGraph}) passes {@link SubjectIndex.rootsByType}. A root absent
|
|
40
|
-
* from the index simply frames nothing.
|
|
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.
|
|
41
39
|
*/
|
|
42
40
|
export declare function frameSubjects(index: SubjectIndex, roots: readonly string[]): AsyncIterable<FramedNode>;
|
|
43
|
-
/**
|
|
44
|
-
* Frame CONSTRUCT quads into one JSON-LD node per subject of `rootType`, for a
|
|
45
|
-
* single root type. Consumes `quads` once (see {@link buildSubjectIndex}), so
|
|
46
|
-
* it accepts any `Iterable` – a materialized array or a chained generator. For
|
|
47
|
-
* a multi-type schema, {@link buildSubjectIndex} + {@link frameSubjects} share
|
|
48
|
-
* one index across types rather than re-scanning per type.
|
|
49
|
-
*/
|
|
50
|
-
export declare function frameByType(quads: Iterable<Quad>, rootType: string): AsyncIterable<FramedNode>;
|
|
51
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,16 +29,8 @@ 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
36
|
* Frame each of the given `roots` from a prebuilt {@link SubjectIndex} into one
|
|
@@ -51,11 +40,9 @@ export function buildSubjectIndex(quads, rootTypes) {
|
|
|
51
40
|
* is held (whole-graph `jsonld.frame()` is ~O(N²)). The frame carries no
|
|
52
41
|
* `@context`, so framed keys are full predicate IRIs.
|
|
53
42
|
*
|
|
54
|
-
* The roots are supplied explicitly rather than discovered from `rdf:type`:
|
|
55
|
-
* caller
|
|
56
|
-
*
|
|
57
|
-
* {@link projectGraph}) passes {@link SubjectIndex.rootsByType}. A root absent
|
|
58
|
-
* from the index simply frames nothing.
|
|
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.
|
|
59
46
|
*/
|
|
60
47
|
export async function* frameSubjects(index, roots) {
|
|
61
48
|
const { bySubject } = index;
|
|
@@ -67,11 +54,12 @@ export async function* frameSubjects(index, roots) {
|
|
|
67
54
|
.flatMap((quad) => bySubject.get(quad.object.value) ?? []);
|
|
68
55
|
const subgraph = [...owned, ...referenced];
|
|
69
56
|
const expanded = await jsonld.fromRDF(subgraph);
|
|
70
|
-
// Frame for THIS specific root subject by `@id
|
|
71
|
-
// one-hop
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
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.
|
|
75
63
|
const framed = await jsonld.frame(expanded, { '@id': rootIri }, FRAME_OPTIONS);
|
|
76
64
|
const node = framed['@graph']?.[0];
|
|
77
65
|
if (node !== undefined) {
|
|
@@ -79,14 +67,3 @@ export async function* frameSubjects(index, roots) {
|
|
|
79
67
|
}
|
|
80
68
|
}
|
|
81
69
|
}
|
|
82
|
-
/**
|
|
83
|
-
* Frame CONSTRUCT quads into one JSON-LD node per subject of `rootType`, for a
|
|
84
|
-
* single root type. Consumes `quads` once (see {@link buildSubjectIndex}), so
|
|
85
|
-
* it accepts any `Iterable` – a materialized array or a chained generator. For
|
|
86
|
-
* a multi-type schema, {@link buildSubjectIndex} + {@link frameSubjects} share
|
|
87
|
-
* one index across types rather than re-scanning per type.
|
|
88
|
-
*/
|
|
89
|
-
export async function* frameByType(quads, rootType) {
|
|
90
|
-
const index = buildSubjectIndex(quads, [rootType]);
|
|
91
|
-
yield* frameSubjects(index, index.rootsByType.get(rootType) ?? []);
|
|
92
|
-
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export type { SearchDocument
|
|
1
|
+
export { projectRoots } 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,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,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
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
// under `@lde/search/adapter`; the engine-port conformance suite under
|
|
5
5
|
// `@lde/search/testing`.
|
|
6
6
|
// Projection: RDF CONSTRUCT quads → flat search documents, driven by the
|
|
7
|
-
// unified SearchField/SearchType model.
|
|
8
|
-
//
|
|
9
|
-
export {
|
|
7
|
+
// unified SearchField/SearchType model. A `derive` reads the projected document,
|
|
8
|
+
// never the graph, so the IR readers stay internal to projection.
|
|
9
|
+
export { projectRoots } from './project.js';
|
|
10
10
|
// Unified field model: one declaration drives projection, engine collection
|
|
11
11
|
// schema, query semantics and the GraphQL surface – a discriminated union by
|
|
12
12
|
// `kind`, validated again at runtime when the schema is built.
|
package/dist/project.d.ts
CHANGED
|
@@ -5,56 +5,31 @@ 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
|
|
21
|
-
* its value from the
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
11
|
+
* its value from the document as populated so far (so a derived field may read
|
|
12
|
+
* fields declared before it), never from the graph – `path` is the complete
|
|
13
|
+
* statement of what the projection reads. {@link isInternalField Internal
|
|
14
|
+
* fields} (those declaring no role) are populated so a later derive can read
|
|
15
|
+
* them, then pruned before the document is returned: they must reach neither a
|
|
16
|
+
* writer nor the collection definition. The physical field names a field fans
|
|
17
|
+
* out to come from {@link physicalFields}, the single source shared with the
|
|
18
|
+
* engine collection definition and the query compiler.
|
|
25
19
|
*/
|
|
26
20
|
export declare function projectDocument(node: FramedNode, searchType: SearchType): SearchDocument;
|
|
27
|
-
/**
|
|
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).
|
|
35
|
-
*
|
|
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.
|
|
40
|
-
*/
|
|
41
|
-
export declare function projectGraph(quads: Iterable<Quad>, schema: SearchSchema): AsyncIterable<TypedSearchDocument>;
|
|
42
21
|
/**
|
|
43
22
|
* Project a single type over a known set of `roots` – the per-type, roots-given
|
|
44
|
-
* projection.
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* –
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* the projection.
|
|
23
|
+
* projection. The roots are supplied by the caller (the pipeline selector)
|
|
24
|
+
* rather than discovered from `rdf:type`, so `quads` need carry no type triples
|
|
25
|
+
* and the projection frames each distinct subject once. {@link assertTypeInSchema}
|
|
26
|
+
* guards that `searchType` belongs to `schema` – the port’s own membership check
|
|
27
|
+
* – so no schema is ever forged to scope a projection to one type. Yields a bare
|
|
28
|
+
* {@link SearchDocument}: pairing a document with its type is a routing concern,
|
|
29
|
+
* owned by the pipeline glue, not the projection.
|
|
52
30
|
*
|
|
53
31
|
* Consumes `quads` once, so it accepts any `Iterable` – a batch’s materialized
|
|
54
32
|
* array or a chained generator merging several readers.
|
|
55
33
|
*/
|
|
56
34
|
export declare function projectRoots(quads: Iterable<Quad>, roots: readonly string[], schema: SearchSchema, searchType: SearchType): AsyncIterable<SearchDocument>;
|
|
57
|
-
export declare function literalsOf(node: FramedNode, path: string): string[];
|
|
58
|
-
export declare function firstLiteralOf(node: FramedNode, path: string): string | undefined;
|
|
59
|
-
export declare function irisOf(node: FramedNode, path: string): string[];
|
|
60
35
|
//# sourceMappingURL=project.d.ts.map
|
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,EASL,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;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,UAAU,EAChB,UAAU,EAAE,UAAU,GACrB,cAAc,CAoBhB;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"}
|
package/dist/project.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { fold } from '@lde/text-normalization';
|
|
2
2
|
import { buildSubjectIndex, frameSubjects, } from './frame-by-type.js';
|
|
3
|
-
import { assertTypeInSchema, displayFieldName, isoToUnixSeconds, physicalFields, } from './schema.js';
|
|
3
|
+
import { assertTypeInSchema, displayFieldName, isInternalField, 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
|
|
7
|
-
* its value from the
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* its value from the document as populated so far (so a derived field may read
|
|
8
|
+
* fields declared before it), never from the graph – `path` is the complete
|
|
9
|
+
* statement of what the projection reads. {@link isInternalField Internal
|
|
10
|
+
* fields} (those declaring no role) are populated so a later derive can read
|
|
11
|
+
* them, then pruned before the document is returned: they must reach neither a
|
|
12
|
+
* writer nor the collection definition. The physical field names a field fans
|
|
13
|
+
* out to come from {@link physicalFields}, the single source shared with the
|
|
14
|
+
* engine collection definition and the query compiler.
|
|
11
15
|
*/
|
|
12
16
|
export function projectDocument(node, searchType) {
|
|
13
17
|
const id = node['@id'];
|
|
@@ -18,61 +22,43 @@ export function projectDocument(node, searchType) {
|
|
|
18
22
|
for (const field of searchType.fields) {
|
|
19
23
|
applyField(document, node, field);
|
|
20
24
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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).
|
|
31
|
-
*
|
|
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.
|
|
36
|
-
*/
|
|
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
|
-
const roots = index.rootsByType.get(searchType.class) ?? [];
|
|
42
|
-
for await (const node of frameSubjects(index, roots)) {
|
|
43
|
-
yield { searchType, document: projectDocument(node, searchType) };
|
|
25
|
+
// Prune internal fields: they were projected only so the derives above could
|
|
26
|
+
// read them (a structural memo shared across every derive that needs the
|
|
27
|
+
// value), and must not reach the writer or the collection definition.
|
|
28
|
+
for (const field of searchType.fields) {
|
|
29
|
+
if (isInternalField(field)) {
|
|
30
|
+
delete document[field.name];
|
|
44
31
|
}
|
|
45
32
|
}
|
|
33
|
+
return document;
|
|
46
34
|
}
|
|
47
35
|
/**
|
|
48
36
|
* Project a single type over a known set of `roots` – the per-type, roots-given
|
|
49
|
-
* projection.
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* –
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* the projection.
|
|
37
|
+
* projection. The roots are supplied by the caller (the pipeline selector)
|
|
38
|
+
* rather than discovered from `rdf:type`, so `quads` need carry no type triples
|
|
39
|
+
* and the projection frames each distinct subject once. {@link assertTypeInSchema}
|
|
40
|
+
* guards that `searchType` belongs to `schema` – the port’s own membership check
|
|
41
|
+
* – so no schema is ever forged to scope a projection to one type. Yields a bare
|
|
42
|
+
* {@link SearchDocument}: pairing a document with its type is a routing concern,
|
|
43
|
+
* owned by the pipeline glue, not the projection.
|
|
57
44
|
*
|
|
58
45
|
* Consumes `quads` once, so it accepts any `Iterable` – a batch’s materialized
|
|
59
46
|
* array or a chained generator merging several readers.
|
|
60
47
|
*/
|
|
61
48
|
export async function* projectRoots(quads, roots, schema, searchType) {
|
|
62
49
|
assertTypeInSchema(schema, searchType);
|
|
63
|
-
const index = buildSubjectIndex(quads
|
|
50
|
+
const index = buildSubjectIndex(quads);
|
|
64
51
|
// Distinct roots only. A selector may return an IRI more than once – a
|
|
65
52
|
// non-`DISTINCT` `SELECT` over a one-to-many join yields the same subject per
|
|
66
53
|
// matched row – and a repeated root would otherwise frame and emit a
|
|
67
|
-
// duplicate document under the same `id`.
|
|
68
|
-
// `rootsByType`, which is inherently unique, so it never needs this.)
|
|
54
|
+
// duplicate document under the same `id`.
|
|
69
55
|
for await (const node of frameSubjects(index, [...new Set(roots)])) {
|
|
70
56
|
yield projectDocument(node, searchType);
|
|
71
57
|
}
|
|
72
58
|
}
|
|
73
59
|
function applyField(document, node, field) {
|
|
74
60
|
if (field.derive !== undefined) {
|
|
75
|
-
const value = field.derive(
|
|
61
|
+
const value = field.derive(document);
|
|
76
62
|
if (value !== undefined) {
|
|
77
63
|
document[field.name] = value;
|
|
78
64
|
}
|
|
@@ -172,15 +158,15 @@ function langValuesOf(node, path) {
|
|
|
172
158
|
.map(toLangValue)
|
|
173
159
|
.filter((value) => value !== undefined);
|
|
174
160
|
}
|
|
175
|
-
|
|
161
|
+
function literalsOf(node, path) {
|
|
176
162
|
return valuesOf(node, path)
|
|
177
163
|
.map(literalString)
|
|
178
164
|
.filter((value) => value !== undefined);
|
|
179
165
|
}
|
|
180
|
-
|
|
166
|
+
function firstLiteralOf(node, path) {
|
|
181
167
|
return literalsOf(node, path)[0];
|
|
182
168
|
}
|
|
183
|
-
|
|
169
|
+
function irisOf(node, path) {
|
|
184
170
|
return valuesOf(node, path)
|
|
185
171
|
.map(iriString)
|
|
186
172
|
.filter((value) => value !== undefined);
|
package/dist/schema.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { FramedNode } from './frame-by-type.js';
|
|
2
1
|
import type { SearchDocument } from './project.js';
|
|
3
2
|
/**
|
|
4
3
|
* The engine-neutral kind of a queryable field. It drives every downstream
|
|
@@ -32,9 +31,11 @@ export declare function filterOperatorFor(kind: FieldKind): FilterOperator | und
|
|
|
32
31
|
* Capability flags (`searchable`/`filterable`/`facetable`/`sortable`/`output`)
|
|
33
32
|
* are independent opt-ins: a field exposes exactly the roles it declares. A
|
|
34
33
|
* field with a {@link SearchFieldBase.derive `derive`} function instead of a
|
|
35
|
-
* `path` is a **derived field** – computed from the
|
|
36
|
-
*
|
|
37
|
-
* behavior (e.g. `status`, the compatibility booleans).
|
|
34
|
+
* `path` is a **derived field** – computed from the document projected so far
|
|
35
|
+
* rather than read from the graph – yet it still carries full query/schema/output
|
|
36
|
+
* behavior (e.g. `status`, the compatibility booleans). A field declaring **no**
|
|
37
|
+
* role at all is an {@link isInternalField **internal field**}: projected so a
|
|
38
|
+
* later derive can read it, then pruned before the writer.
|
|
38
39
|
*
|
|
39
40
|
* The physical field names a declaration fans out to (per-locale search/sort
|
|
40
41
|
* keys) follow one convention, owned by
|
|
@@ -73,15 +74,17 @@ export interface SearchFieldBase {
|
|
|
73
74
|
readonly sortable?: boolean;
|
|
74
75
|
/**
|
|
75
76
|
* Compute this field’s value instead of projecting it from a `path` – a
|
|
76
|
-
* status token, a compatibility boolean, a count over
|
|
77
|
+
* status token, a compatibility boolean, a count over an earlier field.
|
|
77
78
|
* Mutually exclusive with `path`. Runs in declaration order during
|
|
78
|
-
* projection, receiving
|
|
79
|
-
*
|
|
80
|
-
* `statusRank` reading the derived `status
|
|
81
|
-
*
|
|
82
|
-
*
|
|
79
|
+
* projection, receiving **only** the document as populated so far – never the
|
|
80
|
+
* graph – so a derived field reads fields declared before it (e.g. a
|
|
81
|
+
* `statusRank` reading the derived `status`, or a count reading an
|
|
82
|
+
* {@link isInternalField internal} field). Return `undefined` to leave the
|
|
83
|
+
* field absent. The field still carries full query/schema/output behaviour
|
|
84
|
+
* like any other. Reading only the document is what keeps `path` the complete
|
|
85
|
+
* statement of what the projection reads from the graph.
|
|
83
86
|
*/
|
|
84
|
-
readonly derive?: (
|
|
87
|
+
readonly derive?: (document: SearchDocument) => unknown;
|
|
85
88
|
}
|
|
86
89
|
/** Full-text inclusion with a `query_by` weight (folded; per-locale for
|
|
87
90
|
* localized text). Presence is what makes a field searchable. */
|
|
@@ -374,6 +377,19 @@ export declare function filterableFields(searchType: SearchType): readonly Searc
|
|
|
374
377
|
export declare function sortableFields(searchType: SearchType): readonly SearchField[];
|
|
375
378
|
/** Fields that appear in the API output type, in declaration order. */
|
|
376
379
|
export declare function outputFields(searchType: SearchType): readonly SearchField[];
|
|
380
|
+
/**
|
|
381
|
+
* Whether a field declares **no** role – none of `output`, `searchable`,
|
|
382
|
+
* `filterable`, `facetable`, `sortable`. Such a field is an **internal field**:
|
|
383
|
+
* the projection populates it (so a later {@link SearchFieldBase.derive} can
|
|
384
|
+
* read it), then prunes it before the document reaches a writer, and the engine
|
|
385
|
+
* collection definition omits it entirely – not stored, not indexed, no RAM.
|
|
386
|
+
* Absence of a role declares that intent; there is no separate marker flag.
|
|
387
|
+
*
|
|
388
|
+
* The single predicate the projection and the collection definition share, so
|
|
389
|
+
* they cannot disagree on what is internal. See the Search context’s load-bearing
|
|
390
|
+
* line: *a field without a Role is an Internal Field.*
|
|
391
|
+
*/
|
|
392
|
+
export declare function isInternalField(field: SearchField): boolean;
|
|
377
393
|
/** Fields of kind `reference` (IRI-valued, label-resolved), in declaration order. */
|
|
378
394
|
export declare function referenceFields(searchType: SearchType): readonly ReferenceField[];
|
|
379
395
|
/** Look up a field by its logical name. */
|
package/dist/schema.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
|
package/dist/schema.js
CHANGED
|
@@ -317,6 +317,25 @@ export function sortableFields(searchType) {
|
|
|
317
317
|
export function outputFields(searchType) {
|
|
318
318
|
return searchType.fields.filter((field) => field.output === true);
|
|
319
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* Whether a field declares **no** role – none of `output`, `searchable`,
|
|
322
|
+
* `filterable`, `facetable`, `sortable`. Such a field is an **internal field**:
|
|
323
|
+
* the projection populates it (so a later {@link SearchFieldBase.derive} can
|
|
324
|
+
* read it), then prunes it before the document reaches a writer, and the engine
|
|
325
|
+
* collection definition omits it entirely – not stored, not indexed, no RAM.
|
|
326
|
+
* Absence of a role declares that intent; there is no separate marker flag.
|
|
327
|
+
*
|
|
328
|
+
* The single predicate the projection and the collection definition share, so
|
|
329
|
+
* they cannot disagree on what is internal. See the Search context’s load-bearing
|
|
330
|
+
* line: *a field without a Role is an Internal Field.*
|
|
331
|
+
*/
|
|
332
|
+
export function isInternalField(field) {
|
|
333
|
+
return (field.output !== true &&
|
|
334
|
+
field.searchable === undefined &&
|
|
335
|
+
field.filterable !== true &&
|
|
336
|
+
field.facetable !== true &&
|
|
337
|
+
field.sortable !== true);
|
|
338
|
+
}
|
|
320
339
|
/** Fields of kind `reference` (IRI-valued, label-resolved), in declaration order. */
|
|
321
340
|
export function referenceFields(searchType) {
|
|
322
341
|
return searchType.fields.filter((field) => field.kind === 'reference');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lde/search",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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
|
},
|