@lde/search-typesense 0.27.4 → 0.29.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 +7 -8
- package/dist/blue-green-rebuild.d.ts.map +1 -1
- package/dist/blue-green-rebuild.js +27 -41
- package/dist/collection-definition.d.ts +17 -12
- package/dist/collection-definition.d.ts.map +1 -1
- package/dist/collection-definition.js +253 -54
- package/dist/documents.d.ts +147 -0
- package/dist/documents.d.ts.map +1 -0
- package/dist/documents.js +529 -0
- package/dist/in-place-rebuild.d.ts +30 -35
- package/dist/in-place-rebuild.d.ts.map +1 -1
- package/dist/in-place-rebuild.js +56 -128
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/lookup.d.ts +16 -7
- package/dist/lookup.d.ts.map +1 -1
- package/dist/lookup.js +49 -7
- package/dist/query-compiler.d.ts.map +1 -1
- package/dist/query-compiler.js +141 -23
- package/dist/rebuild-support.d.ts +1 -41
- package/dist/rebuild-support.d.ts.map +1 -1
- package/dist/rebuild-support.js +3 -76
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +62 -10
- package/package.json +4 -4
- package/dist/import.d.ts +0 -31
- package/dist/import.d.ts.map +0 -1
- package/dist/import.js +0 -60
- package/dist/sweep.d.ts +0 -70
- package/dist/sweep.d.ts.map +0 -1
- package/dist/sweep.js +0 -113
|
@@ -19,11 +19,14 @@ export type BlueGreenRebuildOptions = RebuildOptions;
|
|
|
19
19
|
* creates the versioned collection (`${name}_<timestamp>`) with the schema
|
|
20
20
|
* derived from the {@link SearchType}.
|
|
21
21
|
* - `write` streams documents into the fresh collection, batched across write
|
|
22
|
-
* calls, each
|
|
22
|
+
* calls, each recording the dataset it came from ({@link openDocuments}).
|
|
23
23
|
* - `flush` on a **failed** dataset, and `reset` (the pipeline’s dump-fallback
|
|
24
|
-
* discard), roll that dataset
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* discard), roll that dataset back out of the not-yet-live collection, so a
|
|
25
|
+
* swap never ships a half-processed dataset; a successful flush leaves the
|
|
26
|
+
* streamed documents in place. On a canonically keyed collection the
|
|
27
|
+
* rollback drops the failed dataset’s *membership*, so a document another
|
|
28
|
+
* dataset also contributed stays – with a reference from that dataset that
|
|
29
|
+
* still resolves.
|
|
27
30
|
* - `commit` imports the remainder, atomically repoints the `name` alias to
|
|
28
31
|
* the new collection, drops the collection it superseded, and releases the
|
|
29
32
|
* lock. Until commit, the live alias never points at a partial build.
|
|
@@ -50,10 +53,6 @@ export declare class BlueGreenRebuild<TDocument extends {
|
|
|
50
53
|
*/
|
|
51
54
|
readonly collectionName: string;
|
|
52
55
|
private readonly resolved;
|
|
53
|
-
/** The column this collection carries its documents’ dataset IRI in: the
|
|
54
|
-
* type’s declared dataset field, or the private `source` when it declares
|
|
55
|
-
* none. Both the stamp and the per-dataset rollback filter read it. */
|
|
56
|
-
private readonly sourceField;
|
|
57
56
|
constructor(client: Client, searchType: SearchType, options?: BlueGreenRebuildOptions);
|
|
58
57
|
openRun(context: RunContext): Promise<RunWriter<TDocument>>;
|
|
59
58
|
/** The collection an alias currently points at, or `undefined` if unset. */
|
|
@@ -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;
|
|
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;AASvB,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,sBAAsB,CAAC;AAE9B;;yDAEyD;AACzD,MAAM,MAAM,uBAAuB,GAAG,cAAc,CAAC;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,qBAAa,gBAAgB,CAC3B,SAAS,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAChC,YAAW,MAAM,CAAC,SAAS,CAAC;IAa1B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAb7B;;;;;;;OAOG;IACH,SAAgB,cAAc,EAAE,MAAM,CAAC;IACvC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyB;gBAG/B,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,UAAU,EACvC,OAAO,GAAE,uBAA4B;IAUjC,OAAO,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IAsGjE,4EAA4E;YAC9D,WAAW;CAW1B"}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { buildCollectionDefinition } from './collection-definition.js';
|
|
2
|
-
import { BatchImporter } from './import.js';
|
|
3
2
|
import { httpStatus, openLockedRun, releaseLock } from './lock.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { assertWritableType, openDocuments, withBookkeeping, } from './documents.js';
|
|
4
|
+
import { resolveRebuildOptions, } from './rebuild-support.js';
|
|
6
5
|
/**
|
|
7
6
|
* Blue/green Rebuild (build a fresh index alongside the live one, then swap to
|
|
8
7
|
* it atomically) as a transactional `Writer`: each run builds the search
|
|
@@ -16,11 +15,14 @@ import { assertNoReservedFields, assertSweepableProvenanceField, deleteByFilter,
|
|
|
16
15
|
* creates the versioned collection (`${name}_<timestamp>`) with the schema
|
|
17
16
|
* derived from the {@link SearchType}.
|
|
18
17
|
* - `write` streams documents into the fresh collection, batched across write
|
|
19
|
-
* calls, each
|
|
18
|
+
* calls, each recording the dataset it came from ({@link openDocuments}).
|
|
20
19
|
* - `flush` on a **failed** dataset, and `reset` (the pipeline’s dump-fallback
|
|
21
|
-
* discard), roll that dataset
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* discard), roll that dataset back out of the not-yet-live collection, so a
|
|
21
|
+
* swap never ships a half-processed dataset; a successful flush leaves the
|
|
22
|
+
* streamed documents in place. On a canonically keyed collection the
|
|
23
|
+
* rollback drops the failed dataset’s *membership*, so a document another
|
|
24
|
+
* dataset also contributed stays – with a reference from that dataset that
|
|
25
|
+
* still resolves.
|
|
24
26
|
* - `commit` imports the remainder, atomically repoints the `name` alias to
|
|
25
27
|
* the new collection, drops the collection it superseded, and releases the
|
|
26
28
|
* lock. Until commit, the live alias never points at a partial build.
|
|
@@ -45,19 +47,12 @@ export class BlueGreenRebuild {
|
|
|
45
47
|
*/
|
|
46
48
|
collectionName;
|
|
47
49
|
resolved;
|
|
48
|
-
/** The column this collection carries its documents’ dataset IRI in: the
|
|
49
|
-
* type’s declared dataset field, or the private `source` when it declares
|
|
50
|
-
* none. Both the stamp and the per-dataset rollback filter read it. */
|
|
51
|
-
sourceField;
|
|
52
50
|
constructor(client, searchType, options = {}) {
|
|
53
51
|
this.client = client;
|
|
54
52
|
this.searchType = searchType;
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// indexed ones, so a declared field need not be facetable here.
|
|
59
|
-
assertSweepableProvenanceField(searchType, { requireFacetable: false });
|
|
60
|
-
this.sourceField = provenanceField(searchType);
|
|
53
|
+
// Rollback drops a known dataset rather than enumerating the indexed ones,
|
|
54
|
+
// so a declared dataset field need not be facetable here.
|
|
55
|
+
assertWritableType(searchType, { requireFacetable: false });
|
|
61
56
|
this.resolved = resolveRebuildOptions(searchType, options);
|
|
62
57
|
this.collectionName =
|
|
63
58
|
this.resolved.definitionOptions.collectionNameFor(searchType);
|
|
@@ -65,7 +60,6 @@ export class BlueGreenRebuild {
|
|
|
65
60
|
async openRun(context) {
|
|
66
61
|
const { batchSize, lockTtlMs, definitionOptions } = this.resolved;
|
|
67
62
|
const name = this.collectionName;
|
|
68
|
-
const sourceField = this.sourceField;
|
|
69
63
|
return openLockedRun(this.client, name, lockTtlMs, async () => {
|
|
70
64
|
// Create the fresh (blue) collection up front, so a failure surfaces
|
|
71
65
|
// before any dataset is processed. startedAt orders the versioned names;
|
|
@@ -87,30 +81,22 @@ export class BlueGreenRebuild {
|
|
|
87
81
|
...definitionOptions,
|
|
88
82
|
collectionNameFor: versioned,
|
|
89
83
|
});
|
|
90
|
-
await this.client
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
? [{ name: SOURCE_FIELD, type: 'string' }]
|
|
99
|
-
: []),
|
|
100
|
-
],
|
|
84
|
+
await this.client
|
|
85
|
+
.collections()
|
|
86
|
+
.create(withBookkeeping(definition, this.searchType));
|
|
87
|
+
const documents = openDocuments(this.client, collection, {
|
|
88
|
+
searchType: this.searchType,
|
|
89
|
+
runId: context.runId,
|
|
90
|
+
startedAt: context.startedAt,
|
|
91
|
+
batchSize,
|
|
101
92
|
});
|
|
102
|
-
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
const rollback = async (dataset) =>
|
|
107
|
-
await importer.flush();
|
|
108
|
-
await deleteByFilter(this.client, collection, sourceDocumentsFilter(sourceField, dataset.iri.toString()));
|
|
109
|
-
};
|
|
93
|
+
// Roll a dataset back out of the not-yet-live collection (a failed
|
|
94
|
+
// dataset, or a reset before the dump re-run). On a keyed collection
|
|
95
|
+
// that drops its membership rather than the documents themselves, so a
|
|
96
|
+
// dataset that succeeded keeps the ones it also contributed.
|
|
97
|
+
const rollback = async (dataset) => documents.dropAll(dataset);
|
|
110
98
|
return {
|
|
111
|
-
write: async (dataset,
|
|
112
|
-
[sourceField]: dataset.iri.toString(),
|
|
113
|
-
})),
|
|
99
|
+
write: async (dataset, items) => documents.add(dataset, items),
|
|
114
100
|
flush: async (dataset, outcome) => {
|
|
115
101
|
// A successful dataset keeps its streamed documents (they go live at
|
|
116
102
|
// the swap); a failed one is rolled back so the swap never ships it.
|
|
@@ -120,7 +106,7 @@ export class BlueGreenRebuild {
|
|
|
120
106
|
},
|
|
121
107
|
reset: async (dataset) => rollback(dataset),
|
|
122
108
|
commit: async () => {
|
|
123
|
-
await
|
|
109
|
+
await documents.flush();
|
|
124
110
|
// The alias swap is the commit point: once it lands the new
|
|
125
111
|
// collection is live. Everything after it is best-effort cleanup that
|
|
126
112
|
// must NOT fail the commit – a post-swap rejection would otherwise
|
|
@@ -19,11 +19,11 @@ export interface CollectionDefinitionOptions {
|
|
|
19
19
|
*/
|
|
20
20
|
readonly collectionNameFor?: (searchType: SearchType) => string;
|
|
21
21
|
/**
|
|
22
|
-
* The Search Schema the type belongs to – required when the type
|
|
23
|
-
* inline reference (whose nested fields are declared from the
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
22
|
+
* The Search Schema the type belongs to – required when the type carries an
|
|
23
|
+
* inline reference (whose nested fields are declared from the reference type
|
|
24
|
+
* the schema resolves), or declares a joinable reference (whose target
|
|
25
|
+
* collection the schema’s {@link joinGraph} resolves).
|
|
26
|
+
* 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
29
|
*
|
|
@@ -82,13 +82,18 @@ export interface CollectionDefinitionOptions {
|
|
|
82
82
|
* entirely: they exist only as a projection-time reading device for the
|
|
83
83
|
* derives, so the collection stores nothing for them.
|
|
84
84
|
*
|
|
85
|
-
*
|
|
86
|
-
* `object
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
85
|
+
* An inline reference is stored as a **nested object** – `object` or
|
|
86
|
+
* `object[]` – with one nested Physical Field per field of the type it nests
|
|
87
|
+
* ({@link nestedFieldName}), which turns on `enable_nested_fields` for the
|
|
88
|
+
* collection. A nested field is `index: false` unless it declares a query Role,
|
|
89
|
+
* so a display-only nesting stays disk weight and costs no RAM, exactly like
|
|
90
|
+
* the display labels; one that opts into `filterable` or `searchable` is
|
|
91
|
+
* indexed, and its parent object with it (see {@link nestedFields}).
|
|
92
|
+
*
|
|
93
|
+
* An inline reference declaring `filterable` or `facetable` also emits its
|
|
94
|
+
* {@link ReferenceStrategy.identity identity companion} – a flat id field the
|
|
95
|
+
* engine can filter and facet, which a nested object is not
|
|
96
|
+
* ({@link identityCompanionFields}).
|
|
92
97
|
*/
|
|
93
98
|
export declare function buildCollectionDefinition(searchType: SearchType, options?: CollectionDefinitionOptions): CollectionCreateSchema;
|
|
94
99
|
//# sourceMappingURL=collection-definition.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection-definition.d.ts","sourceRoot":"","sources":["../src/collection-definition.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AAExD,OAAO,
|
|
1
|
+
{"version":3,"file":"collection-definition.d.ts","sourceRoot":"","sources":["../src/collection-definition.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AAExD,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,UAAU,EAChB,MAAM,aAAa,CAAC;AAcrB,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,UAAU,EACtB,OAAO,GAAE,2BAAgC,GACxC,sBAAsB,CA2BxB"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { displayFieldPattern, ID_FIELD, isInlineReference, isInternalField, joinGraph, nestedFieldName, nestedReferenceType, physicalFields, } from '@lde/search/adapter';
|
|
1
|
+
import { displayFieldPattern, ID_FIELD, isInlineReference, isInternalField, joinGraph, localLookupTypeOf, 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
|
|
@@ -36,13 +36,18 @@ import { deriveCollectionName } from './collection-name.js';
|
|
|
36
36
|
* entirely: they exist only as a projection-time reading device for the
|
|
37
37
|
* derives, so the collection stores nothing for them.
|
|
38
38
|
*
|
|
39
|
-
*
|
|
40
|
-
* `object
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
39
|
+
* An inline reference is stored as a **nested object** – `object` or
|
|
40
|
+
* `object[]` – with one nested Physical Field per field of the type it nests
|
|
41
|
+
* ({@link nestedFieldName}), which turns on `enable_nested_fields` for the
|
|
42
|
+
* collection. A nested field is `index: false` unless it declares a query Role,
|
|
43
|
+
* so a display-only nesting stays disk weight and costs no RAM, exactly like
|
|
44
|
+
* the display labels; one that opts into `filterable` or `searchable` is
|
|
45
|
+
* indexed, and its parent object with it (see {@link nestedFields}).
|
|
46
|
+
*
|
|
47
|
+
* An inline reference declaring `filterable` or `facetable` also emits its
|
|
48
|
+
* {@link ReferenceStrategy.identity identity companion} – a flat id field the
|
|
49
|
+
* engine can filter and facet, which a nested object is not
|
|
50
|
+
* ({@link identityCompanionFields}).
|
|
46
51
|
*/
|
|
47
52
|
export function buildCollectionDefinition(searchType, options = {}) {
|
|
48
53
|
const { defaultLocale, collectionNameFor = deriveCollectionName } = options;
|
|
@@ -64,16 +69,19 @@ export function buildCollectionDefinition(searchType, options = {}) {
|
|
|
64
69
|
return collection;
|
|
65
70
|
}
|
|
66
71
|
/**
|
|
67
|
-
* The
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
72
|
+
* The type a field nests, or `undefined` when it nests nothing. Throws when the
|
|
73
|
+
* type carries an inline reference the caller gave no schema to resolve: the
|
|
74
|
+
* collection would silently store the reference as a string the projection
|
|
75
|
+
* never writes, so every document would fail to import.
|
|
71
76
|
*/
|
|
72
77
|
function nestedTypeOf(searchType, field, schema) {
|
|
73
|
-
const nested = schema === undefined
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
const nested = schema === undefined
|
|
79
|
+
? undefined
|
|
80
|
+
: (nestedReferenceType(schema, field) ??
|
|
81
|
+
localLookupTypeOf(field, schema));
|
|
82
|
+
// Reached only for a field carrying a Role, so an inline reference here
|
|
83
|
+
// stores entries: a Role-less one is an internal reading device, pruned
|
|
84
|
+
// before the writer.
|
|
77
85
|
if (nested === undefined && isInlineReference(field)) {
|
|
78
86
|
throw new Error(`Building the collection for “${searchType.name}” needs the search schema its surfaced inline reference “${field.name}” resolves against; pass it as the collection-definition option “schema”.`);
|
|
79
87
|
}
|
|
@@ -136,7 +144,10 @@ function typesenseFields(searchType, field, defaultLocale, defaultSortingField,
|
|
|
136
144
|
}
|
|
137
145
|
const nested = nestedTypeOf(searchType, field, schema);
|
|
138
146
|
if (nested !== undefined) {
|
|
139
|
-
return
|
|
147
|
+
return [
|
|
148
|
+
...nestedFields(field.name, field, nested, schema, defaultLocale),
|
|
149
|
+
...identityCompanionFields(field, schema),
|
|
150
|
+
];
|
|
140
151
|
}
|
|
141
152
|
const names = physicalFields(field, schema);
|
|
142
153
|
if (field.kind === 'text') {
|
|
@@ -223,53 +234,217 @@ function typesenseFields(searchType, field, defaultLocale, defaultSortingField,
|
|
|
223
234
|
return fields;
|
|
224
235
|
}
|
|
225
236
|
/**
|
|
226
|
-
* The nested object one
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
237
|
+
* The nested object one inline reference stores, plus one nested Physical Field
|
|
238
|
+
* per field of the type it nests – recursing for a nested field that itself
|
|
239
|
+
* nests (another inline reference, or a
|
|
240
|
+
* {@link ReferenceStrategy.local local} lookup, which stores its endpoint
|
|
241
|
+
* shaped by the target’s own declaration). `searchSchema` rejects inline
|
|
242
|
+
* cycles, so the recursion terminates.
|
|
231
243
|
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
244
|
+
* **A nested field is indexed exactly when it declares a query Role.** One that
|
|
245
|
+
* declares only `output` keeps `index: false`: stored with its referent, read
|
|
246
|
+
* back with it, costing disk rather than RAM – the same lever the per-language
|
|
247
|
+
* display labels use. `filterable` or `searchable` turns indexing on for that
|
|
248
|
+
* one field, and nothing else.
|
|
249
|
+
*
|
|
250
|
+
* **The parent object must be indexed whenever any descendant is.** Typesense
|
|
251
|
+
* silently ignores an indexed child under an `index: false` parent: no error,
|
|
252
|
+
* no match, every query answering empty. So the parent’s flag is computed from
|
|
253
|
+
* the children rather than declared, and this is the one place that invariant
|
|
254
|
+
* can be got wrong.
|
|
240
255
|
*/
|
|
241
|
-
function nestedFields(prefix, reference,
|
|
256
|
+
function nestedFields(prefix, reference, nestedType, schema, defaultLocale, withinArray = false, onPath = new Set()) {
|
|
242
257
|
const array = reference.array === true;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
258
|
+
// A type already on this path is not descended into again. `searchSchema`
|
|
259
|
+
// rejects INLINE cycles, but a {@link ReferenceStrategy.local local} lookup
|
|
260
|
+
// may reach a Root Type that reaches back – `Work → Person → Work` is a
|
|
261
|
+
// reasonable schema and nothing forbids it – and without this the builder
|
|
262
|
+
// recurses until the stack gives out. Stopping here matches where the FRAME
|
|
263
|
+
// stops ({@link inlineFramingDepth}), so the collection declares exactly the
|
|
264
|
+
// depth the projection can fill.
|
|
265
|
+
const walked = new Set(onPath).add(nestedType.name);
|
|
266
|
+
// Typesense flattens a nested object, so a value under a multi-valued
|
|
267
|
+
// ancestor arrives as an array however single-valued the declaration is.
|
|
268
|
+
// That only matters for an INDEXED field, whose declared type is checked
|
|
269
|
+
// against what is stored; an unindexed one is stored as-is.
|
|
270
|
+
const flattensToArray = withinArray || array;
|
|
271
|
+
const children = [];
|
|
272
|
+
// A `local` lookup nests its endpoint’s own document, which carries the `id`
|
|
273
|
+
// that identifies it. The target type does not declare `id` – no type
|
|
274
|
+
// declares its own key – so it is added here or it would be the one stored
|
|
275
|
+
// value the collection never mentions.
|
|
276
|
+
if (nestedType.class !== undefined) {
|
|
277
|
+
children.push({
|
|
278
|
+
name: nestedFieldName(prefix, 'id'),
|
|
279
|
+
type: flattensToArray ? 'string[]' : 'string',
|
|
247
280
|
index: false,
|
|
248
|
-
optional:
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
for (const field of
|
|
281
|
+
optional: true,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
for (const field of nestedType.fields) {
|
|
252
285
|
if (isInternalField(field)) {
|
|
253
286
|
continue;
|
|
254
287
|
}
|
|
255
|
-
const
|
|
256
|
-
if (
|
|
257
|
-
|
|
288
|
+
const deeper = nestedTypeOfNestedField(field, schema);
|
|
289
|
+
if (deeper !== undefined && walked.has(deeper.name)) {
|
|
290
|
+
// The cycle stops here, and so does the frame – nothing will ever be
|
|
291
|
+
// stored under this field, so nothing is declared for it. Falling
|
|
292
|
+
// through would declare it as a LEAF, whose scalar type is not what a
|
|
293
|
+
// nested object would be stored as.
|
|
258
294
|
continue;
|
|
259
295
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
296
|
+
if (deeper !== undefined) {
|
|
297
|
+
children.push(...nestedFields(nestedFieldName(prefix, field.name), field, deeper, schema, defaultLocale, flattensToArray, walked),
|
|
298
|
+
// A nested field that stores an object cannot be filtered as itself,
|
|
299
|
+
// and its own id is a level deeper than a condition can be welded to –
|
|
300
|
+
// an engine welds conditions on an entry's LEAF fields only. Its
|
|
301
|
+
// identity companion is that leaf, so it sits beside the object rather
|
|
302
|
+
// than inside it.
|
|
303
|
+
...nestedIdentityFields(prefix, field, schema));
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
children.push(...nestedLeafFields(prefix, field, defaultLocale, flattensToArray));
|
|
307
|
+
}
|
|
308
|
+
return [
|
|
309
|
+
{
|
|
310
|
+
name: prefix,
|
|
311
|
+
// Flattening applies to an object exactly as it does to a value: a
|
|
312
|
+
// single-valued nesting inside a multi-valued one arrives as a list of
|
|
313
|
+
// objects, so it is only declared `object` where nothing above it is an
|
|
314
|
+
// `object[]`.
|
|
315
|
+
type: flattensToArray ? 'object[]' : 'object',
|
|
316
|
+
// Never hard-coded: see the parent-object rule above.
|
|
317
|
+
index: children.some((child) => child.index !== false),
|
|
318
|
+
optional: reference.required !== true,
|
|
319
|
+
},
|
|
320
|
+
...children,
|
|
321
|
+
];
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* The **identity companion** of an inline reference: the flat field holding the
|
|
325
|
+
* ids its entries reference, which the engine filters and facets in place of
|
|
326
|
+
* the nested object it cannot. Empty for an inline reference declaring no
|
|
327
|
+
* `identity` – a display-only nesting keeps costing no RAM.
|
|
328
|
+
*
|
|
329
|
+
* A second, narrowed field joins it where the target declares a facet policy,
|
|
330
|
+
* exactly as it does for a top-level facetable reference: the facet then reads
|
|
331
|
+
* the admitted subset, so an excluded id is never seen by the engine rather
|
|
332
|
+
* than merely unlabelled.
|
|
333
|
+
*/
|
|
334
|
+
function identityCompanionFields(field, schema) {
|
|
335
|
+
const names = physicalFields(field, schema);
|
|
336
|
+
if (names.identity === undefined) {
|
|
337
|
+
return [];
|
|
338
|
+
}
|
|
339
|
+
const fields = [
|
|
340
|
+
{
|
|
341
|
+
name: names.identity,
|
|
342
|
+
type: 'string[]',
|
|
343
|
+
facet: names.facet === names.identity,
|
|
344
|
+
optional: true,
|
|
345
|
+
},
|
|
346
|
+
];
|
|
347
|
+
if (names.facet !== undefined && names.facet !== names.identity) {
|
|
348
|
+
fields.push({
|
|
349
|
+
name: names.facet,
|
|
350
|
+
type: 'string[]',
|
|
351
|
+
facet: true,
|
|
352
|
+
optional: true,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
return fields;
|
|
356
|
+
}
|
|
357
|
+
/** The identity companion of a nested field that stores an object: a flat leaf
|
|
358
|
+
* beside it, holding the ids a filter welds on. Empty for a nested field that
|
|
359
|
+
* declares no query Role, which needs none. */
|
|
360
|
+
function nestedIdentityFields(prefix, field, schema) {
|
|
361
|
+
const names = physicalFields(field, schema);
|
|
362
|
+
if (names.identity === undefined) {
|
|
363
|
+
return [];
|
|
364
|
+
}
|
|
365
|
+
return [
|
|
366
|
+
{
|
|
367
|
+
name: nestedFieldName(prefix, names.identity),
|
|
368
|
+
// Always a list, whatever the enclosing reference's arity: the projection
|
|
369
|
+
// writes it with `setArray`, and an indexed field's declared type is
|
|
370
|
+
// checked against what is stored – a `string` here rejects every
|
|
371
|
+
// document carrying such an edge, at import.
|
|
372
|
+
type: 'string[]',
|
|
373
|
+
index: true,
|
|
374
|
+
optional: true,
|
|
375
|
+
},
|
|
376
|
+
];
|
|
377
|
+
}
|
|
378
|
+
/** The type a *nested* field itself nests: another inline reference’s type, or
|
|
379
|
+
* the Root Type a {@link ReferenceStrategy.local local} lookup projects its
|
|
380
|
+
* endpoint through. */
|
|
381
|
+
function nestedTypeOfNestedField(field, schema) {
|
|
382
|
+
return nestedReferenceType(schema, field) ?? localLookupTypeOf(field, schema);
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* The Physical Fields one non-nesting nested field contributes: exactly what
|
|
386
|
+
* the projection writes under the referent’s prefix, so the collection and the
|
|
387
|
+
* document cannot disagree.
|
|
388
|
+
*
|
|
389
|
+
* A nested `text` field stores its display values as a root one does – one
|
|
390
|
+
* `${name}_<lang>` per present language, matched by a single pattern field,
|
|
391
|
+
* never indexed – plus a folded search companion per locale when it is
|
|
392
|
+
* `searchable`. Anything undeclared here would still arrive in the document,
|
|
393
|
+
* and Typesense indexes what it is not told to leave alone, so the fanout is
|
|
394
|
+
* enumerated rather than left to inference.
|
|
395
|
+
*/
|
|
396
|
+
function nestedLeafFields(prefix, field, defaultLocale, flattensToArray) {
|
|
397
|
+
const names = physicalFields(field);
|
|
398
|
+
const searchString = flattensToArray ? 'string[]' : 'string';
|
|
399
|
+
const fields = [];
|
|
400
|
+
if (field.kind === 'text') {
|
|
401
|
+
const pattern = displayFieldPattern(field);
|
|
402
|
+
if (pattern !== undefined) {
|
|
403
|
+
// Display values are never indexed, whatever Roles the field declares:
|
|
404
|
+
// search hits the folded companions below, so the display copies stay on
|
|
405
|
+
// disk with every language they carry.
|
|
406
|
+
fields.push({
|
|
407
|
+
name: nestedFieldName(prefix, pattern),
|
|
408
|
+
type: 'string',
|
|
409
|
+
index: false,
|
|
410
|
+
optional: true,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
fields.push(...names.search.map((name, index) => {
|
|
414
|
+
const locale = field.locales[index] === 'und' ? defaultLocale : field.locales[index];
|
|
415
|
+
return {
|
|
416
|
+
name: nestedFieldName(prefix, name),
|
|
417
|
+
type: searchString,
|
|
418
|
+
optional: true,
|
|
419
|
+
...(locale !== undefined && { stem: true, locale }),
|
|
420
|
+
};
|
|
421
|
+
}));
|
|
422
|
+
return fields;
|
|
423
|
+
}
|
|
424
|
+
// A nested field can be `filterable` or `searchable`, never `facetable` or
|
|
425
|
+
// `sortable` (`searchSchema`), so `filterable` is what indexes the value
|
|
426
|
+
// itself – its search companion below indexes on its own account.
|
|
427
|
+
const indexed = field.filterable === true;
|
|
428
|
+
const valueType = typesenseValueType(field);
|
|
429
|
+
// Two ways a list arrives: the field declares one, or an ancestor's
|
|
430
|
+
// `object[]` flattens it into one. `typesenseValueType` honours `array` only
|
|
431
|
+
// for the string-shaped kinds, so a multi-valued nested `integer` would
|
|
432
|
+
// otherwise be declared scalar and rejected at import.
|
|
433
|
+
const storesAList = flattensToArray || field.array === true;
|
|
434
|
+
fields.push({
|
|
435
|
+
name: nestedFieldName(prefix, field.name),
|
|
436
|
+
type: indexed && storesAList ? arrayValueType(valueType) : valueType,
|
|
437
|
+
index: indexed,
|
|
438
|
+
// Always optional: `required` is a promise about the *referent* (this value
|
|
439
|
+
// is on every referent), while Typesense’s flag is about the document.
|
|
440
|
+
// Under a multi-valued reference those are different claims, and only the
|
|
441
|
+
// referent-level one is the declaration’s.
|
|
442
|
+
optional: true,
|
|
443
|
+
});
|
|
444
|
+
for (const name of names.search) {
|
|
265
445
|
fields.push({
|
|
266
|
-
name: nestedFieldName(prefix,
|
|
267
|
-
type:
|
|
268
|
-
index: false,
|
|
269
|
-
// Always optional: `required` is a promise about the *referent* (this
|
|
270
|
-
// value is on every referent), while Typesense’s flag is about the
|
|
271
|
-
// document. Under a multi-valued reference those are different claims,
|
|
272
|
-
// and only the referent-level one is the declaration’s.
|
|
446
|
+
name: nestedFieldName(prefix, name),
|
|
447
|
+
type: searchString,
|
|
273
448
|
optional: true,
|
|
274
449
|
});
|
|
275
450
|
}
|
|
@@ -277,6 +452,30 @@ function nestedFields(prefix, reference, referenceType, schema) {
|
|
|
277
452
|
}
|
|
278
453
|
/** The Typesense field type for a non-localized field, from its `kind`. 64-bit
|
|
279
454
|
* integers (and dates, stored as Unix seconds) so large counts never overflow. */
|
|
455
|
+
/**
|
|
456
|
+
* The array form of a value type, for a nested field that an ancestor’s
|
|
457
|
+
* `object[]` flattens into a list. Only an **indexed** field needs it: Typesense
|
|
458
|
+
* checks a declared type against what is stored, and a value under a
|
|
459
|
+
* multi-valued ancestor arrives as an array however single-valued its own
|
|
460
|
+
* declaration is. Enumerated rather than string-appended so an invalid
|
|
461
|
+
* combination cannot be built.
|
|
462
|
+
*/
|
|
463
|
+
function arrayValueType(type) {
|
|
464
|
+
switch (type) {
|
|
465
|
+
case 'string':
|
|
466
|
+
return 'string[]';
|
|
467
|
+
case 'int64':
|
|
468
|
+
return 'int64[]';
|
|
469
|
+
case 'float':
|
|
470
|
+
return 'float[]';
|
|
471
|
+
case 'bool':
|
|
472
|
+
return 'bool[]';
|
|
473
|
+
// Already a list: a multi-valued declaration under a multi-valued ancestor
|
|
474
|
+
// flattens no further.
|
|
475
|
+
case 'string[]':
|
|
476
|
+
return 'string[]';
|
|
477
|
+
}
|
|
478
|
+
}
|
|
280
479
|
function typesenseValueType(field) {
|
|
281
480
|
switch (field.kind) {
|
|
282
481
|
case 'integer':
|