@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.
@@ -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 { LAST_SEEN_FIELD, SOURCE_FIELD, departedSources, membershipSweepFilters, provenanceField, staleDocumentsFilter, thisRunDocumentsFilter, } from './sweep.js';
6
- import { assertNoReservedFields, assertSweepableProvenanceField, deleteByFilter, resolveRebuildOptions, stampDocuments, } from './rebuild-support.js';
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-source atomicity – no swap, no staging. Every document is stamped with
22
- * its `source` (the dataset IRI) and `last_seen` (the run id); deletion is a
23
- * sweep, never special-cased:
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** deletes the source’s documents the run did
26
- * not rewrite (`source = dataset && last_seen != runId`). A failed dataset
27
- * is not swept – its output is incomplete, and the next successful run
28
- * reconciles;
29
- * - **reset** (the pipeline’s dump-fallback discard) deletes only *this run’s*
30
- * writes for the dataset (`source = dataset && last_seen = runId`), so the
31
- * dump re-run rebuilds it cleanly while the source’s prior-run documents are
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 two
42
- * bookkeeping fields. A collection that already exists is used as it is, with
43
- * one exception: it must carry every reference field the declaration asks for
44
- * and every facet companion its facets read, or the run fails with the
45
- * drop-and-rebuild that fixes it
46
- * ({@link assertReferencesAndFacetCompanionsPresent}).
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
- * Document ids must be unique per (source, entity) – the caller keys them –
49
- * or documents from different sources overwrite each other.
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
- assertNoReservedFields(searchType, [SOURCE_FIELD, LAST_SEEN_FIELD]);
75
- assertSweepableProvenanceField(searchType, {
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
- this.sourceField = provenanceField(searchType);
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 + the bookkeeping
92
- // fields. The private `source` – faceted so the membership sweep can
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, or a facet policy on a type this one references – and
113
- // neither a reference nor a facet companion can be added to it here.
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
- await assertReferencesAndFacetCompanionsPresent(this.client, name, declaredFields, facetCompanionsOf(this.searchType, definitionOptions.schema));
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 importer = new BatchImporter(this.client, name, batchSize);
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
- // Stamping the provenance field – rather than trusting the projection
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 deleteByFilter(this.client, name, staleDocumentsFilter(sourceField, dataset.iri.toString(), context.runId));
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 importer.flush();
157
- const departed = departedSources(await this.indexedSources(name, this.maxSweepableSources), context.selectedSources());
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
- async function assertReferencesAndFacetCompanionsPresent(client, name, fields, facetCompanions) {
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((await client.collections(name).retrieve()).fields.map((field) => [
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';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,YAAY,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AACvE,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,YAAY,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,EACL,eAAe,EACf,sBAAsB,EACtB,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AACvE,YAAY,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,YAAY,EAAE,wBAAwB,EAAE,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"}
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 { Client } from 'typesense';
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,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAQ5D,OAAO,EAEL,KAAK,2BAA2B,EACjC,MAAM,4BAA4B,CAAC;AAKpC;;;GAGG;AACH,MAAM,WAAW,cAAe,SAAQ,2BAA2B;IACjE,+DAA+D;IAC/D,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,0EAA0E;IAC1E,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;0FAC0F;AAC1F,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;uEAGmE;IACnE,QAAQ,CAAC,iBAAiB,EAAE,2BAA2B,GAAG;QACxD,QAAQ,CAAC,iBAAiB,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,MAAM,CAAC;KAChE,CAAC;CACH;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE,cAAc,GACtB,sBAAsB,CAkBxB;AAsCD;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,SAAS,MAAM,EAAE,GAC1B,IAAI,CAWN;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,8BAA8B,CAC5C,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE;IACP,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC;CAChC,GACA,IAAI,CAiCN;AAED,4EAA4E;AAC5E,wBAAuB,cAAc,CAAC,SAAS,EAC7C,SAAS,EAAE,aAAa,CAAC,SAAS,CAAC,EACnC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GACtC,aAAa,CAAC,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAInD;AAED,mEAAmE;AACnE,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAKf"}
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"}
@@ -1,8 +1,9 @@
1
- import { datasetField, isInternalField, joinGraph, physicalFields, referenceFields, } from '@lde/search/adapter';
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
- import { DEFAULT_BATCH_SIZE } from './import.js';
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.28.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
@@ -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
@@ -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"}