@lde/search-typesense 0.17.0 → 0.18.1
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 +18 -1
- package/dist/collection-definition.d.ts.map +1 -1
- package/dist/collection-definition.js +89 -3
- package/dist/rebuild-support.d.ts +7 -2
- package/dist/rebuild-support.d.ts.map +1 -1
- package/dist/rebuild-support.js +10 -2
- package/dist/search.d.ts +9 -3
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +59 -12
- package/package.json +3 -3
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import type { CollectionCreateSchema } from 'typesense';
|
|
2
|
-
import { type SearchType } from '@lde/search';
|
|
2
|
+
import { type SearchSchema, type SearchType } from '@lde/search';
|
|
3
3
|
/** Deployment-specific options the generic field model does not carry. */
|
|
4
4
|
export interface CollectionDefinitionOptions {
|
|
5
5
|
/** The Typesense collection (or alias) name. Omit to derive it from the
|
|
6
6
|
* type’s `name` ({@link deriveCollectionName}); supply one to override that
|
|
7
7
|
* (an env prefix, a multi-tenant name, an existing collection). */
|
|
8
8
|
readonly name?: string;
|
|
9
|
+
/**
|
|
10
|
+
* The Search Schema the type belongs to – required only when the type
|
|
11
|
+
* surfaces an inline reference, whose nested fields are declared from the
|
|
12
|
+
* {@link ReferenceType} the schema resolves. A type without one needs no
|
|
13
|
+
* schema, so a caller that declares no nesting passes none; a type that does
|
|
14
|
+
* fails here rather than building a collection that silently omits the
|
|
15
|
+
* nesting.
|
|
16
|
+
*/
|
|
17
|
+
readonly schema?: SearchSchema;
|
|
9
18
|
/** Snowball stemming locale for non-localized searchable fields (e.g. `en`).
|
|
10
19
|
* Unset, those fields are not stemmed – folding still applies – so no
|
|
11
20
|
* language is ever assumed. Localized text search fields always stem in
|
|
@@ -45,6 +54,14 @@ export interface CollectionDefinitionOptions {
|
|
|
45
54
|
* {@link isInternalField Internal fields} (those declaring no role) are omitted
|
|
46
55
|
* entirely: they exist only as a projection-time reading device for the
|
|
47
56
|
* derives, so the collection stores nothing for them.
|
|
57
|
+
*
|
|
58
|
+
* A surfaced (`output`) inline reference is stored as a **nested object** –
|
|
59
|
+
* `object` or `object[]` – with one nested Physical Field per output field of
|
|
60
|
+
* its {@link ReferenceType} ({@link nestedFieldName}), which turns on
|
|
61
|
+
* `enable_nested_fields` for the collection. Nested fields carry `output` only
|
|
62
|
+
* (enforced by `searchSchema`), so they are declared `index: false`: each
|
|
63
|
+
* referent’s values stay grouped on disk and cost no RAM, exactly like the
|
|
64
|
+
* display labels.
|
|
48
65
|
*/
|
|
49
66
|
export declare function buildCollectionDefinition(searchType: SearchType, options?: CollectionDefinitionOptions): CollectionCreateSchema;
|
|
50
67
|
//# sourceMappingURL=collection-definition.d.ts.map
|
|
@@ -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,
|
|
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,EAGL,KAAK,YAAY,EACjB,KAAK,UAAU,EAChB,MAAM,aAAa,CAAC;AAWrB,0EAA0E;AAC1E,MAAM,WAAW,2BAA2B;IAC1C;;wEAEoE;IACpE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;OAOG;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,UAAU,EACtB,OAAO,GAAE,2BAAgC,GACxC,sBAAsB,CA0BxB"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { displayFieldPattern, isInternalField, physicalFields, } from '@lde/search/adapter';
|
|
1
|
+
import { displayFieldPattern, isInlineReference, isInternalField, 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
|
|
@@ -29,13 +29,26 @@ import { deriveCollectionName } from './collection-name.js';
|
|
|
29
29
|
* {@link isInternalField Internal fields} (those declaring no role) are omitted
|
|
30
30
|
* entirely: they exist only as a projection-time reading device for the
|
|
31
31
|
* derives, so the collection stores nothing for them.
|
|
32
|
+
*
|
|
33
|
+
* A surfaced (`output`) inline reference is stored as a **nested object** –
|
|
34
|
+
* `object` or `object[]` – with one nested Physical Field per output field of
|
|
35
|
+
* its {@link ReferenceType} ({@link nestedFieldName}), which turns on
|
|
36
|
+
* `enable_nested_fields` for the collection. Nested fields carry `output` only
|
|
37
|
+
* (enforced by `searchSchema`), so they are declared `index: false`: each
|
|
38
|
+
* referent’s values stay grouped on disk and cost no RAM, exactly like the
|
|
39
|
+
* display labels.
|
|
32
40
|
*/
|
|
33
41
|
export function buildCollectionDefinition(searchType, options = {}) {
|
|
34
42
|
const { defaultLocale } = options;
|
|
35
43
|
const collection = {
|
|
36
44
|
name: options.name ?? deriveCollectionName(searchType),
|
|
37
|
-
fields: searchType.fields.flatMap((field) => typesenseFields(field, defaultLocale, options.defaultSortingField)),
|
|
45
|
+
fields: searchType.fields.flatMap((field) => typesenseFields(searchType, field, defaultLocale, options.defaultSortingField, options.schema)),
|
|
38
46
|
};
|
|
47
|
+
// Typesense rejects an `object`/`object[]` field unless nesting is enabled,
|
|
48
|
+
// so the flag is a consequence of the declaration – never a knob.
|
|
49
|
+
if (collection.fields?.some((field) => field.type.startsWith('object'))) {
|
|
50
|
+
collection.enable_nested_fields = true;
|
|
51
|
+
}
|
|
39
52
|
if (options.defaultSortingField !== undefined) {
|
|
40
53
|
collection.default_sorting_field = options.defaultSortingField;
|
|
41
54
|
}
|
|
@@ -44,8 +57,24 @@ export function buildCollectionDefinition(searchType, options = {}) {
|
|
|
44
57
|
}
|
|
45
58
|
return collection;
|
|
46
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* The {@link ReferenceType} a field nests, or `undefined` when it nests
|
|
62
|
+
* nothing. Throws when the type surfaces an inline reference the caller gave no
|
|
63
|
+
* schema to resolve: the collection would silently store the reference as a
|
|
64
|
+
* string the projection never writes, so every document would fail to import.
|
|
65
|
+
*/
|
|
66
|
+
function nestedTypeOf(searchType, field, schema) {
|
|
67
|
+
const nested = schema === undefined ? undefined : nestedReferenceType(schema, field);
|
|
68
|
+
// Reached only for a field carrying a Role, so an inline reference here is a
|
|
69
|
+
// surfaced one: `searchSchema` allows an inline reference no Role but
|
|
70
|
+
// `output`.
|
|
71
|
+
if (nested === undefined && isInlineReference(field)) {
|
|
72
|
+
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”.`);
|
|
73
|
+
}
|
|
74
|
+
return nested;
|
|
75
|
+
}
|
|
47
76
|
/** The physical Typesense fields one declaration produces. */
|
|
48
|
-
function typesenseFields(field, defaultLocale, defaultSortingField) {
|
|
77
|
+
function typesenseFields(searchType, field, defaultLocale, defaultSortingField, schema) {
|
|
49
78
|
// An internal field (no role) is projected as a reading device for the
|
|
50
79
|
// derives and pruned before the writer, so the collection stores nothing for
|
|
51
80
|
// it: not stored, not indexed, no RAM. Same predicate the projection prunes
|
|
@@ -53,6 +82,10 @@ function typesenseFields(field, defaultLocale, defaultSortingField) {
|
|
|
53
82
|
if (isInternalField(field)) {
|
|
54
83
|
return [];
|
|
55
84
|
}
|
|
85
|
+
const nested = nestedTypeOf(searchType, field, schema);
|
|
86
|
+
if (nested !== undefined) {
|
|
87
|
+
return nestedFields(field.name, field, nested, schema);
|
|
88
|
+
}
|
|
56
89
|
const names = physicalFields(field);
|
|
57
90
|
if (field.kind === 'text') {
|
|
58
91
|
const locales = field.locales;
|
|
@@ -121,6 +154,59 @@ function typesenseFields(field, defaultLocale, defaultSortingField) {
|
|
|
121
154
|
}
|
|
122
155
|
return fields;
|
|
123
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* The nested object one surfaced inline reference stores, plus one nested
|
|
159
|
+
* Physical Field per output field of its {@link ReferenceType} – recursing for
|
|
160
|
+
* a reference type that itself surfaces an inline reference, to the depth the
|
|
161
|
+
* schema declares (`searchSchema` rejects inline cycles, so the recursion
|
|
162
|
+
* terminates).
|
|
163
|
+
*
|
|
164
|
+
* Everything nested is `index: false`: a nested field carries `output` only, so
|
|
165
|
+
* it is stored with its referent and read back with it, never searched, faceted
|
|
166
|
+
* or sorted on. That keeps the RAM lever intact – a nested document is display
|
|
167
|
+
* weight, like the per-language labels – and lets the declared child types be
|
|
168
|
+
* exactly what the projection writes without an indexing contract to satisfy.
|
|
169
|
+
* Declaring the children rather than only the object is what makes the nesting
|
|
170
|
+
* legible in the collection, and what keeps an {@link isInternalField Internal
|
|
171
|
+
* Field} inside a Reference Type visibly contributing nothing.
|
|
172
|
+
*/
|
|
173
|
+
function nestedFields(prefix, reference, referenceType, schema) {
|
|
174
|
+
const array = reference.array === true;
|
|
175
|
+
const fields = [
|
|
176
|
+
{
|
|
177
|
+
name: prefix,
|
|
178
|
+
type: array ? 'object[]' : 'object',
|
|
179
|
+
index: false,
|
|
180
|
+
optional: reference.required !== true,
|
|
181
|
+
},
|
|
182
|
+
];
|
|
183
|
+
for (const field of referenceType.fields) {
|
|
184
|
+
if (isInternalField(field)) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const nested = nestedReferenceType(schema, field);
|
|
188
|
+
if (nested !== undefined) {
|
|
189
|
+
fields.push(...nestedFields(nestedFieldName(prefix, field.name), field, nested, schema));
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
// A nested text field stores its display values exactly as a root one does
|
|
193
|
+
// – one `${name}_<lang>` per present language, matched by one pattern
|
|
194
|
+
// field – only under the referent’s prefix. It has no search or sort
|
|
195
|
+
// companions: those are Roles a nested field cannot declare.
|
|
196
|
+
const pattern = field.kind === 'text' ? displayFieldPattern(field) : undefined;
|
|
197
|
+
fields.push({
|
|
198
|
+
name: nestedFieldName(prefix, pattern ?? field.name),
|
|
199
|
+
type: field.kind === 'text' ? 'string' : typesenseValueType(field),
|
|
200
|
+
index: false,
|
|
201
|
+
// Always optional: `required` is a promise about the *referent* (this
|
|
202
|
+
// value is on every referent), while Typesense’s flag is about the
|
|
203
|
+
// document. Under a multi-valued reference those are different claims,
|
|
204
|
+
// and only the referent-level one is the declaration’s.
|
|
205
|
+
optional: true,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return fields;
|
|
209
|
+
}
|
|
124
210
|
/** The Typesense field type for a non-localized field, from its `kind`. 64-bit
|
|
125
211
|
* integers (and dates, stored as Unix seconds) so large counts never overflow. */
|
|
126
212
|
function typesenseValueType(field) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Client } from 'typesense';
|
|
2
2
|
import type { SearchType } from '@lde/search';
|
|
3
|
-
import type
|
|
3
|
+
import { type CollectionDefinitionOptions } from './collection-definition.js';
|
|
4
4
|
/**
|
|
5
5
|
* The tuning knobs both rebuild writers share, on top of the collection-definition
|
|
6
6
|
* options. Each writer may add its own (In-place adds `maxSweepableSources`).
|
|
@@ -29,7 +29,12 @@ export interface ResolvedRebuildOptions {
|
|
|
29
29
|
* when the deployment supplies none.
|
|
30
30
|
*
|
|
31
31
|
* Writers resolve at construction rather than per run, so an underivable name
|
|
32
|
-
* throws when the writer is built, not on the first rebuild.
|
|
32
|
+
* throws when the writer is built, not on the first rebuild. The collection
|
|
33
|
+
* definition is built here and discarded for the same reason: every way a
|
|
34
|
+
* declaration can fail to describe a collection – an unresolvable surfaced
|
|
35
|
+
* inline reference, whose nesting needs the schema – then fails at construction
|
|
36
|
+
* rather than inside the first run, after a lock is held and a whole extraction
|
|
37
|
+
* has been paid for.
|
|
33
38
|
*/
|
|
34
39
|
export declare function resolveRebuildOptions(searchType: SearchType, options: RebuildOptions): ResolvedRebuildOptions;
|
|
35
40
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rebuild-support.d.ts","sourceRoot":"","sources":["../src/rebuild-support.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"rebuild-support.d.ts","sourceRoot":"","sources":["../src/rebuild-support.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAEL,KAAK,2BAA2B,EACjC,MAAM,4BAA4B,CAAC;AAKpC;;;GAGG;AACH,MAAM,WAAW,cAAe,SAAQ,2BAA2B;IACjE,+DAA+D;IAC/D,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,0EAA0E;IAC1E,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;0FAC0F;AAC1F,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;6DAEyD;IACzD,QAAQ,CAAC,iBAAiB,EAAE,2BAA2B,GAAG;QACxD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;KACvB,CAAC;CACH;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE,cAAc,GACtB,sBAAsB,CAgBxB;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,SAAS,MAAM,EAAE,GAC1B,IAAI,CAWN;AAED,4EAA4E;AAC5E,wBAAuB,cAAc,CAAC,SAAS,EAC7C,SAAS,EAAE,aAAa,CAAC,SAAS,CAAC,EACnC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GACtC,aAAa,CAAC,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAInD;AAED,mEAAmE;AACnE,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAKf"}
|
package/dist/rebuild-support.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildCollectionDefinition, } from './collection-definition.js';
|
|
1
2
|
import { deriveCollectionName } from './collection-name.js';
|
|
2
3
|
import { DEFAULT_LOCK_TTL_MS } from './lock.js';
|
|
3
4
|
import { DEFAULT_BATCH_SIZE } from './import.js';
|
|
@@ -7,11 +8,16 @@ import { DEFAULT_BATCH_SIZE } from './import.js';
|
|
|
7
8
|
* when the deployment supplies none.
|
|
8
9
|
*
|
|
9
10
|
* Writers resolve at construction rather than per run, so an underivable name
|
|
10
|
-
* throws when the writer is built, not on the first rebuild.
|
|
11
|
+
* throws when the writer is built, not on the first rebuild. The collection
|
|
12
|
+
* definition is built here and discarded for the same reason: every way a
|
|
13
|
+
* declaration can fail to describe a collection – an unresolvable surfaced
|
|
14
|
+
* inline reference, whose nesting needs the schema – then fails at construction
|
|
15
|
+
* rather than inside the first run, after a lock is held and a whole extraction
|
|
16
|
+
* has been paid for.
|
|
11
17
|
*/
|
|
12
18
|
export function resolveRebuildOptions(searchType, options) {
|
|
13
19
|
const { batchSize = DEFAULT_BATCH_SIZE, lockTtlMs = DEFAULT_LOCK_TTL_MS, ...definitionOptions } = options;
|
|
14
|
-
|
|
20
|
+
const resolved = {
|
|
15
21
|
batchSize,
|
|
16
22
|
lockTtlMs,
|
|
17
23
|
definitionOptions: {
|
|
@@ -19,6 +25,8 @@ export function resolveRebuildOptions(searchType, options) {
|
|
|
19
25
|
name: definitionOptions.name ?? deriveCollectionName(searchType),
|
|
20
26
|
},
|
|
21
27
|
};
|
|
28
|
+
buildCollectionDefinition(searchType, resolved.definitionOptions);
|
|
29
|
+
return resolved;
|
|
22
30
|
}
|
|
23
31
|
/**
|
|
24
32
|
* Reject a {@link SearchType} that declares any of the bookkeeping field names
|
package/dist/search.d.ts
CHANGED
|
@@ -118,8 +118,14 @@ export interface TypesenseSearchResponse {
|
|
|
118
118
|
* Reconstruct a Typesense response into the engine-neutral {@link SearchResult}:
|
|
119
119
|
* the flat, fanned-out document is turned back into a logical one (per-locale
|
|
120
120
|
* display fields → a language map, reference IRIs → labelled references via the
|
|
121
|
-
* label-source lookup,
|
|
122
|
-
* to its resolved label; an IRI absent
|
|
121
|
+
* label-source lookup, nested objects → nested Search Documents, scalars passed
|
|
122
|
+
* through). `labels` maps a reference IRI to its resolved label; an IRI absent
|
|
123
|
+
* from it yields an id-only reference. `schema` resolves the Reference Type a
|
|
124
|
+
* surfaced inline reference nests – required, not optional: reconstruction is a
|
|
125
|
+
* function of the whole schema exactly as the engine is, and a caller that
|
|
126
|
+
* omitted it would silently serve nothing for every nested field. A schema that
|
|
127
|
+
* does not declare a field’s reference type reconstructs nothing for it rather
|
|
128
|
+
* than the wrong shape, the same call the projection makes.
|
|
123
129
|
*/
|
|
124
|
-
export declare function parseSearchResponse(response: TypesenseSearchResponse, searchType: SearchType, labels: ReadonlyMap<string, LocalizedValue
|
|
130
|
+
export declare function parseSearchResponse(response: TypesenseSearchResponse, searchType: SearchType, labels: ReadonlyMap<string, LocalizedValue>, schema: SearchSchema): SearchResult;
|
|
125
131
|
//# sourceMappingURL=search.d.ts.map
|
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,EAGL,KAAK,cAAc,
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../src/search.ts"],"names":[],"mappings":"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"}
|
package/dist/search.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertTypeInSchema, assertValidQuery, displayLangOf, fieldNamed, isRangeFacet, isUnsatisfiable, labelFieldOf, outputFields, physicalFields, referenceFields, } from '@lde/search/adapter';
|
|
1
|
+
import { assertTypeInSchema, assertValidQuery, displayLangOf, fieldNamed, isInlineReference, isRangeFacet, isUnsatisfiable, labelFieldOf, nestedReferenceType, outputFields, physicalFields, referenceFields, } from '@lde/search/adapter';
|
|
2
2
|
import { buildSearchParams, escapeFilterValue, } from './query-compiler.js';
|
|
3
3
|
import { deriveCollectionName } from './collection-name.js';
|
|
4
4
|
/**
|
|
@@ -192,7 +192,7 @@ export function createTypesenseSearchEngine(client, schema, options = {}) {
|
|
|
192
192
|
// the URL.
|
|
193
193
|
const response = await performSingleSearch(client, collections.get(searchType.class), params);
|
|
194
194
|
const labels = await resolveLabels(cachedLabelsPromise, () => labelLookupGroups([response], labelSources.get(searchType.class), outputReferenceSources.get(searchType.class) ?? []));
|
|
195
|
-
return parseSearchResponse(response, searchType, labels);
|
|
195
|
+
return parseSearchResponse(response, searchType, labels, schema);
|
|
196
196
|
},
|
|
197
197
|
async searchFacets(searchType, queries) {
|
|
198
198
|
assertTypeInSchema(schema, searchType);
|
|
@@ -243,7 +243,8 @@ export function createTypesenseSearchEngine(client, schema, options = {}) {
|
|
|
243
243
|
error: new Error(`Typesense facet search for “${queries[index].facets.join('”, “')}” failed${result?.code !== undefined ? ` (${result.code})` : ''}: ${result?.error ?? 'no result entry'}`),
|
|
244
244
|
}
|
|
245
245
|
: {
|
|
246
|
-
facets: parseSearchResponse(result, searchType, labels)
|
|
246
|
+
facets: parseSearchResponse(result, searchType, labels, schema)
|
|
247
|
+
.facets,
|
|
247
248
|
};
|
|
248
249
|
});
|
|
249
250
|
return outcomes;
|
|
@@ -453,13 +454,19 @@ function mergeLabels(maps) {
|
|
|
453
454
|
* Reconstruct a Typesense response into the engine-neutral {@link SearchResult}:
|
|
454
455
|
* the flat, fanned-out document is turned back into a logical one (per-locale
|
|
455
456
|
* display fields → a language map, reference IRIs → labelled references via the
|
|
456
|
-
* label-source lookup,
|
|
457
|
-
* to its resolved label; an IRI absent
|
|
457
|
+
* label-source lookup, nested objects → nested Search Documents, scalars passed
|
|
458
|
+
* through). `labels` maps a reference IRI to its resolved label; an IRI absent
|
|
459
|
+
* from it yields an id-only reference. `schema` resolves the Reference Type a
|
|
460
|
+
* surfaced inline reference nests – required, not optional: reconstruction is a
|
|
461
|
+
* function of the whole schema exactly as the engine is, and a caller that
|
|
462
|
+
* omitted it would silently serve nothing for every nested field. A schema that
|
|
463
|
+
* does not declare a field’s reference type reconstructs nothing for it rather
|
|
464
|
+
* than the wrong shape, the same call the projection makes.
|
|
458
465
|
*/
|
|
459
|
-
export function parseSearchResponse(response, searchType, labels) {
|
|
466
|
+
export function parseSearchResponse(response, searchType, labels, schema) {
|
|
460
467
|
const hits = (response.hits ?? []).map((hit) => ({
|
|
461
468
|
id: String(hit.document.id),
|
|
462
|
-
document: reconstructDocument(hit.document, searchType, labels),
|
|
469
|
+
document: reconstructDocument(hit.document, searchType, labels, schema),
|
|
463
470
|
}));
|
|
464
471
|
// Reference facets are IRI-keyed; their buckets carry a resolved data label.
|
|
465
472
|
// Plain facets (tokens, free strings) carry no label – the consumer owns display.
|
|
@@ -493,22 +500,40 @@ export function parseSearchResponse(response, searchType, labels) {
|
|
|
493
500
|
return { hits, total: response.found, facets };
|
|
494
501
|
}
|
|
495
502
|
/** Rebuild one logical document from a flat Typesense document. */
|
|
496
|
-
function reconstructDocument(flat, searchType, labels) {
|
|
503
|
+
function reconstructDocument(flat, searchType, labels, schema) {
|
|
497
504
|
const document = {};
|
|
498
505
|
for (const field of outputFields(searchType)) {
|
|
499
|
-
const value = logicalValue(flat, field, labels);
|
|
506
|
+
const value = logicalValue(flat, field, labels, schema);
|
|
500
507
|
if (value !== undefined) {
|
|
501
508
|
document[field.name] = value;
|
|
502
509
|
}
|
|
503
510
|
}
|
|
504
511
|
return document;
|
|
505
512
|
}
|
|
506
|
-
function logicalValue(flat, field, labels) {
|
|
513
|
+
function logicalValue(flat, field, labels, schema) {
|
|
507
514
|
switch (field.kind) {
|
|
508
515
|
case 'text':
|
|
509
516
|
return localizedValue(flat, field);
|
|
510
|
-
case 'reference':
|
|
511
|
-
|
|
517
|
+
case 'reference': {
|
|
518
|
+
// A surfaced inline reference carries its referent’s own fields, so it
|
|
519
|
+
// reconstructs as a nested Search Document rather than as an IRI plus a
|
|
520
|
+
// label. Nesting is rebuilt HERE, below every surface, so a second
|
|
521
|
+
// surface inherits it instead of reimplementing it (ADR 11).
|
|
522
|
+
const nested = nestedReferenceType(schema, field);
|
|
523
|
+
if (nested !== undefined) {
|
|
524
|
+
return nestedValue(flat[field.name], field, nested, labels, schema);
|
|
525
|
+
}
|
|
526
|
+
// A surfaced inline reference stores nested documents, not IRIs, so a
|
|
527
|
+
// schema that does not declare its reference type (a type parsed against
|
|
528
|
+
// a foreign schema) reconstructs nothing rather than the wrong shape –
|
|
529
|
+
// the same call the projection makes. Only output fields are
|
|
530
|
+
// reconstructed, and `searchSchema` allows an inline reference no Role
|
|
531
|
+
// but `output`, so every inline reference reaching this point is a
|
|
532
|
+
// surfaced one.
|
|
533
|
+
return isInlineReference(field)
|
|
534
|
+
? undefined
|
|
535
|
+
: referenceValue(flat, field, labels);
|
|
536
|
+
}
|
|
512
537
|
case 'keyword': {
|
|
513
538
|
const value = flat[field.name];
|
|
514
539
|
return Array.isArray(value) || typeof value === 'string'
|
|
@@ -546,6 +571,28 @@ function localizedValue(flat, field) {
|
|
|
546
571
|
}
|
|
547
572
|
return Object.keys(map).length > 0 ? map : undefined;
|
|
548
573
|
}
|
|
574
|
+
/**
|
|
575
|
+
* Rebuild a surfaced inline reference from the nested object(s) the engine
|
|
576
|
+
* stored: one {@link NestedDocument} per referent, each carrying its Reference
|
|
577
|
+
* Type’s own output fields – so a multi-valued reference keeps every referent’s
|
|
578
|
+
* values grouped rather than smeared across parallel arrays. `id` is carried
|
|
579
|
+
* only when the referent had one; a blank-node referent nests without it, since
|
|
580
|
+
* a nested document is read, not addressed. A nested reference type may itself
|
|
581
|
+
* surface an inline reference, which recurses through {@link reconstructDocument}.
|
|
582
|
+
*/
|
|
583
|
+
function nestedValue(raw, field, referenceType, labels, schema) {
|
|
584
|
+
const referents = (Array.isArray(raw) ? raw : [raw]).filter((referent) => typeof referent === 'object' && referent !== null);
|
|
585
|
+
const documents = referents.map((referent) => {
|
|
586
|
+
const document = reconstructDocument(referent, referenceType, labels, schema);
|
|
587
|
+
return typeof referent.id === 'string'
|
|
588
|
+
? { id: referent.id, ...document }
|
|
589
|
+
: document;
|
|
590
|
+
});
|
|
591
|
+
if (documents.length === 0) {
|
|
592
|
+
return undefined;
|
|
593
|
+
}
|
|
594
|
+
return field.array === true ? documents : documents[0];
|
|
595
|
+
}
|
|
549
596
|
/** Map stored reference IRIs to labelled references; id-only when no label. */
|
|
550
597
|
function referenceValue(flat, field, labels) {
|
|
551
598
|
const raw = flat[field.name];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lde/search-typesense",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.1",
|
|
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.15.0",
|
|
29
29
|
"@lde/text-normalization": "^0.1.2",
|
|
30
30
|
"tslib": "^2.3.0",
|
|
31
31
|
"typesense": "^3.0.6"
|
|
@@ -35,6 +35,6 @@
|
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"@lde/dataset": "^0.7.9",
|
|
38
|
-
"@lde/pipeline": "^0.35.
|
|
38
|
+
"@lde/pipeline": "^0.35.5"
|
|
39
39
|
}
|
|
40
40
|
}
|