@lde/search-typesense 0.33.1 → 0.35.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/dist/collection-definition.d.ts.map +1 -1
- package/dist/collection-definition.js +75 -24
- package/dist/lookup.d.ts +16 -6
- package/dist/lookup.d.ts.map +1 -1
- package/dist/lookup.js +47 -22
- package/dist/query-compiler.d.ts.map +1 -1
- package/dist/query-compiler.js +13 -5
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +110 -57
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection-definition.d.ts","sourceRoot":"","sources":["../src/collection-definition.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AAExD,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,UAAU,EAChB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"collection-definition.d.ts","sourceRoot":"","sources":["../src/collection-definition.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AAExD,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,UAAU,EAChB,MAAM,aAAa,CAAC;AAerB,0EAA0E;AAC1E,MAAM,WAAW,2BAA2B;IAC1C;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,MAAM,CAAC;IAChE;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC;IAC/B;;;4BAGwB;IACxB,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,kEAAkE;IAClE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IACtC,kEAAkE;IAClE,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,UAAU,EACtB,OAAO,GAAE,2BAAgC,GACxC,sBAAsB,CA2BxB"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { displayFieldPattern, ID_FIELD, isInlineReference, isInternalField, joinGraph,
|
|
1
|
+
import { displayFieldPattern, ID_FIELD, isInlineReference, isInternalField, joinGraph, localLookupTargetsOf, TARGET_FIELD, nestedFieldName, nestedReferenceType, physicalFields, } from '@lde/search/adapter';
|
|
2
2
|
import { deriveCollectionName } from './collection-name.js';
|
|
3
3
|
/**
|
|
4
4
|
* Build a Typesense collection definition from the unified {@link SearchType}, so
|
|
@@ -70,20 +70,19 @@ export function buildCollectionDefinition(searchType, options = {}) {
|
|
|
70
70
|
return collection;
|
|
71
71
|
}
|
|
72
72
|
/**
|
|
73
|
-
* The
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
73
|
+
* The types a field nests – one for an inline reference or a single-target
|
|
74
|
+
* `local` lookup, several for a `local` lookup naming several targets – or
|
|
75
|
+
* none when it nests nothing. Throws when the type carries an inline reference
|
|
76
|
+
* the caller gave no schema to resolve: the collection would silently store
|
|
77
|
+
* the reference as a string the projection never writes, so every document
|
|
78
|
+
* would fail to import.
|
|
77
79
|
*/
|
|
78
|
-
function
|
|
79
|
-
const nested = schema === undefined
|
|
80
|
-
? undefined
|
|
81
|
-
: (nestedReferenceType(schema, field) ??
|
|
82
|
-
localLookupTypeOf(field, schema));
|
|
80
|
+
function nestedTypesOf(searchType, field, schema) {
|
|
81
|
+
const nested = schema === undefined ? [] : nestedTypesOfNestedField(field, schema);
|
|
83
82
|
// Reached only for a field carrying a Role, so an inline reference here
|
|
84
83
|
// stores entries: a Role-less one is an internal reading device, pruned
|
|
85
84
|
// before the writer.
|
|
86
|
-
if (nested ===
|
|
85
|
+
if (nested.length === 0 && isInlineReference(field)) {
|
|
87
86
|
throw new Error(`Building the collection for “${searchType.name}” needs the search schema its surfaced inline reference “${field.name}” resolves against; pass it as the collection-definition option “schema”.`);
|
|
88
87
|
}
|
|
89
88
|
return nested;
|
|
@@ -143,10 +142,10 @@ function typesenseFields(searchType, field, defaultLocale, defaultSortingField,
|
|
|
143
142
|
if (isInternalField(field)) {
|
|
144
143
|
return [];
|
|
145
144
|
}
|
|
146
|
-
const nested =
|
|
147
|
-
if (nested
|
|
145
|
+
const nested = nestedTypesOf(searchType, field, schema);
|
|
146
|
+
if (nested.length > 0) {
|
|
148
147
|
return [
|
|
149
|
-
...
|
|
148
|
+
...nestedObjectFields(field.name, field, nested, nested.length > 1, schema, defaultLocale, false,
|
|
150
149
|
// The declaring type is already on the path, exactly as the frame and
|
|
151
150
|
// the extraction seed theirs, so a lookup back to it is cut at the
|
|
152
151
|
// same depth here as it is there.
|
|
@@ -260,6 +259,39 @@ function typesenseFields(searchType, field, defaultLocale, defaultSortingField,
|
|
|
260
259
|
* the children rather than declared, and this is the one place that invariant
|
|
261
260
|
* can be got wrong.
|
|
262
261
|
*/
|
|
262
|
+
function nestedObjectFields(prefix, reference, nestedTypes, polymorphic, schema, defaultLocale, withinArray, onPath) {
|
|
263
|
+
// `polymorphic` is about the DECLARATION, `nestedTypes` about what is
|
|
264
|
+
// descended into here: a lookup naming several targets of which only one is
|
|
265
|
+
// still off the path stores a discriminator all the same.
|
|
266
|
+
if (!polymorphic) {
|
|
267
|
+
return nestedFields(prefix, reference, nestedTypes[0], schema, defaultLocale, withinArray, onPath);
|
|
268
|
+
}
|
|
269
|
+
// A lookup naming several targets stores a referent of whichever kind it
|
|
270
|
+
// turns out to be under ONE nested object, so the object declares the union
|
|
271
|
+
// of the targets’ fields – each declared once, and `searchSchema` has
|
|
272
|
+
// already refused two targets declaring one name differently – plus the
|
|
273
|
+
// discriminator the projection writes to say which kind an entry is.
|
|
274
|
+
const byName = new Map();
|
|
275
|
+
for (const nestedType of nestedTypes) {
|
|
276
|
+
for (const declared of nestedFields(prefix, reference, nestedType, schema, defaultLocale, withinArray, onPath)) {
|
|
277
|
+
if (!byName.has(declared.name)) {
|
|
278
|
+
byName.set(declared.name, declared);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const flattensToArray = withinArray || reference.array === true;
|
|
283
|
+
const children = [...byName.values()].filter((declared) => declared.name !== prefix);
|
|
284
|
+
children.push(targetField(prefix, flattensToArray));
|
|
285
|
+
return [
|
|
286
|
+
{
|
|
287
|
+
name: prefix,
|
|
288
|
+
type: flattensToArray ? 'object[]' : 'object',
|
|
289
|
+
index: children.some((child) => child.index !== false),
|
|
290
|
+
optional: reference.required !== true,
|
|
291
|
+
},
|
|
292
|
+
...children,
|
|
293
|
+
];
|
|
294
|
+
}
|
|
263
295
|
function nestedFields(prefix, reference, nestedType, schema, defaultLocale, withinArray = false, onPath = new Set()) {
|
|
264
296
|
const array = reference.array === true;
|
|
265
297
|
// A type already on this path is not descended into again. `searchSchema`
|
|
@@ -292,8 +324,11 @@ function nestedFields(prefix, reference, nestedType, schema, defaultLocale, with
|
|
|
292
324
|
if (isInternalField(field)) {
|
|
293
325
|
continue;
|
|
294
326
|
}
|
|
295
|
-
const deeper =
|
|
296
|
-
|
|
327
|
+
const deeper = nestedTypesOfNestedField(field, schema);
|
|
328
|
+
// Descend into the types not yet on the path; where every one is, the
|
|
329
|
+
// cycle stops here.
|
|
330
|
+
const descend = deeper.filter((type) => !walked.has(type.name));
|
|
331
|
+
if (deeper.length > 0 && descend.length === 0) {
|
|
297
332
|
// The cycle stops here, and so does the frame – but a value is still
|
|
298
333
|
// stored: the extraction falls back to the target's key hop, and the
|
|
299
334
|
// projection writes the referent as an `{id}` object beside its identity
|
|
@@ -309,7 +344,10 @@ function nestedFields(prefix, reference, nestedType, schema, defaultLocale, with
|
|
|
309
344
|
// leaf a filter welds on sits beside the object, not inside it.
|
|
310
345
|
index: false,
|
|
311
346
|
optional: true,
|
|
312
|
-
},
|
|
347
|
+
},
|
|
348
|
+
// A cut lookup naming several targets still says which one it
|
|
349
|
+
// stored, exactly as an expanded one does.
|
|
350
|
+
...(deeper.length > 1 ? [targetField(boundary, boundaryArray)] : []), {
|
|
313
351
|
// Always a Root Type, so always identified: `searchSchema` rejects
|
|
314
352
|
// inline cycles, so the only way to arrive at a type already on the
|
|
315
353
|
// path is a `local` lookup, whose target declares a class.
|
|
@@ -320,8 +358,8 @@ function nestedFields(prefix, reference, nestedType, schema, defaultLocale, with
|
|
|
320
358
|
}, ...nestedIdentityFields(prefix, field, schema, flattensToArray));
|
|
321
359
|
continue;
|
|
322
360
|
}
|
|
323
|
-
if (
|
|
324
|
-
children.push(...
|
|
361
|
+
if (descend.length > 0) {
|
|
362
|
+
children.push(...nestedObjectFields(nestedFieldName(prefix, field.name), field, descend, deeper.length > 1, schema, defaultLocale, flattensToArray, walked),
|
|
325
363
|
// A nested field that stores an object cannot be filtered as itself,
|
|
326
364
|
// and its own id is a level deeper than a condition can be welded to –
|
|
327
365
|
// an engine welds conditions on an entry's LEAF fields only. Its
|
|
@@ -414,11 +452,24 @@ function nestedIdentityFields(prefix, field, schema, flattensToArray) {
|
|
|
414
452
|
},
|
|
415
453
|
];
|
|
416
454
|
}
|
|
417
|
-
/** The
|
|
418
|
-
*
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
455
|
+
/** The stored discriminator of a lookup naming several targets
|
|
456
|
+
* ({@link TARGET_FIELD}): a stored value, never indexed. */
|
|
457
|
+
function targetField(prefix, array) {
|
|
458
|
+
return {
|
|
459
|
+
name: nestedFieldName(prefix, TARGET_FIELD),
|
|
460
|
+
type: array ? 'string[]' : 'string',
|
|
461
|
+
index: false,
|
|
462
|
+
optional: true,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
/** The types a field nests: another inline reference’s type, or the Root
|
|
466
|
+
* Types a {@link ReferenceStrategy.local local} lookup projects its endpoint
|
|
467
|
+
* through. */
|
|
468
|
+
function nestedTypesOfNestedField(field, schema) {
|
|
469
|
+
const referenceType = nestedReferenceType(schema, field);
|
|
470
|
+
return referenceType === undefined
|
|
471
|
+
? localLookupTargetsOf(field, schema)
|
|
472
|
+
: [referenceType];
|
|
422
473
|
}
|
|
423
474
|
/**
|
|
424
475
|
* The Physical Fields one non-nesting nested field contributes: exactly what
|
package/dist/lookup.d.ts
CHANGED
|
@@ -16,16 +16,26 @@ import { type ReferenceProjection, type RootType, type SearchSchema, type Search
|
|
|
16
16
|
*/
|
|
17
17
|
export type ResolvedReferents = {
|
|
18
18
|
readonly via: 'lookup';
|
|
19
|
-
/**
|
|
20
|
-
* them
|
|
21
|
-
readonly
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
readonly
|
|
19
|
+
/** By IRI. A lookup naming several targets holds referents of any of
|
|
20
|
+
* them here, each saying which. */
|
|
21
|
+
readonly documents: ReadonlyMap<string, ResolvedReferent>;
|
|
22
|
+
/** The levels below, by target name – what a stored referent no
|
|
23
|
+
* collection answered for is reconstructed against. */
|
|
24
|
+
readonly childrenByTarget: ReadonlyMap<string, ReadonlyMap<string, ResolvedReferents>>;
|
|
25
25
|
} | {
|
|
26
26
|
readonly via: 'nested';
|
|
27
27
|
readonly children: ReadonlyMap<string, ResolvedReferents>;
|
|
28
28
|
};
|
|
29
|
+
/** One referent a lookup level fetched. */
|
|
30
|
+
export interface ResolvedReferent {
|
|
31
|
+
/** The Root Type this referent is declared by – the target whose collection
|
|
32
|
+
* answered for it. Reconstruction reads it through that target’s own
|
|
33
|
+
* declaration, never the referrer’s. */
|
|
34
|
+
readonly target: RootType;
|
|
35
|
+
readonly document: Record<string, unknown>;
|
|
36
|
+
/** The levels below, keyed by the reference field’s name on `target`. */
|
|
37
|
+
readonly children: ReadonlyMap<string, ResolvedReferents>;
|
|
38
|
+
}
|
|
29
39
|
/**
|
|
30
40
|
* Resolve a projection against the documents that name its referents: one
|
|
31
41
|
* batched `multi_search` **per level**, never per document, because the IRIs of
|
package/dist/lookup.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lookup.d.ts","sourceRoot":"","sources":["../src/lookup.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,UAAU,EAChB,MAAM,aAAa,CAAC;AAUrB;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,iBAAiB,GACzB;IACE,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC;IACvB;
|
|
1
|
+
{"version":3,"file":"lookup.d.ts","sourceRoot":"","sources":["../src/lookup.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,UAAU,EAChB,MAAM,aAAa,CAAC;AAUrB;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,iBAAiB,GACzB;IACE,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC;IACvB;wCACoC;IACpC,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC1D;4DACwD;IACxD,QAAQ,CAAC,gBAAgB,EAAE,WAAW,CACpC,MAAM,EACN,WAAW,CAAC,MAAM,EAAE,iBAAiB,CAAC,CACvC,CAAC;CACH,GACD;IACE,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;CAC3D,CAAC;AAEN,2CAA2C;AAC3C,MAAM,WAAW,gBAAgB;IAC/B;;6CAEyC;IACzC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC3C,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;CAC3D;AAKD;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,aAAa,CAAC,EACnC,UAAU,EAAE,mBAAmB,GAAG,SAAS,EAC3C,UAAU,EAAE,UAAU,EACtB,MAAM,EAAE,YAAY,EACpB,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EACxC,OAAO,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAC3C,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GACjC,OAAO,CAAC,WAAW,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAmHjD"}
|
package/dist/lookup.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { displayFieldName, fieldNamed, labelFieldOf, nestedReferenceType,
|
|
1
|
+
import { displayFieldName, fieldNamed, labelFieldOf, nestedReferenceType, referencedTargetsOf, } from '@lde/search/adapter';
|
|
2
2
|
import { escapeFilterValue } from './query-compiler.js';
|
|
3
3
|
/** Typesense caps a filter list; the same batch size the label lookup uses. */
|
|
4
4
|
const BATCH_SIZE = 200;
|
|
@@ -43,30 +43,51 @@ export async function resolveProjection(client, projection, searchType, schema,
|
|
|
43
43
|
// hand-built query from throwing here rather than at the port’s guard.
|
|
44
44
|
return undefined;
|
|
45
45
|
}
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
46
|
+
const targets = referencedTargetsOf(field, schema).filter((target) => collections.has(target.class));
|
|
47
|
+
const iris = distinctIris(parents, name);
|
|
48
|
+
if (targets.length === 0 || iris.length === 0) {
|
|
49
49
|
return undefined;
|
|
50
50
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
// `id
|
|
55
|
-
|
|
51
|
+
// One fetch per target, concurrently: a referent lives in one of the
|
|
52
|
+
// collections, and which one is what a lookup naming several targets
|
|
53
|
+
// exists to find out – so with several, every collection is asked even
|
|
54
|
+
// for a selection reduced to `id`, since the answer types the referent.
|
|
55
|
+
// With one target, a selection reduced to the `id` the referring
|
|
56
|
+
// document already carries reads nothing.
|
|
57
|
+
const perTarget = await Promise.all(targets.map(async (target) => {
|
|
58
|
+
const include = includeFields(target, level.fields, level.resolve);
|
|
59
|
+
if (targets.length === 1 && include.length <= 1) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
const fetched = await fetchReferents(client, collections.get(target.class), iris, include, onError);
|
|
63
|
+
// The level below reads the IRIs off the documents this one just
|
|
64
|
+
// fetched – one more round-trip for the page, whatever its size.
|
|
65
|
+
const children = await resolveProjection(client, level.resolve, target, schema, collections, [...fetched.values()], onError);
|
|
66
|
+
return { target, fetched, children };
|
|
67
|
+
}));
|
|
68
|
+
// Declaration order is precedence: an IRI two collections hold belongs
|
|
69
|
+
// to the target declared first.
|
|
70
|
+
const documents = new Map();
|
|
71
|
+
const childrenByTarget = new Map();
|
|
72
|
+
for (const resolved of perTarget) {
|
|
73
|
+
if (resolved === undefined) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
childrenByTarget.set(resolved.target.name, resolved.children);
|
|
77
|
+
for (const [iri, document] of resolved.fetched) {
|
|
78
|
+
if (!documents.has(iri)) {
|
|
79
|
+
documents.set(iri, {
|
|
80
|
+
target: resolved.target,
|
|
81
|
+
document,
|
|
82
|
+
children: resolved.children,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (perTarget.every((resolved) => resolved === undefined)) {
|
|
56
88
|
return undefined;
|
|
57
89
|
}
|
|
58
|
-
|
|
59
|
-
return [
|
|
60
|
-
name,
|
|
61
|
-
{
|
|
62
|
-
via: 'lookup',
|
|
63
|
-
target,
|
|
64
|
-
documents,
|
|
65
|
-
// The level below reads the IRIs off the documents this one just
|
|
66
|
-
// fetched – one more round-trip for the page, whatever its size.
|
|
67
|
-
children: await resolveProjection(client, level.resolve, target, schema, collections, [...documents.values()], onError),
|
|
68
|
-
},
|
|
69
|
-
];
|
|
90
|
+
return [name, { via: 'lookup', documents, childrenByTarget }];
|
|
70
91
|
}));
|
|
71
92
|
for (const level of levels) {
|
|
72
93
|
if (level !== undefined) {
|
|
@@ -142,8 +163,12 @@ function includeFields(target, wanted, below) {
|
|
|
142
163
|
physical.add(field.name);
|
|
143
164
|
}
|
|
144
165
|
}
|
|
166
|
+
// With several targets the level below merges every target’s selection,
|
|
167
|
+
// so ask each collection only for the reference fields it declares.
|
|
145
168
|
for (const name of Object.keys(below ?? {})) {
|
|
146
|
-
|
|
169
|
+
if (fieldNamed(target, name) !== undefined) {
|
|
170
|
+
physical.add(name);
|
|
171
|
+
}
|
|
147
172
|
}
|
|
148
173
|
return [...physical];
|
|
149
174
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query-compiler.d.ts","sourceRoot":"","sources":["../src/query-compiler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAC;AAEzE,OAAO,EAGL,KAAK,MAAM,EACX,KAAK,QAAQ,EAEb,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,UAAU,EAIhB,MAAM,aAAa,CAAC;AAoBrB;mDACmD;AACnD,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED;;;;GAIG;AACH,MAAM,WAAW,wBAAwB;IACvC;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,CACvB,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,SAAS,MAAM,EAAE,KACpB,UAAU,GAAG,SAAS,CAAC;IAC5B;;;;;;OAMG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC;;;;;;;;OAQG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC;IAC/B;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACrD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,WAAW,EAClB,UAAU,EAAE,UAAU,EACtB,OAAO,GAAE,wBAA6B,GACrC,YAAY,CAAC,MAAM,CAAC,CAkCtB;
|
|
1
|
+
{"version":3,"file":"query-compiler.d.ts","sourceRoot":"","sources":["../src/query-compiler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAC;AAEzE,OAAO,EAGL,KAAK,MAAM,EACX,KAAK,QAAQ,EAEb,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,UAAU,EAIhB,MAAM,aAAa,CAAC;AAoBrB;mDACmD;AACnD,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED;;;;GAIG;AACH,MAAM,WAAW,wBAAwB;IACvC;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,CACvB,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,SAAS,MAAM,EAAE,KACpB,UAAU,GAAG,SAAS,CAAC;IAC5B;;;;;;OAMG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC;;;;;;;;OAQG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC;IAC/B;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACrD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,WAAW,EAClB,UAAU,EAAE,UAAU,EACtB,OAAO,GAAE,wBAA6B,GACrC,YAAY,CAAC,MAAM,CAAC,CAkCtB;AAwgBD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEvD"}
|
package/dist/query-compiler.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fold } from '@lde/text-normalization';
|
|
2
|
-
import { fieldNamed, filterOperator, filterOperatorFor, ID_FIELD, isoToUnixSeconds, isInternalField, isRangeFacet, isWelded, joinGraph,
|
|
2
|
+
import { fieldNamed, filterOperator, filterOperatorFor, ID_FIELD, isoToUnixSeconds, isInternalField, isRangeFacet, isWelded, joinGraph, localLookupTargetsOf, matchLanguage, pageForOffset, physicalFields, nestedReferenceType, resolvePath, searchableFields, } from '@lde/search/adapter';
|
|
3
3
|
/**
|
|
4
4
|
* Compile the engine-neutral {@link SearchQuery} into Typesense search
|
|
5
5
|
* parameters – the query half of the engine adapter. Pure (no client, no env),
|
|
@@ -138,14 +138,22 @@ function collectSearchable(searchType, locale, schema, prefix, onPath, names, we
|
|
|
138
138
|
if (isInternalField(field)) {
|
|
139
139
|
continue;
|
|
140
140
|
}
|
|
141
|
-
const
|
|
141
|
+
const referenceType = nestedReferenceType(schema, field);
|
|
142
|
+
const nested = referenceType === undefined
|
|
143
|
+
? localLookupTargetsOf(field, schema)
|
|
144
|
+
: [referenceType];
|
|
142
145
|
// Cut on the CHILD, not on entry, and against the set this level was
|
|
143
146
|
// reached with – the same boundary the collection's own walk cuts at
|
|
144
147
|
// (`nestedFields`). Returning on entry instead skipped the companions of
|
|
145
148
|
// the level the collection HAD declared, leaving them indexed and absent
|
|
146
|
-
// from `query_by` wherever a type reached itself.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
+
// from `query_by` wherever a type reached itself. Several targets each
|
|
150
|
+
// contribute their searchable fields: the one nested object declares them
|
|
151
|
+
// all, and free text has to reach a referent of either kind.
|
|
152
|
+
for (const nestedType of nested) {
|
|
153
|
+
if (onPath.has(nestedType.name)) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
collectSearchable(nestedType, locale, schema, qualify(prefix, field.name), walked, names, weights);
|
|
149
157
|
}
|
|
150
158
|
}
|
|
151
159
|
}
|
package/dist/search.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../src/search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAExC,OAAO,
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../src/search.ts"],"names":[],"mappings":"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"}
|
package/dist/search.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { NESTED_DOCUMENT_TYPE, } from '@lde/search';
|
|
2
|
+
import { assertTypeInSchema, assertValidQuery, displayLangOf, facetableFields, fieldNamed, isInlineReference, isRangeFacet, isUnsatisfiable, joinGraph, labelFieldOf, labelSourceNamesOf, labelTargetNamesOf, localLookupTargetsOf, nestedReferenceType, outputFields, physicalFields, referencedTargetsOf, referenceFields, storedTargetOf, } from '@lde/search/adapter';
|
|
2
3
|
import { buildSearchParams, escapeFilterValue, } from './query-compiler.js';
|
|
3
4
|
import { deriveCollectionName } from './collection-name.js';
|
|
4
|
-
import { resolveProjection } from './lookup.js';
|
|
5
|
+
import { resolveProjection, } from './lookup.js';
|
|
5
6
|
/**
|
|
6
7
|
* A Typesense-backed {@link SearchEngine}, bound to the whole
|
|
7
8
|
* {@link SearchSchema} at construction – like every other schema consumer.
|
|
@@ -65,25 +66,27 @@ export function createTypesenseSearchEngine(client, schema, options = {}) {
|
|
|
65
66
|
// (`searchSchema`/`labelFieldOf`), and every type resolved a collection
|
|
66
67
|
// above, so both lookups below always hit.
|
|
67
68
|
const typesByName = new Map([...schema.values()].map((searchType) => [searchType.name, searchType]));
|
|
69
|
+
// Per field, one source per target it names, in order of precedence – a
|
|
70
|
+
// reference naming several targets reads labels from every one of them.
|
|
68
71
|
const labelSources = new Map([...schema.values()].map((searchType) => [
|
|
69
72
|
searchType.class,
|
|
70
73
|
new Map(referenceFields(searchType)
|
|
71
74
|
// An INLINE reference names its label target one level in, through
|
|
72
75
|
// its identity companion: the companion holds that field's ids, so
|
|
73
76
|
// that field's target is what can label them.
|
|
74
|
-
.filter((field) =>
|
|
75
|
-
.map((field) =>
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
{
|
|
77
|
+
.filter((field) => labelTargetNamesOf(field, schema).length > 0)
|
|
78
|
+
.map((field) => [
|
|
79
|
+
field.name,
|
|
80
|
+
labelTargetNamesOf(field, schema).map((targetName) => {
|
|
81
|
+
const source = typesByName.get(targetName);
|
|
82
|
+
const labelField = labelFieldOf(source);
|
|
83
|
+
return {
|
|
81
84
|
collection: collections.get(source.class),
|
|
82
85
|
labelField,
|
|
83
86
|
queryBy: physicalFields(labelField).search.join(','),
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
};
|
|
88
|
+
}),
|
|
89
|
+
])),
|
|
87
90
|
]));
|
|
88
91
|
// Typesense reports a facet under the field it faceted, which for a reference
|
|
89
92
|
// inheriting a facet policy is the `${name}_facet` companion, not the
|
|
@@ -106,7 +109,9 @@ export function createTypesenseSearchEngine(client, schema, options = {}) {
|
|
|
106
109
|
const distinctLabelSources = new Map([...labelSources].map(([type, sources]) => [
|
|
107
110
|
type,
|
|
108
111
|
[
|
|
109
|
-
...new Map([...sources.values()]
|
|
112
|
+
...new Map([...sources.values()]
|
|
113
|
+
.flat()
|
|
114
|
+
.map((source) => [source.collection, source])).values(),
|
|
110
115
|
],
|
|
111
116
|
]));
|
|
112
117
|
// The output reference fields that carry hit labels, each paired with its
|
|
@@ -128,10 +133,10 @@ export function createTypesenseSearchEngine(client, schema, options = {}) {
|
|
|
128
133
|
// the entry – and their FACET buckets are labelled by a separate
|
|
129
134
|
// path that reads the companion.
|
|
130
135
|
nestedReferenceType(schema, field) === undefined &&
|
|
131
|
-
|
|
136
|
+
localLookupTargetsOf(field, schema).length === 0)
|
|
132
137
|
.map((field) => ({
|
|
133
138
|
name: field.name,
|
|
134
|
-
|
|
139
|
+
sources: sources.get(field.name),
|
|
135
140
|
})),
|
|
136
141
|
];
|
|
137
142
|
}));
|
|
@@ -242,7 +247,7 @@ export function createTypesenseSearchEngine(client, schema, options = {}) {
|
|
|
242
247
|
// in the body, so the filter is bounded by the caller's request, not by
|
|
243
248
|
// the URL.
|
|
244
249
|
const response = withDeclaredFacetNames(await performSingleSearch(client, collections.get(searchType.class), params), declaredFacetNames.get(searchType.class));
|
|
245
|
-
const labels = await resolveLabels(cachedLabelsPromise, () => labelLookupGroups([response], labelSources.get(searchType.class), outputReferenceSources.get(searchType.class) ?? []));
|
|
250
|
+
const labels = await resolveLabels(cachedLabelsPromise, () => labelLookupGroups([response], labelSources.get(searchType.class), outputReferenceSources.get(searchType.class) ?? [], distinctLabelSources.get(searchType.class) ?? []));
|
|
246
251
|
// What the caller asked to resolve, level by level. Independent of the
|
|
247
252
|
// label lookup above: that one serves facet buckets and unprojected
|
|
248
253
|
// references, and stays cacheable because a label is one small value.
|
|
@@ -286,7 +291,7 @@ export function createTypesenseSearchEngine(client, schema, options = {}) {
|
|
|
286
291
|
: withDeclaredFacetNames(result, declaredNames));
|
|
287
292
|
// One label lookup serves every successful facet result in the batch.
|
|
288
293
|
const responses = results.filter((result) => !('error' in result));
|
|
289
|
-
const labels = await resolveLabels(cachedLabelsPromise, () => labelLookupGroups(responses, labelSources.get(searchType.class), outputReferenceSources.get(searchType.class) ?? []));
|
|
294
|
+
const labels = await resolveLabels(cachedLabelsPromise, () => labelLookupGroups(responses, labelSources.get(searchType.class), outputReferenceSources.get(searchType.class) ?? [], distinctLabelSources.get(searchType.class) ?? []));
|
|
290
295
|
// multi_search reports a failed entry inline instead of rejecting the
|
|
291
296
|
// call; pass that through as a per-query outcome – naming the facets,
|
|
292
297
|
// not the position, since the batch order is the caller's internal –
|
|
@@ -371,33 +376,40 @@ async function loadAllLabels(client, source) {
|
|
|
371
376
|
* source, one group per source collection. Fields without a `labelSource`
|
|
372
377
|
* stay id-only, so their IRIs never travel.
|
|
373
378
|
*/
|
|
374
|
-
function labelLookupGroups(responses, sources, outputSources) {
|
|
379
|
+
function labelLookupGroups(responses, sources, outputSources, ordered) {
|
|
375
380
|
if (sources === undefined || sources.size === 0) {
|
|
376
381
|
return [];
|
|
377
382
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
383
|
+
// One group per collection, in the order the TYPE’s fields first name them
|
|
384
|
+
// – seeded up front rather than in the order the hits happen to mention
|
|
385
|
+
// them, because `fetchLabels` reads group order as precedence and a label
|
|
386
|
+
// is resolved per type, not per field: an IRI two collections hold gets the
|
|
387
|
+
// label of the one the type declares first, whichever field carried it.
|
|
388
|
+
const irisByCollection = new Map(ordered.map((source) => [
|
|
389
|
+
source.collection,
|
|
390
|
+
{ source, iris: new Set() },
|
|
391
|
+
]));
|
|
392
|
+
// An IRI of a field naming several targets travels to EVERY one of their
|
|
393
|
+
// collections: which holds it is not known until one answers.
|
|
394
|
+
const add = (fieldSources, iri) => {
|
|
395
|
+
for (const source of fieldSources) {
|
|
396
|
+
irisByCollection.get(source.collection)?.iris.add(iri);
|
|
384
397
|
}
|
|
385
|
-
group.iris.add(iri);
|
|
386
398
|
};
|
|
387
399
|
for (const response of responses) {
|
|
388
400
|
// Hits only carry labels for OUTPUT reference fields (reconstructDocument
|
|
389
401
|
// skips non-output fields); `outputSources` pairs each with its resolved
|
|
390
402
|
// source, precomputed per type.
|
|
391
403
|
for (const hit of response.hits ?? []) {
|
|
392
|
-
for (const { name,
|
|
404
|
+
for (const { name, sources: fieldSources } of outputSources) {
|
|
393
405
|
const raw = hit.document[name];
|
|
394
406
|
if (Array.isArray(raw)) {
|
|
395
407
|
for (const value of raw) {
|
|
396
|
-
add(
|
|
408
|
+
add(fieldSources, String(value));
|
|
397
409
|
}
|
|
398
410
|
}
|
|
399
411
|
else if (typeof raw === 'string') {
|
|
400
|
-
add(
|
|
412
|
+
add(fieldSources, raw);
|
|
401
413
|
}
|
|
402
414
|
}
|
|
403
415
|
}
|
|
@@ -405,19 +417,18 @@ function labelLookupGroups(responses, sources, outputSources) {
|
|
|
405
417
|
// like `class`); resolve them in the same lookup. Skip a non-source facet
|
|
406
418
|
// (e.g. a keyword facet) in one check instead of probing every bucket.
|
|
407
419
|
for (const facet of response.facet_counts ?? []) {
|
|
408
|
-
const
|
|
409
|
-
if (
|
|
420
|
+
const facetSources = sources.get(facet.field_name);
|
|
421
|
+
if (facetSources === undefined) {
|
|
410
422
|
continue;
|
|
411
423
|
}
|
|
412
424
|
for (const bucket of facet.counts) {
|
|
413
|
-
add(
|
|
425
|
+
add(facetSources, bucket.value);
|
|
414
426
|
}
|
|
415
427
|
}
|
|
416
428
|
}
|
|
417
|
-
return [...irisByCollection.values()]
|
|
418
|
-
|
|
419
|
-
iris: [...iris]
|
|
420
|
-
}));
|
|
429
|
+
return [...irisByCollection.values()]
|
|
430
|
+
.filter(({ iris }) => iris.size > 0)
|
|
431
|
+
.map(({ source, iris }) => ({ source, iris: [...iris] }));
|
|
421
432
|
}
|
|
422
433
|
/**
|
|
423
434
|
* The response with each facet filed under the declared field name its engine
|
|
@@ -504,9 +515,17 @@ export async function fetchLabels(client, groups, onError) {
|
|
|
504
515
|
return;
|
|
505
516
|
}
|
|
506
517
|
for (const hit of result.hits ?? []) {
|
|
518
|
+
const id = String(hit.document.id);
|
|
519
|
+
// First group wins: the groups arrive in the order a field declares its
|
|
520
|
+
// targets, so an IRI two collections hold is labelled by the target
|
|
521
|
+
// declared first – the same precedence the lookup and the projection
|
|
522
|
+
// apply.
|
|
523
|
+
if (labels.has(id)) {
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
507
526
|
const label = localizedValue(hit.document, groupPerSearch[index].source.labelField);
|
|
508
527
|
if (label !== undefined) {
|
|
509
|
-
labels.set(
|
|
528
|
+
labels.set(id, label);
|
|
510
529
|
}
|
|
511
530
|
}
|
|
512
531
|
});
|
|
@@ -523,7 +542,10 @@ function mergeLabels(maps) {
|
|
|
523
542
|
const merged = new Map();
|
|
524
543
|
for (const map of maps) {
|
|
525
544
|
for (const [iri, label] of map) {
|
|
526
|
-
|
|
545
|
+
// First source wins, as it does for a fetched lookup (`fetchLabels`).
|
|
546
|
+
if (!merged.has(iri)) {
|
|
547
|
+
merged.set(iri, label);
|
|
548
|
+
}
|
|
527
549
|
}
|
|
528
550
|
}
|
|
529
551
|
return merged;
|
|
@@ -552,7 +574,7 @@ export function parseSearchResponse(response, searchType, labels, schema, refere
|
|
|
552
574
|
// reference facet stays id-only even when a cached full-collection map holds
|
|
553
575
|
// its IRIs.
|
|
554
576
|
const referenceFacets = new Set(referenceFields(searchType)
|
|
555
|
-
.filter((field) =>
|
|
577
|
+
.filter((field) => labelTargetNamesOf(field, schema).length > 0)
|
|
556
578
|
.map((field) => field.name));
|
|
557
579
|
const facets = {};
|
|
558
580
|
for (const facet of response.facet_counts ?? []) {
|
|
@@ -606,15 +628,15 @@ function logicalValue(flat, field, labels, schema, referents) {
|
|
|
606
628
|
if (nested !== undefined) {
|
|
607
629
|
// A nested level resolves nothing itself; what it carries are the
|
|
608
630
|
// levels below it, which the entries are reconstructed against.
|
|
609
|
-
return nestedValue(flat[field.name], field, nested, labels, schema, resolved?.children);
|
|
631
|
+
return nestedValue(flat[field.name], field, nested, labels, schema, resolved?.via === 'nested' ? resolved.children : undefined);
|
|
610
632
|
}
|
|
611
633
|
// Checked BEFORE the plain lookup below, because a `local` lookup
|
|
612
634
|
// resolves through the same kind of level while storing a different
|
|
613
635
|
// shape: its stored value is the endpoint's own document, not an id, so
|
|
614
636
|
// reading it as one would stringify the object into a bogus `id`.
|
|
615
|
-
const
|
|
616
|
-
if (
|
|
617
|
-
return localLookupValue(flat[field.name], field,
|
|
637
|
+
const localTargets = localLookupTargetsOf(field, schema);
|
|
638
|
+
if (localTargets.length > 0) {
|
|
639
|
+
return localLookupValue(flat[field.name], field, localTargets, resolved, labels, schema);
|
|
618
640
|
}
|
|
619
641
|
// A resolved lookup carries the referent's own fields, fetched from the
|
|
620
642
|
// target's collection. It reconstructs through the SAME path a hit does,
|
|
@@ -687,12 +709,7 @@ function localizedValue(flat, field) {
|
|
|
687
709
|
*/
|
|
688
710
|
function nestedValue(raw, field, referenceType, labels, schema, children) {
|
|
689
711
|
const referents = (Array.isArray(raw) ? raw : [raw]).filter((referent) => typeof referent === 'object' && referent !== null);
|
|
690
|
-
const documents = referents.map((referent) =>
|
|
691
|
-
const document = reconstructDocument(referent, referenceType, labels, schema, children);
|
|
692
|
-
return typeof referent.id === 'string'
|
|
693
|
-
? { id: referent.id, ...document }
|
|
694
|
-
: document;
|
|
695
|
-
});
|
|
712
|
+
const documents = referents.map((referent) => nestedDocument(referent, referenceType, labels, schema, children));
|
|
696
713
|
if (documents.length === 0) {
|
|
697
714
|
return undefined;
|
|
698
715
|
}
|
|
@@ -717,16 +734,51 @@ function nestedValue(raw, field, referenceType, labels, schema, children) {
|
|
|
717
734
|
* than a bare IRI;
|
|
718
735
|
* - not identified → what was stated here, with no `id` at all.
|
|
719
736
|
*/
|
|
720
|
-
function localLookupValue(raw, field,
|
|
721
|
-
const fetched = resolved?.via === 'lookup'
|
|
722
|
-
|
|
737
|
+
function localLookupValue(raw, field, targets, resolved, labels, schema) {
|
|
738
|
+
const fetched = resolved?.via === 'lookup'
|
|
739
|
+
? resolved.documents
|
|
740
|
+
: new Map();
|
|
741
|
+
const documents = (Array.isArray(raw) ? raw : [raw])
|
|
723
742
|
.filter((entry) => typeof entry === 'object' && entry !== null)
|
|
724
743
|
.map((entry) => {
|
|
725
744
|
const id = typeof entry.id === 'string' ? entry.id : undefined;
|
|
726
745
|
const authoritative = id === undefined ? undefined : fetched.get(id);
|
|
727
|
-
|
|
746
|
+
// An entry no collection answered for is read through the declaration
|
|
747
|
+
// it was stored under – which it says itself where there were several
|
|
748
|
+
// to choose from, and the only one otherwise.
|
|
749
|
+
if (authoritative !== undefined) {
|
|
750
|
+
return typedDocument(targets.length > 1, authoritative.target, nestedDocument(authoritative.document, authoritative.target, labels, schema, authoritative.children));
|
|
751
|
+
}
|
|
752
|
+
// Its own lookups resolve against the level its target’s fetch carried
|
|
753
|
+
// down, so a nested IRI it shares with a fetched referent still nests.
|
|
754
|
+
const target = storedTargetOf(entry, targets);
|
|
755
|
+
return typedDocument(targets.length > 1, target, nestedDocument(entry, target, labels, schema, resolved?.via === 'lookup'
|
|
756
|
+
? resolved.childrenByTarget.get(target.name)
|
|
757
|
+
: undefined));
|
|
728
758
|
});
|
|
729
|
-
|
|
759
|
+
if (documents.length === 0) {
|
|
760
|
+
return undefined;
|
|
761
|
+
}
|
|
762
|
+
return field.array === true ? documents : documents[0];
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* A nested document read through one of a lookup’s **several** targets, marked
|
|
766
|
+
* with that type’s name ({@link NESTED_DOCUMENT_TYPE}) so a surface can tell
|
|
767
|
+
* which it is – the marker is a symbol, so it never surfaces as a field. A
|
|
768
|
+
* single-target lookup marks nothing: there is nothing to tell apart.
|
|
769
|
+
*/
|
|
770
|
+
function typedDocument(polymorphic, target, document) {
|
|
771
|
+
return polymorphic
|
|
772
|
+
? { ...document, [NESTED_DOCUMENT_TYPE]: target.name }
|
|
773
|
+
: document;
|
|
774
|
+
}
|
|
775
|
+
/** One nested document rebuilt from a flat referent, carrying its `id` where
|
|
776
|
+
* it has one – the shared body of every nesting that reads a referent. */
|
|
777
|
+
function nestedDocument(referent, searchType, labels, schema, children) {
|
|
778
|
+
const document = reconstructDocument(referent, searchType, labels, schema, children);
|
|
779
|
+
return typeof referent.id === 'string'
|
|
780
|
+
? { id: referent.id, ...document }
|
|
781
|
+
: document;
|
|
730
782
|
}
|
|
731
783
|
/**
|
|
732
784
|
* Rebuild a resolved `lookup` reference: each stored IRI becomes the referent’s
|
|
@@ -741,14 +793,15 @@ function lookupValue(raw, field, resolved, labels, schema) {
|
|
|
741
793
|
return undefined;
|
|
742
794
|
}
|
|
743
795
|
const iris = Array.isArray(raw) ? raw : [String(raw)];
|
|
796
|
+
const polymorphic = referencedTargetsOf(field, schema).length > 1;
|
|
744
797
|
const documents = iris.map((iri) => {
|
|
745
798
|
const referent = resolved.documents.get(iri);
|
|
746
799
|
return referent === undefined
|
|
747
800
|
? { id: iri }
|
|
748
|
-
: {
|
|
801
|
+
: typedDocument(polymorphic, referent.target, {
|
|
749
802
|
id: iri,
|
|
750
|
-
...reconstructDocument(referent,
|
|
751
|
-
};
|
|
803
|
+
...reconstructDocument(referent.document, referent.target, labels, schema, referent.children),
|
|
804
|
+
});
|
|
752
805
|
});
|
|
753
806
|
return field.array === true ? documents : documents[0];
|
|
754
807
|
}
|
|
@@ -763,7 +816,7 @@ function referenceValue(flat, field, labels) {
|
|
|
763
816
|
// A reference that resolves nothing is id-only by declaration; never attach
|
|
764
817
|
// a label, even if the (cached, full-collection) map happens to hold this
|
|
765
818
|
// IRI from another source.
|
|
766
|
-
const label =
|
|
819
|
+
const label = labelSourceNamesOf(field).length === 0 ? undefined : labels.get(iri);
|
|
767
820
|
return label === undefined ? { id: iri } : { id: iri, label };
|
|
768
821
|
});
|
|
769
822
|
return field.array === true ? references : references[0];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lde/search-typesense",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
4
4
|
"description": "Typesense implementation of the @lde/search SearchEngine port: collection-schema builder, query compiler, label-resolving result reconstruction, and blue/green index lifecycle. Engine-specific (Typesense) but domain-agnostic.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "git+https://github.com/ldelements/lde.git",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"!**/*.tsbuildinfo"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@lde/search": "^0.
|
|
28
|
+
"@lde/search": "^0.29.0",
|
|
29
29
|
"@lde/text-normalization": "^0.2.0",
|
|
30
30
|
"tslib": "^2.3.0",
|
|
31
31
|
"typesense": "^3.1.1"
|
|
@@ -36,6 +36,6 @@
|
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@lde/dataset": "^0.8.0",
|
|
39
|
-
"@lde/pipeline": "^0.
|
|
39
|
+
"@lde/pipeline": "^0.38.0"
|
|
40
40
|
}
|
|
41
41
|
}
|