@lde/search-pipeline 0.30.0 → 0.31.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.
@@ -49,6 +49,12 @@ export interface ExtractionOptions {
49
49
  * `OPTIONAL`, so a referent with no key candidate keeps its row. The root side
50
50
  * needs nothing: a key field is a declared field, so its own branch and
51
51
  * template triple are already there.
52
+ * - **references naming several targets**: the referent may be of any of them,
53
+ * so its branch also reads the referent’s `rdf:type` (`OPTIONAL`, emitted
54
+ * as is – framing carries it as `@type`), which is what the projection
55
+ * matches against each target’s `class` to tell which one it is. Every
56
+ * target then contributes its own expansion or key hop, so whichever the
57
+ * referent turns out to be, its declaration is in the frame.
52
58
  *
53
59
  * Wire the result into a `SparqlConstructReader` (see `searchStages`), which
54
60
  * runs it per batch with the roots injected as VALUES.
@@ -1 +1 @@
1
- {"version":3,"file":"extraction.d.ts","sourceRoot":"","sources":["../src/extraction.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,cAAc,EAIpB,MAAM,2BAA2B,CAAC;AAYnC,OAAO,KAAK,EAIV,YAAY,EACZ,UAAU,EACX,MAAM,aAAa,CAAC;AAMrB,2CAA2C;AAC3C,MAAM,WAAW,iBAAiB;IAChC;;;;;;;OAOG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CACnC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAgB,eAAe,CAC7B,UAAU,EAAE,UAAU,EACtB,MAAM,EAAE,YAAY,EACpB,OAAO,GAAE,iBAAsB,GAC9B,cAAc,CAqBhB;AAED,+EAA+E;AAC/E,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,UAAU,EACtB,MAAM,EAAE,YAAY,EACpB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,MAAM,CAER"}
1
+ {"version":3,"file":"extraction.d.ts","sourceRoot":"","sources":["../src/extraction.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,cAAc,EAIpB,MAAM,2BAA2B,CAAC;AAWnC,OAAO,KAAK,EAGV,YAAY,EACZ,UAAU,EACX,MAAM,aAAa,CAAC;AAQrB,2CAA2C;AAC3C,MAAM,WAAW,iBAAiB;IAChC;;;;;;;OAOG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CACnC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,wBAAgB,eAAe,CAC7B,UAAU,EAAE,UAAU,EACtB,MAAM,EAAE,YAAY,EACpB,OAAO,GAAE,iBAAsB,GAC9B,cAAc,CAqBhB;AAED,+EAA+E;AAC/E,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,UAAU,EACtB,MAAM,EAAE,YAAY,EACpB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,MAAM,CAER"}
@@ -1,10 +1,11 @@
1
1
  import { AstFactory, } from '@traqula/rules-sparql-1-1';
2
2
  import { Parser } from '@traqula/parser-sparql-1-1';
3
3
  import { Generator } from '@traqula/generator-sparql-1-1';
4
- import { fieldNamed, irAlias, isInlineReference, labelSourceNameOf, localLookupTypeOf, referenceTypeNamed, rootTypeNamed, } from '@lde/search/adapter';
4
+ import { fieldNamed, irAlias, isInlineReference, localLookupTargetsOf, referencedTargetsOf, referenceTypeNamed, } from '@lde/search/adapter';
5
5
  const factory = new AstFactory();
6
6
  const parser = new Parser();
7
7
  const generator = new Generator();
8
+ const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
8
9
  /**
9
10
  * Generate a type’s **Extraction** CONSTRUCT from its {@link SearchType}
10
11
  * declaration: the query whose output the projection frames and reads. Pure –
@@ -42,6 +43,12 @@ const generator = new Generator();
42
43
  * `OPTIONAL`, so a referent with no key candidate keeps its row. The root side
43
44
  * needs nothing: a key field is a declared field, so its own branch and
44
45
  * template triple are already there.
46
+ * - **references naming several targets**: the referent may be of any of them,
47
+ * so its branch also reads the referent’s `rdf:type` (`OPTIONAL`, emitted
48
+ * as is – framing carries it as `@type`), which is what the projection
49
+ * matches against each target’s `class` to tell which one it is. Every
50
+ * target then contributes its own expansion or key hop, so whichever the
51
+ * referent turns out to be, its declaration is in the frame.
45
52
  *
46
53
  * Wire the result into a `SparqlConstructReader` (see `searchStages`), which
47
54
  * runs it per batch with the roots injected as VALUES.
@@ -110,27 +117,42 @@ function buildFor(searchType, subject, schema, counter, visiting) {
110
117
  const patterns = [
111
118
  factory.patternBgp([factory.triple(subject, sourcePath, value)], factory.gen()),
112
119
  ];
113
- const local = localTargetOf(field, schema, onPath);
114
- if (local === undefined) {
115
- // Only where no local expansion follows: a `local` lookup reads the
116
- // target’s every path-bearing field, and a keyed target’s key field is
117
- // one of them, so emitting the hop as well would state it twice.
118
- const keyed = keyedTargetOf(field, schema);
119
- if (keyed !== undefined) {
120
- const built = buildKeyHop(keyed.target, keyed.keyField, value, counter);
121
- template.push(built.triple);
122
- patterns.push(built.pattern);
123
- }
120
+ const targets = referencedTargetsOf(field, schema);
121
+ // Every hop off the referent – its type, each key, each expansion – is
122
+ // its own branch of ONE optional union: each binds only its own
123
+ // variables, so a multi-valued type or key multiplies against nothing.
124
+ const expansions = [];
125
+ if (targets.length > 1) {
126
+ const built = buildTypeHop(value, counter);
127
+ template.push(built.triple);
128
+ expansions.push(built.pattern);
124
129
  }
125
- else {
126
- const nested = buildFor(local, value, schema, counter, onPath);
127
- template.push(...nested.template);
128
- // `OPTIONAL`, unlike an inline reference’s conjoined nesting: this
129
- // referent is stored by id whether or not the referring document says
130
- // anything about it, and conjoining would drop both together.
131
- if (nested.branches.length > 0) {
132
- patterns.push(factory.patternOptional([factory.patternUnion(nested.branches, factory.gen())], factory.gen()));
130
+ const local = localTargetsOf(field, schema, onPath);
131
+ for (const target of targets) {
132
+ if (!local.includes(target)) {
133
+ // Only where no local expansion follows: a `local` lookup reads the
134
+ // target’s every path-bearing field, and a keyed target’s key field
135
+ // is one of them, so emitting the hop as well would state it twice.
136
+ const keyed = keyedFieldOf(target);
137
+ if (keyed !== undefined) {
138
+ const built = buildKeyHop(target, keyed, value, counter);
139
+ template.push(built.triple);
140
+ expansions.push(built.pattern);
141
+ }
142
+ continue;
133
143
  }
144
+ const nested = buildFor(target, value, schema, counter, onPath);
145
+ template.push(...nested.template);
146
+ expansions.push(...nested.branches);
147
+ }
148
+ // `OPTIONAL`, unlike an inline reference’s conjoined nesting: this
149
+ // referent is stored by id whether or not the referring document says
150
+ // anything about it, and conjoining would drop both together. Several
151
+ // targets’ expansions share the one UNION: a referent is of one kind,
152
+ // and the branches of the others simply bind nothing for it. A
153
+ // single-branch union serialises to just its group.
154
+ if (expansions.length > 0) {
155
+ patterns.push(factory.patternOptional([factory.patternUnion(expansions, factory.gen())], factory.gen()));
134
156
  }
135
157
  branches.push(factory.patternGroup(patterns, factory.gen()));
136
158
  }
@@ -138,53 +160,60 @@ function buildFor(searchType, subject, schema, counter, visiting) {
138
160
  return { template, branches };
139
161
  }
140
162
  /**
141
- * The Root Type a {@link ReferenceStrategy.local} lookup expands into here, or
142
- * `undefined` where the field declares none – or where that type is already on
143
- * this path, which is where the recursion stops.
163
+ * The Root Types a {@link ReferenceStrategy.local} lookup expands into here:
164
+ * none where the field declares none, and never one already on this path,
165
+ * which is where the recursion stops.
144
166
  */
145
- function localTargetOf(field, schema, visiting) {
146
- const local = localLookupTypeOf(field, schema);
147
- return local === undefined || visiting.has(local.name) ? undefined : local;
167
+ function localTargetsOf(field, schema, visiting) {
168
+ return localLookupTargetsOf(field, schema).filter((target) => !visiting.has(target.name));
148
169
  }
149
170
  /**
150
- * The keyed Root Type a reference points at, with the field its key is read
151
- * from: a `lookup`’s `target` or an `idOnly`’s `labelSource`
152
- * ({@link labelSourceNameOf}) that declares a {@link RootType.key}, or
153
- * `undefined` for every other field. Naming the target is exactly the boundary
154
- * the projection re-keys along, so the extraction reads the same declarations
155
- * rather than a rule of its own: a reference that names no target keeps the
156
- * node IRI, and needs no hop.
171
+ * The field a Root Type’s key is read from, when it declares a
172
+ * {@link RootType.key}. Naming the target is exactly the boundary the
173
+ * projection re-keys along ({@link referencedTargetsOf}), so the extraction
174
+ * reads the same declarations rather than a rule of its own: a reference that
175
+ * names no target keeps the node IRI, and needs no hop.
157
176
  */
158
- function keyedTargetOf(field, schema) {
159
- if (field.kind !== 'reference') {
160
- return undefined;
161
- }
162
- const targetName = labelSourceNameOf(field);
163
- const target = targetName === undefined ? undefined : rootTypeNamed(schema, targetName);
164
- if (target?.key === undefined) {
177
+ function keyedFieldOf(target) {
178
+ if (target.key === undefined) {
165
179
  return undefined;
166
180
  }
167
181
  // `searchSchema` guarantees a declared, path-bearing key field, so the target
168
182
  // – which came out of the schema – always has one.
169
- const keyField = fieldNamed(target, target.key.field);
170
- return { target, keyField };
183
+ return fieldNamed(target, target.key.field);
184
+ }
185
+ /**
186
+ * The one-triple hop that reads a referent’s `rdf:type`, for a reference whose
187
+ * referent may be of several kinds: emitted under `rdf:type` itself, which
188
+ * framing turns into the node’s `@type`. The caller binds it inside the
189
+ * reference’s `OPTIONAL` union, so an untyped referent keeps its row and falls
190
+ * back to the first target declared.
191
+ */
192
+ function buildTypeHop(referent, counter) {
193
+ const type = factory.termVariable(`t${counter.next++}`, factory.gen());
194
+ const predicate = factory.termNamed(factory.gen(), RDF_TYPE);
195
+ return {
196
+ triple: factory.triple(referent, predicate, type),
197
+ pattern: factory.patternGroup([
198
+ factory.patternBgp([factory.triple(referent, predicate, type)], factory.gen()),
199
+ ], factory.gen()),
200
+ };
171
201
  }
172
202
  /**
173
203
  * The one-field hop that reads a referent’s key: a template triple emitting the
174
- * key field under the **target’s** IR Alias, and an `OPTIONAL` branch binding
175
- * it. The referent recursion an inline reference already performs, for a single
176
- * field and wrapped in `OPTIONAL` rather than conjoined – so a referent with no
177
- * key candidate keeps its row and still stores its own IRI, instead of the
178
- * whole reference dropping out of the CONSTRUCT.
204
+ * key field under the **target’s** IR Alias, and a branch binding it. The
205
+ * referent recursion an inline reference already performs, for a single field –
206
+ * bound by the caller inside the reference’s `OPTIONAL` union rather than
207
+ * conjoined, so a referent with no key candidate keeps its row and still stores
208
+ * its own IRI, instead of the whole reference dropping out of the CONSTRUCT.
179
209
  *
180
- * It sits inside its own UNION branch (the one the reference contributes), so it
181
- * multiplies against nothing.
210
+ * It is a branch of its own in that union, so it multiplies against nothing.
182
211
  */
183
212
  function buildKeyHop(target, keyField, referent, counter) {
184
213
  const key = factory.termVariable(`k${counter.next++}`, factory.gen());
185
214
  return {
186
215
  triple: factory.triple(referent, factory.termNamed(factory.gen(), irAlias(target, keyField)), key),
187
- pattern: factory.patternOptional([
216
+ pattern: factory.patternGroup([
188
217
  factory.patternBgp([factory.triple(referent, liftPath(keyField.path), key)], factory.gen()),
189
218
  ], factory.gen()),
190
219
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lde/search-pipeline",
3
- "version": "0.30.0",
3
+ "version": "0.31.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.0",
28
+ "@lde/search": "^0.29.0",
29
29
  "@traqula/generator-sparql-1-1": "^1.3.1",
30
30
  "@traqula/parser-sparql-1-1": "^1.3.1",
31
31
  "@traqula/rules-sparql-1-1": "^1.2.0",
32
32
  "tslib": "^2.3.0"
33
33
  },
34
34
  "devDependencies": {
35
- "@lde/search-typesense": "^0.33.0",
35
+ "@lde/search-typesense": "^0.34.0",
36
36
  "@rdfjs/types": "^2.0.1",
37
37
  "n3": "^2.7.12",
38
38
  "testcontainers": "^12.1.0",