@lde/search-typesense 0.2.0 → 0.4.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/README.md +78 -27
- package/dist/blue-green-rebuild.d.ts +46 -0
- package/dist/blue-green-rebuild.d.ts.map +1 -0
- package/dist/blue-green-rebuild.js +119 -0
- package/dist/import.d.ts +31 -0
- package/dist/import.d.ts.map +1 -0
- package/dist/import.js +60 -0
- package/dist/in-place-rebuild.d.ts +62 -0
- package/dist/in-place-rebuild.d.ts.map +1 -0
- package/dist/in-place-rebuild.js +140 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/lock.d.ts +42 -0
- package/dist/lock.d.ts.map +1 -0
- package/dist/lock.js +134 -0
- package/dist/rebuild-support.d.ts +34 -0
- package/dist/rebuild-support.d.ts.map +1 -0
- package/dist/rebuild-support.js +33 -0
- package/dist/search.d.ts +6 -2
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +79 -25
- package/dist/sweep.d.ts +50 -0
- package/dist/sweep.d.ts.map +1 -0
- package/dist/sweep.js +87 -0
- package/package.json +6 -2
- package/dist/adapter.d.ts +0 -56
- package/dist/adapter.d.ts.map +0 -1
- package/dist/adapter.js +0 -214
package/dist/adapter.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAkB,MAAM,WAAW,CAAC;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,wBAAwB,CAAC;AAKhC;8EAC8E;AAC9E,MAAM,WAAW,cAAe,SAAQ,uBAAuB;IAC7D,+DAA+D;IAC/D,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,0EAA0E;IAC1E,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAsB,OAAO,CAAC,QAAQ,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAC3D,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,aAAa,CAAC,QAAQ,CAAC,EAClC,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAwC1D"}
|
package/dist/adapter.js
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
import { buildCollectionSchema, } from './collection-schema.js';
|
|
2
|
-
const LOCK_COLLECTION = 'rebuild_locks';
|
|
3
|
-
const DEFAULT_LOCK_TTL_MS = 10 * 60 * 1000;
|
|
4
|
-
/**
|
|
5
|
-
* Blue/green-rebuild the search index `options.name` from one declarative
|
|
6
|
-
* source: the collection schema is derived from `searchType`
|
|
7
|
-
* ({@link buildCollectionSchema}) and the documents are streamed in — one call
|
|
8
|
-
* from declaration to live index.
|
|
9
|
-
*
|
|
10
|
-
* 1. create a fresh versioned collection (`${name}_<timestamp>`)
|
|
11
|
-
* 2. stream `documents` into it in batches
|
|
12
|
-
* 3. atomically repoint the `name` alias to the new collection, then
|
|
13
|
-
* drop the collection it superseded. The caller passes only the logical
|
|
14
|
-
* index `name`; the versioned collection name and the alias are managed here.
|
|
15
|
-
*
|
|
16
|
-
* The rebuild is **single-flight per index**: it first takes a lock (a marker
|
|
17
|
-
* document in a `rebuild_locks` collection, created on demand) via Typesense’s
|
|
18
|
-
* atomic create, so concurrent callers across pods never rebuild the same index
|
|
19
|
-
* at once. This keeps blue/green safe under replication.
|
|
20
|
-
*
|
|
21
|
-
* `documents` is an async iterable (e.g. a streaming projection); only one
|
|
22
|
-
* `batchSize`-sized chunk is held in memory at a time. On any failure before the
|
|
23
|
-
* swap nothing is repointed, so the live alias never points at a partial build,
|
|
24
|
-
* and the orphaned half-built collection is dropped.
|
|
25
|
-
*
|
|
26
|
-
* @returns the live collection name and the number of documents imported, or
|
|
27
|
-
* `null` when the rebuild was skipped because another rebuild for the same index
|
|
28
|
-
* was already running.
|
|
29
|
-
*
|
|
30
|
-
* Limitations:
|
|
31
|
-
* - **Advisory, not a strict mutex.** The lock is built on Typesense, not a
|
|
32
|
-
* consensus store. Under a TTL-reclaim race two rebuilds can briefly run at
|
|
33
|
-
* once; this is safe because blue/green is idempotent (worst case: redundant
|
|
34
|
-
* work and a transient orphaned collection).
|
|
35
|
-
* - **Single-flight, not coalescing.** A call made while a rebuild is in flight
|
|
36
|
-
* is skipped (returns `null`), not queued. If you must capture state that
|
|
37
|
-
* changed mid-build, re-trigger after the running rebuild finishes.
|
|
38
|
-
* - **Lock TTL.** A rebuild that runs longer than `lockTtlMs` (default 10
|
|
39
|
-
* minutes) can be reclaimed by another caller and run concurrently; size the
|
|
40
|
-
* TTL above your longest rebuild.
|
|
41
|
-
*/
|
|
42
|
-
export async function rebuild(client, documents, searchType, options) {
|
|
43
|
-
const { batchSize = 1000, lockTtlMs = DEFAULT_LOCK_TTL_MS, ...schemaOptions } = options;
|
|
44
|
-
const schema = buildCollectionSchema(searchType, schemaOptions);
|
|
45
|
-
const name = schemaOptions.name;
|
|
46
|
-
if (!(await acquireLock(client, name, lockTtlMs))) {
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
const collection = `${name}_${Date.now()}`;
|
|
50
|
-
try {
|
|
51
|
-
const previous = await aliasTarget(client, name);
|
|
52
|
-
await client.collections().create({ ...schema, name: collection });
|
|
53
|
-
let imported;
|
|
54
|
-
try {
|
|
55
|
-
imported = await importStreamed(client, collection, documents, batchSize);
|
|
56
|
-
await client.aliases().upsert(name, { collection_name: collection });
|
|
57
|
-
}
|
|
58
|
-
catch (error) {
|
|
59
|
-
// The build failed before the swap: the live alias is untouched, so just
|
|
60
|
-
// drop the orphaned half-built collection rather than let it accumulate.
|
|
61
|
-
await client
|
|
62
|
-
.collections(collection)
|
|
63
|
-
.delete()
|
|
64
|
-
.catch(() => undefined);
|
|
65
|
-
throw error;
|
|
66
|
-
}
|
|
67
|
-
if (previous !== undefined && previous !== collection) {
|
|
68
|
-
await client
|
|
69
|
-
.collections(previous)
|
|
70
|
-
.delete()
|
|
71
|
-
.catch(() => undefined);
|
|
72
|
-
}
|
|
73
|
-
return { collection, imported };
|
|
74
|
-
}
|
|
75
|
-
finally {
|
|
76
|
-
await releaseLock(client, name);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
/** The collection an alias currently points at, or `undefined` if unset. */
|
|
80
|
-
async function aliasTarget(client, alias) {
|
|
81
|
-
try {
|
|
82
|
-
const { collection_name } = await client.aliases(alias).retrieve();
|
|
83
|
-
return collection_name;
|
|
84
|
-
}
|
|
85
|
-
catch (error) {
|
|
86
|
-
if (httpStatus(error) === 404) {
|
|
87
|
-
return undefined;
|
|
88
|
-
}
|
|
89
|
-
throw error;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
/** Upsert a stream of documents in `batchSize` chunks; returns the count. */
|
|
93
|
-
async function importStreamed(client, collection, documents, batchSize) {
|
|
94
|
-
let imported = 0;
|
|
95
|
-
let batch = [];
|
|
96
|
-
for await (const document of documents) {
|
|
97
|
-
batch.push(document);
|
|
98
|
-
if (batch.length >= batchSize) {
|
|
99
|
-
await importBatch(client, collection, batch);
|
|
100
|
-
imported += batch.length;
|
|
101
|
-
batch = [];
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
if (batch.length > 0) {
|
|
105
|
-
await importBatch(client, collection, batch);
|
|
106
|
-
imported += batch.length;
|
|
107
|
-
}
|
|
108
|
-
return imported;
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Create-or-replace one batch of whole documents, keyed on `id`, throwing if any
|
|
112
|
-
* individual document fails (Typesense’s bulk import otherwise reports
|
|
113
|
-
* per-document failures without rejecting).
|
|
114
|
-
*/
|
|
115
|
-
async function importBatch(client, collection, batch) {
|
|
116
|
-
const results = (await client
|
|
117
|
-
.collections(collection)
|
|
118
|
-
.documents()
|
|
119
|
-
.import(batch, {
|
|
120
|
-
action: 'upsert',
|
|
121
|
-
// Collect per-document outcomes instead of throwing the client’s opaque
|
|
122
|
-
// ImportError, so we can report which documents failed and why.
|
|
123
|
-
throwOnFail: false,
|
|
124
|
-
}));
|
|
125
|
-
const failures = results.filter((result) => !result.success);
|
|
126
|
-
if (failures.length > 0) {
|
|
127
|
-
throw new Error(`Typesense upsert into “${collection}” failed for ${failures.length}/${results.length} documents: ${failures
|
|
128
|
-
.map((failure) => failure.error)
|
|
129
|
-
.join('; ')}`);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Take the per-alias rebuild lock via an atomic create, reclaiming it if the
|
|
134
|
-
* current holder is older than `ttlMs`. Returns `false` if another caller holds
|
|
135
|
-
* a fresh lock.
|
|
136
|
-
*/
|
|
137
|
-
async function acquireLock(client, alias, ttlMs) {
|
|
138
|
-
await ensureLockCollection(client);
|
|
139
|
-
try {
|
|
140
|
-
await client
|
|
141
|
-
.collections(LOCK_COLLECTION)
|
|
142
|
-
.documents()
|
|
143
|
-
.create({ id: alias, acquired_at: Date.now() });
|
|
144
|
-
return true;
|
|
145
|
-
}
|
|
146
|
-
catch (error) {
|
|
147
|
-
if (httpStatus(error) === 409) {
|
|
148
|
-
return reclaimIfStale(client, alias, ttlMs);
|
|
149
|
-
}
|
|
150
|
-
throw error;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
/** Take over the lock if its holder has not refreshed it within `ttlMs`. */
|
|
154
|
-
async function reclaimIfStale(client, alias, ttlMs) {
|
|
155
|
-
let held;
|
|
156
|
-
try {
|
|
157
|
-
held = (await client
|
|
158
|
-
.collections(LOCK_COLLECTION)
|
|
159
|
-
.documents(alias)
|
|
160
|
-
.retrieve());
|
|
161
|
-
}
|
|
162
|
-
catch (error) {
|
|
163
|
-
// Released between our create and this read — leave it for the next try.
|
|
164
|
-
if (httpStatus(error) === 404) {
|
|
165
|
-
return false;
|
|
166
|
-
}
|
|
167
|
-
throw error;
|
|
168
|
-
}
|
|
169
|
-
if (Date.now() - held.acquired_at <= ttlMs) {
|
|
170
|
-
return false;
|
|
171
|
-
}
|
|
172
|
-
await client
|
|
173
|
-
.collections(LOCK_COLLECTION)
|
|
174
|
-
.documents()
|
|
175
|
-
.upsert({ id: alias, acquired_at: Date.now() });
|
|
176
|
-
return true;
|
|
177
|
-
}
|
|
178
|
-
/** Release the per-alias lock; a no-op when it is not currently held. */
|
|
179
|
-
async function releaseLock(client, alias) {
|
|
180
|
-
try {
|
|
181
|
-
await client.collections(LOCK_COLLECTION).documents(alias).delete();
|
|
182
|
-
}
|
|
183
|
-
catch (error) {
|
|
184
|
-
if (httpStatus(error) !== 404) {
|
|
185
|
-
throw error;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
/** Create the lock collection on demand, tolerating a concurrent creator. */
|
|
190
|
-
async function ensureLockCollection(client) {
|
|
191
|
-
try {
|
|
192
|
-
await client.collections(LOCK_COLLECTION).retrieve();
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
catch (error) {
|
|
196
|
-
if (httpStatus(error) !== 404) {
|
|
197
|
-
throw error;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
try {
|
|
201
|
-
await client.collections().create({
|
|
202
|
-
name: LOCK_COLLECTION,
|
|
203
|
-
fields: [{ name: 'acquired_at', type: 'int64' }],
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
catch (error) {
|
|
207
|
-
if (httpStatus(error) !== 409) {
|
|
208
|
-
throw error;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
function httpStatus(error) {
|
|
213
|
-
return error.httpStatus;
|
|
214
|
-
}
|