@lde/search-typesense 0.21.3 → 0.23.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/blue-green-rebuild.d.ts.map +1 -1
- package/dist/blue-green-rebuild.js +27 -4
- package/dist/collection-definition.d.ts +32 -13
- package/dist/collection-definition.d.ts.map +1 -1
- package/dist/collection-definition.js +61 -8
- package/dist/in-place-rebuild.d.ts +4 -1
- package/dist/in-place-rebuild.d.ts.map +1 -1
- package/dist/in-place-rebuild.js +49 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lock.d.ts +6 -1
- package/dist/lock.d.ts.map +1 -1
- package/dist/lock.js +10 -2
- package/dist/query-compiler.d.ts +30 -1
- package/dist/query-compiler.d.ts.map +1 -1
- package/dist/query-compiler.js +45 -7
- package/dist/rebuild-support.d.ts +10 -8
- package/dist/rebuild-support.d.ts.map +1 -1
- package/dist/rebuild-support.js +36 -6
- package/dist/search.d.ts +1 -1
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +24 -5
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blue-green-rebuild.d.ts","sourceRoot":"","sources":["../src/blue-green-rebuild.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EAEV,UAAU,EACV,SAAS,EACT,MAAM,EACP,MAAM,eAAe,CAAC;AAUvB,OAAO,EAML,KAAK,cAAc,EAEpB,MAAM,sBAAsB,CAAC;AAE9B;;yDAEyD;AACzD,MAAM,MAAM,uBAAuB,GAAG,cAAc,CAAC;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,qBAAa,gBAAgB,CAC3B,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;;;;;;;OAOG;IACH,SAAgB,cAAc,EAAE,MAAM,CAAC;IACvC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyB;IAClD;;4EAEwE;IACxE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;gBAGlB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,UAAU,EACvC,OAAO,GAAE,uBAA4B;
|
|
1
|
+
{"version":3,"file":"blue-green-rebuild.d.ts","sourceRoot":"","sources":["../src/blue-green-rebuild.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EAEV,UAAU,EACV,SAAS,EACT,MAAM,EACP,MAAM,eAAe,CAAC;AAUvB,OAAO,EAML,KAAK,cAAc,EAEpB,MAAM,sBAAsB,CAAC;AAE9B;;yDAEyD;AACzD,MAAM,MAAM,uBAAuB,GAAG,cAAc,CAAC;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,qBAAa,gBAAgB,CAC3B,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;;;;;;;OAOG;IACH,SAAgB,cAAc,EAAE,MAAM,CAAC;IACvC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyB;IAClD;;4EAEwE;IACxE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;gBAGlB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,UAAU,EACvC,OAAO,GAAE,uBAA4B;IAajC,OAAO,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IAwHjE,4EAA4E;YAC9D,WAAW;CAW1B"}
|
|
@@ -59,7 +59,8 @@ export class BlueGreenRebuild {
|
|
|
59
59
|
assertSweepableProvenanceField(searchType, { requireFacetable: false });
|
|
60
60
|
this.sourceField = provenanceField(searchType);
|
|
61
61
|
this.resolved = resolveRebuildOptions(searchType, options);
|
|
62
|
-
this.collectionName =
|
|
62
|
+
this.collectionName =
|
|
63
|
+
this.resolved.definitionOptions.collectionNameFor(searchType);
|
|
63
64
|
}
|
|
64
65
|
async openRun(context) {
|
|
65
66
|
const { batchSize, lockTtlMs, definitionOptions } = this.resolved;
|
|
@@ -69,12 +70,25 @@ export class BlueGreenRebuild {
|
|
|
69
70
|
// Create the fresh (blue) collection up front, so a failure surfaces
|
|
70
71
|
// before any dataset is processed. startedAt orders the versioned names;
|
|
71
72
|
// concurrent same-name runs are excluded by the lock.
|
|
72
|
-
|
|
73
|
+
//
|
|
74
|
+
// The SAME versioning applies to every peer this type joins to, so an
|
|
75
|
+
// emitted reference field names the peer’s FRESH collection rather than
|
|
76
|
+
// its live one – Typesense resolves an alias to a concrete collection at
|
|
77
|
+
// create time and keeps the concrete name, so referencing the alias would
|
|
78
|
+
// pin the new build to the collection the peer is about to supersede. No
|
|
79
|
+
// coordinator is needed: every writer in the run gets the same
|
|
80
|
+
// `RunContext`, so `startedAt` – and therefore every derived name – is
|
|
81
|
+
// identical across them.
|
|
82
|
+
const version = Date.parse(context.startedAt);
|
|
83
|
+
const versioned = (searchType) => `${definitionOptions.collectionNameFor(searchType)}_${version}`;
|
|
84
|
+
const collection = versioned(this.searchType);
|
|
73
85
|
const previous = await this.aliasTarget(name);
|
|
74
|
-
const definition = buildCollectionDefinition(this.searchType,
|
|
86
|
+
const definition = buildCollectionDefinition(this.searchType, {
|
|
87
|
+
...definitionOptions,
|
|
88
|
+
collectionNameFor: versioned,
|
|
89
|
+
});
|
|
75
90
|
await this.client.collections().create({
|
|
76
91
|
...definition,
|
|
77
|
-
name: collection,
|
|
78
92
|
// The private `source` only when the type declares no dataset field of
|
|
79
93
|
// its own; a declared one is already in the definition and carries the
|
|
80
94
|
// same IRI.
|
|
@@ -134,6 +148,15 @@ export class BlueGreenRebuild {
|
|
|
134
148
|
.catch(() => undefined);
|
|
135
149
|
await releaseLock(this.client, name);
|
|
136
150
|
},
|
|
151
|
+
abandon: async () => {
|
|
152
|
+
// Everything `abort` does EXCEPT the drop. Asked for when a peer
|
|
153
|
+
// whose documents reference this collection by concrete name has
|
|
154
|
+
// already gone live: dropping it would break that live collection’s
|
|
155
|
+
// joins, so the collection is kept for a later run to supersede.
|
|
156
|
+
// Releasing the lock is the whole point – without it the index stays
|
|
157
|
+
// locked for its full TTL and the next run cannot rebuild it.
|
|
158
|
+
await releaseLock(this.client, name);
|
|
159
|
+
},
|
|
137
160
|
};
|
|
138
161
|
});
|
|
139
162
|
}
|
|
@@ -2,17 +2,30 @@ import type { CollectionCreateSchema } from 'typesense';
|
|
|
2
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
|
-
/** The Typesense collection (or alias) name. Omit to derive it from the
|
|
6
|
-
* type’s `name` ({@link deriveCollectionName}); supply one to override that
|
|
7
|
-
* (an env prefix, a multi-tenant name, an existing collection). */
|
|
8
|
-
readonly name?: string;
|
|
9
5
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
6
|
+
* How a search type is named in Typesense – for **this** type and, where it
|
|
7
|
+
* declares a {@link ReferenceField.joinable} reference, for the peer type
|
|
8
|
+
* that reference points at. Omit to derive every name from the type’s own
|
|
9
|
+
* `name` ({@link deriveCollectionName}); supply one to override that (an env
|
|
10
|
+
* prefix, a multi-tenant name, an existing collection).
|
|
11
|
+
*
|
|
12
|
+
* A function of the type rather than a bare name, because an emitted
|
|
13
|
+
* reference field must name the peer’s **concrete** collection: a
|
|
14
|
+
* blue/green writer therefore hands in a function that appends the run’s
|
|
15
|
+
* version to every name in the component, so a fresh build references its
|
|
16
|
+
* peer’s fresh collection rather than the live one. No coordinator is
|
|
17
|
+
* needed – every writer in a run receives the same `RunContext`, so they
|
|
18
|
+
* derive identical names.
|
|
19
|
+
*/
|
|
20
|
+
readonly collectionNameFor?: (searchType: SearchType) => string;
|
|
21
|
+
/**
|
|
22
|
+
* The Search Schema the type belongs to – required when the type surfaces an
|
|
23
|
+
* inline reference (whose nested fields are declared from the
|
|
24
|
+
* {@link ReferenceType} the schema resolves) or declares a joinable
|
|
25
|
+
* reference (whose target collection the schema’s {@link joinGraph}
|
|
26
|
+
* resolves). A type doing neither needs no schema, so a caller passes none;
|
|
27
|
+
* a type that does fails here rather than building a collection that
|
|
28
|
+
* silently omits the nesting or the reference.
|
|
16
29
|
*/
|
|
17
30
|
readonly schema?: SearchSchema;
|
|
18
31
|
/** Snowball stemming locale for non-localized searchable fields (e.g. `en`).
|
|
@@ -37,9 +50,15 @@ export interface CollectionDefinitionOptions {
|
|
|
37
50
|
* stems in `defaultLocale` when one is set, and is left unstemmed (folded
|
|
38
51
|
* only) otherwise.
|
|
39
52
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* states
|
|
53
|
+
* Collection naming is optional: omitted, every name – this type’s and the peer
|
|
54
|
+
* types its joins point at – is derived from the type’s own `name`
|
|
55
|
+
* ({@link deriveCollectionName}), so a caller that has no naming opinion states
|
|
56
|
+
* none.
|
|
57
|
+
*
|
|
58
|
+
* A {@link ReferenceField.joinable} reference is emitted as a Typesense
|
|
59
|
+
* **reference field** pointing at its target collection’s `id`, so a query can
|
|
60
|
+
* filter this type by a condition on the referent
|
|
61
|
+
* ({@link referenceDeclaration}).
|
|
43
62
|
*
|
|
44
63
|
* Memory lever: Typesense holds the index in RAM (with a raw copy of each
|
|
45
64
|
* document on disk), so RAM tracks the *indexed* surface – roughly 2–3× the
|
|
@@ -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;
|
|
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;;;;;;;;OAQG;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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { displayFieldPattern, isInlineReference, isInternalField, nestedFieldName, nestedReferenceType, physicalFields, } from '@lde/search/adapter';
|
|
1
|
+
import { displayFieldPattern, ID_FIELD, isInlineReference, isInternalField, joinGraph, 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
|
|
@@ -12,9 +12,15 @@ import { deriveCollectionName } from './collection-name.js';
|
|
|
12
12
|
* stems in `defaultLocale` when one is set, and is left unstemmed (folded
|
|
13
13
|
* only) otherwise.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* states
|
|
15
|
+
* Collection naming is optional: omitted, every name – this type’s and the peer
|
|
16
|
+
* types its joins point at – is derived from the type’s own `name`
|
|
17
|
+
* ({@link deriveCollectionName}), so a caller that has no naming opinion states
|
|
18
|
+
* none.
|
|
19
|
+
*
|
|
20
|
+
* A {@link ReferenceField.joinable} reference is emitted as a Typesense
|
|
21
|
+
* **reference field** pointing at its target collection’s `id`, so a query can
|
|
22
|
+
* filter this type by a condition on the referent
|
|
23
|
+
* ({@link referenceDeclaration}).
|
|
18
24
|
*
|
|
19
25
|
* Memory lever: Typesense holds the index in RAM (with a raw copy of each
|
|
20
26
|
* document on disk), so RAM tracks the *indexed* surface – roughly 2–3× the
|
|
@@ -39,10 +45,10 @@ import { deriveCollectionName } from './collection-name.js';
|
|
|
39
45
|
* display labels.
|
|
40
46
|
*/
|
|
41
47
|
export function buildCollectionDefinition(searchType, options = {}) {
|
|
42
|
-
const { defaultLocale } = options;
|
|
48
|
+
const { defaultLocale, collectionNameFor = deriveCollectionName } = options;
|
|
43
49
|
const collection = {
|
|
44
|
-
name:
|
|
45
|
-
fields: searchType.fields.flatMap((field) => typesenseFields(searchType, field, defaultLocale, options.defaultSortingField, options.schema)),
|
|
50
|
+
name: collectionNameFor(searchType),
|
|
51
|
+
fields: searchType.fields.flatMap((field) => typesenseFields(searchType, field, defaultLocale, options.defaultSortingField, options.schema, collectionNameFor)),
|
|
46
52
|
};
|
|
47
53
|
// Typesense rejects an `object`/`object[]` field unless nesting is enabled,
|
|
48
54
|
// so the flag is a consequence of the declaration – never a knob.
|
|
@@ -73,8 +79,54 @@ function nestedTypeOf(searchType, field, schema) {
|
|
|
73
79
|
}
|
|
74
80
|
return nested;
|
|
75
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* The Typesense reference declaration a {@link ReferenceField.joinable} field
|
|
84
|
+
* carries, or `undefined` for every other field. All three keys are forced,
|
|
85
|
+
* none is a knob:
|
|
86
|
+
*
|
|
87
|
+
* - **`reference` always targets `.id`.** A reference match hitting more than
|
|
88
|
+
* one document is a 400, and `id` is the only field the schema guarantees
|
|
89
|
+
* unique.
|
|
90
|
+
* - **`async_reference: true`.** Without it, a document whose referent is not
|
|
91
|
+
* indexed yet is rejected with a 400 – and the batch import runs
|
|
92
|
+
* `throwOnFail: false`, so those would be silently dropped documents.
|
|
93
|
+
* Documents stream per dataset ([ADR 13](../../../docs/decisions/0013-project-inside-the-batch-per-root-type.md)),
|
|
94
|
+
* so out-of-order arrival is normal rather than exceptional; the engine
|
|
95
|
+
* back-fills the reference when the referent lands.
|
|
96
|
+
* - **`cascade_delete: false`.** It defaults to `true`, so a sweep removing a
|
|
97
|
+
* departed source’s referent documents would delete other sources’ referring
|
|
98
|
+
* documents with them. Disabling it requires `async_reference`, so the two
|
|
99
|
+
* travel together.
|
|
100
|
+
*
|
|
101
|
+
* Throws when the type declares a joinable reference the caller gave no schema
|
|
102
|
+
* to resolve the target of – the collection would otherwise come into existence
|
|
103
|
+
* without its reference and 400 on every join query.
|
|
104
|
+
*/
|
|
105
|
+
function referenceDeclaration(searchType, field, schema, collectionNameFor) {
|
|
106
|
+
if (field.kind !== 'reference' || field.joinable !== true) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
if (schema === undefined) {
|
|
110
|
+
throw new Error(`Building the collection for “${searchType.name}” needs the search schema its joinable reference “${field.name}” resolves against; pass it as the collection-definition option “schema”.`);
|
|
111
|
+
}
|
|
112
|
+
const target = joinGraph(schema).resolve(searchType, [field.name]);
|
|
113
|
+
if (target === undefined) {
|
|
114
|
+
// A schema WAS given and the edge still does not resolve. The rules that
|
|
115
|
+
// would reject the declaration itself all ran when the schema was built, so
|
|
116
|
+
// what is left is identity: `joinGraph` resolves from the exact declaration
|
|
117
|
+
// object the schema holds, and this is a lookalike – a re-declared type, or
|
|
118
|
+
// one belonging to another schema. Saying “pass the schema” here would send
|
|
119
|
+
// a caller to do the thing they already did.
|
|
120
|
+
throw new Error(`Building the collection for “${searchType.name}” cannot resolve its joinable reference “${field.name}”: the given search schema does not declare this exact type. Pass the declaration the schema was built from, not a copy of it.`);
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
reference: `${collectionNameFor(target)}.${ID_FIELD}`,
|
|
124
|
+
async_reference: true,
|
|
125
|
+
cascade_delete: false,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
76
128
|
/** The physical Typesense fields one declaration produces. */
|
|
77
|
-
function typesenseFields(searchType, field, defaultLocale, defaultSortingField, schema) {
|
|
129
|
+
function typesenseFields(searchType, field, defaultLocale, defaultSortingField, schema, collectionNameFor) {
|
|
78
130
|
// An internal field (no role) is projected as a reading device for the
|
|
79
131
|
// derives and pruned before the writer, so the collection stores nothing for
|
|
80
132
|
// it: not stored, not indexed, no RAM. Same predicate the projection prunes
|
|
@@ -137,6 +189,7 @@ function typesenseFields(searchType, field, defaultLocale, defaultSortingField,
|
|
|
137
189
|
// A `required` field is non-optional; so is the `default_sorting_field`,
|
|
138
190
|
// which Typesense requires to be present. Everything else may be absent.
|
|
139
191
|
optional: field.required !== true && field.name !== defaultSortingField,
|
|
192
|
+
...referenceDeclaration(searchType, field, schema, collectionNameFor),
|
|
140
193
|
},
|
|
141
194
|
];
|
|
142
195
|
// `names.search` is non-empty exactly when the field projects a folded
|
|
@@ -34,7 +34,10 @@ export interface InPlaceRebuildOptions extends RebuildOptions {
|
|
|
34
34
|
* `openRun` takes the single-flight cross-pod lock (throwing
|
|
35
35
|
* `RebuildAlreadyRunning` when another rebuild holds it) and creates
|
|
36
36
|
* the collection on demand from the {@link SearchType} plus the two
|
|
37
|
-
* bookkeeping fields.
|
|
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 drop-and-rebuild that fixes it
|
|
40
|
+
* ({@link assertReferencesPresent}).
|
|
38
41
|
*
|
|
39
42
|
* Document ids must be unique per (source, entity) – the caller keys them –
|
|
40
43
|
* 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,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,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
|
|
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,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;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;IAe/B,OAAO,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IAuHjE;;;;;;OAMG;YACW,cAAc;CAsB7B"}
|
package/dist/in-place-rebuild.js
CHANGED
|
@@ -38,7 +38,10 @@ const DEFAULT_MAX_SWEEPABLE_SOURCES = 10_000;
|
|
|
38
38
|
* `openRun` takes the single-flight cross-pod lock (throwing
|
|
39
39
|
* `RebuildAlreadyRunning` when another rebuild holds it) and creates
|
|
40
40
|
* the collection on demand from the {@link SearchType} plus the two
|
|
41
|
-
* bookkeeping fields.
|
|
41
|
+
* 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 drop-and-rebuild that fixes it
|
|
44
|
+
* ({@link assertReferencesPresent}).
|
|
42
45
|
*
|
|
43
46
|
* Document ids must be unique per (source, entity) – the caller keys them –
|
|
44
47
|
* or documents from different sources overwrite each other.
|
|
@@ -72,7 +75,8 @@ export class InPlaceRebuild {
|
|
|
72
75
|
const { maxSweepableSources = DEFAULT_MAX_SWEEPABLE_SOURCES, ...rebuildOptions } = options;
|
|
73
76
|
this.maxSweepableSources = maxSweepableSources;
|
|
74
77
|
this.resolved = resolveRebuildOptions(searchType, rebuildOptions);
|
|
75
|
-
this.collectionName =
|
|
78
|
+
this.collectionName =
|
|
79
|
+
this.resolved.definitionOptions.collectionNameFor(searchType);
|
|
76
80
|
}
|
|
77
81
|
async openRun(context) {
|
|
78
82
|
const { batchSize, lockTtlMs, definitionOptions } = this.resolved;
|
|
@@ -85,8 +89,9 @@ export class InPlaceRebuild {
|
|
|
85
89
|
// field of its own; when it does, that declared field already carries the
|
|
86
90
|
// IRI (and its own `facet: true`), and a second column would be the same
|
|
87
91
|
// value twice, free to drift.
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
const definition = buildCollectionDefinition(this.searchType, definitionOptions);
|
|
93
|
+
const declaredFields = definition.fields ?? [];
|
|
94
|
+
const created = await ensureCollectionExists(this.client, name, () => {
|
|
90
95
|
const bookkeeping = [
|
|
91
96
|
...(sourceField === SOURCE_FIELD
|
|
92
97
|
? [{ name: SOURCE_FIELD, type: 'string', facet: true }]
|
|
@@ -95,9 +100,14 @@ export class InPlaceRebuild {
|
|
|
95
100
|
];
|
|
96
101
|
return {
|
|
97
102
|
...definition,
|
|
98
|
-
fields: [...
|
|
103
|
+
fields: [...declaredFields, ...bookkeeping],
|
|
99
104
|
};
|
|
100
105
|
});
|
|
106
|
+
// A collection this run did NOT create may predate a `joinable` being
|
|
107
|
+
// declared, and a reference cannot be added to it here.
|
|
108
|
+
if (!created) {
|
|
109
|
+
await assertReferencesPresent(this.client, name, declaredFields);
|
|
110
|
+
}
|
|
101
111
|
const importer = new BatchImporter(this.client, name, batchSize);
|
|
102
112
|
return {
|
|
103
113
|
// Stamping the provenance field – rather than trusting the projection
|
|
@@ -175,3 +185,37 @@ export class InPlaceRebuild {
|
|
|
175
185
|
return counts.map((count) => count.value);
|
|
176
186
|
}
|
|
177
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Fail loudly when an EXISTING collection does not carry every reference field
|
|
190
|
+
* the declaration asks for, naming the drop-and-rebuild that fixes it. Never
|
|
191
|
+
* alters: an In-place rebuild creates a collection on demand and otherwise
|
|
192
|
+
* leaves its shape alone, and a reference field is the one difference that is
|
|
193
|
+
* not self-correcting.
|
|
194
|
+
*
|
|
195
|
+
* Without this a deployment that added `joinable` to an existing schema would
|
|
196
|
+
* index and commit perfectly happily, and then 400 on every join query – the
|
|
197
|
+
* collection has the *values*, it just has no reference to join through.
|
|
198
|
+
* Failing here keeps the invariant a component-scoped rebuild rests on: a
|
|
199
|
+
* component’s collections come into existence WITH their references, and never
|
|
200
|
+
* acquire them later.
|
|
201
|
+
*
|
|
202
|
+
* Deliberately scoped to reference fields only. Every other schema difference
|
|
203
|
+
* is self-correcting (a new plain field simply starts being written) or
|
|
204
|
+
* harmless, and general drift detection is a feature of its own.
|
|
205
|
+
*/
|
|
206
|
+
async function assertReferencesPresent(client, name, fields) {
|
|
207
|
+
const declared = fields.filter((field) => field.reference !== undefined);
|
|
208
|
+
if (declared.length === 0) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const existing = new Map((await client.collections(name).retrieve()).fields.map((field) => [
|
|
212
|
+
field.name,
|
|
213
|
+
field.reference,
|
|
214
|
+
]));
|
|
215
|
+
const missing = declared.filter((field) => existing.get(field.name) !== field.reference);
|
|
216
|
+
if (missing.length > 0) {
|
|
217
|
+
throw new Error(`Typesense collection “${name}” exists without the reference field(s) ${missing
|
|
218
|
+
.map((field) => `“${field.name}” → “${field.reference}”`)
|
|
219
|
+
.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
|
+
}
|
|
221
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export { buildCollectionDefinition } from './collection-definition.js';
|
|
|
8
8
|
export type { CollectionDefinitionOptions } from './collection-definition.js';
|
|
9
9
|
export { deriveCollectionName } from './collection-name.js';
|
|
10
10
|
export { buildSearchParams } from './query-compiler.js';
|
|
11
|
-
export type { BuildSearchParamsOptions } from './query-compiler.js';
|
|
11
|
+
export type { BuildSearchParamsOptions, JoinTarget } from './query-compiler.js';
|
|
12
12
|
export { createTypesenseSearchEngine, parseSearchResponse } from './search.js';
|
|
13
13
|
export type { TypesenseSearchEngine, TypesenseSearchEngineOptions, TypesenseSearchResponse, } from './search.js';
|
|
14
14
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,YAAY,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AACvE,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,YAAY,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,EACL,eAAe,EACf,sBAAsB,EACtB,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AACvE,YAAY,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,YAAY,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,YAAY,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AACvE,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,YAAY,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,EACL,eAAe,EACf,sBAAsB,EACtB,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AACvE,YAAY,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,YAAY,EAAE,wBAAwB,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAChF,OAAO,EAAE,2BAA2B,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAC/E,YAAY,EACV,qBAAqB,EACrB,4BAA4B,EAC5B,uBAAuB,GACxB,MAAM,aAAa,CAAC"}
|
package/dist/lock.d.ts
CHANGED
|
@@ -35,8 +35,13 @@ export declare function releaseLock(client: Client, name: string): Promise<void>
|
|
|
35
35
|
/**
|
|
36
36
|
* Create a collection on demand: retrieve it, and only on a 404 create it
|
|
37
37
|
* from the lazily built `schema`, tolerating a concurrent creator (409).
|
|
38
|
+
*
|
|
39
|
+
* Returns whether the collection was **brought into existence** by this call –
|
|
40
|
+
* which is what tells a caller that everything the definition declares is
|
|
41
|
+
* actually in the collection. An existing one may predate a declaration
|
|
42
|
+
* change, and some of those differences an engine cannot correct in place.
|
|
38
43
|
*/
|
|
39
|
-
export declare function ensureCollectionExists(client: Client, name: string, schema: () => CollectionCreateSchema): Promise<
|
|
44
|
+
export declare function ensureCollectionExists(client: Client, name: string, schema: () => CollectionCreateSchema): Promise<boolean>;
|
|
40
45
|
/** The HTTP status a Typesense client error carries, if any. */
|
|
41
46
|
export declare function httpStatus(error: unknown): number | undefined;
|
|
42
47
|
//# sourceMappingURL=lock.d.ts.map
|
package/dist/lock.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lock.d.ts","sourceRoot":"","sources":["../src/lock.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AAIhE,qDAAqD;AACrD,eAAO,MAAM,mBAAmB,QAAiB,CAAC;AAElD;;;;GAIG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,IAAI,EAAE,MAAM;CAIzB;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CAAC,GAAG,EACrC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,GACxB,OAAO,CAAC,GAAG,CAAC,CAUd;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,OAAO,CAAC,CAwBlB;AAED,yEAAyE;AACzE,wBAAsB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ7E;AAED
|
|
1
|
+
{"version":3,"file":"lock.d.ts","sourceRoot":"","sources":["../src/lock.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AAIhE,qDAAqD;AACrD,eAAO,MAAM,mBAAmB,QAAiB,CAAC;AAElD;;;;GAIG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,IAAI,EAAE,MAAM;CAIzB;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CAAC,GAAG,EACrC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,GACxB,OAAO,CAAC,GAAG,CAAC,CAUd;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,OAAO,CAAC,CAwBlB;AAED,yEAAyE;AACzE,wBAAsB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ7E;AAED;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,sBAAsB,GACnC,OAAO,CAAC,OAAO,CAAC,CAmBlB;AA+BD,gEAAgE;AAChE,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAE7D"}
|
package/dist/lock.js
CHANGED
|
@@ -83,11 +83,16 @@ export async function releaseLock(client, name) {
|
|
|
83
83
|
/**
|
|
84
84
|
* Create a collection on demand: retrieve it, and only on a 404 create it
|
|
85
85
|
* from the lazily built `schema`, tolerating a concurrent creator (409).
|
|
86
|
+
*
|
|
87
|
+
* Returns whether the collection was **brought into existence** by this call –
|
|
88
|
+
* which is what tells a caller that everything the definition declares is
|
|
89
|
+
* actually in the collection. An existing one may predate a declaration
|
|
90
|
+
* change, and some of those differences an engine cannot correct in place.
|
|
86
91
|
*/
|
|
87
92
|
export async function ensureCollectionExists(client, name, schema) {
|
|
88
93
|
try {
|
|
89
94
|
await client.collections(name).retrieve();
|
|
90
|
-
return;
|
|
95
|
+
return false;
|
|
91
96
|
}
|
|
92
97
|
catch (error) {
|
|
93
98
|
if (httpStatus(error) !== 404) {
|
|
@@ -96,11 +101,14 @@ export async function ensureCollectionExists(client, name, schema) {
|
|
|
96
101
|
}
|
|
97
102
|
try {
|
|
98
103
|
await client.collections().create(schema());
|
|
104
|
+
return true;
|
|
99
105
|
}
|
|
100
106
|
catch (error) {
|
|
101
107
|
if (httpStatus(error) !== 409) {
|
|
102
108
|
throw error;
|
|
103
109
|
}
|
|
110
|
+
// A concurrent caller won the race; the collection exists, but not by us.
|
|
111
|
+
return false;
|
|
104
112
|
}
|
|
105
113
|
}
|
|
106
114
|
/** Take over the lock if its holder has not refreshed it within `ttlMs`. */
|
|
@@ -113,7 +121,7 @@ async function reclaimIfStale(client, name, ttlMs) {
|
|
|
113
121
|
.retrieve());
|
|
114
122
|
}
|
|
115
123
|
catch (error) {
|
|
116
|
-
// Released between our create and this read
|
|
124
|
+
// Released between our create and this read – leave it for the next try.
|
|
117
125
|
if (httpStatus(error) === 404) {
|
|
118
126
|
return false;
|
|
119
127
|
}
|
package/dist/query-compiler.d.ts
CHANGED
|
@@ -1,11 +1,35 @@
|
|
|
1
1
|
import type { SearchParams } from 'typesense/lib/Typesense/Documents.js';
|
|
2
|
-
import { type Filter, type SearchQuery, type SearchType } from '@lde/search';
|
|
2
|
+
import { type Filter, type RootType, type SearchQuery, type SearchType } from '@lde/search';
|
|
3
|
+
/** One hop of a join path, resolved: the Root Type it reaches and the
|
|
4
|
+
* Typesense collection that type is stored in. */
|
|
5
|
+
export interface JoinTarget {
|
|
6
|
+
readonly searchType: RootType;
|
|
7
|
+
readonly collection: string;
|
|
8
|
+
}
|
|
3
9
|
/**
|
|
4
10
|
* Options for {@link buildSearchParams} – the query half of the engine
|
|
5
11
|
* adapter. {@link TypesenseSearchEngineOptions} extends this, so each knob is
|
|
6
12
|
* declared once and the engine forwards its options wholesale.
|
|
7
13
|
*/
|
|
8
14
|
export interface BuildSearchParamsOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Resolves a criterion’s {@link CriterionBase.on} join path to the collection
|
|
17
|
+
* it constrains – the one thing the engine-neutral {@link JoinGraph}
|
|
18
|
+
* deliberately does not answer, because collection naming is engine- and
|
|
19
|
+
* deployment-specific.
|
|
20
|
+
*
|
|
21
|
+
* Called once per **prefix** of a path, so a two-hop
|
|
22
|
+
* `['dataset', 'publisher']` yields the collection of each hop and compiles
|
|
23
|
+
* to the nested `$datasets($publishers(…))`. The declaration comes back with
|
|
24
|
+
* it, because the leaf term is compiled against the *target* type’s field
|
|
25
|
+
* (its kind, its facet flag), not the searched one.
|
|
26
|
+
*
|
|
27
|
+
* Left unset – or returning `undefined` for a path – a joined criterion
|
|
28
|
+
* compiles to nothing and drops out of its clause. Through the engine it can
|
|
29
|
+
* never get that far: `assertValidQuery` rejects an unresolvable path up
|
|
30
|
+
* front.
|
|
31
|
+
*/
|
|
32
|
+
readonly joinTargetFor?: (from: SearchType, path: readonly string[]) => JoinTarget | undefined;
|
|
9
33
|
/**
|
|
10
34
|
* Cap on the number of buckets returned per facet (`max_facet_values`). Left
|
|
11
35
|
* unset, Typesense defaults to 10 – too few for high-cardinality facets
|
|
@@ -31,6 +55,11 @@ export interface BuildSearchParamsOptions {
|
|
|
31
55
|
* so the mapping is asserted directly in unit tests. Field names come from
|
|
32
56
|
* {@link physicalFields}, the same convention the projection and the collection
|
|
33
57
|
* schema use, so a query can never reference a field the index does not carry.
|
|
58
|
+
*
|
|
59
|
+
* A criterion carrying a {@link CriterionBase.on} join path compiles to a
|
|
60
|
+
* nested `$collection(…)` clause – one per hop – which is why the compiler
|
|
61
|
+
* takes {@link BuildSearchParamsOptions.joinTargetFor}: the join graph names
|
|
62
|
+
* the type, the deployment names the collection.
|
|
34
63
|
*/
|
|
35
64
|
export declare function buildSearchParams(query: SearchQuery, searchType: SearchType, options?: BuildSearchParamsOptions): SearchParams<object>;
|
|
36
65
|
/**
|
|
@@ -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,
|
|
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;;;;;;;;OAQG;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;AAsSD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEvD"}
|
package/dist/query-compiler.js
CHANGED
|
@@ -6,13 +6,18 @@ import { fieldNamed, filterOperator, filterOperatorFor, ID_FIELD, isoToUnixSecon
|
|
|
6
6
|
* so the mapping is asserted directly in unit tests. Field names come from
|
|
7
7
|
* {@link physicalFields}, the same convention the projection and the collection
|
|
8
8
|
* schema use, so a query can never reference a field the index does not carry.
|
|
9
|
+
*
|
|
10
|
+
* A criterion carrying a {@link CriterionBase.on} join path compiles to a
|
|
11
|
+
* nested `$collection(…)` clause – one per hop – which is why the compiler
|
|
12
|
+
* takes {@link BuildSearchParamsOptions.joinTargetFor}: the join graph names
|
|
13
|
+
* the type, the deployment names the collection.
|
|
9
14
|
*/
|
|
10
15
|
export function buildSearchParams(query, searchType, options = {}) {
|
|
11
16
|
const folded = query.text !== undefined && query.text.length > 0
|
|
12
17
|
? fold(query.text)
|
|
13
18
|
: undefined;
|
|
14
19
|
const { names, weights } = queryFields(searchType, query.locale);
|
|
15
|
-
const filterBy = compileFilterBy(query.where, searchType, options
|
|
20
|
+
const filterBy = compileFilterBy(query.where, searchType, options);
|
|
16
21
|
const sortBy = query.orderBy
|
|
17
22
|
.map((sort) => compileSort(sort, searchType, query.locale))
|
|
18
23
|
.join(',');
|
|
@@ -97,12 +102,12 @@ function queryFields(searchType, locale) {
|
|
|
97
102
|
}
|
|
98
103
|
/** AND-join the compiled `where` clauses; a clause that compiles to nothing is
|
|
99
104
|
* skipped and reported to `onIgnoredFilter`. */
|
|
100
|
-
function compileFilterBy(where, searchType,
|
|
105
|
+
function compileFilterBy(where, searchType, options) {
|
|
101
106
|
return where
|
|
102
107
|
.map((filter) => {
|
|
103
|
-
const clause = compileFilter(filter, searchType);
|
|
108
|
+
const clause = compileFilter(filter, searchType, options);
|
|
104
109
|
if (clause === undefined) {
|
|
105
|
-
onIgnoredFilter?.(filter);
|
|
110
|
+
options.onIgnoredFilter?.(filter);
|
|
106
111
|
}
|
|
107
112
|
return clause;
|
|
108
113
|
})
|
|
@@ -132,10 +137,10 @@ function compileFilterBy(where, searchType, onIgnoredFilter) {
|
|
|
132
137
|
* A clause left with no terms either way is skipped and reported to
|
|
133
138
|
* `onIgnoredFilter`.
|
|
134
139
|
*/
|
|
135
|
-
function compileFilter(filter, searchType) {
|
|
140
|
+
function compileFilter(filter, searchType, options) {
|
|
136
141
|
const terms = [];
|
|
137
142
|
for (const criterion of filter.or) {
|
|
138
|
-
const outcome = compileCriterion(criterion, searchType);
|
|
143
|
+
const outcome = compileCriterion(criterion, searchType, options);
|
|
139
144
|
if (outcome === VACUOUS) {
|
|
140
145
|
return undefined;
|
|
141
146
|
}
|
|
@@ -158,7 +163,40 @@ const UNUSABLE = Symbol('unusable');
|
|
|
158
163
|
* and cannot be recovered from a bare `undefined`, so it is returned rather than
|
|
159
164
|
* re-derived by a second pass over the same rules.
|
|
160
165
|
*/
|
|
161
|
-
function compileCriterion(criterion, searchType) {
|
|
166
|
+
function compileCriterion(criterion, searchType, options) {
|
|
167
|
+
const on = criterion.on ?? [];
|
|
168
|
+
if (on.length === 0) {
|
|
169
|
+
return compileLeaf(criterion, searchType);
|
|
170
|
+
}
|
|
171
|
+
// A joined criterion constrains a document in ANOTHER collection, so the leaf
|
|
172
|
+
// is compiled against the type that path reaches and then wrapped, one
|
|
173
|
+
// `$collection(…)` per hop, outermost hop first:
|
|
174
|
+
// `['dataset', 'publisher']` → `$datasets($publishers(id:=…))`.
|
|
175
|
+
const collections = [];
|
|
176
|
+
let target = searchType;
|
|
177
|
+
for (let hop = 1; hop <= on.length; hop++) {
|
|
178
|
+
const resolved = options.joinTargetFor?.(searchType, on.slice(0, hop));
|
|
179
|
+
if (resolved === undefined) {
|
|
180
|
+
// An unresolvable path is a criterion that cannot be compiled at all –
|
|
181
|
+
// false, so it drops out and its siblings still stand. Through the
|
|
182
|
+
// engine, `assertValidQuery` has already rejected it.
|
|
183
|
+
return UNUSABLE;
|
|
184
|
+
}
|
|
185
|
+
collections.push(resolved.collection);
|
|
186
|
+
target = resolved.searchType;
|
|
187
|
+
}
|
|
188
|
+
const leaf = compileLeaf(criterion, target);
|
|
189
|
+
// A vacuous leaf states no constraint on the referent, so the join as a whole
|
|
190
|
+
// states none either – the reading, and so the outcome, passes straight
|
|
191
|
+
// through the hops.
|
|
192
|
+
if (leaf === VACUOUS || leaf === UNUSABLE) {
|
|
193
|
+
return leaf;
|
|
194
|
+
}
|
|
195
|
+
return collections.reduceRight((inner, collection) => `$${collection}(${inner})`, leaf);
|
|
196
|
+
}
|
|
197
|
+
/** The criterion’s own term, against the type it actually constrains – the
|
|
198
|
+
* searched type, or the one its join path reaches. */
|
|
199
|
+
function compileLeaf(criterion, searchType) {
|
|
162
200
|
// `id` is the Typesense document key, not a declared field. Exact `:=`
|
|
163
201
|
// membership, like a non-facet field ({@link compileMembership}), so an IRI
|
|
164
202
|
// cannot partial-match on a shared path segment. (`fetchLabels` resolves
|
|
@@ -16,25 +16,27 @@ export interface RebuildOptions extends CollectionDefinitionOptions {
|
|
|
16
16
|
export interface ResolvedRebuildOptions {
|
|
17
17
|
readonly batchSize: number;
|
|
18
18
|
readonly lockTtlMs: number;
|
|
19
|
-
/** The collection-definition options with
|
|
20
|
-
*
|
|
21
|
-
* definition it creates
|
|
19
|
+
/** The collection-definition options with `collectionNameFor` resolved to a
|
|
20
|
+
* total function – the one place the naming convention lives, so the
|
|
21
|
+
* collection a writer talks to, the definition it creates and the peer
|
|
22
|
+
* collections its references name cannot say different things. */
|
|
22
23
|
readonly definitionOptions: CollectionDefinitionOptions & {
|
|
23
|
-
readonly
|
|
24
|
+
readonly collectionNameFor: (searchType: SearchType) => string;
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
/**
|
|
27
28
|
* Apply the shared defaults, once, so neither writer restates them – including
|
|
28
|
-
* the collection
|
|
29
|
+
* the collection naming, derived from each type ({@link deriveCollectionName})
|
|
29
30
|
* when the deployment supplies none.
|
|
30
31
|
*
|
|
31
32
|
* Writers resolve at construction rather than per run, so an underivable name
|
|
32
33
|
* throws when the writer is built, not on the first rebuild. The collection
|
|
33
34
|
* definition is built here and discarded for the same reason: every way a
|
|
34
35
|
* declaration can fail to describe a collection – an unresolvable surfaced
|
|
35
|
-
* inline reference, whose nesting needs the schema
|
|
36
|
-
*
|
|
37
|
-
*
|
|
36
|
+
* inline reference, whose nesting needs the schema, or a joinable reference
|
|
37
|
+
* whose target it resolves the same way – then fails at construction rather
|
|
38
|
+
* than inside the first run, after a lock is held and a whole extraction has
|
|
39
|
+
* been paid for.
|
|
38
40
|
*/
|
|
39
41
|
export declare function resolveRebuildOptions(searchType: SearchType, options: RebuildOptions): ResolvedRebuildOptions;
|
|
40
42
|
/**
|
|
@@ -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,UAAU,EAAE,MAAM,aAAa,CAAC;AAO9C,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;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,8BAA8B,CAC5C,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE;IAAE,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAA;CAAE,GAC9C,IAAI,CA0BN;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,20 +1,21 @@
|
|
|
1
|
-
import { datasetField, isInternalField } from '@lde/search/adapter';
|
|
1
|
+
import { datasetField, isInternalField, joinGraph, 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';
|
|
5
5
|
import { DEFAULT_BATCH_SIZE } from './import.js';
|
|
6
6
|
/**
|
|
7
7
|
* Apply the shared defaults, once, so neither writer restates them – including
|
|
8
|
-
* the collection
|
|
8
|
+
* the collection naming, derived from each type ({@link deriveCollectionName})
|
|
9
9
|
* when the deployment supplies none.
|
|
10
10
|
*
|
|
11
11
|
* Writers resolve at construction rather than per run, so an underivable name
|
|
12
12
|
* throws when the writer is built, not on the first rebuild. The collection
|
|
13
13
|
* definition is built here and discarded for the same reason: every way a
|
|
14
14
|
* declaration can fail to describe a collection – an unresolvable surfaced
|
|
15
|
-
* inline reference, whose nesting needs the schema
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* inline reference, whose nesting needs the schema, or a joinable reference
|
|
16
|
+
* whose target it resolves the same way – then fails at construction rather
|
|
17
|
+
* than inside the first run, after a lock is held and a whole extraction has
|
|
18
|
+
* been paid for.
|
|
18
19
|
*/
|
|
19
20
|
export function resolveRebuildOptions(searchType, options) {
|
|
20
21
|
const { batchSize = DEFAULT_BATCH_SIZE, lockTtlMs = DEFAULT_LOCK_TTL_MS, ...definitionOptions } = options;
|
|
@@ -23,12 +24,41 @@ export function resolveRebuildOptions(searchType, options) {
|
|
|
23
24
|
lockTtlMs,
|
|
24
25
|
definitionOptions: {
|
|
25
26
|
...definitionOptions,
|
|
26
|
-
|
|
27
|
+
collectionNameFor: definitionOptions.collectionNameFor ?? deriveCollectionName,
|
|
27
28
|
},
|
|
28
29
|
};
|
|
30
|
+
assertDistinctJoinTargetNames(searchType, resolved.definitionOptions);
|
|
29
31
|
buildCollectionDefinition(searchType, resolved.definitionOptions);
|
|
30
32
|
return resolved;
|
|
31
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Reject a `collectionNameFor` that gives a join target the same name as the
|
|
36
|
+
* type doing the joining.
|
|
37
|
+
*
|
|
38
|
+
* The trap is the obvious migration from the `name` option this replaced:
|
|
39
|
+
* `name: 'staging_works'` reads as `collectionNameFor: () => 'staging_works'`,
|
|
40
|
+
* a constant function – which now also names every peer, so the emitted
|
|
41
|
+
* reference points the collection at ITSELF. Nothing downstream complains:
|
|
42
|
+
* Typesense accepts the self-reference and every join through it then answers
|
|
43
|
+
* nothing.
|
|
44
|
+
*
|
|
45
|
+
* Only the (type, target) pairs this type actually joins to are checked, so a
|
|
46
|
+
* deployment deliberately sharing one collection between unrelated types – the
|
|
47
|
+
* several-label-sources-in-one-`labels` case – stays allowed.
|
|
48
|
+
*/
|
|
49
|
+
function assertDistinctJoinTargetNames(searchType, options) {
|
|
50
|
+
const { schema, collectionNameFor } = options;
|
|
51
|
+
if (schema === undefined) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const own = collectionNameFor(searchType);
|
|
55
|
+
for (const field of referenceFields(searchType)) {
|
|
56
|
+
const target = joinGraph(schema).resolve(searchType, [field.name]);
|
|
57
|
+
if (target !== undefined && collectionNameFor(target) === own) {
|
|
58
|
+
throw new Error(`The collection naming for “${searchType.name}” gives its join target “${target.name}” the same collection “${own}”, so the reference on “${field.name}” would point the collection at itself and every join through it would answer nothing. A constant “collectionNameFor” does this – derive the name from the type it is given (e.g. \`(type) => \`prefix_\${deriveCollectionName(type)}\`\`).`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
32
62
|
/**
|
|
33
63
|
* Reject a {@link SearchType} that declares any of the bookkeeping field names
|
|
34
64
|
* a writer stamps, so a domain field can never collide with `source` /
|
package/dist/search.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { type BuildSearchParamsOptions } from './query-compiler.js';
|
|
|
6
6
|
* wholesale into each search. Reference labels resolve per field from the
|
|
7
7
|
* collection of the SearchType its `labelSource` names – a label source is
|
|
8
8
|
* just another type, so it is named the same way. */
|
|
9
|
-
export interface TypesenseSearchEngineOptions<TypeName extends string = string> extends BuildSearchParamsOptions {
|
|
9
|
+
export interface TypesenseSearchEngineOptions<TypeName extends string = string> extends Omit<BuildSearchParamsOptions, 'joinTargetFor'> {
|
|
10
10
|
/**
|
|
11
11
|
* Overrides the collection (or alias) a search type reads, keyed by the
|
|
12
12
|
* type’s `name`. Every type not named here reads the collection derived from
|
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":"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;AAgBrB,OAAO,EAGL,KAAK,wBAAwB,EAC9B,MAAM,qBAAqB,CAAC;AAG7B;;;;sDAIsD;AACtD,MAAM,WAAW,4BAA4B,CAC3C,QAAQ,SAAS,MAAM,GAAG,MAAM,CAIhC,SAAQ,IAAI,CAAC,wBAAwB,EAAE,eAAe,CAAC;IACvD;;;;;;;;OAQG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;IACnE;;;OAGG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACjD;;;;;;;OAOG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CACnC;AAED;;;GAGG;AACH,MAAM,WAAW,qBAAqB,CACpC,KAAK,SAAS,SAAS,UAAU,EAAE,GAAG,SAAS,UAAU,EAAE,CAC3D,SAAQ,YAAY,CAAC,KAAK,CAAC;IAC3B;;;;;;;OAOG;IACH,iBAAiB,CAAC,UAAU,EAAE,UAAU,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC;CAC1D;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,CAAC,KAAK,SAAS,SAAS,UAAU,EAAE,EAEzC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,YAAY,CAAC,KAAK,CAAC,EAC3B,OAAO,GAAE,4BAA4B,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAM,GAChE,qBAAqB,CAAC,KAAK,CAAC,CAmV9B;AAyCD,8EAA8E;AAC9E,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AA0HD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,aAAa,CAAC,EACnC,MAAM,EAAE,SAAS,gBAAgB,EAAE,EACnC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAC/B,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAgDtC;AA6BD,oEAAoE;AACpE,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS;QAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,EAAE,CAAC;IAC1E,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS;QAC/B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;QAC5B,QAAQ,CAAC,MAAM,EAAE,SAAS;YACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;YACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;SACxB,EAAE,CAAC;KACL,EAAE,CAAC;CACL;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,uBAAuB,EACjC,UAAU,EAAE,UAAU,EACtB,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,cAAc,CAAC,EAC3C,MAAM,EAAE,YAAY,GACnB,YAAY,CA0Cd"}
|
package/dist/search.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertTypeInSchema, assertValidQuery, displayLangOf, fieldNamed, isInlineReference, isRangeFacet, isUnsatisfiable, labelFieldOf, nestedReferenceType, outputFields, physicalFields, referenceFields, } from '@lde/search/adapter';
|
|
1
|
+
import { assertTypeInSchema, assertValidQuery, displayLangOf, fieldNamed, isInlineReference, isRangeFacet, isUnsatisfiable, joinGraph, 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
|
/**
|
|
@@ -39,6 +39,25 @@ export function createTypesenseSearchEngine(client, schema, options = {}) {
|
|
|
39
39
|
searchType.class,
|
|
40
40
|
collection,
|
|
41
41
|
]));
|
|
42
|
+
// The schema's join graph, built once (it is cached per schema anyway), plus
|
|
43
|
+
// the collection half the graph deliberately leaves to the adapter. Together
|
|
44
|
+
// they are what lets a `where` criterion carry an `on` path: validation
|
|
45
|
+
// resolves the path to a type, the compiler turns each hop into
|
|
46
|
+
// `$collection(…)`.
|
|
47
|
+
const joins = joinGraph(schema);
|
|
48
|
+
const searchParamsOptions = {
|
|
49
|
+
...options,
|
|
50
|
+
joinTargetFor: (from, path) => {
|
|
51
|
+
// Both lookups always hit: `assertValidQuery` runs first and rejects
|
|
52
|
+
// every path this could not resolve, and each type resolved a collection
|
|
53
|
+
// above.
|
|
54
|
+
const target = joins.resolve(from, path);
|
|
55
|
+
return {
|
|
56
|
+
searchType: target,
|
|
57
|
+
collection: collections.get(target.class),
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
};
|
|
42
61
|
// Resolve every reference field's label source ONCE at construction. The
|
|
43
62
|
// schema already guarantees the named type exists and serves labels
|
|
44
63
|
// (`searchSchema`/`labelFieldOf`), and every type resolved a collection
|
|
@@ -175,14 +194,14 @@ export function createTypesenseSearchEngine(client, schema, options = {}) {
|
|
|
175
194
|
// invalid query (unknown field, wrong operator, unknown facet) are both
|
|
176
195
|
// rejected up front, for EVERY caller.
|
|
177
196
|
assertTypeInSchema(schema, searchType);
|
|
178
|
-
assertValidQuery(query, searchType);
|
|
197
|
+
assertValidQuery(query, searchType, joins);
|
|
179
198
|
// Asks for no document (an empty `id` membership): answer it without a
|
|
180
199
|
// round-trip rather than dispatching a query whose only filter compiles
|
|
181
200
|
// away, which would hand back the whole collection.
|
|
182
201
|
if (isUnsatisfiable(query)) {
|
|
183
202
|
return { total: 0, hits: [], facets: {} };
|
|
184
203
|
}
|
|
185
|
-
const params = buildSearchParams(query, searchType,
|
|
204
|
+
const params = buildSearchParams(query, searchType, searchParamsOptions);
|
|
186
205
|
const cachedLabelsPromise = startCachedLabels(searchType);
|
|
187
206
|
// ONE search, but through `multi_search` (POST) like the facet batch and
|
|
188
207
|
// the label lookup: `documents().search()` is a GET, so a long
|
|
@@ -197,7 +216,7 @@ export function createTypesenseSearchEngine(client, schema, options = {}) {
|
|
|
197
216
|
async searchFacets(searchType, queries) {
|
|
198
217
|
assertTypeInSchema(schema, searchType);
|
|
199
218
|
for (const query of queries) {
|
|
200
|
-
assertValidQuery(query, searchType);
|
|
219
|
+
assertValidQuery(query, searchType, joins);
|
|
201
220
|
}
|
|
202
221
|
if (queries.length === 0) {
|
|
203
222
|
return [];
|
|
@@ -222,7 +241,7 @@ export function createTypesenseSearchEngine(client, schema, options = {}) {
|
|
|
222
241
|
const { results } = (await client.multiSearch.perform({
|
|
223
242
|
searches: dispatched.map(({ query }) => ({
|
|
224
243
|
collection,
|
|
225
|
-
...buildSearchParams({ ...query, orderBy: [], limit: 0, offset: 0 }, searchType,
|
|
244
|
+
...buildSearchParams({ ...query, orderBy: [], limit: 0, offset: 0 }, searchType, searchParamsOptions),
|
|
226
245
|
})),
|
|
227
246
|
}));
|
|
228
247
|
// One label lookup serves every successful facet result in the batch.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lde/search-typesense",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.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.20.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.
|
|
38
|
+
"@lde/pipeline": "^0.36.0"
|
|
39
39
|
}
|
|
40
40
|
}
|