@lde/search-pipeline 0.12.5 → 0.13.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 +5 -269
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -6,276 +6,12 @@ and a search engine’s document world, so indexing reuses the existing spine
|
|
|
6
6
|
Discovery → Selection → Iteration → change-gate (skip-unchanged) – instead of
|
|
7
7
|
rebuilding it per consumer.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
[ADR 13](../../docs/decisions/0013-project-inside-the-batch-per-root-type.md)):
|
|
9
|
+
## Installation
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
- `@lde/search-<engine>` (e.g. [`@lde/search-typesense`](../search-typesense))
|
|
15
|
-
is the engine adapter: a transactional, **single-collection** `Writer`
|
|
16
|
-
consuming projected documents, owning the run lifecycle (Blue/green alias swap
|
|
17
|
-
or In-place sweeps, cross-pod lock);
|
|
18
|
-
- **this package** composes them:
|
|
19
|
-
- `searchStages` builds one projecting `Stage` per root type. Each stage
|
|
20
|
-
selects its own roots, extracts each root’s quads, and projects the
|
|
21
|
-
**root-complete batch** into documents paired with their `SearchType`
|
|
22
|
-
(`TypedSearchDocument`) – so projection happens inside the batch and memory
|
|
23
|
-
is bounded by `batchSize` roots, not the dataset
|
|
24
|
-
([ADR 13](../../docs/decisions/0013-project-inside-the-batch-per-root-type.md)).
|
|
25
|
-
Extraction is **generated from the schema**: `extractionQuery` mints a
|
|
26
|
-
CONSTRUCT that reads each field’s source `path` (a SPARQL property path) and
|
|
27
|
-
emits it under the field’s IR Alias (`urn:lde:‹Type›/‹field›`) – the same key
|
|
28
|
-
the projection reads back – so the reader and the projection cannot drift. A
|
|
29
|
-
stage without an explicit `readers` defaults to this generated reader;
|
|
30
|
-
- `searchIndexWriter` is the pipeline’s single terminal: an engine-agnostic
|
|
31
|
-
router that dispatches each document to the engine writer for **its**
|
|
32
|
-
type’s collection ([ADR 9](../../docs/decisions/0009-route-a-whole-schema-projection-to-per-type-collections.md)).
|
|
33
|
-
It owns no projection and buffers nothing.
|
|
34
|
-
|
|
35
|
-
So a search pipeline is **one terminal, N stages**: `new Pipeline<TypedSearchDocument>({ datasetSelector, stages: searchStages(...), writers: searchIndexWriter(...) })`.
|
|
36
|
-
|
|
37
|
-
## Quick start: `searchIndexerPipeline`
|
|
38
|
-
|
|
39
|
-
For the common **object grain** – index every IRI-identified instance of each
|
|
40
|
-
root type’s source class – `searchIndexerPipeline` wires that whole composition
|
|
41
|
-
and returns the ready-to-run `Pipeline`. It generates one stage per root type
|
|
42
|
-
in the schema, each selecting the type’s non-blank roots by class
|
|
43
|
-
(`selectByClass`; a blank node has no stable document key, so it can never
|
|
44
|
-
become a document), extracting with the schema-generated CONSTRUCT, and routing
|
|
45
|
-
every document to the engine writer for its type’s collection. The consumer
|
|
46
|
-
supplies only the domain (the schema, which datasets) and the deployment shell
|
|
47
|
-
(the engine writer, the SPARQL/import adapter):
|
|
48
|
-
|
|
49
|
-
```typescript
|
|
50
|
-
import { searchIndexerPipeline } from '@lde/search-pipeline';
|
|
51
|
-
import {
|
|
52
|
-
FileProvenanceStore,
|
|
53
|
-
ImportResolver,
|
|
54
|
-
SparqlDistributionResolver,
|
|
55
|
-
} from '@lde/pipeline';
|
|
56
|
-
import { ConsoleReporter } from '@lde/pipeline-console-reporter';
|
|
57
|
-
import { createQlever } from '@lde/sparql-qlever';
|
|
58
|
-
import { InPlaceRebuild } from '@lde/search-typesense';
|
|
59
|
-
|
|
60
|
-
const pipeline = searchIndexerPipeline({
|
|
61
|
-
schema,
|
|
62
|
-
datasets,
|
|
63
|
-
// Import each dataset’s data dump into a local QLever and query that.
|
|
64
|
-
distributionResolver: new ImportResolver(new SparqlDistributionResolver(), {
|
|
65
|
-
...createQlever({ mode: 'docker', image: 'adfreiburg/qlever:latest' }),
|
|
66
|
-
strategy: 'import',
|
|
67
|
-
}),
|
|
68
|
-
writerFor: (searchType) => new InPlaceRebuild(typesenseClient, searchType),
|
|
69
|
-
// Optional: skip datasets whose source and pipeline version are unchanged.
|
|
70
|
-
provenanceStore: new FileProvenanceStore({ path: 'data/provenance.json' }),
|
|
71
|
-
pipelineVersion: '1',
|
|
72
|
-
reporter: new ConsoleReporter(),
|
|
73
|
-
});
|
|
74
|
-
await pipeline.run();
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
Each role in that wiring maps to a package:
|
|
78
|
-
|
|
79
|
-
| Role | Package |
|
|
80
|
-
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
81
|
-
| Dataset selection | [`@lde/pipeline`](../pipeline) – `Dataset[]` directly, or a `DatasetSelector` such as `RegistrySelector` |
|
|
82
|
-
| Distribution resolution and import | [`@lde/pipeline`](../pipeline) `ImportResolver` + [`@lde/sparql-qlever`](../sparql-qlever) `createQlever` (the concrete [`@lde/sparql-server`](../sparql-server)/[`@lde/sparql-importer`](../sparql-importer) adapter) |
|
|
83
|
-
| Root selection, extraction, projection | this package (`searchStages` over [`@lde/search`](../search)) |
|
|
84
|
-
| Engine writer, one collection per type | [`@lde/search-typesense`](../search-typesense) – `InPlaceRebuild` or `BlueGreenRebuild` |
|
|
85
|
-
| Provenance (skip-unchanged) | [`@lde/pipeline`](../pipeline) – `FileProvenanceStore`, or a triplestore-backed store |
|
|
86
|
-
| Reporting | [`@lde/pipeline-console-reporter`](../pipeline-console-reporter) `ConsoleReporter` |
|
|
87
|
-
|
|
88
|
-
A deployment that needs more – a bespoke root selector (where the entry point
|
|
89
|
-
is a domain fact, not a class), per-stage tuning, non-SPARQL readers, or
|
|
90
|
-
quad-level plugins – composes the parts directly, as below; the convenience
|
|
91
|
-
owns no capability of its own.
|
|
92
|
-
|
|
93
|
-
## Usage
|
|
94
|
-
|
|
95
|
-
```typescript
|
|
96
|
-
import { Pipeline } from '@lde/pipeline';
|
|
97
|
-
import { searchSchema } from '@lde/search';
|
|
98
|
-
import { BlueGreenRebuild } from '@lde/search-typesense';
|
|
99
|
-
import {
|
|
100
|
-
searchStages,
|
|
101
|
-
selectByClass,
|
|
102
|
-
searchIndexWriter,
|
|
103
|
-
type TypedSearchDocument,
|
|
104
|
-
} from '@lde/search-pipeline';
|
|
105
|
-
|
|
106
|
-
// One schema, several root types: the `datasets` catalog plus the Organization
|
|
107
|
-
// label collection its references resolve against.
|
|
108
|
-
const schema = searchSchema(
|
|
109
|
-
{
|
|
110
|
-
name: 'Dataset',
|
|
111
|
-
class: 'http://www.w3.org/ns/dcat#Dataset',
|
|
112
|
-
fields: [
|
|
113
|
-
{
|
|
114
|
-
name: 'title',
|
|
115
|
-
kind: 'text',
|
|
116
|
-
path: 'http://purl.org/dc/terms/title',
|
|
117
|
-
locales: ['und'],
|
|
118
|
-
output: true,
|
|
119
|
-
searchable: { weight: 5 },
|
|
120
|
-
},
|
|
121
|
-
],
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
name: 'Organization',
|
|
125
|
-
class: 'http://xmlns.com/foaf/0.1/Organization',
|
|
126
|
-
fields: [
|
|
127
|
-
{
|
|
128
|
-
name: 'label',
|
|
129
|
-
kind: 'text',
|
|
130
|
-
path: 'http://xmlns.com/foaf/0.1/name',
|
|
131
|
-
locales: ['und'],
|
|
132
|
-
output: true,
|
|
133
|
-
searchable: { weight: 1 },
|
|
134
|
-
},
|
|
135
|
-
],
|
|
136
|
-
},
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
const dataset = schema.get('http://www.w3.org/ns/dcat#Dataset')!;
|
|
140
|
-
const organization = schema.get('http://xmlns.com/foaf/0.1/Organization')!;
|
|
141
|
-
|
|
142
|
-
const pipeline = new Pipeline<TypedSearchDocument>({
|
|
143
|
-
datasetSelector,
|
|
144
|
-
// One stage per root type: each selects its own roots (here by class –
|
|
145
|
-
// `selectByClass` is a convenience for the object grain; a deployment writes
|
|
146
|
-
// its own selector where the entry point is a domain fact) and extracts each
|
|
147
|
-
// root’s quads. Omitting `readers` defaults each stage to the Extraction
|
|
148
|
-
// CONSTRUCT generated from the schema (pass one only for a non-SPARQL source or
|
|
149
|
-
// to merge readers). `rootVariable` couples the selector’s projected variable
|
|
150
|
-
// to the CONSTRUCT’s free subject; it must not be `dataset` (reserved by the
|
|
151
|
-
// reader).
|
|
152
|
-
stages: searchStages({
|
|
153
|
-
schema,
|
|
154
|
-
types: [
|
|
155
|
-
{
|
|
156
|
-
searchType: dataset,
|
|
157
|
-
rootVariable: 'root',
|
|
158
|
-
itemSelector: selectByClass(dataset),
|
|
159
|
-
},
|
|
160
|
-
{
|
|
161
|
-
searchType: organization,
|
|
162
|
-
rootVariable: 'root',
|
|
163
|
-
itemSelector: selectByClass(organization),
|
|
164
|
-
},
|
|
165
|
-
],
|
|
166
|
-
}),
|
|
167
|
-
// Each type gets its own collection – an independent blue/green rebuild –
|
|
168
|
-
// named by the adapter from the type itself (`Dataset` → `datasets`,
|
|
169
|
-
// `Organization` → `organizations`), so no naming map is passed here and the
|
|
170
|
-
// engine reading these collections cannot disagree about where they are.
|
|
171
|
-
writers: searchIndexWriter({
|
|
172
|
-
schema,
|
|
173
|
-
writerFor: (searchType) =>
|
|
174
|
-
new BlueGreenRebuild(typesenseClient, searchType),
|
|
175
|
-
}),
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
await pipeline.run();
|
|
11
|
+
```sh
|
|
12
|
+
npm install @lde/search-pipeline
|
|
179
13
|
```
|
|
180
14
|
|
|
181
|
-
|
|
182
|
-
[Collection naming](../search-typesense#collection-naming). A deployment that
|
|
183
|
-
needs other names passes one per type, and environment prefixing **composes
|
|
184
|
-
with** the convention rather than replacing it, since the adapter exports the
|
|
185
|
-
derivation:
|
|
186
|
-
|
|
187
|
-
```ts
|
|
188
|
-
import { BlueGreenRebuild, deriveCollectionName } from '@lde/search-typesense';
|
|
189
|
-
|
|
190
|
-
writerFor: (searchType) =>
|
|
191
|
-
new BlueGreenRebuild(typesenseClient, searchType, {
|
|
192
|
-
name: `${process.env.INDEX_PREFIX}_${deriveCollectionName(searchType)}`,
|
|
193
|
-
});
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
Whatever the writers are named, the engine reading these collections must be
|
|
197
|
-
given the same names (`collections`), or it reads the derived ones and finds an
|
|
198
|
-
empty index.
|
|
199
|
-
|
|
200
|
-
Each stage projects its own root-complete batches (`@lde/search`’s
|
|
201
|
-
`projectRoots`) into documents paired with its `SearchType`, and the terminal
|
|
202
|
-
dispatches each to the engine run for its type’s collection as it arrives –
|
|
203
|
-
before the engine acts on the dataset’s completion, so an In-place stale sweep
|
|
204
|
-
never races its own documents. The run lifecycle (run context, per-dataset flush
|
|
205
|
-
outcome, commit/abort) passes through unchanged: the engine writer’s update mode
|
|
206
|
-
governs, and the pipeline never branches on it, nor on how many collections
|
|
207
|
-
there are.
|
|
208
|
-
|
|
209
|
-
**Root selection is the deployment’s contract.** A selector that does not bind
|
|
210
|
-
the CONSTRUCT’s subject yields a batch that is not root-complete, and projection
|
|
211
|
-
will then silently emit partial documents – a promise `@lde/pipeline` cannot
|
|
212
|
-
check ([ADR 13](../../docs/decisions/0013-project-inside-the-batch-per-root-type.md)).
|
|
213
|
-
`selectByClass(searchType)` is a convenience for the object grain (where the
|
|
214
|
-
type’s `class` really is the source class), **not** a default. It excludes
|
|
215
|
-
blank-node subjects (`FILTER(!isBlank(?root))`): a blank node has no stable
|
|
216
|
-
document key, so it can never become a search document – a custom selector
|
|
217
|
-
should exclude them too.
|
|
218
|
-
|
|
219
|
-
## Per-collection isolation
|
|
220
|
-
|
|
221
|
-
Each root type is an independent engine run – its own collection, alias and
|
|
222
|
-
cross-pod lock – so the collections commit, sweep and fail in isolation:
|
|
223
|
-
|
|
224
|
-
- a type whose projection is empty this run affects only its own collection,
|
|
225
|
-
never another’s – in particular the `datasets` index still goes live;
|
|
226
|
-
- `commit` finalizes every collection independently and, if any fails, throws an
|
|
227
|
-
`AggregateError` _after_ attempting them all, so a non-critical
|
|
228
|
-
label-collection failure never blocks the collections that did commit, while
|
|
229
|
-
the failure is still surfaced (the run is marked failed);
|
|
230
|
-
- because the pipeline aborts a run whose `commit` throws, `abort` finalizes only
|
|
231
|
-
the collections that did **not** already go live (aborting a committed
|
|
232
|
-
blue/green rebuild would drop its now-live collection), dropping the half-built
|
|
233
|
-
ones and releasing their locks.
|
|
234
|
-
|
|
235
|
-
See [ADR 9](../../docs/decisions/0009-route-a-whole-schema-projection-to-per-type-collections.md).
|
|
236
|
-
|
|
237
|
-
## Memory
|
|
238
|
-
|
|
239
|
-
Memory is bounded by **`batchSize` roots**, not by the input
|
|
240
|
-
([ADR 12](../../docs/decisions/0012-bound-memory-by-the-unit-of-work-not-the-input.md),
|
|
241
|
-
[ADR 13](../../docs/decisions/0013-project-inside-the-batch-per-root-type.md)).
|
|
242
|
-
Projection runs inside each root-complete batch and streams straight through the
|
|
243
|
-
terminal to the engine run, which holds only its own batch – nothing accumulates
|
|
244
|
-
across a dataset or across datasets, so the same pipeline indexes a 15-quad
|
|
245
|
-
catalog entry and a dataset of millions of objects. The one irreducible atom is a
|
|
246
|
-
single root’s quads: unbounded for a pathological root, and nothing here changes
|
|
247
|
-
that.
|
|
248
|
-
|
|
249
|
-
## Extraction queries and non-deduplicating engines
|
|
250
|
-
|
|
251
|
-
The generated extraction CONSTRUCTs are deliberately shaped to be well-formed for
|
|
252
|
-
**non-deduplicating** SPARQL engines such as
|
|
253
|
-
[QLever](https://github.com/ad-freiburg/qlever), which emit one copy of the
|
|
254
|
-
CONSTRUCT template per solution row rather than a deduplicated set. The generator
|
|
255
|
-
guarantees one output triple per genuine value:
|
|
256
|
-
|
|
257
|
-
- **UNION per field, never conjunction.** Each field is its own `UNION` branch
|
|
258
|
-
(`?root <path> ?value`), so two independent multi-valued fields never form a
|
|
259
|
-
cross-product – the failure mode that inflates a non-deduplicating engine’s
|
|
260
|
-
output by the product of their cardinalities (roughly 18-fold on a measured
|
|
261
|
-
QLever workload).
|
|
262
|
-
- **No projected-away template triple.** Every template triple binds its own
|
|
263
|
-
variables (`?root <alias> ?value`); there is no constant triple (e.g.
|
|
264
|
-
`?root a <Type>`) to be re-emitted once per solution.
|
|
265
|
-
- **Given roots.** Roots arrive as a `VALUES` set, not a pattern that fans out.
|
|
266
|
-
- **Inline references keep the discipline recursively:** a reference type’s
|
|
267
|
-
fields are UNION’d off the referent variable, so even a multi-hop nested
|
|
268
|
-
template never conjoins independent multi-valued fields – only the intermediate
|
|
269
|
-
link triple repeats, and only linearly.
|
|
15
|
+
## Documentation
|
|
270
16
|
|
|
271
|
-
|
|
272
|
-
output volume do **not** depend on a client-side **post-processing deduplication
|
|
273
|
-
step** – buffering a whole result to deduplicate it would defeat the
|
|
274
|
-
batch-bounded streaming memory model above. The only residual duplication is
|
|
275
|
-
linear and comes from duplicate _input_ – a non-`DISTINCT` selector feeding a
|
|
276
|
-
multi-typed root more than once – which the streaming per-quad subject index
|
|
277
|
-
(`buildSubjectIndex`) collapses as a cheap backstop, not as a compensator for
|
|
278
|
-
query-shape inflation. Validated on a real QLever index over the full source
|
|
279
|
-
dump: raw CONSTRUCT output equalled the distinct set for a type with distinct
|
|
280
|
-
roots, and the projected documents were byte-identical to those from a
|
|
281
|
-
deduplicating engine.
|
|
17
|
+
See the [full documentation](https://ldelements.org/reference/search-pipeline).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lde/search-pipeline",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Search indexing as an @lde/pipeline instance: a Writer<Quad> that applies the @lde/search projection to extracted RDF and fans each document out to the transactional engine writer for its type's collection. Glue between the pipeline quad world and the search-engine document world; owns no projection or engine logic itself.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "git+https://github.com/ldelements/lde.git",
|
|
@@ -25,14 +25,14 @@
|
|
|
25
25
|
"!**/*.tsbuildinfo"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@lde/search": "^0.
|
|
28
|
+
"@lde/search": "^0.13.0",
|
|
29
29
|
"@traqula/generator-sparql-1-1": "^1.1.8",
|
|
30
30
|
"@traqula/parser-sparql-1-1": "^1.1.5",
|
|
31
31
|
"@traqula/rules-sparql-1-1": "^1.1.0",
|
|
32
32
|
"tslib": "^2.3.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@lde/search-typesense": "^0.
|
|
35
|
+
"@lde/search-typesense": "^0.16.0",
|
|
36
36
|
"@rdfjs/types": "^2.0.1",
|
|
37
37
|
"n3": "^2.1.1",
|
|
38
38
|
"testcontainers": "^12.0.3",
|