@lde/search-pipeline 0.12.5 → 0.12.6

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.
Files changed (2) hide show
  1. package/README.md +5 -269
  2. 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
- The division of labour ([ADR 6](../../docs/decisions/0006-make-the-writer-transaction-aware.md),
10
- [ADR 13](../../docs/decisions/0013-project-inside-the-batch-per-root-type.md)):
9
+ ## Installation
11
10
 
12
- - [`@lde/search`](../search) owns the engine-agnostic projection (framed
13
- document + field model) and stays pipeline-free;
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
- Collection names are the engine adapter’s to decide, not this package’s – see
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
- Because the queries are duplicate-free by construction, correctness and bounded
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.12.5",
3
+ "version": "0.12.6",
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.12.2",
28
+ "@lde/search": "^0.12.3",
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.15.5",
35
+ "@lde/search-typesense": "^0.15.6",
36
36
  "@rdfjs/types": "^2.0.1",
37
37
  "n3": "^2.1.1",
38
38
  "testcontainers": "^12.0.3",