@lde/search-typesense 0.28.0 → 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/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/rebuild-support.d.ts +1 -41
- package/dist/rebuild-support.d.ts.map +1 -1
- package/dist/rebuild-support.js +3 -76
- package/package.json +1 -1
- 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
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';
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { SearchSchema, SearchType } from '@lde/search';
|
|
1
|
+
import type { SearchType } from '@lde/search';
|
|
3
2
|
import { type CollectionDefinitionOptions } from './collection-definition.js';
|
|
4
3
|
/**
|
|
5
4
|
* The tuning knobs both rebuild writers share, on top of the collection-definition
|
|
@@ -39,43 +38,4 @@ export interface ResolvedRebuildOptions {
|
|
|
39
38
|
* been paid for.
|
|
40
39
|
*/
|
|
41
40
|
export declare function resolveRebuildOptions(searchType: SearchType, options: RebuildOptions): ResolvedRebuildOptions;
|
|
42
|
-
/**
|
|
43
|
-
* Reject a {@link SearchType} that declares any of the bookkeeping field names
|
|
44
|
-
* a writer stamps, so a domain field can never collide with `source` /
|
|
45
|
-
* `last_seen`.
|
|
46
|
-
*/
|
|
47
|
-
export declare function assertNoReservedFields(searchType: SearchType, reserved: readonly string[]): void;
|
|
48
|
-
/**
|
|
49
|
-
* Reject a declared dataset field the provenance bookkeeping cannot be kept on.
|
|
50
|
-
* A type declaring one makes it the collection’s provenance field – the column
|
|
51
|
-
* the membership sweep enumerates and deletes by – so the declaration has to answer
|
|
52
|
-
* *which single dataset is this document’s* the way the private `source` field
|
|
53
|
-
* did:
|
|
54
|
-
*
|
|
55
|
-
* - **not `array`**: Typesense reads `field:=[a,b]` over a `string[]` as
|
|
56
|
-
* *contains any*, so a departed source would take every document that merely
|
|
57
|
-
* mentions it – including entities another selected dataset still
|
|
58
|
-
* contributes;
|
|
59
|
-
* - **no `transform`**: the sweep compares the stored value against the run’s
|
|
60
|
-
* selection, which carries raw dataset IRIs, so a transformed value would
|
|
61
|
-
* match nothing and the sweep would silently stop deleting;
|
|
62
|
-
* - **`facetable`** where the writer enumerates the indexed sources by faceting
|
|
63
|
-
* it (the In-place writer does; Blue/green only ever filters by a known IRI)
|
|
64
|
-
* – and faceted **on its own name**: a reference inheriting a facet policy
|
|
65
|
-
* from the type it names facets a companion holding only the admitted keys
|
|
66
|
-
* (`physicalFields(field, schema).facet`), so enumerating the sources
|
|
67
|
-
* through it would miss every dataset the policy excludes. `schema` is what
|
|
68
|
-
* resolves that; without one no policy is visible, as nowhere else.
|
|
69
|
-
*
|
|
70
|
-
* Thrown at writer construction, so a declaration that cannot be swept fails
|
|
71
|
-
* before a run touches the index rather than at the sweep, after the writes.
|
|
72
|
-
*/
|
|
73
|
-
export declare function assertSweepableProvenanceField(searchType: SearchType, options: {
|
|
74
|
-
readonly requireFacetable: boolean;
|
|
75
|
-
readonly schema?: SearchSchema;
|
|
76
|
-
}): void;
|
|
77
|
-
/** Stamp each document with fixed bookkeeping fields as it streams past. */
|
|
78
|
-
export declare function stampDocuments<TDocument>(documents: AsyncIterable<TDocument>, stamp: Readonly<Record<string, string>>): AsyncIterable<TDocument & Record<string, string>>;
|
|
79
|
-
/** Delete a collection’s documents matching a Typesense filter. */
|
|
80
|
-
export declare function deleteByFilter(client: Client, collection: string, filterBy: string): Promise<void>;
|
|
81
41
|
//# sourceMappingURL=rebuild-support.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rebuild-support.d.ts","sourceRoot":"","sources":["../src/rebuild-support.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"rebuild-support.d.ts","sourceRoot":"","sources":["../src/rebuild-support.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,EAEL,KAAK,2BAA2B,EACjC,MAAM,4BAA4B,CAAC;AAOpC;;;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"}
|
package/dist/rebuild-support.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { 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
|
+
/** Documents imported per Typesense request unless a writer is told otherwise. */
|
|
6
|
+
const DEFAULT_BATCH_SIZE = 1000;
|
|
6
7
|
/**
|
|
7
8
|
* Apply the shared defaults, once, so neither writer restates them – including
|
|
8
9
|
* the collection naming, derived from each type ({@link deriveCollectionName})
|
|
@@ -59,77 +60,3 @@ function assertDistinctJoinTargetNames(searchType, options) {
|
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
|
-
/**
|
|
63
|
-
* Reject a {@link SearchType} that declares any of the bookkeeping field names
|
|
64
|
-
* a writer stamps, so a domain field can never collide with `source` /
|
|
65
|
-
* `last_seen`.
|
|
66
|
-
*/
|
|
67
|
-
export function assertNoReservedFields(searchType, reserved) {
|
|
68
|
-
const clashing = searchType.fields.filter((field) => reserved.includes(field.name));
|
|
69
|
-
if (clashing.length > 0) {
|
|
70
|
-
throw new Error(`SearchType “${searchType.name}” declares reserved bookkeeping field(s) ${clashing
|
|
71
|
-
.map((field) => `“${field.name}”`)
|
|
72
|
-
.join(', ')}`);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Reject a declared dataset field the provenance bookkeeping cannot be kept on.
|
|
77
|
-
* A type declaring one makes it the collection’s provenance field – the column
|
|
78
|
-
* the membership sweep enumerates and deletes by – so the declaration has to answer
|
|
79
|
-
* *which single dataset is this document’s* the way the private `source` field
|
|
80
|
-
* did:
|
|
81
|
-
*
|
|
82
|
-
* - **not `array`**: Typesense reads `field:=[a,b]` over a `string[]` as
|
|
83
|
-
* *contains any*, so a departed source would take every document that merely
|
|
84
|
-
* mentions it – including entities another selected dataset still
|
|
85
|
-
* contributes;
|
|
86
|
-
* - **no `transform`**: the sweep compares the stored value against the run’s
|
|
87
|
-
* selection, which carries raw dataset IRIs, so a transformed value would
|
|
88
|
-
* match nothing and the sweep would silently stop deleting;
|
|
89
|
-
* - **`facetable`** where the writer enumerates the indexed sources by faceting
|
|
90
|
-
* it (the In-place writer does; Blue/green only ever filters by a known IRI)
|
|
91
|
-
* – and faceted **on its own name**: a reference inheriting a facet policy
|
|
92
|
-
* from the type it names facets a companion holding only the admitted keys
|
|
93
|
-
* (`physicalFields(field, schema).facet`), so enumerating the sources
|
|
94
|
-
* through it would miss every dataset the policy excludes. `schema` is what
|
|
95
|
-
* resolves that; without one no policy is visible, as nowhere else.
|
|
96
|
-
*
|
|
97
|
-
* Thrown at writer construction, so a declaration that cannot be swept fails
|
|
98
|
-
* before a run touches the index rather than at the sweep, after the writes.
|
|
99
|
-
*/
|
|
100
|
-
export function assertSweepableProvenanceField(searchType, options) {
|
|
101
|
-
const field = datasetField(searchType);
|
|
102
|
-
if (field === undefined || isInternalField(field)) {
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
const problems = [];
|
|
106
|
-
if (field.array === true) {
|
|
107
|
-
problems.push('it is an array, and a membership sweep over one would delete every document merely mentioning a departed dataset');
|
|
108
|
-
}
|
|
109
|
-
if (field.transform !== undefined) {
|
|
110
|
-
problems.push('it declares a transform, and the sweep matches the stored value against the run’s raw dataset IRIs');
|
|
111
|
-
}
|
|
112
|
-
if (options.requireFacetable && field.facetable !== true) {
|
|
113
|
-
problems.push('it is not facetable, and the membership sweep enumerates the indexed datasets by faceting it');
|
|
114
|
-
}
|
|
115
|
-
else if (options.requireFacetable &&
|
|
116
|
-
physicalFields(field, options.schema).facet !== field.name) {
|
|
117
|
-
problems.push('it inherits a facet policy from the type it names, so the engine facets only the datasets the policy admits, and the membership sweep enumerates the indexed datasets by faceting it');
|
|
118
|
-
}
|
|
119
|
-
if (problems.length > 0) {
|
|
120
|
-
throw new Error(`SearchType “${searchType.name}” declares “${field.name}” over the indexed dataset, which this writer keeps its provenance bookkeeping on, but ${problems.join('; and ')}.`);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
/** Stamp each document with fixed bookkeeping fields as it streams past. */
|
|
124
|
-
export async function* stampDocuments(documents, stamp) {
|
|
125
|
-
for await (const document of documents) {
|
|
126
|
-
yield { ...document, ...stamp };
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
/** Delete a collection’s documents matching a Typesense filter. */
|
|
130
|
-
export async function deleteByFilter(client, collection, filterBy) {
|
|
131
|
-
await client
|
|
132
|
-
.collections(collection)
|
|
133
|
-
.documents()
|
|
134
|
-
.delete({ filter_by: filterBy });
|
|
135
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lde/search-typesense",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.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",
|
package/dist/import.d.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import type { Client } from 'typesense';
|
|
2
|
-
/** Documents imported per Typesense request unless a writer is told otherwise. */
|
|
3
|
-
export declare const DEFAULT_BATCH_SIZE = 1000;
|
|
4
|
-
/**
|
|
5
|
-
* Accumulates documents across writes and upserts them into a collection in
|
|
6
|
-
* fixed-size batches, so many small writes (one dataset at a time) still land
|
|
7
|
-
* as few Typesense requests. Call {@link flush} to import the remainder.
|
|
8
|
-
*/
|
|
9
|
-
export declare class BatchImporter<TDocument extends {
|
|
10
|
-
id: string;
|
|
11
|
-
}> {
|
|
12
|
-
private readonly client;
|
|
13
|
-
private readonly collection;
|
|
14
|
-
private readonly batchSize;
|
|
15
|
-
private buffer;
|
|
16
|
-
constructor(client: Client, collection: string, batchSize: number);
|
|
17
|
-
/** Add a stream of documents, importing a batch whenever one fills up. */
|
|
18
|
-
add(documents: AsyncIterable<TDocument>): Promise<void>;
|
|
19
|
-
/** Import any remaining documents. */
|
|
20
|
-
flush(): Promise<void>;
|
|
21
|
-
private importBuffer;
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Create-or-replace one batch of whole documents, keyed on `id`, throwing if any
|
|
25
|
-
* individual document fails (Typesense’s bulk import otherwise reports
|
|
26
|
-
* per-document failures without rejecting).
|
|
27
|
-
*/
|
|
28
|
-
export declare function importBatch(client: Client, collection: string, batch: readonly {
|
|
29
|
-
id: string;
|
|
30
|
-
}[]): Promise<void>;
|
|
31
|
-
//# sourceMappingURL=import.d.ts.map
|
package/dist/import.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"import.d.ts","sourceRoot":"","sources":["../src/import.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAkB,MAAM,WAAW,CAAC;AAExD,kFAAkF;AAClF,eAAO,MAAM,kBAAkB,OAAO,CAAC;AAEvC;;;;GAIG;AACH,qBAAa,aAAa,CAAC,SAAS,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE;IAIvD,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAL5B,OAAO,CAAC,MAAM,CAAmB;gBAGd,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM;IAGpC,0EAA0E;IACpE,GAAG,CAAC,SAAS,EAAE,aAAa,CAAC,SAAS,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAS7D,sCAAsC;IAChC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAMd,YAAY;CAK3B;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAAE,GAC/B,OAAO,CAAC,IAAI,CAAC,CAkBf"}
|
package/dist/import.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/** Documents imported per Typesense request unless a writer is told otherwise. */
|
|
2
|
-
export const DEFAULT_BATCH_SIZE = 1000;
|
|
3
|
-
/**
|
|
4
|
-
* Accumulates documents across writes and upserts them into a collection in
|
|
5
|
-
* fixed-size batches, so many small writes (one dataset at a time) still land
|
|
6
|
-
* as few Typesense requests. Call {@link flush} to import the remainder.
|
|
7
|
-
*/
|
|
8
|
-
export class BatchImporter {
|
|
9
|
-
client;
|
|
10
|
-
collection;
|
|
11
|
-
batchSize;
|
|
12
|
-
buffer = [];
|
|
13
|
-
constructor(client, collection, batchSize) {
|
|
14
|
-
this.client = client;
|
|
15
|
-
this.collection = collection;
|
|
16
|
-
this.batchSize = batchSize;
|
|
17
|
-
}
|
|
18
|
-
/** Add a stream of documents, importing a batch whenever one fills up. */
|
|
19
|
-
async add(documents) {
|
|
20
|
-
for await (const document of documents) {
|
|
21
|
-
this.buffer.push(document);
|
|
22
|
-
if (this.buffer.length >= this.batchSize) {
|
|
23
|
-
await this.importBuffer();
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
/** Import any remaining documents. */
|
|
28
|
-
async flush() {
|
|
29
|
-
if (this.buffer.length > 0) {
|
|
30
|
-
await this.importBuffer();
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
async importBuffer() {
|
|
34
|
-
const batch = this.buffer;
|
|
35
|
-
this.buffer = [];
|
|
36
|
-
await importBatch(this.client, this.collection, batch);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Create-or-replace one batch of whole documents, keyed on `id`, throwing if any
|
|
41
|
-
* individual document fails (Typesense’s bulk import otherwise reports
|
|
42
|
-
* per-document failures without rejecting).
|
|
43
|
-
*/
|
|
44
|
-
export async function importBatch(client, collection, batch) {
|
|
45
|
-
const results = (await client
|
|
46
|
-
.collections(collection)
|
|
47
|
-
.documents()
|
|
48
|
-
.import(batch, {
|
|
49
|
-
action: 'upsert',
|
|
50
|
-
// Collect per-document outcomes instead of throwing the client’s opaque
|
|
51
|
-
// ImportError, so we can report which documents failed and why.
|
|
52
|
-
throwOnFail: false,
|
|
53
|
-
}));
|
|
54
|
-
const failures = results.filter((result) => !result.success);
|
|
55
|
-
if (failures.length > 0) {
|
|
56
|
-
throw new Error(`Typesense upsert into “${collection}” failed for ${failures.length}/${results.length} documents: ${failures
|
|
57
|
-
.map((failure) => failure.error)
|
|
58
|
-
.join('; ')}`);
|
|
59
|
-
}
|
|
60
|
-
}
|
package/dist/sweep.d.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import type { SearchType } from '@lde/search';
|
|
2
|
-
/** The private field carrying the dataset IRI a document came from, for a type
|
|
3
|
-
* that declares none of its own – see {@link provenanceField}. */
|
|
4
|
-
export declare const SOURCE_FIELD = "source";
|
|
5
|
-
/** The document field carrying the id of the run that last wrote a document. */
|
|
6
|
-
export declare const LAST_SEEN_FIELD = "last_seen";
|
|
7
|
-
/**
|
|
8
|
-
* The field a collection carries its documents’ dataset IRI in – the one the
|
|
9
|
-
* membership sweep enumerates, filters and deletes by.
|
|
10
|
-
*
|
|
11
|
-
* A type that declares a field over the dataset
|
|
12
|
-
* ({@link datasetField `from: 'dataset'`}) *is* its provenance: the writer keeps
|
|
13
|
-
* its bookkeeping on that field rather than stamping a private one beside it,
|
|
14
|
-
* so the facet, the label resolution, any `derive` and the sweep all read one
|
|
15
|
-
* column that cannot drift. A type declaring nothing – or declaring the dataset
|
|
16
|
-
* only as an *internal* field, which the projection prunes before the writer
|
|
17
|
-
* ever sees it – falls back to the private {@link SOURCE_FIELD}, and behaves
|
|
18
|
-
* exactly as it always has.
|
|
19
|
-
*/
|
|
20
|
-
export declare function provenanceField(searchType: SearchType): string;
|
|
21
|
-
/**
|
|
22
|
-
* Sources whose documents must leave the index: indexed, but no longer part
|
|
23
|
-
* of the run’s selection. Selection is membership, not processing – a dataset
|
|
24
|
-
* skipped as unchanged is still selected, so its documents survive.
|
|
25
|
-
*
|
|
26
|
-
* @param indexedSources Source IRIs present in the collection
|
|
27
|
-
* @param selectedSources Source IRIs the run’s selector produced
|
|
28
|
-
*/
|
|
29
|
-
export declare function departedSources(indexedSources: Iterable<string>, selectedSources: Iterable<string>): string[];
|
|
30
|
-
/**
|
|
31
|
-
* Typesense filter matching a source’s documents that this run did not touch:
|
|
32
|
-
* everything the source no longer contains, ready for a per-source sweep.
|
|
33
|
-
*
|
|
34
|
-
* @param sourceField The collection’s {@link provenanceField}
|
|
35
|
-
* @param sourceIri The dataset IRI the documents carry in that field
|
|
36
|
-
* @param runId The current run; documents it wrote carry it as `last_seen`
|
|
37
|
-
*/
|
|
38
|
-
export declare function staleDocumentsFilter(sourceField: string, sourceIri: string, runId: string): string;
|
|
39
|
-
/**
|
|
40
|
-
* Typesense filter matching all of a source’s documents, ready to drop a whole
|
|
41
|
-
* source (a departed source’s membership sweep, or a Blue/green writer rolling
|
|
42
|
-
* a failed dataset out of its not-yet-live collection).
|
|
43
|
-
*
|
|
44
|
-
* @param sourceField The collection’s {@link provenanceField}
|
|
45
|
-
* @param sourceIri The dataset IRI the documents carry in that field
|
|
46
|
-
*/
|
|
47
|
-
export declare function sourceDocumentsFilter(sourceField: string, sourceIri: string): string;
|
|
48
|
-
/**
|
|
49
|
-
* Typesense filter matching the documents **this run** wrote for a source: the
|
|
50
|
-
* inverse of {@link staleDocumentsFilter}. An In-place writer deletes these to
|
|
51
|
-
* discard a source’s in-progress writes (the dump-fallback reset) without
|
|
52
|
-
* touching its prior-run documents, which the success sweep or a failed run
|
|
53
|
-
* still owns.
|
|
54
|
-
*
|
|
55
|
-
* @param sourceField The collection’s {@link provenanceField}
|
|
56
|
-
* @param sourceIri The dataset IRI the documents carry in that field
|
|
57
|
-
* @param runId The current run, stamped on its documents as `last_seen`
|
|
58
|
-
*/
|
|
59
|
-
export declare function thisRunDocumentsFilter(sourceField: string, sourceIri: string, runId: string): string;
|
|
60
|
-
/**
|
|
61
|
-
* Typesense filters deleting every departed source’s documents, combined into
|
|
62
|
-
* as few filters as fit: deletes travel in the URL query string, so each
|
|
63
|
-
* filter stays under a conservative length budget rather than listing every
|
|
64
|
-
* source in one string.
|
|
65
|
-
*
|
|
66
|
-
* @param sourceField The collection’s {@link provenanceField}
|
|
67
|
-
* @param departed The departed source IRIs ({@link departedSources})
|
|
68
|
-
*/
|
|
69
|
-
export declare function membershipSweepFilters(sourceField: string, departed: readonly string[]): string[];
|
|
70
|
-
//# sourceMappingURL=sweep.d.ts.map
|
package/dist/sweep.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sweep.d.ts","sourceRoot":"","sources":["../src/sweep.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG9C;mEACmE;AACnE,eAAO,MAAM,YAAY,WAAW,CAAC;AAErC,gFAAgF;AAChF,eAAO,MAAM,eAAe,cAAc,CAAC;AAE3C;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,UAAU,GAAG,MAAM,CAK9D;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,EAChC,eAAe,EAAE,QAAQ,CAAC,MAAM,CAAC,GAChC,MAAM,EAAE,CAGV;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACZ,MAAM,CAER;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAChB,MAAM,CAER;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACZ,MAAM,CAER;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,SAAS,MAAM,EAAE,GAC1B,MAAM,EAAE,CAqBV"}
|