@lde/search-typesense 0.27.1 → 0.27.3
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 +9 -1
- package/dist/collection-definition.d.ts.map +1 -1
- package/dist/collection-definition.js +17 -2
- package/dist/in-place-rebuild.d.ts +4 -3
- package/dist/in-place-rebuild.d.ts.map +1 -1
- package/dist/in-place-rebuild.js +46 -15
- package/dist/lookup.d.ts.map +1 -1
- package/dist/lookup.js +2 -3
- package/dist/query-compiler.d.ts +11 -1
- package/dist/query-compiler.d.ts.map +1 -1
- package/dist/query-compiler.js +25 -14
- package/dist/rebuild-support.d.ts +8 -2
- package/dist/rebuild-support.d.ts.map +1 -1
- package/dist/rebuild-support.js +11 -2
- package/dist/search.d.ts +3 -0
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +43 -3
- package/package.json +4 -3
|
@@ -21,11 +21,19 @@ export interface CollectionDefinitionOptions {
|
|
|
21
21
|
/**
|
|
22
22
|
* The Search Schema the type belongs to – required when the type surfaces an
|
|
23
23
|
* inline reference (whose nested fields are declared from the
|
|
24
|
-
* {@link ReferenceType} the schema resolves)
|
|
24
|
+
* {@link ReferenceType} the schema resolves), declares a joinable
|
|
25
25
|
* reference (whose target collection the schema’s {@link joinGraph}
|
|
26
26
|
* resolves). A type doing neither needs no schema, so a caller passes none;
|
|
27
27
|
* a type that does fails here rather than building a collection that
|
|
28
28
|
* silently omits the nesting or the reference.
|
|
29
|
+
*
|
|
30
|
+
* Also what resolves the facet policy a facetable reference inherits from
|
|
31
|
+
* the type it names ({@link physicalFields}): without the schema the field
|
|
32
|
+
* itself is declared the facet and no `${name}_facet` companion is, the same
|
|
33
|
+
* reading the projection makes without one. Whether a policy applies cannot
|
|
34
|
+
* be told without the schema, so this is not guarded: a deployment that
|
|
35
|
+
* declares one and builds its collections schema-less meets the engine’s own
|
|
36
|
+
* error on the first facet query, not a silently narrowed-to-nothing facet.
|
|
29
37
|
*/
|
|
30
38
|
readonly schema?: SearchSchema;
|
|
31
39
|
/** Snowball stemming locale for non-localized searchable fields (e.g. `en`).
|
|
@@ -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,EAGL,KAAK,YAAY,EACjB,KAAK,UAAU,EAChB,MAAM,aAAa,CAAC;AAarB,0EAA0E;AAC1E,MAAM,WAAW,2BAA2B;IAC1C;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,MAAM,CAAC;IAChE
|
|
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;AAarB,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,UAAU,EACtB,OAAO,GAAE,2BAAgC,GACxC,sBAAsB,CA2BxB"}
|
|
@@ -138,7 +138,7 @@ function typesenseFields(searchType, field, defaultLocale, defaultSortingField,
|
|
|
138
138
|
if (nested !== undefined) {
|
|
139
139
|
return nestedFields(field.name, field, nested, schema);
|
|
140
140
|
}
|
|
141
|
-
const names = physicalFields(field);
|
|
141
|
+
const names = physicalFields(field, schema);
|
|
142
142
|
if (field.kind === 'text') {
|
|
143
143
|
const locales = field.locales;
|
|
144
144
|
const displayPattern = displayFieldPattern(field);
|
|
@@ -184,7 +184,10 @@ function typesenseFields(searchType, field, defaultLocale, defaultSortingField,
|
|
|
184
184
|
{
|
|
185
185
|
name: field.name,
|
|
186
186
|
type: valueType,
|
|
187
|
-
facet
|
|
187
|
+
// A reference inheriting a facet policy facets its companion below, and
|
|
188
|
+
// the field itself stays a plain stored value – which is also what keeps
|
|
189
|
+
// a membership filter on it exact (`compileMembership`).
|
|
190
|
+
facet: names.facet === field.name,
|
|
188
191
|
sort: field.sortable ?? false,
|
|
189
192
|
// A `required` field is non-optional; so is the `default_sorting_field`,
|
|
190
193
|
// which Typesense requires to be present. Everything else may be absent.
|
|
@@ -205,6 +208,18 @@ function typesenseFields(searchType, field, defaultLocale, defaultSortingField,
|
|
|
205
208
|
}),
|
|
206
209
|
});
|
|
207
210
|
}
|
|
211
|
+
// The `${name}_facet` companion of a reference inheriting a facet policy:
|
|
212
|
+
// the admitted subset of the field’s values, and the only one of the two
|
|
213
|
+
// the engine facets. Optional, because a document none of whose values the
|
|
214
|
+
// policy admits carries no companion at all.
|
|
215
|
+
if (names.facet !== undefined && names.facet !== field.name) {
|
|
216
|
+
fields.push({
|
|
217
|
+
name: names.facet,
|
|
218
|
+
type: valueType,
|
|
219
|
+
facet: true,
|
|
220
|
+
optional: true,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
208
223
|
return fields;
|
|
209
224
|
}
|
|
210
225
|
/**
|
|
@@ -35,9 +35,10 @@ export interface InPlaceRebuildOptions extends RebuildOptions {
|
|
|
35
35
|
* `RebuildAlreadyRunning` when another rebuild holds it) and creates
|
|
36
36
|
* the collection on demand from the {@link SearchType} plus the two
|
|
37
37
|
* bookkeeping fields. A collection that already exists is used as it is, with
|
|
38
|
-
* one exception: it must carry every reference field the declaration asks for
|
|
39
|
-
* or the run fails with the
|
|
40
|
-
*
|
|
38
|
+
* one exception: it must carry every reference field the declaration asks for
|
|
39
|
+
* and every facet companion its facets read, or the run fails with the
|
|
40
|
+
* drop-and-rebuild that fixes it
|
|
41
|
+
* ({@link assertReferencesAndFacetCompanionsPresent}).
|
|
41
42
|
*
|
|
42
43
|
* Document ids must be unique per (source, entity) – the caller keys them –
|
|
43
44
|
* or documents from different sources overwrite each other.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"in-place-rebuild.d.ts","sourceRoot":"","sources":["../src/in-place-rebuild.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAExC,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"in-place-rebuild.d.ts","sourceRoot":"","sources":["../src/in-place-rebuild.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAExC,OAAO,KAAK,EAAgB,UAAU,EAAE,MAAM,aAAa,CAAC;AAG5D,OAAO,KAAK,EAEV,UAAU,EACV,SAAS,EACT,MAAM,EACP,MAAM,eAAe,CAAC;AAavB,OAAO,EAML,KAAK,cAAc,EAEpB,MAAM,sBAAsB,CAAC;AAa9B;gCACgC;AAChC,MAAM,WAAW,qBAAsB,SAAQ,cAAc;IAC3D;sEACkE;IAClE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CACvC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,qBAAa,cAAc,CACzB,SAAS,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAChC,YAAW,MAAM,CAAC,SAAS,CAAC;IAiB1B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAjB7B;;;;;OAKG;IACH,SAAgB,cAAc,EAAE,MAAM,CAAC;IACvC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAS;IAC7C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyB;IAClD;;;6CAGyC;IACzC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;gBAGlB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,UAAU,EACvC,OAAO,GAAE,qBAA0B;IAkB/B,OAAO,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IA6HjE;;;;;;OAMG;YACW,cAAc;CAsB7B"}
|
package/dist/in-place-rebuild.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { facetableFields, physicalFields } from '@lde/search/adapter';
|
|
1
2
|
import { buildCollectionDefinition } from './collection-definition.js';
|
|
2
3
|
import { BatchImporter } from './import.js';
|
|
3
4
|
import { ensureCollectionExists, openLockedRun, releaseLock } from './lock.js';
|
|
@@ -39,9 +40,10 @@ const DEFAULT_MAX_SWEEPABLE_SOURCES = 10_000;
|
|
|
39
40
|
* `RebuildAlreadyRunning` when another rebuild holds it) and creates
|
|
40
41
|
* the collection on demand from the {@link SearchType} plus the two
|
|
41
42
|
* bookkeeping fields. A collection that already exists is used as it is, with
|
|
42
|
-
* one exception: it must carry every reference field the declaration asks for
|
|
43
|
-
* or the run fails with the
|
|
44
|
-
*
|
|
43
|
+
* one exception: it must carry every reference field the declaration asks for
|
|
44
|
+
* and every facet companion its facets read, or the run fails with the
|
|
45
|
+
* drop-and-rebuild that fixes it
|
|
46
|
+
* ({@link assertReferencesAndFacetCompanionsPresent}).
|
|
45
47
|
*
|
|
46
48
|
* Document ids must be unique per (source, entity) – the caller keys them –
|
|
47
49
|
* or documents from different sources overwrite each other.
|
|
@@ -70,7 +72,10 @@ export class InPlaceRebuild {
|
|
|
70
72
|
this.client = client;
|
|
71
73
|
this.searchType = searchType;
|
|
72
74
|
assertNoReservedFields(searchType, [SOURCE_FIELD, LAST_SEEN_FIELD]);
|
|
73
|
-
assertSweepableProvenanceField(searchType, {
|
|
75
|
+
assertSweepableProvenanceField(searchType, {
|
|
76
|
+
requireFacetable: true,
|
|
77
|
+
schema: options.schema,
|
|
78
|
+
});
|
|
74
79
|
this.sourceField = provenanceField(searchType);
|
|
75
80
|
const { maxSweepableSources = DEFAULT_MAX_SWEEPABLE_SOURCES, ...rebuildOptions } = options;
|
|
76
81
|
this.maxSweepableSources = maxSweepableSources;
|
|
@@ -104,9 +109,10 @@ export class InPlaceRebuild {
|
|
|
104
109
|
};
|
|
105
110
|
});
|
|
106
111
|
// A collection this run did NOT create may predate a `joinable` being
|
|
107
|
-
// declared,
|
|
112
|
+
// declared, or a facet policy on a type this one references – and
|
|
113
|
+
// neither a reference nor a facet companion can be added to it here.
|
|
108
114
|
if (!created) {
|
|
109
|
-
await
|
|
115
|
+
await assertReferencesAndFacetCompanionsPresent(this.client, name, declaredFields, facetCompanionsOf(this.searchType, definitionOptions.schema));
|
|
110
116
|
}
|
|
111
117
|
const importer = new BatchImporter(this.client, name, batchSize);
|
|
112
118
|
return {
|
|
@@ -199,23 +205,48 @@ export class InPlaceRebuild {
|
|
|
199
205
|
* component’s collections come into existence WITH their references, and never
|
|
200
206
|
* acquire them later.
|
|
201
207
|
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
208
|
+
* The facet companion of a reference inheriting a facet policy
|
|
209
|
+
* (`RootType.facetKeys` on the type it names) is the other field a live
|
|
210
|
+
* collection cannot acquire: the documents carry `${name}_facet`, the
|
|
211
|
+
* collection does not declare it, and every facet on the field fails with
|
|
212
|
+
* *could not find a facet field* – on every type referencing the policy’s
|
|
213
|
+
* type, until someone drops the collection by hand. Rotating a pipeline
|
|
214
|
+
* version reprocesses datasets; it does not recreate collections. So it is
|
|
215
|
+
* checked here too, with the same instruction.
|
|
216
|
+
*
|
|
217
|
+
* Deliberately scoped to those two. Every other schema difference is
|
|
218
|
+
* self-correcting (a new plain field simply starts being written) or
|
|
204
219
|
* harmless, and general drift detection is a feature of its own.
|
|
205
220
|
*/
|
|
206
|
-
async function
|
|
207
|
-
const
|
|
208
|
-
if (
|
|
221
|
+
async function assertReferencesAndFacetCompanionsPresent(client, name, fields, facetCompanions) {
|
|
222
|
+
const references = fields.filter((field) => field.reference !== undefined);
|
|
223
|
+
if (references.length === 0 && facetCompanions.length === 0) {
|
|
209
224
|
return;
|
|
210
225
|
}
|
|
211
226
|
const existing = new Map((await client.collections(name).retrieve()).fields.map((field) => [
|
|
212
227
|
field.name,
|
|
213
|
-
field
|
|
228
|
+
field,
|
|
214
229
|
]));
|
|
215
|
-
const
|
|
216
|
-
if (
|
|
217
|
-
throw new Error(`Typesense collection “${name}” exists without the reference field(s) ${
|
|
230
|
+
const missingReferences = references.filter((field) => existing.get(field.name)?.reference !== field.reference);
|
|
231
|
+
if (missingReferences.length > 0) {
|
|
232
|
+
throw new Error(`Typesense collection “${name}” exists without the reference field(s) ${missingReferences
|
|
218
233
|
.map((field) => `“${field.name}” → “${field.reference}”`)
|
|
219
234
|
.join(', ')} its search type declares. A reference cannot be added to a live collection here: drop “${name}” (and the collections that join to it) and let the next run rebuild them.`);
|
|
220
235
|
}
|
|
236
|
+
const missingCompanions = facetCompanions.filter((companion) => existing.get(companion)?.facet !== true);
|
|
237
|
+
if (missingCompanions.length > 0) {
|
|
238
|
+
throw new Error(`Typesense collection “${name}” exists without the facet field(s) ${missingCompanions
|
|
239
|
+
.map((companion) => `“${companion}”`)
|
|
240
|
+
.join(', ')} its search type facets on – the companion of a reference to a type that declares a facet policy (“facetKeys”). A facet field cannot be added to a live collection here: drop “${name}” (and the collections that join to it) and let the next run rebuild them.`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/** The `${name}_facet` companions a type’s facets read instead of the fields
|
|
244
|
+
* themselves – one per facetable reference inheriting a facet policy
|
|
245
|
+
* ({@link physicalFields}). */
|
|
246
|
+
function facetCompanionsOf(searchType, schema) {
|
|
247
|
+
return facetableFields(searchType).flatMap((field) => {
|
|
248
|
+
// A facetable field always has a facet field.
|
|
249
|
+
const facet = physicalFields(field, schema).facet;
|
|
250
|
+
return facet === field.name ? [] : [facet];
|
|
251
|
+
});
|
|
221
252
|
}
|
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;
|
|
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;AASrB;;;;;;;;GAQG;AACH,MAAM,WAAW,iBAAiB;IAChC;sEACkE;IAClE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACjE,0EAA0E;IAC1E,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,CAkEjD"}
|
package/dist/lookup.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { displayFieldName, fieldNamed, labelFieldOf, } from '@lde/search/adapter';
|
|
1
|
+
import { displayFieldName, fieldNamed, labelFieldOf, rootTypeNamed, } 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;
|
|
@@ -16,7 +16,6 @@ export async function resolveProjection(client, projection, searchType, schema,
|
|
|
16
16
|
if (projection === undefined || parents.length === 0) {
|
|
17
17
|
return resolved;
|
|
18
18
|
}
|
|
19
|
-
const targetsByName = new Map([...schema.values()].map((rootType) => [rootType.name, rootType]));
|
|
20
19
|
// The level's fields resolve CONCURRENTLY: they read from different
|
|
21
20
|
// collections and nothing links them, so running them in turn would make the
|
|
22
21
|
// stated bound – one round-trip per level – one per field instead.
|
|
@@ -29,7 +28,7 @@ export async function resolveProjection(client, projection, searchType, schema,
|
|
|
29
28
|
// hand-built query from throwing here rather than at the port’s guard.
|
|
30
29
|
return undefined;
|
|
31
30
|
}
|
|
32
|
-
const target =
|
|
31
|
+
const target = rootTypeNamed(schema, field.ref.target);
|
|
33
32
|
const collection = target === undefined ? undefined : collections.get(target.class);
|
|
34
33
|
if (target === undefined || collection === undefined) {
|
|
35
34
|
return undefined;
|
package/dist/query-compiler.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SearchParams } from 'typesense/lib/Typesense/Documents.js';
|
|
2
|
-
import { type Filter, type RootType, type SearchQuery, type SearchType } from '@lde/search';
|
|
2
|
+
import { type Filter, type RootType, type SearchQuery, type SearchSchema, type SearchType } from '@lde/search';
|
|
3
3
|
/** One hop of a join path, resolved: the Root Type it reaches and the
|
|
4
4
|
* Typesense collection that type is stored in. */
|
|
5
5
|
export interface JoinTarget {
|
|
@@ -38,6 +38,16 @@ export interface BuildSearchParamsOptions {
|
|
|
38
38
|
* range count is still safe.
|
|
39
39
|
*/
|
|
40
40
|
readonly maxFacetValues?: number;
|
|
41
|
+
/**
|
|
42
|
+
* The Search Schema the searched type belongs to – what resolves the facet
|
|
43
|
+
* policy a reference inherits from the type it names
|
|
44
|
+
* (`physicalFields(field, schema).facet`): the `${name}_facet` companion a
|
|
45
|
+
* facet on such a field reads, and the membership operator a filter on it
|
|
46
|
+
* compiles to. Left unset, a facet reads the field itself – which, against a
|
|
47
|
+
* collection built with the schema, is a field the engine does not facet.
|
|
48
|
+
* Through the engine it is always set.
|
|
49
|
+
*/
|
|
50
|
+
readonly schema?: SearchSchema;
|
|
41
51
|
/**
|
|
42
52
|
* Called for each `where` clause that does not compile as the caller wrote
|
|
43
53
|
* it – either because it states no constraint (an empty `in` list, a `range`
|
|
@@ -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,UAAU,EAEhB,MAAM,aAAa,CAAC;AAarB;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;;;;;;;;;;;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,CA8BtB;
|
|
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,EAEhB,MAAM,aAAa,CAAC;AAarB;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,CA8BtB;AA6VD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEvD"}
|
package/dist/query-compiler.js
CHANGED
|
@@ -35,7 +35,7 @@ export function buildSearchParams(query, searchType, options = {}) {
|
|
|
35
35
|
params.sort_by = sortBy;
|
|
36
36
|
}
|
|
37
37
|
if (query.facets.length > 0) {
|
|
38
|
-
params.facet_by = compileFacetBy(query.facets, searchType);
|
|
38
|
+
params.facet_by = compileFacetBy(query.facets, searchType, options.schema);
|
|
39
39
|
if (options.maxFacetValues !== undefined) {
|
|
40
40
|
params.max_facet_values = options.maxFacetValues;
|
|
41
41
|
}
|
|
@@ -45,17 +45,22 @@ export function buildSearchParams(query, searchType, options = {}) {
|
|
|
45
45
|
/**
|
|
46
46
|
* The `facet_by` clause. A facet on a numeric field that declares
|
|
47
47
|
* {@link SearchField.facetRanges} faceted into those fixed half-open `[min, max)`
|
|
48
|
-
* bins (a histogram); every other facet is a plain per-value facet on
|
|
49
|
-
*
|
|
48
|
+
* bins (a histogram); every other facet is a plain per-value facet on the
|
|
49
|
+
* field the declaration facets ({@link physicalFields} – the field itself, or
|
|
50
|
+
* the `${name}_facet` companion of a reference inheriting a facet policy).
|
|
51
|
+
* Typesense range syntax is already start-inclusive/end-exclusive, so the
|
|
50
52
|
* declared bounds pass straight through with no boundary fix-up.
|
|
51
53
|
*/
|
|
52
|
-
function compileFacetBy(facets, searchType) {
|
|
54
|
+
function compileFacetBy(facets, searchType, schema) {
|
|
53
55
|
return facets
|
|
54
56
|
.map((name) => {
|
|
55
57
|
const field = fieldNamed(searchType, name);
|
|
56
|
-
|
|
58
|
+
if (field === undefined) {
|
|
59
|
+
return name;
|
|
60
|
+
}
|
|
61
|
+
return isRangeFacet(field)
|
|
57
62
|
? compileRangeFacet(field.name, field.facetRanges)
|
|
58
|
-
: name;
|
|
63
|
+
: (physicalFields(field, schema).facet ?? name);
|
|
59
64
|
})
|
|
60
65
|
.join(',');
|
|
61
66
|
}
|
|
@@ -196,7 +201,7 @@ const UNUSABLE = Symbol('unusable');
|
|
|
196
201
|
function compileCriterion(criterion, searchType, options) {
|
|
197
202
|
const on = criterion.on ?? [];
|
|
198
203
|
if (on.length === 0) {
|
|
199
|
-
return compileLeaf(criterion, searchType);
|
|
204
|
+
return compileLeaf(criterion, searchType, options.schema);
|
|
200
205
|
}
|
|
201
206
|
// A joined criterion constrains a document in ANOTHER collection, so the leaf
|
|
202
207
|
// is compiled against the type that path reaches and then wrapped, one
|
|
@@ -215,7 +220,7 @@ function compileCriterion(criterion, searchType, options) {
|
|
|
215
220
|
collections.push(resolved.collection);
|
|
216
221
|
target = resolved.searchType;
|
|
217
222
|
}
|
|
218
|
-
const leaf = compileLeaf(criterion, target);
|
|
223
|
+
const leaf = compileLeaf(criterion, target, options.schema);
|
|
219
224
|
// A vacuous leaf states no constraint on the referent, so the join as a whole
|
|
220
225
|
// states none either – the reading, and so the outcome, passes straight
|
|
221
226
|
// through the hops.
|
|
@@ -226,7 +231,7 @@ function compileCriterion(criterion, searchType, options) {
|
|
|
226
231
|
}
|
|
227
232
|
/** The criterion’s own term, against the type it actually constrains – the
|
|
228
233
|
* searched type, or the one its join path reaches. */
|
|
229
|
-
function compileLeaf(criterion, searchType) {
|
|
234
|
+
function compileLeaf(criterion, searchType, schema) {
|
|
230
235
|
// `id` is the Typesense document key, not a declared field. Exact `:=`
|
|
231
236
|
// membership, like a non-facet field ({@link compileMembership}), so an IRI
|
|
232
237
|
// cannot partial-match on a shared path segment. (`fetchLabels` resolves
|
|
@@ -249,7 +254,7 @@ function compileLeaf(criterion, searchType) {
|
|
|
249
254
|
}
|
|
250
255
|
if ('in' in criterion) {
|
|
251
256
|
return criterion.in.length > 0
|
|
252
|
-
? compileMembership(field, criterion.in)
|
|
257
|
+
? compileMembership(field, criterion.in, schema)
|
|
253
258
|
: VACUOUS;
|
|
254
259
|
}
|
|
255
260
|
if ('range' in criterion) {
|
|
@@ -260,12 +265,18 @@ function compileLeaf(criterion, searchType) {
|
|
|
260
265
|
/**
|
|
261
266
|
* A membership clause. A non-facet (tokenized) field uses the exact `:=`
|
|
262
267
|
* operator so an IRI cannot partial-match on a shared path segment.
|
|
268
|
+
*
|
|
269
|
+
* *Non-facet* is the **engine’s** facet status, not the declaration’s: a
|
|
270
|
+
* reference inheriting a facet policy is `facetable` in the schema while the
|
|
271
|
+
* engine facets its companion and stores the field itself plain – and a
|
|
272
|
+
* filter on such a field is exactly what the policy promises stays whole, so
|
|
273
|
+
* it must compile exact, not tokenized.
|
|
263
274
|
*/
|
|
264
|
-
function compileMembership(field, values) {
|
|
275
|
+
function compileMembership(field, values, schema) {
|
|
265
276
|
const list = `[${values.map(escapeFilterValue).join(',')}]`;
|
|
266
|
-
return field.
|
|
267
|
-
? `${field.name}
|
|
268
|
-
: `${field.name}
|
|
277
|
+
return physicalFields(field, schema).facet === field.name
|
|
278
|
+
? `${field.name}:${list}`
|
|
279
|
+
: `${field.name}:=${list}`;
|
|
269
280
|
}
|
|
270
281
|
/** An inclusive Typesense range clause, or `undefined` when neither bound is
|
|
271
282
|
* usable. Which of the two readings that is – the caller set no bounds, or the
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Client } from 'typesense';
|
|
2
|
-
import type { SearchType } from '@lde/search';
|
|
2
|
+
import type { SearchSchema, SearchType } from '@lde/search';
|
|
3
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
|
|
@@ -60,13 +60,19 @@ export declare function assertNoReservedFields(searchType: SearchType, reserved:
|
|
|
60
60
|
* selection, which carries raw dataset IRIs, so a transformed value would
|
|
61
61
|
* match nothing and the sweep would silently stop deleting;
|
|
62
62
|
* - **`facetable`** where the writer enumerates the indexed sources by faceting
|
|
63
|
-
* it (the In-place writer does; Blue/green only ever filters by a known IRI)
|
|
63
|
+
* it (the In-place writer does; Blue/green only ever filters by a known IRI)
|
|
64
|
+
* – and faceted **on its own name**: a reference inheriting a facet policy
|
|
65
|
+
* from the type it names facets a companion holding only the admitted keys
|
|
66
|
+
* (`physicalFields(field, schema).facet`), so enumerating the sources
|
|
67
|
+
* through it would miss every dataset the policy excludes. `schema` is what
|
|
68
|
+
* resolves that; without one no policy is visible, as nowhere else.
|
|
64
69
|
*
|
|
65
70
|
* Thrown at writer construction, so a declaration that cannot be swept fails
|
|
66
71
|
* before a run touches the index rather than at the sweep, after the writes.
|
|
67
72
|
*/
|
|
68
73
|
export declare function assertSweepableProvenanceField(searchType: SearchType, options: {
|
|
69
74
|
readonly requireFacetable: boolean;
|
|
75
|
+
readonly schema?: SearchSchema;
|
|
70
76
|
}): void;
|
|
71
77
|
/** Stamp each document with fixed bookkeeping fields as it streams past. */
|
|
72
78
|
export declare function stampDocuments<TDocument>(documents: AsyncIterable<TDocument>, stamp: Readonly<Record<string, string>>): AsyncIterable<TDocument & Record<string, string>>;
|
|
@@ -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;
|
|
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,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAQ5D,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;;;uEAGmE;IACnE,QAAQ,CAAC,iBAAiB,EAAE,2BAA2B,GAAG;QACxD,QAAQ,CAAC,iBAAiB,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,MAAM,CAAC;KAChE,CAAC;CACH;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE,cAAc,GACtB,sBAAsB,CAkBxB;AAsCD;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,SAAS,MAAM,EAAE,GAC1B,IAAI,CAWN;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,8BAA8B,CAC5C,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE;IACP,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC;CAChC,GACA,IAAI,CAiCN;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,4 +1,4 @@
|
|
|
1
|
-
import { datasetField, isInternalField, joinGraph, referenceFields, } from '@lde/search/adapter';
|
|
1
|
+
import { datasetField, isInternalField, joinGraph, physicalFields, referenceFields, } from '@lde/search/adapter';
|
|
2
2
|
import { buildCollectionDefinition, } from './collection-definition.js';
|
|
3
3
|
import { deriveCollectionName } from './collection-name.js';
|
|
4
4
|
import { DEFAULT_LOCK_TTL_MS } from './lock.js';
|
|
@@ -87,7 +87,12 @@ export function assertNoReservedFields(searchType, reserved) {
|
|
|
87
87
|
* selection, which carries raw dataset IRIs, so a transformed value would
|
|
88
88
|
* match nothing and the sweep would silently stop deleting;
|
|
89
89
|
* - **`facetable`** where the writer enumerates the indexed sources by faceting
|
|
90
|
-
* it (the In-place writer does; Blue/green only ever filters by a known IRI)
|
|
90
|
+
* it (the In-place writer does; Blue/green only ever filters by a known IRI)
|
|
91
|
+
* – and faceted **on its own name**: a reference inheriting a facet policy
|
|
92
|
+
* from the type it names facets a companion holding only the admitted keys
|
|
93
|
+
* (`physicalFields(field, schema).facet`), so enumerating the sources
|
|
94
|
+
* through it would miss every dataset the policy excludes. `schema` is what
|
|
95
|
+
* resolves that; without one no policy is visible, as nowhere else.
|
|
91
96
|
*
|
|
92
97
|
* Thrown at writer construction, so a declaration that cannot be swept fails
|
|
93
98
|
* before a run touches the index rather than at the sweep, after the writes.
|
|
@@ -107,6 +112,10 @@ export function assertSweepableProvenanceField(searchType, options) {
|
|
|
107
112
|
if (options.requireFacetable && field.facetable !== true) {
|
|
108
113
|
problems.push('it is not facetable, and the membership sweep enumerates the indexed datasets by faceting it');
|
|
109
114
|
}
|
|
115
|
+
else if (options.requireFacetable &&
|
|
116
|
+
physicalFields(field, options.schema).facet !== field.name) {
|
|
117
|
+
problems.push('it inherits a facet policy from the type it names, so the engine facets only the datasets the policy admits, and the membership sweep enumerates the indexed datasets by faceting it');
|
|
118
|
+
}
|
|
110
119
|
if (problems.length > 0) {
|
|
111
120
|
throw new Error(`SearchType “${searchType.name}” declares “${field.name}” over the indexed dataset, which this writer keeps its provenance bookkeeping on, but ${problems.join('; and ')}.`);
|
|
112
121
|
}
|
package/dist/search.d.ts
CHANGED
|
@@ -108,6 +108,9 @@ export interface TypesenseSearchResponse {
|
|
|
108
108
|
readonly document: Record<string, unknown>;
|
|
109
109
|
}[];
|
|
110
110
|
readonly facet_counts?: readonly {
|
|
111
|
+
/** The field faceted – by the time a response reaches
|
|
112
|
+
* {@link parseSearchResponse}, the **declared** field name: the engine
|
|
113
|
+
* maps a facet companion’s name back at the response boundary. */
|
|
111
114
|
readonly field_name: string;
|
|
112
115
|
readonly counts: readonly {
|
|
113
116
|
readonly value: string;
|
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,EAOnB,KAAK,UAAU,EAGf,KAAK,YAAY,EAEjB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,UAAU,EAEf,KAAK,SAAS,EACf,MAAM,aAAa,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, isInlineReference, isRangeFacet, isUnsatisfiable, joinGraph, labelFieldOf, labelSourceNameOf, nestedReferenceType, outputFields, physicalFields, referenceFields, } from '@lde/search/adapter';
|
|
1
|
+
import { assertTypeInSchema, assertValidQuery, displayLangOf, facetableFields, fieldNamed, isInlineReference, isRangeFacet, isUnsatisfiable, joinGraph, labelFieldOf, labelSourceNameOf, 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
|
import { resolveProjection } from './lookup.js';
|
|
@@ -48,6 +48,7 @@ export function createTypesenseSearchEngine(client, schema, options = {}) {
|
|
|
48
48
|
const joins = joinGraph(schema);
|
|
49
49
|
const searchParamsOptions = {
|
|
50
50
|
...options,
|
|
51
|
+
schema,
|
|
51
52
|
joinTargetFor: (from, path) => {
|
|
52
53
|
// Both lookups always hit: `assertValidQuery` runs first and rejects
|
|
53
54
|
// every path this could not resolve, and each type resolved a collection
|
|
@@ -81,6 +82,22 @@ export function createTypesenseSearchEngine(client, schema, options = {}) {
|
|
|
81
82
|
];
|
|
82
83
|
})),
|
|
83
84
|
]));
|
|
85
|
+
// Typesense reports a facet under the field it faceted, which for a reference
|
|
86
|
+
// inheriting a facet policy is the `${name}_facet` companion, not the
|
|
87
|
+
// declared field. Resolved back to the declared name per type ONCE, from the
|
|
88
|
+
// same `physicalFields` that named it, and applied to every response at the
|
|
89
|
+
// boundary – before `labelLookupGroups` (which finds a facet’s label source
|
|
90
|
+
// by declared name) and `parseSearchResponse` (which files its counts under
|
|
91
|
+
// it). Either consumer meeting the engine name would silently lose the
|
|
92
|
+
// facet’s labels, or its counts, or both.
|
|
93
|
+
const declaredFacetNames = new Map([...schema.values()].map((searchType) => [
|
|
94
|
+
searchType.class,
|
|
95
|
+
new Map(facetableFields(searchType).map((field) => [
|
|
96
|
+
// A facetable field always has a facet field.
|
|
97
|
+
physicalFields(field, schema).facet,
|
|
98
|
+
field.name,
|
|
99
|
+
])),
|
|
100
|
+
]));
|
|
84
101
|
// The distinct source collections per type, for the cached path – fixed at
|
|
85
102
|
// construction, so no per-search dedup dance.
|
|
86
103
|
const distinctLabelSources = new Map([...labelSources].map(([type, sources]) => [
|
|
@@ -210,7 +227,7 @@ export function createTypesenseSearchEngine(client, schema, options = {}) {
|
|
|
210
227
|
// would overflow Typesense's 4000-char query-string limit. POST puts it
|
|
211
228
|
// in the body, so the filter is bounded by the caller's request, not by
|
|
212
229
|
// the URL.
|
|
213
|
-
const response = await performSingleSearch(client, collections.get(searchType.class), params);
|
|
230
|
+
const response = withDeclaredFacetNames(await performSingleSearch(client, collections.get(searchType.class), params), declaredFacetNames.get(searchType.class));
|
|
214
231
|
const labels = await resolveLabels(cachedLabelsPromise, () => labelLookupGroups([response], labelSources.get(searchType.class), outputReferenceSources.get(searchType.class) ?? []));
|
|
215
232
|
// What the caller asked to resolve, level by level. Independent of the
|
|
216
233
|
// label lookup above: that one serves facet buckets and unprojected
|
|
@@ -243,12 +260,16 @@ export function createTypesenseSearchEngine(client, schema, options = {}) {
|
|
|
243
260
|
// compiles as facet-only regardless of what it carries: no hits
|
|
244
261
|
// (per_page 0) and no ordering – nothing is transferred or sorted that
|
|
245
262
|
// this method cannot return.
|
|
246
|
-
const { results } = (await client.multiSearch.perform({
|
|
263
|
+
const { results: rawResults } = (await client.multiSearch.perform({
|
|
247
264
|
searches: dispatched.map(({ query }) => ({
|
|
248
265
|
collection,
|
|
249
266
|
...buildSearchParams({ ...query, orderBy: [], limit: 0, offset: 0 }, searchType, searchParamsOptions),
|
|
250
267
|
})),
|
|
251
268
|
}));
|
|
269
|
+
const declaredNames = declaredFacetNames.get(searchType.class);
|
|
270
|
+
const results = rawResults.map((result) => 'error' in result
|
|
271
|
+
? result
|
|
272
|
+
: withDeclaredFacetNames(result, declaredNames));
|
|
252
273
|
// One label lookup serves every successful facet result in the batch.
|
|
253
274
|
const responses = results.filter((result) => !('error' in result));
|
|
254
275
|
const labels = await resolveLabels(cachedLabelsPromise, () => labelLookupGroups(responses, labelSources.get(searchType.class), outputReferenceSources.get(searchType.class) ?? []));
|
|
@@ -384,6 +405,25 @@ function labelLookupGroups(responses, sources, outputSources) {
|
|
|
384
405
|
iris: [...iris],
|
|
385
406
|
}));
|
|
386
407
|
}
|
|
408
|
+
/**
|
|
409
|
+
* The response with each facet filed under the declared field name its engine
|
|
410
|
+
* facet field stands for – the one place an engine facet name is translated,
|
|
411
|
+
* so that nothing downstream of it meets one.
|
|
412
|
+
*/
|
|
413
|
+
function withDeclaredFacetNames(response, declaredNames) {
|
|
414
|
+
if (response.facet_counts === undefined) {
|
|
415
|
+
return response;
|
|
416
|
+
}
|
|
417
|
+
return {
|
|
418
|
+
...response,
|
|
419
|
+
facet_counts: response.facet_counts.map((facet) => ({
|
|
420
|
+
...facet,
|
|
421
|
+
// Every facet the engine reports was asked for by a declared, facetable
|
|
422
|
+
// field (`assertValidQuery`), and the map holds each of those.
|
|
423
|
+
field_name: declaredNames.get(facet.field_name),
|
|
424
|
+
})),
|
|
425
|
+
};
|
|
426
|
+
}
|
|
387
427
|
/**
|
|
388
428
|
* Dispatch one search as a single-entry `multi_search` (POST). `multi_search`
|
|
389
429
|
* reports a failed entry inline instead of rejecting the call, so the entry is
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lde/search-typesense",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.3",
|
|
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,16 +25,17 @@
|
|
|
25
25
|
"!**/*.tsbuildinfo"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@lde/search": "^0.23.
|
|
28
|
+
"@lde/search": "^0.23.2",
|
|
29
29
|
"@lde/text-normalization": "^0.1.2",
|
|
30
30
|
"tslib": "^2.3.0",
|
|
31
31
|
"typesense": "^3.0.6"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
+
"n3": "^2.1.2",
|
|
34
35
|
"testcontainers": "^12.1.0"
|
|
35
36
|
},
|
|
36
37
|
"peerDependencies": {
|
|
37
38
|
"@lde/dataset": "^0.7.9",
|
|
38
|
-
"@lde/pipeline": "^0.36.
|
|
39
|
+
"@lde/pipeline": "^0.36.2"
|
|
39
40
|
}
|
|
40
41
|
}
|