@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
|
@@ -5,27 +5,33 @@ import { type RebuildOptions } from './rebuild-support.js';
|
|
|
5
5
|
/** {@link InPlaceRebuild} options: the shared rebuild options plus the
|
|
6
6
|
* membership-sweep ceiling. */
|
|
7
7
|
export interface InPlaceRebuildOptions extends RebuildOptions {
|
|
8
|
-
/**
|
|
9
|
-
*
|
|
8
|
+
/**
|
|
9
|
+
* Most distinct datasets the membership sweep may enumerate before it throws
|
|
10
|
+
* rather than risk missing departed ones (default 10 000). The sweep needs
|
|
11
|
+
* the complete set to spot which datasets left, so a truncated facet would
|
|
12
|
+
* silently miss deletions. Raise it (up to the engine’s `max_facet_values`
|
|
13
|
+
* limit) before an index approaches it, so the ceiling is a tunable guard
|
|
14
|
+
* rather than a hard wall.
|
|
15
|
+
*/
|
|
10
16
|
readonly maxSweepableSources?: number;
|
|
11
17
|
}
|
|
12
18
|
/**
|
|
13
19
|
* In-place Rebuild (update the live index directly – upsert changed sources,
|
|
14
20
|
* sweep the rest – rather than swap in a fresh one) as a transactional
|
|
15
21
|
* `Writer`: documents are upserted into one long-lived collection with
|
|
16
|
-
* per-
|
|
17
|
-
*
|
|
18
|
-
*
|
|
22
|
+
* per-dataset atomicity – no swap, no staging. Deletion is a sweep, never
|
|
23
|
+
* special-cased, and {@link openDocuments} decides what a deletion physically
|
|
24
|
+
* is – for a canonically keyed type, dropping one dataset’s membership of a
|
|
25
|
+
* document several datasets share, and the document itself only with the last
|
|
26
|
+
* of them:
|
|
19
27
|
*
|
|
20
|
-
* - a **successful dataset flush**
|
|
21
|
-
* not rewrite
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* left for the success sweep to reconcile;
|
|
28
|
-
* - **commit** deletes every document whose source left the run’s selection
|
|
28
|
+
* - a **successful dataset flush** drops what the dataset had and this run did
|
|
29
|
+
* not rewrite. A failed dataset is not swept – its output is incomplete, and
|
|
30
|
+
* the next successful run reconciles;
|
|
31
|
+
* - **reset** (the pipeline’s dump-fallback discard) drops only *this run’s*
|
|
32
|
+
* writes for the dataset, so the dump re-run rebuilds it cleanly while the
|
|
33
|
+
* dataset’s prior-run documents are left for the success sweep to reconcile;
|
|
34
|
+
* - **commit** drops what belongs to datasets that left the run’s selection
|
|
29
35
|
* (registry-membership sweep over {@link RunContext.selectedSources}, which
|
|
30
36
|
* includes datasets skipped as unchanged) and releases the lock;
|
|
31
37
|
* - **abort** only releases the lock: upserts are idempotent, so whatever
|
|
@@ -33,15 +39,17 @@ export interface InPlaceRebuildOptions extends RebuildOptions {
|
|
|
33
39
|
*
|
|
34
40
|
* `openRun` takes the single-flight cross-pod lock (throwing
|
|
35
41
|
* `RebuildAlreadyRunning` when another rebuild holds it) and creates
|
|
36
|
-
* the collection on demand from the {@link SearchType} plus the
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* ({@link
|
|
42
|
+
* the collection on demand from the {@link SearchType} plus the bookkeeping
|
|
43
|
+
* fields. A collection that already exists is used as it is, with two
|
|
44
|
+
* exceptions: it must carry every reference field the declaration asks for and
|
|
45
|
+
* every facet companion its facets read
|
|
46
|
+
* ({@link assertReferencesAndFacetCompanionsPresent}), and the membership field
|
|
47
|
+
* a keyed type records in ({@link assertBookkeepingPresent}) – each failing
|
|
48
|
+
* with the rebuild that fixes it.
|
|
42
49
|
*
|
|
43
|
-
*
|
|
44
|
-
* or documents from different
|
|
50
|
+
* Unless the type is canonically keyed, document ids must be unique per
|
|
51
|
+
* (dataset, entity) – the caller keys them – or documents from different
|
|
52
|
+
* datasets overwrite each other.
|
|
45
53
|
*
|
|
46
54
|
* The name is the NDE Stack’s pattern
|
|
47
55
|
* ({@link https://docs.nde.nl/stack/patterns | Stack patterns}).
|
|
@@ -60,20 +68,7 @@ export declare class InPlaceRebuild<TDocument extends {
|
|
|
60
68
|
readonly collectionName: string;
|
|
61
69
|
private readonly maxSweepableSources;
|
|
62
70
|
private readonly resolved;
|
|
63
|
-
/** The column this collection carries its documents’ dataset IRI in: the
|
|
64
|
-
* type’s declared dataset field, or the private `source` when it declares
|
|
65
|
-
* none. Everything below – the stamp, every sweep filter, the source
|
|
66
|
-
* enumeration – reads this one name. */
|
|
67
|
-
private readonly sourceField;
|
|
68
71
|
constructor(client: Client, searchType: SearchType, options?: InPlaceRebuildOptions);
|
|
69
72
|
openRun(context: RunContext): Promise<RunWriter<TDocument>>;
|
|
70
|
-
/**
|
|
71
|
-
* The distinct sources present in the collection, via a single facet over its
|
|
72
|
-
* {@link provenanceField}. Requests one bucket beyond `maxSources` so genuine truncation is
|
|
73
|
-
* distinguishable from an exactly-full result: `maxSources` buckets are
|
|
74
|
-
* returned intact, `maxSources + 1` proves more exist and the sweep would
|
|
75
|
-
* miss some, so it throws rather than delete blind.
|
|
76
|
-
*/
|
|
77
|
-
private indexedSources;
|
|
78
73
|
}
|
|
79
74
|
//# sourceMappingURL=in-place-rebuild.d.ts.map
|
|
@@ -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,EAAgB,UAAU,EAAE,MAAM,aAAa,CAAC;AAG5D,OAAO,KAAK,EAEV,UAAU,EACV,SAAS,EACT,MAAM,EACP,MAAM,eAAe,CAAC;
|
|
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;AASvB,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,sBAAsB,CAAC;AAE9B;gCACgC;AAChC,MAAM,WAAW,qBAAsB,SAAQ,cAAc;IAC3D;;;;;;;OAOG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CACvC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,qBAAa,cAAc,CACzB,SAAS,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAChC,YAAW,MAAM,CAAC,SAAS,CAAC;IAY1B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAZ7B;;;;;OAKG;IACH,SAAgB,cAAc,EAAE,MAAM,CAAC;IACvC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IACzD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyB;gBAG/B,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;CAuElE"}
|
package/dist/in-place-rebuild.js
CHANGED
|
@@ -1,36 +1,25 @@
|
|
|
1
1
|
import { facetableFields, physicalFields } from '@lde/search/adapter';
|
|
2
2
|
import { buildCollectionDefinition } from './collection-definition.js';
|
|
3
|
-
import { BatchImporter } from './import.js';
|
|
4
3
|
import { ensureCollectionExists, openLockedRun, releaseLock } from './lock.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
/**
|
|
8
|
-
* Default ceiling on the distinct sources the membership sweep enumerates via
|
|
9
|
-
* a single `source` facet. The sweep needs the complete source set to spot
|
|
10
|
-
* departed ones, so a truncated facet would silently miss deletions;
|
|
11
|
-
* {@link InPlaceRebuild} throws instead. Raise it (up to the engine’s
|
|
12
|
-
* `max_facet_values` limit) with {@link InPlaceRebuildOptions.maxSweepableSources}
|
|
13
|
-
* before an index approaches it, so the ceiling is a tunable guard rather than
|
|
14
|
-
* a hard wall.
|
|
15
|
-
*/
|
|
16
|
-
const DEFAULT_MAX_SWEEPABLE_SOURCES = 10_000;
|
|
4
|
+
import { assertBookkeepingPresent, assertWritableType, openDocuments, withBookkeeping, } from './documents.js';
|
|
5
|
+
import { resolveRebuildOptions, } from './rebuild-support.js';
|
|
17
6
|
/**
|
|
18
7
|
* In-place Rebuild (update the live index directly – upsert changed sources,
|
|
19
8
|
* sweep the rest – rather than swap in a fresh one) as a transactional
|
|
20
9
|
* `Writer`: documents are upserted into one long-lived collection with
|
|
21
|
-
* per-
|
|
22
|
-
*
|
|
23
|
-
*
|
|
10
|
+
* per-dataset atomicity – no swap, no staging. Deletion is a sweep, never
|
|
11
|
+
* special-cased, and {@link openDocuments} decides what a deletion physically
|
|
12
|
+
* is – for a canonically keyed type, dropping one dataset’s membership of a
|
|
13
|
+
* document several datasets share, and the document itself only with the last
|
|
14
|
+
* of them:
|
|
24
15
|
*
|
|
25
|
-
* - a **successful dataset flush**
|
|
26
|
-
* not rewrite
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* left for the success sweep to reconcile;
|
|
33
|
-
* - **commit** deletes every document whose source left the run’s selection
|
|
16
|
+
* - a **successful dataset flush** drops what the dataset had and this run did
|
|
17
|
+
* not rewrite. A failed dataset is not swept – its output is incomplete, and
|
|
18
|
+
* the next successful run reconciles;
|
|
19
|
+
* - **reset** (the pipeline’s dump-fallback discard) drops only *this run’s*
|
|
20
|
+
* writes for the dataset, so the dump re-run rebuilds it cleanly while the
|
|
21
|
+
* dataset’s prior-run documents are left for the success sweep to reconcile;
|
|
22
|
+
* - **commit** drops what belongs to datasets that left the run’s selection
|
|
34
23
|
* (registry-membership sweep over {@link RunContext.selectedSources}, which
|
|
35
24
|
* includes datasets skipped as unchanged) and releases the lock;
|
|
36
25
|
* - **abort** only releases the lock: upserts are idempotent, so whatever
|
|
@@ -38,15 +27,17 @@ const DEFAULT_MAX_SWEEPABLE_SOURCES = 10_000;
|
|
|
38
27
|
*
|
|
39
28
|
* `openRun` takes the single-flight cross-pod lock (throwing
|
|
40
29
|
* `RebuildAlreadyRunning` when another rebuild holds it) and creates
|
|
41
|
-
* the collection on demand from the {@link SearchType} plus the
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* ({@link
|
|
30
|
+
* the collection on demand from the {@link SearchType} plus the bookkeeping
|
|
31
|
+
* fields. A collection that already exists is used as it is, with two
|
|
32
|
+
* exceptions: it must carry every reference field the declaration asks for and
|
|
33
|
+
* every facet companion its facets read
|
|
34
|
+
* ({@link assertReferencesAndFacetCompanionsPresent}), and the membership field
|
|
35
|
+
* a keyed type records in ({@link assertBookkeepingPresent}) – each failing
|
|
36
|
+
* with the rebuild that fixes it.
|
|
47
37
|
*
|
|
48
|
-
*
|
|
49
|
-
* or documents from different
|
|
38
|
+
* Unless the type is canonically keyed, document ids must be unique per
|
|
39
|
+
* (dataset, entity) – the caller keys them – or documents from different
|
|
40
|
+
* datasets overwrite each other.
|
|
50
41
|
*
|
|
51
42
|
* The name is the NDE Stack’s pattern
|
|
52
43
|
* ({@link https://docs.nde.nl/stack/patterns | Stack patterns}).
|
|
@@ -63,21 +54,16 @@ export class InPlaceRebuild {
|
|
|
63
54
|
collectionName;
|
|
64
55
|
maxSweepableSources;
|
|
65
56
|
resolved;
|
|
66
|
-
/** The column this collection carries its documents’ dataset IRI in: the
|
|
67
|
-
* type’s declared dataset field, or the private `source` when it declares
|
|
68
|
-
* none. Everything below – the stamp, every sweep filter, the source
|
|
69
|
-
* enumeration – reads this one name. */
|
|
70
|
-
sourceField;
|
|
71
57
|
constructor(client, searchType, options = {}) {
|
|
72
58
|
this.client = client;
|
|
73
59
|
this.searchType = searchType;
|
|
74
|
-
|
|
75
|
-
|
|
60
|
+
// The membership sweep enumerates the indexed datasets by faceting the
|
|
61
|
+
// column they are recorded in, so a declared one has to be facetable.
|
|
62
|
+
assertWritableType(searchType, {
|
|
76
63
|
requireFacetable: true,
|
|
77
64
|
schema: options.schema,
|
|
78
65
|
});
|
|
79
|
-
|
|
80
|
-
const { maxSweepableSources = DEFAULT_MAX_SWEEPABLE_SOURCES, ...rebuildOptions } = options;
|
|
66
|
+
const { maxSweepableSources, ...rebuildOptions } = options;
|
|
81
67
|
this.maxSweepableSources = maxSweepableSources;
|
|
82
68
|
this.resolved = resolveRebuildOptions(searchType, rebuildOptions);
|
|
83
69
|
this.collectionName =
|
|
@@ -86,78 +72,47 @@ export class InPlaceRebuild {
|
|
|
86
72
|
async openRun(context) {
|
|
87
73
|
const { batchSize, lockTtlMs, definitionOptions } = this.resolved;
|
|
88
74
|
const name = this.collectionName;
|
|
89
|
-
const sourceField = this.sourceField;
|
|
90
75
|
return openLockedRun(this.client, name, lockTtlMs, async () => {
|
|
91
|
-
// Create the collection on demand: SearchType schema
|
|
92
|
-
//
|
|
93
|
-
// enumerate the distinct sources – only when the type declares no dataset
|
|
94
|
-
// field of its own; when it does, that declared field already carries the
|
|
95
|
-
// IRI (and its own `facet: true`), and a second column would be the same
|
|
96
|
-
// value twice, free to drift.
|
|
76
|
+
// Create the collection on demand: the SearchType’s schema plus whatever
|
|
77
|
+
// bookkeeping its membership regime keeps.
|
|
97
78
|
const definition = buildCollectionDefinition(this.searchType, definitionOptions);
|
|
98
79
|
const declaredFields = definition.fields ?? [];
|
|
99
|
-
const created = await ensureCollectionExists(this.client, name, () =>
|
|
100
|
-
const bookkeeping = [
|
|
101
|
-
...(sourceField === SOURCE_FIELD
|
|
102
|
-
? [{ name: SOURCE_FIELD, type: 'string', facet: true }]
|
|
103
|
-
: []),
|
|
104
|
-
{ name: LAST_SEEN_FIELD, type: 'string' },
|
|
105
|
-
];
|
|
106
|
-
return {
|
|
107
|
-
...definition,
|
|
108
|
-
fields: [...declaredFields, ...bookkeeping],
|
|
109
|
-
};
|
|
110
|
-
});
|
|
80
|
+
const created = await ensureCollectionExists(this.client, name, () => withBookkeeping(definition, this.searchType));
|
|
111
81
|
// A collection this run did NOT create may predate a `joinable` being
|
|
112
|
-
// declared,
|
|
113
|
-
//
|
|
82
|
+
// declared, a facet policy on a type this one references, or the type
|
|
83
|
+
// being keyed – and none of the three can be added to it here.
|
|
114
84
|
if (!created) {
|
|
115
|
-
|
|
85
|
+
const existing = (await this.client.collections(name).retrieve())
|
|
86
|
+
.fields;
|
|
87
|
+
assertReferencesAndFacetCompanionsPresent(name, declaredFields, facetCompanionsOf(this.searchType, definitionOptions.schema), existing);
|
|
88
|
+
assertBookkeepingPresent(this.searchType, existing, name);
|
|
116
89
|
}
|
|
117
|
-
const
|
|
90
|
+
const documents = openDocuments(this.client, name, {
|
|
91
|
+
searchType: this.searchType,
|
|
92
|
+
runId: context.runId,
|
|
93
|
+
startedAt: context.startedAt,
|
|
94
|
+
batchSize,
|
|
95
|
+
maxSweepableSources: this.maxSweepableSources,
|
|
96
|
+
});
|
|
118
97
|
return {
|
|
119
|
-
|
|
120
|
-
// to have filled a declared one – is what keeps the sweep total: a
|
|
121
|
-
// caller projecting without a dataset context, or a document reaching
|
|
122
|
-
// the writer any other way, still lands in a swept collection. Where
|
|
123
|
-
// the projection did fill it, the value is the same dataset IRI, so the
|
|
124
|
-
// stamp only ever reasserts it.
|
|
125
|
-
//
|
|
126
|
-
// It reasserts the sweep’s column only, never the physical fanout a
|
|
127
|
-
// declared field also produces (the folded `${name}_search` companion
|
|
128
|
-
// of a `searchable` one). Folding is the projection’s convention, and
|
|
129
|
-
// restating it here would put it in two places – the split this change
|
|
130
|
-
// exists to close. So a document that reached the writer unprojected is
|
|
131
|
-
// sweepable but not full-text findable by dataset: absent, never wrong.
|
|
132
|
-
write: async (dataset, documents) => importer.add(stampDocuments(documents, {
|
|
133
|
-
[sourceField]: dataset.iri.toString(),
|
|
134
|
-
[LAST_SEEN_FIELD]: context.runId,
|
|
135
|
-
})),
|
|
98
|
+
write: async (dataset, items) => documents.add(dataset, items),
|
|
136
99
|
flush: async (dataset, outcome) => {
|
|
137
|
-
// Land the buffered documents first, so the sweep below never
|
|
138
|
-
// deletes what this run just rewrote.
|
|
139
|
-
await importer.flush();
|
|
140
100
|
if (outcome !== 'success') {
|
|
141
101
|
// A failed dataset’s output is incomplete: sweeping against it
|
|
142
102
|
// would delete documents the run never got to rewrite. Leave the
|
|
143
|
-
// stale ones for the next successful run to reconcile.
|
|
103
|
+
// stale ones for the next successful run to reconcile. The buffered
|
|
104
|
+
// documents stay buffered; the next flush lands them.
|
|
144
105
|
return;
|
|
145
106
|
}
|
|
146
|
-
await
|
|
147
|
-
},
|
|
148
|
-
reset: async (dataset) => {
|
|
149
|
-
// Discard only this run’s partial writes for the source (the failed
|
|
150
|
-
// endpoint attempt) so the dump re-run rebuilds it cleanly; the
|
|
151
|
-
// source’s prior-run documents stay for the success sweep.
|
|
152
|
-
await importer.flush();
|
|
153
|
-
await deleteByFilter(this.client, name, thisRunDocumentsFilter(sourceField, dataset.iri.toString(), context.runId));
|
|
107
|
+
await documents.dropUnwritten(dataset);
|
|
154
108
|
},
|
|
109
|
+
// Discard only this run’s partial writes for the dataset (the failed
|
|
110
|
+
// endpoint attempt) so the dump re-run rebuilds it cleanly; the
|
|
111
|
+
// dataset’s prior-run documents stay for the success sweep.
|
|
112
|
+
reset: async (dataset) => documents.dropWrittenThisRun(dataset),
|
|
155
113
|
commit: async () => {
|
|
156
|
-
await
|
|
157
|
-
|
|
158
|
-
for (const filter of membershipSweepFilters(sourceField, departed)) {
|
|
159
|
-
await deleteByFilter(this.client, name, filter);
|
|
160
|
-
}
|
|
114
|
+
await documents.flush();
|
|
115
|
+
await documents.dropUnselected(context.selectedSources());
|
|
161
116
|
await releaseLock(this.client, name);
|
|
162
117
|
},
|
|
163
118
|
abort: async () => {
|
|
@@ -166,30 +121,6 @@ export class InPlaceRebuild {
|
|
|
166
121
|
};
|
|
167
122
|
});
|
|
168
123
|
}
|
|
169
|
-
/**
|
|
170
|
-
* The distinct sources present in the collection, via a single facet over its
|
|
171
|
-
* {@link provenanceField}. Requests one bucket beyond `maxSources` so genuine truncation is
|
|
172
|
-
* distinguishable from an exactly-full result: `maxSources` buckets are
|
|
173
|
-
* returned intact, `maxSources + 1` proves more exist and the sweep would
|
|
174
|
-
* miss some, so it throws rather than delete blind.
|
|
175
|
-
*/
|
|
176
|
-
async indexedSources(name, maxSources) {
|
|
177
|
-
const response = await this.client
|
|
178
|
-
.collections(name)
|
|
179
|
-
.documents()
|
|
180
|
-
.search({
|
|
181
|
-
q: '*',
|
|
182
|
-
query_by: this.sourceField,
|
|
183
|
-
per_page: 0,
|
|
184
|
-
facet_by: this.sourceField,
|
|
185
|
-
max_facet_values: maxSources + 1,
|
|
186
|
-
});
|
|
187
|
-
const counts = response.facet_counts?.[0]?.counts ?? [];
|
|
188
|
-
if (counts.length > maxSources) {
|
|
189
|
-
throw new Error(`Membership sweep cannot see beyond ${maxSources} distinct sources in “${name}”; raise maxSweepableSources or departed sources might be missed`);
|
|
190
|
-
}
|
|
191
|
-
return counts.map((count) => count.value);
|
|
192
|
-
}
|
|
193
124
|
}
|
|
194
125
|
/**
|
|
195
126
|
* Fail loudly when an EXISTING collection does not carry every reference field
|
|
@@ -218,15 +149,12 @@ export class InPlaceRebuild {
|
|
|
218
149
|
* self-correcting (a new plain field simply starts being written) or
|
|
219
150
|
* harmless, and general drift detection is a feature of its own.
|
|
220
151
|
*/
|
|
221
|
-
|
|
152
|
+
function assertReferencesAndFacetCompanionsPresent(name, fields, facetCompanions, existingFields) {
|
|
222
153
|
const references = fields.filter((field) => field.reference !== undefined);
|
|
223
154
|
if (references.length === 0 && facetCompanions.length === 0) {
|
|
224
155
|
return;
|
|
225
156
|
}
|
|
226
|
-
const existing = new Map(
|
|
227
|
-
field.name,
|
|
228
|
-
field,
|
|
229
|
-
]));
|
|
157
|
+
const existing = new Map(existingFields.map((field) => [field.name, field]));
|
|
230
158
|
const missingReferences = references.filter((field) => existing.get(field.name)?.reference !== field.reference);
|
|
231
159
|
if (missingReferences.length > 0) {
|
|
232
160
|
throw new Error(`Typesense collection “${name}” exists without the reference field(s) ${missingReferences
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,6 @@ export type { BlueGreenRebuildOptions } from './blue-green-rebuild.js';
|
|
|
3
3
|
export { InPlaceRebuild } from './in-place-rebuild.js';
|
|
4
4
|
export type { InPlaceRebuildOptions } from './in-place-rebuild.js';
|
|
5
5
|
export { RebuildAlreadyRunning } from './lock.js';
|
|
6
|
-
export { departedSources, membershipSweepFilters, sourceDocumentsFilter, staleDocumentsFilter, } from './sweep.js';
|
|
7
6
|
export { buildCollectionDefinition } from './collection-definition.js';
|
|
8
7
|
export type { CollectionDefinitionOptions } from './collection-definition.js';
|
|
9
8
|
export { deriveCollectionName } from './collection-name.js';
|
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,
|
|
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,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/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export { BlueGreenRebuild } from './blue-green-rebuild.js';
|
|
2
2
|
export { InPlaceRebuild } from './in-place-rebuild.js';
|
|
3
3
|
export { RebuildAlreadyRunning } from './lock.js';
|
|
4
|
-
export { departedSources, membershipSweepFilters, sourceDocumentsFilter, staleDocumentsFilter, } from './sweep.js';
|
|
5
4
|
export { buildCollectionDefinition } from './collection-definition.js';
|
|
6
5
|
export { deriveCollectionName } from './collection-name.js';
|
|
7
6
|
export { buildSearchParams } from './query-compiler.js';
|
package/dist/lookup.d.ts
CHANGED
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
import type { Client } from 'typesense';
|
|
2
2
|
import { type ReferenceProjection, type RootType, type SearchSchema, type SearchType } from '@lde/search';
|
|
3
3
|
/**
|
|
4
|
-
* One resolved level of a {@link ReferenceProjection}
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* One resolved level of a {@link ReferenceProjection}. A level is one of two
|
|
5
|
+
* things, discriminated by `via`, because the two cost different things:
|
|
6
|
+
*
|
|
7
|
+
* - **`lookup`** – referents fetched from the target’s own collection, keyed by
|
|
8
|
+
* id and still in the engine’s flat physical shape. One batched round trip.
|
|
9
|
+
* - **`nested`** – an inline reference descended into. Its entries are already
|
|
10
|
+
* in the hit, so nothing is fetched and there is no target; the level exists
|
|
11
|
+
* only to carry the levels resolved *below* it.
|
|
7
12
|
*
|
|
8
13
|
* Kept flat rather than reconstructed here so one reconstruction path serves
|
|
9
14
|
* hits and referents alike – the surface never learns that a nested document
|
|
10
15
|
* arrived by a second round-trip rather than from the hit itself.
|
|
11
16
|
*/
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
export type ResolvedReferents = {
|
|
18
|
+
readonly via: 'lookup';
|
|
19
|
+
/** The Root Type these referents are declared by – reconstruction reads
|
|
20
|
+
* them through the target’s own declaration, never the referrer’s. */
|
|
15
21
|
readonly target: RootType;
|
|
16
22
|
readonly documents: ReadonlyMap<string, Record<string, unknown>>;
|
|
17
23
|
/** Keyed by the reference field’s name on the type this level carries. */
|
|
18
24
|
readonly children: ReadonlyMap<string, ResolvedReferents>;
|
|
19
|
-
}
|
|
25
|
+
} | {
|
|
26
|
+
readonly via: 'nested';
|
|
27
|
+
readonly children: ReadonlyMap<string, ResolvedReferents>;
|
|
28
|
+
};
|
|
20
29
|
/**
|
|
21
30
|
* Resolve a projection against the documents that name its referents: one
|
|
22
31
|
* batched `multi_search` **per level**, never per document, because the IRIs of
|
package/dist/lookup.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lookup.d.ts","sourceRoot":"","sources":["../src/lookup.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,UAAU,EAChB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"lookup.d.ts","sourceRoot":"","sources":["../src/lookup.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,UAAU,EAChB,MAAM,aAAa,CAAC;AAUrB;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,iBAAiB,GACzB;IACE,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC;IACvB;2EACuE;IACvE,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,GACD;IACE,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;CAC3D,CAAC;AAKN;;;;;;;;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,CAwFjD"}
|
package/dist/lookup.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { displayFieldName, fieldNamed, labelFieldOf, rootTypeNamed, } from '@lde/search/adapter';
|
|
1
|
+
import { displayFieldName, fieldNamed, labelFieldOf, nestedReferenceType, 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;
|
|
@@ -21,9 +21,24 @@ export async function resolveProjection(client, projection, searchType, schema,
|
|
|
21
21
|
// stated bound – one round-trip per level – one per field instead.
|
|
22
22
|
const levels = await Promise.all(Object.entries(projection).map(async ([name, level]) => {
|
|
23
23
|
const field = fieldNamed(searchType, name);
|
|
24
|
-
if (field === undefined
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
if (field === undefined) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
// An inline level costs no round trip: its entries are already in the
|
|
28
|
+
// parents. Flatten them out and hand them down, so the level BELOW reads
|
|
29
|
+
// its ids off the entries rather than off the document – which is the
|
|
30
|
+
// whole of what makes a lookup inside a qualified edge resolvable.
|
|
31
|
+
const nested = nestedReferenceType(schema, field);
|
|
32
|
+
if (nested !== undefined) {
|
|
33
|
+
return [
|
|
34
|
+
name,
|
|
35
|
+
{
|
|
36
|
+
via: 'nested',
|
|
37
|
+
children: await resolveProjection(client, level.resolve, nested, schema, collections, entriesOf(parents, name), onError),
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
if (field.kind !== 'reference' || field.ref?.strategy !== 'lookup') {
|
|
27
42
|
// `assertValidQuery` rejects this for every caller; skipping keeps a
|
|
28
43
|
// hand-built query from throwing here rather than at the port’s guard.
|
|
29
44
|
return undefined;
|
|
@@ -44,6 +59,7 @@ export async function resolveProjection(client, projection, searchType, schema,
|
|
|
44
59
|
return [
|
|
45
60
|
name,
|
|
46
61
|
{
|
|
62
|
+
via: 'lookup',
|
|
47
63
|
target,
|
|
48
64
|
documents,
|
|
49
65
|
// The level below reads the IRIs off the documents this one just
|
|
@@ -59,14 +75,40 @@ export async function resolveProjection(client, projection, searchType, schema,
|
|
|
59
75
|
}
|
|
60
76
|
return resolved;
|
|
61
77
|
}
|
|
62
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* The nested entries a page of documents carries under one inline reference,
|
|
80
|
+
* flattened into one list – the “documents” the level below resolves against.
|
|
81
|
+
*
|
|
82
|
+
* Flattening rather than grouping is what keeps the round-trip count per
|
|
83
|
+
* *level* rather than per document: every entry on the page contributes its ids
|
|
84
|
+
* to one batch, and reconstruction re-associates them afterwards by id.
|
|
85
|
+
*/
|
|
86
|
+
function entriesOf(documents, field) {
|
|
87
|
+
return documents.flatMap((document) => {
|
|
88
|
+
const raw = document[field];
|
|
89
|
+
return (Array.isArray(raw) ? raw : [raw]).filter((entry) => typeof entry === 'object' && entry !== null);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Every distinct id a page of documents carries under one reference field.
|
|
94
|
+
*
|
|
95
|
+
* A reference stores its ids one of two ways, and which one is about how much
|
|
96
|
+
* of the referent the deployment wants stored, not about identity: a plain
|
|
97
|
+
* reference stores the id itself, while a
|
|
98
|
+
* {@link ReferenceStrategy.local local} lookup stores the endpoint’s own
|
|
99
|
+
* document, whose `id` is the same value. Both are read, so a level resolves
|
|
100
|
+
* the same either way.
|
|
101
|
+
*/
|
|
63
102
|
function distinctIris(documents, field) {
|
|
64
103
|
const iris = new Set();
|
|
65
104
|
for (const document of documents) {
|
|
66
105
|
const raw = document[field];
|
|
67
106
|
for (const value of Array.isArray(raw) ? raw : [raw]) {
|
|
68
|
-
|
|
69
|
-
|
|
107
|
+
const id = typeof value === 'object' && value !== null
|
|
108
|
+
? value.id
|
|
109
|
+
: value;
|
|
110
|
+
if (typeof id === 'string' && id !== '') {
|
|
111
|
+
iris.add(id);
|
|
70
112
|
}
|
|
71
113
|
}
|
|
72
114
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query-compiler.d.ts","sourceRoot":"","sources":["../src/query-compiler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAC;AAEzE,OAAO,EAGL,KAAK,MAAM,EACX,KAAK,QAAQ,EAEb,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,UAAU,
|
|
1
|
+
{"version":3,"file":"query-compiler.d.ts","sourceRoot":"","sources":["../src/query-compiler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAC;AAEzE,OAAO,EAGL,KAAK,MAAM,EACX,KAAK,QAAQ,EAEb,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,UAAU,EAIhB,MAAM,aAAa,CAAC;AAmBrB;mDACmD;AACnD,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED;;;;GAIG;AACH,MAAM,WAAW,wBAAwB;IACvC;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,CACvB,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,SAAS,MAAM,EAAE,KACpB,UAAU,GAAG,SAAS,CAAC;IAC5B;;;;;;OAMG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC;;;;;;;;OAQG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC;IAC/B;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACrD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,WAAW,EAClB,UAAU,EAAE,UAAU,EACtB,OAAO,GAAE,wBAA6B,GACrC,YAAY,CAAC,MAAM,CAAC,CAkCtB;AAwfD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEvD"}
|