@lde/search-typesense 0.18.2 → 0.20.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 +4 -0
- package/dist/blue-green-rebuild.d.ts.map +1 -1
- package/dist/blue-green-rebuild.js +19 -5
- package/dist/in-place-rebuild.d.ts +7 -2
- package/dist/in-place-rebuild.d.ts.map +1 -1
- package/dist/in-place-rebuild.js +39 -13
- package/dist/rebuild-support.d.ts +23 -0
- package/dist/rebuild-support.d.ts.map +1 -1
- package/dist/rebuild-support.js +40 -0
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +4 -0
- package/dist/sweep.d.ts +28 -8
- package/dist/sweep.d.ts.map +1 -1
- package/dist/sweep.js +40 -14
- package/package.json +2 -2
|
@@ -50,6 +50,10 @@ export declare class BlueGreenRebuild<TDocument extends {
|
|
|
50
50
|
*/
|
|
51
51
|
readonly collectionName: string;
|
|
52
52
|
private readonly resolved;
|
|
53
|
+
/** The column this collection carries its documents’ dataset IRI in: the
|
|
54
|
+
* type’s declared dataset field, or the private `source` when it declares
|
|
55
|
+
* none. Both the stamp and the per-dataset rollback filter read it. */
|
|
56
|
+
private readonly sourceField;
|
|
53
57
|
constructor(client: Client, searchType: SearchType, options?: BlueGreenRebuildOptions);
|
|
54
58
|
openRun(context: RunContext): Promise<RunWriter<TDocument>>;
|
|
55
59
|
/** The collection an alias currently points at, or `undefined` if unset. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blue-green-rebuild.d.ts","sourceRoot":"","sources":["../src/blue-green-rebuild.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EAEV,UAAU,EACV,SAAS,EACT,MAAM,EACP,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"blue-green-rebuild.d.ts","sourceRoot":"","sources":["../src/blue-green-rebuild.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EAEV,UAAU,EACV,SAAS,EACT,MAAM,EACP,MAAM,eAAe,CAAC;AAUvB,OAAO,EAML,KAAK,cAAc,EAEpB,MAAM,sBAAsB,CAAC;AAE9B;;yDAEyD;AACzD,MAAM,MAAM,uBAAuB,GAAG,cAAc,CAAC;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,qBAAa,gBAAgB,CAC3B,SAAS,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAChC,YAAW,MAAM,CAAC,SAAS,CAAC;IAiB1B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAjB7B;;;;;;;OAOG;IACH,SAAgB,cAAc,EAAE,MAAM,CAAC;IACvC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyB;IAClD;;4EAEwE;IACxE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;gBAGlB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,UAAU,EACvC,OAAO,GAAE,uBAA4B;IAYjC,OAAO,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IAmGjE,4EAA4E;YAC9D,WAAW;CAW1B"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { buildCollectionDefinition } from './collection-definition.js';
|
|
2
2
|
import { BatchImporter } from './import.js';
|
|
3
3
|
import { httpStatus, openLockedRun, releaseLock } from './lock.js';
|
|
4
|
-
import { SOURCE_FIELD, sourceDocumentsFilter } from './sweep.js';
|
|
5
|
-
import { assertNoReservedFields, deleteByFilter, resolveRebuildOptions, stampDocuments, } from './rebuild-support.js';
|
|
4
|
+
import { SOURCE_FIELD, provenanceField, sourceDocumentsFilter, } from './sweep.js';
|
|
5
|
+
import { assertNoReservedFields, assertSweepableProvenanceField, deleteByFilter, resolveRebuildOptions, stampDocuments, } from './rebuild-support.js';
|
|
6
6
|
/**
|
|
7
7
|
* Blue/green Rebuild (build a fresh index alongside the live one, then swap to
|
|
8
8
|
* it atomically) as a transactional `Writer`: each run builds the search
|
|
@@ -45,17 +45,26 @@ export class BlueGreenRebuild {
|
|
|
45
45
|
*/
|
|
46
46
|
collectionName;
|
|
47
47
|
resolved;
|
|
48
|
+
/** The column this collection carries its documents’ dataset IRI in: the
|
|
49
|
+
* type’s declared dataset field, or the private `source` when it declares
|
|
50
|
+
* none. Both the stamp and the per-dataset rollback filter read it. */
|
|
51
|
+
sourceField;
|
|
48
52
|
constructor(client, searchType, options = {}) {
|
|
49
53
|
this.client = client;
|
|
50
54
|
this.searchType = searchType;
|
|
51
55
|
// `source` is stamped on every document for per-dataset rollback.
|
|
52
56
|
assertNoReservedFields(searchType, [SOURCE_FIELD]);
|
|
57
|
+
// Rollback filters by a known dataset IRI rather than enumerating the
|
|
58
|
+
// indexed ones, so a declared field need not be facetable here.
|
|
59
|
+
assertSweepableProvenanceField(searchType, { requireFacetable: false });
|
|
60
|
+
this.sourceField = provenanceField(searchType);
|
|
53
61
|
this.resolved = resolveRebuildOptions(searchType, options);
|
|
54
62
|
this.collectionName = this.resolved.definitionOptions.name;
|
|
55
63
|
}
|
|
56
64
|
async openRun(context) {
|
|
57
65
|
const { batchSize, lockTtlMs, definitionOptions } = this.resolved;
|
|
58
66
|
const name = this.collectionName;
|
|
67
|
+
const sourceField = this.sourceField;
|
|
59
68
|
return openLockedRun(this.client, name, lockTtlMs, async () => {
|
|
60
69
|
// Create the fresh (blue) collection up front, so a failure surfaces
|
|
61
70
|
// before any dataset is processed. startedAt orders the versioned names;
|
|
@@ -66,9 +75,14 @@ export class BlueGreenRebuild {
|
|
|
66
75
|
await this.client.collections().create({
|
|
67
76
|
...definition,
|
|
68
77
|
name: collection,
|
|
78
|
+
// The private `source` only when the type declares no dataset field of
|
|
79
|
+
// its own; a declared one is already in the definition and carries the
|
|
80
|
+
// same IRI.
|
|
69
81
|
fields: [
|
|
70
82
|
...(definition.fields ?? []),
|
|
71
|
-
|
|
83
|
+
...(sourceField === SOURCE_FIELD
|
|
84
|
+
? [{ name: SOURCE_FIELD, type: 'string' }]
|
|
85
|
+
: []),
|
|
72
86
|
],
|
|
73
87
|
});
|
|
74
88
|
const importer = new BatchImporter(this.client, collection, batchSize);
|
|
@@ -77,11 +91,11 @@ export class BlueGreenRebuild {
|
|
|
77
91
|
// still buffered must land first, so the delete filter sees them.
|
|
78
92
|
const rollback = async (dataset) => {
|
|
79
93
|
await importer.flush();
|
|
80
|
-
await deleteByFilter(this.client, collection, sourceDocumentsFilter(dataset.iri.toString()));
|
|
94
|
+
await deleteByFilter(this.client, collection, sourceDocumentsFilter(sourceField, dataset.iri.toString()));
|
|
81
95
|
};
|
|
82
96
|
return {
|
|
83
97
|
write: async (dataset, documents) => importer.add(stampDocuments(documents, {
|
|
84
|
-
[
|
|
98
|
+
[sourceField]: dataset.iri.toString(),
|
|
85
99
|
})),
|
|
86
100
|
flush: async (dataset, outcome) => {
|
|
87
101
|
// A successful dataset keeps its streamed documents (they go live at
|
|
@@ -56,11 +56,16 @@ export declare class InPlaceRebuild<TDocument extends {
|
|
|
56
56
|
readonly collectionName: string;
|
|
57
57
|
private readonly maxSweepableSources;
|
|
58
58
|
private readonly resolved;
|
|
59
|
+
/** The column this collection carries its documents’ dataset IRI in: the
|
|
60
|
+
* type’s declared dataset field, or the private `source` when it declares
|
|
61
|
+
* none. Everything below – the stamp, every sweep filter, the source
|
|
62
|
+
* enumeration – reads this one name. */
|
|
63
|
+
private readonly sourceField;
|
|
59
64
|
constructor(client: Client, searchType: SearchType, options?: InPlaceRebuildOptions);
|
|
60
65
|
openRun(context: RunContext): Promise<RunWriter<TDocument>>;
|
|
61
66
|
/**
|
|
62
|
-
* The distinct sources present in the collection, via a single
|
|
63
|
-
*
|
|
67
|
+
* The distinct sources present in the collection, via a single facet over its
|
|
68
|
+
* {@link provenanceField}. Requests one bucket beyond `maxSources` so genuine truncation is
|
|
64
69
|
* distinguishable from an exactly-full result: `maxSources` buckets are
|
|
65
70
|
* returned intact, `maxSources + 1` proves more exist and the sweep would
|
|
66
71
|
* miss some, so it throws rather than delete blind.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"in-place-rebuild.d.ts","sourceRoot":"","sources":["../src/in-place-rebuild.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAExC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,KAAK,EAEV,UAAU,EACV,SAAS,EACT,MAAM,EACP,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"in-place-rebuild.d.ts","sourceRoot":"","sources":["../src/in-place-rebuild.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAExC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,KAAK,EAEV,UAAU,EACV,SAAS,EACT,MAAM,EACP,MAAM,eAAe,CAAC;AAavB,OAAO,EAML,KAAK,cAAc,EAEpB,MAAM,sBAAsB,CAAC;AAa9B;gCACgC;AAChC,MAAM,WAAW,qBAAsB,SAAQ,cAAc;IAC3D;sEACkE;IAClE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CACvC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,qBAAa,cAAc,CACzB,SAAS,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAChC,YAAW,MAAM,CAAC,SAAS,CAAC;IAiB1B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAjB7B;;;;;OAKG;IACH,SAAgB,cAAc,EAAE,MAAM,CAAC;IACvC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAS;IAC7C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyB;IAClD;;;6CAGyC;IACzC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;gBAGlB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,UAAU,EACvC,OAAO,GAAE,qBAA0B;IAc/B,OAAO,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IAiHjE;;;;;;OAMG;YACW,cAAc;CAsB7B"}
|
package/dist/in-place-rebuild.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { buildCollectionDefinition } from './collection-definition.js';
|
|
2
2
|
import { BatchImporter } from './import.js';
|
|
3
3
|
import { ensureCollectionExists, openLockedRun, releaseLock } from './lock.js';
|
|
4
|
-
import { LAST_SEEN_FIELD, SOURCE_FIELD, departedSources, membershipSweepFilters, staleDocumentsFilter, thisRunDocumentsFilter, } from './sweep.js';
|
|
5
|
-
import { assertNoReservedFields, deleteByFilter, resolveRebuildOptions, stampDocuments, } from './rebuild-support.js';
|
|
4
|
+
import { LAST_SEEN_FIELD, SOURCE_FIELD, departedSources, membershipSweepFilters, provenanceField, staleDocumentsFilter, thisRunDocumentsFilter, } from './sweep.js';
|
|
5
|
+
import { assertNoReservedFields, assertSweepableProvenanceField, deleteByFilter, resolveRebuildOptions, stampDocuments, } from './rebuild-support.js';
|
|
6
6
|
/**
|
|
7
7
|
* Default ceiling on the distinct sources the membership sweep enumerates via
|
|
8
8
|
* a single `source` facet. The sweep needs the complete source set to spot
|
|
@@ -58,10 +58,17 @@ export class InPlaceRebuild {
|
|
|
58
58
|
collectionName;
|
|
59
59
|
maxSweepableSources;
|
|
60
60
|
resolved;
|
|
61
|
+
/** The column this collection carries its documents’ dataset IRI in: the
|
|
62
|
+
* type’s declared dataset field, or the private `source` when it declares
|
|
63
|
+
* none. Everything below – the stamp, every sweep filter, the source
|
|
64
|
+
* enumeration – reads this one name. */
|
|
65
|
+
sourceField;
|
|
61
66
|
constructor(client, searchType, options = {}) {
|
|
62
67
|
this.client = client;
|
|
63
68
|
this.searchType = searchType;
|
|
64
69
|
assertNoReservedFields(searchType, [SOURCE_FIELD, LAST_SEEN_FIELD]);
|
|
70
|
+
assertSweepableProvenanceField(searchType, { requireFacetable: true });
|
|
71
|
+
this.sourceField = provenanceField(searchType);
|
|
65
72
|
const { maxSweepableSources = DEFAULT_MAX_SWEEPABLE_SOURCES, ...rebuildOptions } = options;
|
|
66
73
|
this.maxSweepableSources = maxSweepableSources;
|
|
67
74
|
this.resolved = resolveRebuildOptions(searchType, rebuildOptions);
|
|
@@ -70,14 +77,20 @@ export class InPlaceRebuild {
|
|
|
70
77
|
async openRun(context) {
|
|
71
78
|
const { batchSize, lockTtlMs, definitionOptions } = this.resolved;
|
|
72
79
|
const name = this.collectionName;
|
|
80
|
+
const sourceField = this.sourceField;
|
|
73
81
|
return openLockedRun(this.client, name, lockTtlMs, async () => {
|
|
74
82
|
// Create the collection on demand: SearchType schema + the bookkeeping
|
|
75
|
-
// fields
|
|
76
|
-
// distinct sources
|
|
83
|
+
// fields. The private `source` – faceted so the membership sweep can
|
|
84
|
+
// enumerate the distinct sources – only when the type declares no dataset
|
|
85
|
+
// field of its own; when it does, that declared field already carries the
|
|
86
|
+
// IRI (and its own `facet: true`), and a second column would be the same
|
|
87
|
+
// value twice, free to drift.
|
|
77
88
|
await ensureCollectionExists(this.client, name, () => {
|
|
78
89
|
const definition = buildCollectionDefinition(this.searchType, definitionOptions);
|
|
79
90
|
const bookkeeping = [
|
|
80
|
-
|
|
91
|
+
...(sourceField === SOURCE_FIELD
|
|
92
|
+
? [{ name: SOURCE_FIELD, type: 'string', facet: true }]
|
|
93
|
+
: []),
|
|
81
94
|
{ name: LAST_SEEN_FIELD, type: 'string' },
|
|
82
95
|
];
|
|
83
96
|
return {
|
|
@@ -87,8 +100,21 @@ export class InPlaceRebuild {
|
|
|
87
100
|
});
|
|
88
101
|
const importer = new BatchImporter(this.client, name, batchSize);
|
|
89
102
|
return {
|
|
103
|
+
// Stamping the provenance field – rather than trusting the projection
|
|
104
|
+
// to have filled a declared one – is what keeps the sweep total: a
|
|
105
|
+
// caller projecting without a dataset context, or a document reaching
|
|
106
|
+
// the writer any other way, still lands in a swept collection. Where
|
|
107
|
+
// the projection did fill it, the value is the same dataset IRI, so the
|
|
108
|
+
// stamp only ever reasserts it.
|
|
109
|
+
//
|
|
110
|
+
// It reasserts the sweep’s column only, never the physical fanout a
|
|
111
|
+
// declared field also produces (the folded `${name}_search` companion
|
|
112
|
+
// of a `searchable` one). Folding is the projection’s convention, and
|
|
113
|
+
// restating it here would put it in two places – the split this change
|
|
114
|
+
// exists to close. So a document that reached the writer unprojected is
|
|
115
|
+
// sweepable but not full-text findable by dataset: absent, never wrong.
|
|
90
116
|
write: async (dataset, documents) => importer.add(stampDocuments(documents, {
|
|
91
|
-
[
|
|
117
|
+
[sourceField]: dataset.iri.toString(),
|
|
92
118
|
[LAST_SEEN_FIELD]: context.runId,
|
|
93
119
|
})),
|
|
94
120
|
flush: async (dataset, outcome) => {
|
|
@@ -101,19 +127,19 @@ export class InPlaceRebuild {
|
|
|
101
127
|
// stale ones for the next successful run to reconcile.
|
|
102
128
|
return;
|
|
103
129
|
}
|
|
104
|
-
await deleteByFilter(this.client, name, staleDocumentsFilter(dataset.iri.toString(), context.runId));
|
|
130
|
+
await deleteByFilter(this.client, name, staleDocumentsFilter(sourceField, dataset.iri.toString(), context.runId));
|
|
105
131
|
},
|
|
106
132
|
reset: async (dataset) => {
|
|
107
133
|
// Discard only this run’s partial writes for the source (the failed
|
|
108
134
|
// endpoint attempt) so the dump re-run rebuilds it cleanly; the
|
|
109
135
|
// source’s prior-run documents stay for the success sweep.
|
|
110
136
|
await importer.flush();
|
|
111
|
-
await deleteByFilter(this.client, name, thisRunDocumentsFilter(dataset.iri.toString(), context.runId));
|
|
137
|
+
await deleteByFilter(this.client, name, thisRunDocumentsFilter(sourceField, dataset.iri.toString(), context.runId));
|
|
112
138
|
},
|
|
113
139
|
commit: async () => {
|
|
114
140
|
await importer.flush();
|
|
115
141
|
const departed = departedSources(await this.indexedSources(name, this.maxSweepableSources), context.selectedSources());
|
|
116
|
-
for (const filter of membershipSweepFilters(departed)) {
|
|
142
|
+
for (const filter of membershipSweepFilters(sourceField, departed)) {
|
|
117
143
|
await deleteByFilter(this.client, name, filter);
|
|
118
144
|
}
|
|
119
145
|
await releaseLock(this.client, name);
|
|
@@ -125,8 +151,8 @@ export class InPlaceRebuild {
|
|
|
125
151
|
});
|
|
126
152
|
}
|
|
127
153
|
/**
|
|
128
|
-
* The distinct sources present in the collection, via a single
|
|
129
|
-
*
|
|
154
|
+
* The distinct sources present in the collection, via a single facet over its
|
|
155
|
+
* {@link provenanceField}. Requests one bucket beyond `maxSources` so genuine truncation is
|
|
130
156
|
* distinguishable from an exactly-full result: `maxSources` buckets are
|
|
131
157
|
* returned intact, `maxSources + 1` proves more exist and the sweep would
|
|
132
158
|
* miss some, so it throws rather than delete blind.
|
|
@@ -137,9 +163,9 @@ export class InPlaceRebuild {
|
|
|
137
163
|
.documents()
|
|
138
164
|
.search({
|
|
139
165
|
q: '*',
|
|
140
|
-
query_by:
|
|
166
|
+
query_by: this.sourceField,
|
|
141
167
|
per_page: 0,
|
|
142
|
-
facet_by:
|
|
168
|
+
facet_by: this.sourceField,
|
|
143
169
|
max_facet_values: maxSources + 1,
|
|
144
170
|
});
|
|
145
171
|
const counts = response.facet_counts?.[0]?.counts ?? [];
|
|
@@ -43,6 +43,29 @@ export declare function resolveRebuildOptions(searchType: SearchType, options: R
|
|
|
43
43
|
* `last_seen`.
|
|
44
44
|
*/
|
|
45
45
|
export declare function assertNoReservedFields(searchType: SearchType, reserved: readonly string[]): void;
|
|
46
|
+
/**
|
|
47
|
+
* Reject a declared dataset field the provenance bookkeeping cannot be kept on.
|
|
48
|
+
* A type declaring one makes it the collection’s provenance field – the column
|
|
49
|
+
* the membership sweep enumerates and deletes by – so the declaration has to answer
|
|
50
|
+
* *which single dataset is this document’s* the way the private `source` field
|
|
51
|
+
* did:
|
|
52
|
+
*
|
|
53
|
+
* - **not `array`**: Typesense reads `field:=[a,b]` over a `string[]` as
|
|
54
|
+
* *contains any*, so a departed source would take every document that merely
|
|
55
|
+
* mentions it – including entities another selected dataset still
|
|
56
|
+
* contributes;
|
|
57
|
+
* - **no `transform`**: the sweep compares the stored value against the run’s
|
|
58
|
+
* selection, which carries raw dataset IRIs, so a transformed value would
|
|
59
|
+
* match nothing and the sweep would silently stop deleting;
|
|
60
|
+
* - **`facetable`** where the writer enumerates the indexed sources by faceting
|
|
61
|
+
* it (the In-place writer does; Blue/green only ever filters by a known IRI).
|
|
62
|
+
*
|
|
63
|
+
* Thrown at writer construction, so a declaration that cannot be swept fails
|
|
64
|
+
* before a run touches the index rather than at the sweep, after the writes.
|
|
65
|
+
*/
|
|
66
|
+
export declare function assertSweepableProvenanceField(searchType: SearchType, options: {
|
|
67
|
+
readonly requireFacetable: boolean;
|
|
68
|
+
}): void;
|
|
46
69
|
/** Stamp each document with fixed bookkeeping fields as it streams past. */
|
|
47
70
|
export declare function stampDocuments<TDocument>(documents: AsyncIterable<TDocument>, stamp: Readonly<Record<string, string>>): AsyncIterable<TDocument & Record<string, string>>;
|
|
48
71
|
/** Delete a collection’s documents matching a Typesense filter. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rebuild-support.d.ts","sourceRoot":"","sources":["../src/rebuild-support.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"rebuild-support.d.ts","sourceRoot":"","sources":["../src/rebuild-support.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,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;;6DAEyD;IACzD,QAAQ,CAAC,iBAAiB,EAAE,2BAA2B,GAAG;QACxD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;KACvB,CAAC;CACH;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE,cAAc,GACtB,sBAAsB,CAgBxB;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,SAAS,MAAM,EAAE,GAC1B,IAAI,CAWN;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,8BAA8B,CAC5C,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE;IAAE,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAA;CAAE,GAC9C,IAAI,CA0BN;AAED,4EAA4E;AAC5E,wBAAuB,cAAc,CAAC,SAAS,EAC7C,SAAS,EAAE,aAAa,CAAC,SAAS,CAAC,EACnC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GACtC,aAAa,CAAC,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAInD;AAED,mEAAmE;AACnE,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAKf"}
|
package/dist/rebuild-support.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { datasetField, isInternalField } from '@lde/search/adapter';
|
|
1
2
|
import { buildCollectionDefinition, } from './collection-definition.js';
|
|
2
3
|
import { deriveCollectionName } from './collection-name.js';
|
|
3
4
|
import { DEFAULT_LOCK_TTL_MS } from './lock.js';
|
|
@@ -41,6 +42,45 @@ export function assertNoReservedFields(searchType, reserved) {
|
|
|
41
42
|
.join(', ')}`);
|
|
42
43
|
}
|
|
43
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Reject a declared dataset field the provenance bookkeeping cannot be kept on.
|
|
47
|
+
* A type declaring one makes it the collection’s provenance field – the column
|
|
48
|
+
* the membership sweep enumerates and deletes by – so the declaration has to answer
|
|
49
|
+
* *which single dataset is this document’s* the way the private `source` field
|
|
50
|
+
* did:
|
|
51
|
+
*
|
|
52
|
+
* - **not `array`**: Typesense reads `field:=[a,b]` over a `string[]` as
|
|
53
|
+
* *contains any*, so a departed source would take every document that merely
|
|
54
|
+
* mentions it – including entities another selected dataset still
|
|
55
|
+
* contributes;
|
|
56
|
+
* - **no `transform`**: the sweep compares the stored value against the run’s
|
|
57
|
+
* selection, which carries raw dataset IRIs, so a transformed value would
|
|
58
|
+
* match nothing and the sweep would silently stop deleting;
|
|
59
|
+
* - **`facetable`** where the writer enumerates the indexed sources by faceting
|
|
60
|
+
* it (the In-place writer does; Blue/green only ever filters by a known IRI).
|
|
61
|
+
*
|
|
62
|
+
* Thrown at writer construction, so a declaration that cannot be swept fails
|
|
63
|
+
* before a run touches the index rather than at the sweep, after the writes.
|
|
64
|
+
*/
|
|
65
|
+
export function assertSweepableProvenanceField(searchType, options) {
|
|
66
|
+
const field = datasetField(searchType);
|
|
67
|
+
if (field === undefined || isInternalField(field)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const problems = [];
|
|
71
|
+
if (field.array === true) {
|
|
72
|
+
problems.push('it is an array, and a membership sweep over one would delete every document merely mentioning a departed dataset');
|
|
73
|
+
}
|
|
74
|
+
if (field.transform !== undefined) {
|
|
75
|
+
problems.push('it declares a transform, and the sweep matches the stored value against the run’s raw dataset IRIs');
|
|
76
|
+
}
|
|
77
|
+
if (options.requireFacetable && field.facetable !== true) {
|
|
78
|
+
problems.push('it is not facetable, and the membership sweep enumerates the indexed datasets by faceting it');
|
|
79
|
+
}
|
|
80
|
+
if (problems.length > 0) {
|
|
81
|
+
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 ')}.`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
44
84
|
/** Stamp each document with fixed bookkeeping fields as it streams past. */
|
|
45
85
|
export async function* stampDocuments(documents, stamp) {
|
|
46
86
|
for await (const document of documents) {
|
package/dist/search.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../src/search.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../src/search.ts"],"names":[],"mappings":"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"}
|
package/dist/search.js
CHANGED
|
@@ -485,6 +485,9 @@ export function parseSearchResponse(response, searchType, labels, schema) {
|
|
|
485
485
|
const rangesByKey = field !== undefined && isRangeFacet(field)
|
|
486
486
|
? new Map(field.facetRanges.map((range) => [range.key, range]))
|
|
487
487
|
: undefined;
|
|
488
|
+
// A boolean facet is reported with the values stringified; recover the
|
|
489
|
+
// boolean so the bucket carries the term the `is` filter expects.
|
|
490
|
+
const booleanFacet = field?.kind === 'boolean';
|
|
488
491
|
facets[facet.field_name] = facet.counts.map((bucket) => {
|
|
489
492
|
const label = labelled ? labels.get(bucket.value) : undefined;
|
|
490
493
|
const range = rangesByKey?.get(bucket.value);
|
|
@@ -494,6 +497,7 @@ export function parseSearchResponse(response, searchType, labels, schema) {
|
|
|
494
497
|
...(label !== undefined ? { label } : {}),
|
|
495
498
|
...(range?.min !== undefined ? { min: range.min } : {}),
|
|
496
499
|
...(range?.max !== undefined ? { max: range.max } : {}),
|
|
500
|
+
...(booleanFacet ? { is: bucket.value === 'true' } : {}),
|
|
497
501
|
};
|
|
498
502
|
});
|
|
499
503
|
}
|
package/dist/sweep.d.ts
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
|
-
|
|
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}. */
|
|
2
4
|
export declare const SOURCE_FIELD = "source";
|
|
3
5
|
/** The document field carrying the id of the run that last wrote a document. */
|
|
4
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;
|
|
5
21
|
/**
|
|
6
22
|
* Sources whose documents must leave the index: indexed, but no longer part
|
|
7
23
|
* of the run’s selection. Selection is membership, not processing – a dataset
|
|
@@ -15,18 +31,20 @@ export declare function departedSources(indexedSources: Iterable<string>, select
|
|
|
15
31
|
* Typesense filter matching a source’s documents that this run did not touch:
|
|
16
32
|
* everything the source no longer contains, ready for a per-source sweep.
|
|
17
33
|
*
|
|
18
|
-
* @param
|
|
34
|
+
* @param sourceField The collection’s {@link provenanceField}
|
|
35
|
+
* @param sourceIri The dataset IRI the documents carry in that field
|
|
19
36
|
* @param runId The current run; documents it wrote carry it as `last_seen`
|
|
20
37
|
*/
|
|
21
|
-
export declare function staleDocumentsFilter(sourceIri: string, runId: string): string;
|
|
38
|
+
export declare function staleDocumentsFilter(sourceField: string, sourceIri: string, runId: string): string;
|
|
22
39
|
/**
|
|
23
40
|
* Typesense filter matching all of a source’s documents, ready to drop a whole
|
|
24
41
|
* source (a departed source’s membership sweep, or a Blue/green writer rolling
|
|
25
42
|
* a failed dataset out of its not-yet-live collection).
|
|
26
43
|
*
|
|
27
|
-
* @param
|
|
44
|
+
* @param sourceField The collection’s {@link provenanceField}
|
|
45
|
+
* @param sourceIri The dataset IRI the documents carry in that field
|
|
28
46
|
*/
|
|
29
|
-
export declare function sourceDocumentsFilter(sourceIri: string): string;
|
|
47
|
+
export declare function sourceDocumentsFilter(sourceField: string, sourceIri: string): string;
|
|
30
48
|
/**
|
|
31
49
|
* Typesense filter matching the documents **this run** wrote for a source: the
|
|
32
50
|
* inverse of {@link staleDocumentsFilter}. An In-place writer deletes these to
|
|
@@ -34,17 +52,19 @@ export declare function sourceDocumentsFilter(sourceIri: string): string;
|
|
|
34
52
|
* touching its prior-run documents, which the success sweep or a failed run
|
|
35
53
|
* still owns.
|
|
36
54
|
*
|
|
37
|
-
* @param
|
|
55
|
+
* @param sourceField The collection’s {@link provenanceField}
|
|
56
|
+
* @param sourceIri The dataset IRI the documents carry in that field
|
|
38
57
|
* @param runId The current run, stamped on its documents as `last_seen`
|
|
39
58
|
*/
|
|
40
|
-
export declare function thisRunDocumentsFilter(sourceIri: string, runId: string): string;
|
|
59
|
+
export declare function thisRunDocumentsFilter(sourceField: string, sourceIri: string, runId: string): string;
|
|
41
60
|
/**
|
|
42
61
|
* Typesense filters deleting every departed source’s documents, combined into
|
|
43
62
|
* as few filters as fit: deletes travel in the URL query string, so each
|
|
44
63
|
* filter stays under a conservative length budget rather than listing every
|
|
45
64
|
* source in one string.
|
|
46
65
|
*
|
|
66
|
+
* @param sourceField The collection’s {@link provenanceField}
|
|
47
67
|
* @param departed The departed source IRIs ({@link departedSources})
|
|
48
68
|
*/
|
|
49
|
-
export declare function membershipSweepFilters(departed: readonly string[]): string[];
|
|
69
|
+
export declare function membershipSweepFilters(sourceField: string, departed: readonly string[]): string[];
|
|
50
70
|
//# sourceMappingURL=sweep.d.ts.map
|
package/dist/sweep.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sweep.d.ts","sourceRoot":"","sources":["../src/sweep.ts"],"names":[],"mappings":"AAQA,
|
|
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"}
|
package/dist/sweep.js
CHANGED
|
@@ -1,13 +1,35 @@
|
|
|
1
1
|
// Pure deletion planning shared by the rebuild writers: which documents leave
|
|
2
2
|
// a collection, expressed as source sets and Typesense filter strings. Kept
|
|
3
3
|
// free of the Typesense client so the logic is unit-testable. This module owns
|
|
4
|
-
// the bookkeeping field names
|
|
5
|
-
//
|
|
4
|
+
// the private bookkeeping field names and resolves which field a collection
|
|
5
|
+
// carries its provenance in, so stamping and every filter that reads it can
|
|
6
|
+
// never disagree.
|
|
7
|
+
import { datasetField, isInternalField } from '@lde/search/adapter';
|
|
6
8
|
import { escapeFilterValue } from './query-compiler.js';
|
|
7
|
-
/** The
|
|
9
|
+
/** The private field carrying the dataset IRI a document came from, for a type
|
|
10
|
+
* that declares none of its own – see {@link provenanceField}. */
|
|
8
11
|
export const SOURCE_FIELD = 'source';
|
|
9
12
|
/** The document field carrying the id of the run that last wrote a document. */
|
|
10
13
|
export const LAST_SEEN_FIELD = 'last_seen';
|
|
14
|
+
/**
|
|
15
|
+
* The field a collection carries its documents’ dataset IRI in – the one the
|
|
16
|
+
* membership sweep enumerates, filters and deletes by.
|
|
17
|
+
*
|
|
18
|
+
* A type that declares a field over the dataset
|
|
19
|
+
* ({@link datasetField `from: 'dataset'`}) *is* its provenance: the writer keeps
|
|
20
|
+
* its bookkeeping on that field rather than stamping a private one beside it,
|
|
21
|
+
* so the facet, the label resolution, any `derive` and the sweep all read one
|
|
22
|
+
* column that cannot drift. A type declaring nothing – or declaring the dataset
|
|
23
|
+
* only as an *internal* field, which the projection prunes before the writer
|
|
24
|
+
* ever sees it – falls back to the private {@link SOURCE_FIELD}, and behaves
|
|
25
|
+
* exactly as it always has.
|
|
26
|
+
*/
|
|
27
|
+
export function provenanceField(searchType) {
|
|
28
|
+
const declared = datasetField(searchType);
|
|
29
|
+
return declared === undefined || isInternalField(declared)
|
|
30
|
+
? SOURCE_FIELD
|
|
31
|
+
: declared.name;
|
|
32
|
+
}
|
|
11
33
|
/**
|
|
12
34
|
* Sources whose documents must leave the index: indexed, but no longer part
|
|
13
35
|
* of the run’s selection. Selection is membership, not processing – a dataset
|
|
@@ -24,21 +46,23 @@ export function departedSources(indexedSources, selectedSources) {
|
|
|
24
46
|
* Typesense filter matching a source’s documents that this run did not touch:
|
|
25
47
|
* everything the source no longer contains, ready for a per-source sweep.
|
|
26
48
|
*
|
|
27
|
-
* @param
|
|
49
|
+
* @param sourceField The collection’s {@link provenanceField}
|
|
50
|
+
* @param sourceIri The dataset IRI the documents carry in that field
|
|
28
51
|
* @param runId The current run; documents it wrote carry it as `last_seen`
|
|
29
52
|
*/
|
|
30
|
-
export function staleDocumentsFilter(sourceIri, runId) {
|
|
31
|
-
return `${sourceDocumentsFilter(sourceIri)} && ${LAST_SEEN_FIELD}:!=${escapeFilterValue(runId)}`;
|
|
53
|
+
export function staleDocumentsFilter(sourceField, sourceIri, runId) {
|
|
54
|
+
return `${sourceDocumentsFilter(sourceField, sourceIri)} && ${LAST_SEEN_FIELD}:!=${escapeFilterValue(runId)}`;
|
|
32
55
|
}
|
|
33
56
|
/**
|
|
34
57
|
* Typesense filter matching all of a source’s documents, ready to drop a whole
|
|
35
58
|
* source (a departed source’s membership sweep, or a Blue/green writer rolling
|
|
36
59
|
* a failed dataset out of its not-yet-live collection).
|
|
37
60
|
*
|
|
38
|
-
* @param
|
|
61
|
+
* @param sourceField The collection’s {@link provenanceField}
|
|
62
|
+
* @param sourceIri The dataset IRI the documents carry in that field
|
|
39
63
|
*/
|
|
40
|
-
export function sourceDocumentsFilter(sourceIri) {
|
|
41
|
-
return `${
|
|
64
|
+
export function sourceDocumentsFilter(sourceField, sourceIri) {
|
|
65
|
+
return `${sourceField}:=${escapeFilterValue(sourceIri)}`;
|
|
42
66
|
}
|
|
43
67
|
/**
|
|
44
68
|
* Typesense filter matching the documents **this run** wrote for a source: the
|
|
@@ -47,11 +71,12 @@ export function sourceDocumentsFilter(sourceIri) {
|
|
|
47
71
|
* touching its prior-run documents, which the success sweep or a failed run
|
|
48
72
|
* still owns.
|
|
49
73
|
*
|
|
50
|
-
* @param
|
|
74
|
+
* @param sourceField The collection’s {@link provenanceField}
|
|
75
|
+
* @param sourceIri The dataset IRI the documents carry in that field
|
|
51
76
|
* @param runId The current run, stamped on its documents as `last_seen`
|
|
52
77
|
*/
|
|
53
|
-
export function thisRunDocumentsFilter(sourceIri, runId) {
|
|
54
|
-
return `${sourceDocumentsFilter(sourceIri)} && ${LAST_SEEN_FIELD}:=${escapeFilterValue(runId)}`;
|
|
78
|
+
export function thisRunDocumentsFilter(sourceField, sourceIri, runId) {
|
|
79
|
+
return `${sourceDocumentsFilter(sourceField, sourceIri)} && ${LAST_SEEN_FIELD}:=${escapeFilterValue(runId)}`;
|
|
55
80
|
}
|
|
56
81
|
/**
|
|
57
82
|
* Typesense filters deleting every departed source’s documents, combined into
|
|
@@ -59,15 +84,16 @@ export function thisRunDocumentsFilter(sourceIri, runId) {
|
|
|
59
84
|
* filter stays under a conservative length budget rather than listing every
|
|
60
85
|
* source in one string.
|
|
61
86
|
*
|
|
87
|
+
* @param sourceField The collection’s {@link provenanceField}
|
|
62
88
|
* @param departed The departed source IRIs ({@link departedSources})
|
|
63
89
|
*/
|
|
64
|
-
export function membershipSweepFilters(departed) {
|
|
90
|
+
export function membershipSweepFilters(sourceField, departed) {
|
|
65
91
|
const filters = [];
|
|
66
92
|
let chunk = [];
|
|
67
93
|
let chunkLength = 0;
|
|
68
94
|
const flush = () => {
|
|
69
95
|
if (chunk.length > 0) {
|
|
70
|
-
filters.push(`${
|
|
96
|
+
filters.push(`${sourceField}:=[${chunk.join(',')}]`);
|
|
71
97
|
chunk = [];
|
|
72
98
|
chunkLength = 0;
|
|
73
99
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lde/search-typesense",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Typesense implementation of the @lde/search SearchEngine port: collection-schema builder, query compiler, label-resolving result reconstruction, and blue/green index lifecycle. Engine-specific (Typesense) but domain-agnostic.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "git+https://github.com/ldelements/lde.git",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"!**/*.tsbuildinfo"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@lde/search": "^0.
|
|
28
|
+
"@lde/search": "^0.17.0",
|
|
29
29
|
"@lde/text-normalization": "^0.1.2",
|
|
30
30
|
"tslib": "^2.3.0",
|
|
31
31
|
"typesense": "^3.0.6"
|