sliftutils 1.4.81 → 1.4.83

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,96 +1,219 @@
1
+ import cborx from "cbor-x";
1
2
  import { Database, namespaceDatabase } from "./Database";
2
- import { TransactionSetStore, transactionRead, transactionMutate, replayTransactionStore } from "./transactionSet";
3
- import { StoredEmbedding, EmbeddingFormat, getCloseness, averageEmbeddings, hashEmbedding } from "../embeddingFormats";
3
+ import { TransactionSetStore, transactionRead, transactionMutate, transactionDelete, replayTransactionStore } from "./transactionSet";
4
+ import { StoredEmbedding, EmbeddingFormat, getCloseness, embeddingToFloat32, releaseFloat32, encodeEmbedding, hashEmbedding } from "../embeddingFormats";
4
5
 
5
6
  export type IvfConfig = {
6
7
  model: string;
7
8
  format: EmbeddingFormat;
8
- cellTarget: number;
9
- splitAt: number;
9
+ // Target number of embeddings per cell.
10
+ cellTargetSize: number;
10
11
  };
11
12
 
12
- // Cell ids ARE the hash of the cell's centroid, so there's no id counter. centroids maps that id to the
13
- // centroid embedding (read whole as the preload, ranked in RAM). Each cell is its own transaction set of
14
- // ref member embedding. refIndex maps a ref to the cell holding it, so a remove can find it.
13
+ // Tiered storage. Below FLAT_LIMIT embeddings the whole set lives in one flat transaction set (no IVF);
14
+ // once it grows past that we build the IVF (centroids + per-cell sets) and stay there forever. `count` is
15
+ // the live embedding count (re-derived exactly on a rebuild). `steps` records one-time upgrades that must
16
+ // never re-run even if the set later shrinks and regrows.
15
17
  export type IvfEmbeddingRoot = {
16
18
  config: IvfConfig;
17
- centroids: TransactionSetStore;
18
- cells: { [cellId: string]: TransactionSetStore };
19
- refIndex: TransactionSetStore;
19
+ count: number;
20
+ flat: TransactionSetStore<StoredEmbedding>;
21
+ // Direct ref -> embedding index. Each entry is one CBOR blob at its own key, so a lookup or delete is a
22
+ // single key-value op (no transaction-set replay) and deleteData can drop it (it's a primitive leaf). Refs
23
+ // and their embeddings never move, so a rebuild leaves this untouched.
24
+ byRef: { [ref: string]: Uint8Array };
25
+ steps: { [step: string]: boolean };
26
+ centroids: TransactionSetStore<StoredEmbedding>;
27
+ cells: { [cellId: string]: TransactionSetStore<StoredEmbedding> };
20
28
  };
21
29
 
22
30
  export type EmbeddingInput = { ref: string; embedding: StoredEmbedding };
23
31
  export type SearchHit = { ref: string; closeness: number };
32
+ type CellEntry = { ref: string; embedding: StoredEmbedding };
24
33
 
25
- function centroidStore(database: Database<IvfEmbeddingRoot>): Database<TransactionSetStore> {
34
+ const embeddingCbor = new cborx.Encoder({ structuredClone: true });
35
+ function encodeRefValue(embedding: StoredEmbedding): Uint8Array {
36
+ return embeddingCbor.encode(embedding) as Uint8Array;
37
+ }
38
+ function decodeRefValue(value: Uint8Array): StoredEmbedding {
39
+ return embeddingCbor.decode(value) as StoredEmbedding;
40
+ }
41
+
42
+ // Below this many embeddings we skip the IVF entirely (flat store). 1024 = a clean power of two.
43
+ const FLAT_LIMIT = 1024;
44
+ // Force a from-scratch rebuild once at each of these (multiples of 4 above FLAT_LIMIT, up to ~16k), gated by a step flag so each runs at most once ever.
45
+ const REGENERATE_AT = [4096, 16384];
46
+ const STEP_IVF = "ivf";
47
+ // On delete, the member's exact cell plus this many nearby cells are checked, in case a rebuild left it non-optimal.
48
+ const DELETE_FALLBACK_CELLS = 10;
49
+ const REBALANCE_ITERATIONS = 4;
50
+
51
+ function flatStore(database: Database<IvfEmbeddingRoot>): Database<TransactionSetStore<StoredEmbedding>> {
52
+ return namespaceDatabase(database, root => root.flat);
53
+ }
54
+ function centroidStore(database: Database<IvfEmbeddingRoot>): Database<TransactionSetStore<StoredEmbedding>> {
26
55
  return namespaceDatabase(database, root => root.centroids);
27
56
  }
28
- function cellStore(database: Database<IvfEmbeddingRoot>, cellId: string): Database<TransactionSetStore> {
57
+ function cellStore(database: Database<IvfEmbeddingRoot>, cellId: string): Database<TransactionSetStore<StoredEmbedding>> {
29
58
  return namespaceDatabase(database, root => root.cells[cellId]);
30
59
  }
31
- function refIndexStore(database: Database<IvfEmbeddingRoot>): Database<TransactionSetStore> {
32
- return namespaceDatabase(database, root => root.refIndex);
60
+
61
+ function rankCellsByCloseness(embedding: StoredEmbedding, centroids: Map<string, StoredEmbedding>): string[] {
62
+ const ranked: { cellId: string; closeness: number }[] = [];
63
+ for (const cellId of centroids.keys()) {
64
+ ranked.push({ cellId, closeness: getCloseness(embedding, centroids.get(cellId)!) });
65
+ }
66
+ ranked.sort((left, right) => right.closeness - left.closeness);
67
+ return ranked.map(entry => entry.cellId);
33
68
  }
34
69
 
35
- function nearestCell(embedding: StoredEmbedding, centroids: Map<string, StoredEmbedding>): string {
36
- let bestCellId = "";
37
- let bestCloseness = -Infinity;
38
- for (const [cellId, centroid] of centroids) {
39
- const closeness = getCloseness(embedding, centroid);
40
- if (closeness > bestCloseness) {
41
- bestCloseness = closeness;
42
- bestCellId = cellId;
43
- }
70
+ // Per-write chance of a full rebuild. Zero at/under the target size, then rises (cubically) past it so cells
71
+ // stay roughly between target and ~2x target.
72
+ function rebalanceProbability(fillRatio: number): number {
73
+ if (fillRatio <= 1) {
74
+ return 0;
44
75
  }
45
- return bestCellId;
76
+ const over = fillRatio - 1;
77
+ return Math.min(1, over * over * over * 0.25);
46
78
  }
47
79
 
48
- // 2-means on a cell's members so an overgrown cell can split in place. Seeds with one member and the one
49
- // farthest from it, then alternates assign / recompute-centroid a few times.
50
- function splitInTwo(
51
- members: [string, StoredEmbedding][],
52
- config: IvfConfig,
53
- ): { centroid: StoredEmbedding; members: [string, StoredEmbedding][] }[] {
54
- let centroidA = members[0][1];
55
- let centroidB = members[0][1];
56
- let worst = Infinity;
57
- for (const [, embedding] of members) {
58
- const closeness = getCloseness(centroidA, embedding);
59
- if (closeness < worst) {
60
- worst = closeness;
61
- centroidB = embedding;
62
- }
63
- }
64
- let groupA: [string, StoredEmbedding][] = [];
65
- let groupB: [string, StoredEmbedding][] = [];
66
- for (let iteration = 0; iteration < 5; iteration++) {
67
- groupA = [];
68
- groupB = [];
69
- for (const member of members) {
70
- if (getCloseness(centroidA, member[1]) >= getCloseness(centroidB, member[1])) {
71
- groupA.push(member);
72
- } else {
73
- groupB.push(member);
80
+ // k-means. Decodes every member to a pooled float32 buffer ONCE, then assigns with a plain internal float
81
+ // dot (no getCloseness call comparing two float vectors is trivial) and keeps centroids as float means,
82
+ // encoding them to StoredEmbedding only at the end. Releases the borrowed buffers when done.
83
+ function clusterMembers(members: CellEntry[], clusterCount: number, config: IvfConfig): { centroid: StoredEmbedding; members: CellEntry[] }[] {
84
+ const memberFloats: Float32Array[] = [];
85
+ for (const member of members) {
86
+ memberFloats.push(embeddingToFloat32(member.embedding, true));
87
+ }
88
+ const length = memberFloats.length ? memberFloats[0].length : 0;
89
+ let centroids: Float32Array[] = [];
90
+ const seedStep = members.length / clusterCount;
91
+ for (let clusterIndex = 0; clusterIndex < clusterCount; clusterIndex++) {
92
+ centroids.push(new Float32Array(memberFloats[Math.floor(clusterIndex * seedStep)]));
93
+ }
94
+ let groups: number[][] = [];
95
+ for (let iteration = 0; iteration < REBALANCE_ITERATIONS; iteration++) {
96
+ groups = [];
97
+ for (let clusterIndex = 0; clusterIndex < centroids.length; clusterIndex++) {
98
+ groups.push([]);
99
+ }
100
+ for (let memberIndex = 0; memberIndex < memberFloats.length; memberIndex++) {
101
+ const memberFloat = memberFloats[memberIndex];
102
+ let bestIndex = 0;
103
+ let bestDot = -Infinity;
104
+ for (let centroidIndex = 0; centroidIndex < centroids.length; centroidIndex++) {
105
+ const centroidFloat = centroids[centroidIndex];
106
+ let dot = 0;
107
+ for (let dim = 0; dim < length; dim++) {
108
+ dot += memberFloat[dim] * centroidFloat[dim];
109
+ }
110
+ if (dot > bestDot) {
111
+ bestDot = dot;
112
+ bestIndex = centroidIndex;
113
+ }
74
114
  }
115
+ groups[bestIndex].push(memberIndex);
75
116
  }
76
- if (!groupA.length || !groupB.length) {
77
- break;
117
+ const nextCentroids: Float32Array[] = [];
118
+ const nextGroups: number[][] = [];
119
+ for (const group of groups) {
120
+ if (!group.length) {
121
+ continue;
122
+ }
123
+ const mean = new Float32Array(length);
124
+ for (const memberIndex of group) {
125
+ const memberFloat = memberFloats[memberIndex];
126
+ for (let dim = 0; dim < length; dim++) {
127
+ mean[dim] += memberFloat[dim];
128
+ }
129
+ }
130
+ let norm = 0;
131
+ for (let dim = 0; dim < length; dim++) {
132
+ norm += mean[dim] * mean[dim];
133
+ }
134
+ const magnitude = Math.sqrt(norm) || 1;
135
+ for (let dim = 0; dim < length; dim++) {
136
+ mean[dim] /= magnitude;
137
+ }
138
+ nextCentroids.push(mean);
139
+ nextGroups.push(group);
78
140
  }
79
- centroidA = averageEmbeddings(groupA.map(member => member[1]), config);
80
- centroidB = averageEmbeddings(groupB.map(member => member[1]), config);
141
+ centroids = nextCentroids;
142
+ groups = nextGroups;
81
143
  }
82
- const result: { centroid: StoredEmbedding; members: [string, StoredEmbedding][] }[] = [];
83
- if (groupA.length) {
84
- result.push({ centroid: centroidA, members: groupA });
144
+ const result: { centroid: StoredEmbedding; members: CellEntry[] }[] = [];
145
+ for (let clusterIndex = 0; clusterIndex < centroids.length; clusterIndex++) {
146
+ const centroid = encodeEmbedding({ input: centroids[clusterIndex], format: config.format, model: config.model });
147
+ const cellMembers: CellEntry[] = [];
148
+ for (const memberIndex of groups[clusterIndex]) {
149
+ cellMembers.push(members[memberIndex]);
150
+ }
151
+ result.push({ centroid, members: cellMembers });
85
152
  }
86
- if (groupB.length) {
87
- result.push({ centroid: centroidB, members: groupB });
153
+ for (const memberFloat of memberFloats) {
154
+ releaseFloat32(memberFloat);
88
155
  }
89
156
  return result;
90
157
  }
91
158
 
92
- // Nearest cells until probeBudget members are covered, scored exactly against the query. undefined while
93
- // not synced. All probed cells are fetched in one batched read so probing doesn't cascade per cell.
159
+ // Re-cluster EVERY embedding (flat tier + all cells) into a fresh IVF, clear the flat tier, and mark IVF
160
+ // mode. This is the rearranger: run probabilistically as the set grows, once at each regenerate threshold,
161
+ // and once to upgrade from flat. Reads everything it uses, so it bails (does nothing) if anything isn't
162
+ // synced and the caller's retry re-runs it; no caller cares about its result.
163
+ export function rebuildStructure(database: Database<IvfEmbeddingRoot>): void {
164
+ const config = database.readData(root => root.config);
165
+ if (!config) return;
166
+ const steps = database.readData(root => root.steps);
167
+ if (!steps) return;
168
+
169
+ const allMembers: CellEntry[] = [];
170
+ // Only the one-time flat->IVF upgrade pulls from the flat tier. Once STEP_IVF is set the flat data has
171
+ // already been folded into cells, so later rebuilds ignore it. It's left in place, never deleted: deleteData
172
+ // only removes primitive leaves, not a whole store, and the flat tier is small enough not to matter.
173
+ if (!steps[STEP_IVF]) {
174
+ const flat = transactionRead(flatStore(database));
175
+ if (!flat) return;
176
+ for (const ref of flat.keys()) {
177
+ allMembers.push({ ref, embedding: flat.get(ref)! });
178
+ }
179
+ }
180
+ const centroids = transactionRead(centroidStore(database));
181
+ if (!centroids) return;
182
+ const oldCellIds = Array.from(centroids.keys());
183
+ const cellStores = database.readData(root => oldCellIds.map(cellId => root.cells[cellId]));
184
+ if (!cellStores) return;
185
+ for (const store of cellStores) {
186
+ const members = replayTransactionStore<StoredEmbedding>(store);
187
+ for (const ref of members.keys()) {
188
+ allMembers.push({ ref, embedding: members.get(ref)! });
189
+ }
190
+ }
191
+ if (!allMembers.length) return;
192
+
193
+ const clusterCount = Math.max(1, Math.round(allMembers.length / config.cellTargetSize));
194
+ const clusters = clusterMembers(allMembers, clusterCount, config);
195
+
196
+ const newCellIds = new Set<string>();
197
+ const centroidWrites: { key: string; value: StoredEmbedding | undefined }[] = [];
198
+ for (const cluster of clusters) {
199
+ const cellId = hashEmbedding(cluster.centroid);
200
+ newCellIds.add(cellId);
201
+ centroidWrites.push({ key: cellId, value: cluster.centroid });
202
+ const memberWrites = cluster.members.map(member => ({ key: member.ref, value: member.embedding }));
203
+ transactionMutate(cellStore(database, cellId), memberWrites);
204
+ }
205
+ for (const oldCellId of oldCellIds) {
206
+ if (newCellIds.has(oldCellId)) {
207
+ continue;
208
+ }
209
+ centroidWrites.push({ key: oldCellId, value: undefined });
210
+ transactionDelete(cellStore(database, oldCellId));
211
+ }
212
+ transactionMutate(centroidStore(database), centroidWrites);
213
+ database.writeData(root => root.count, allMembers.length);
214
+ database.writeData(root => root.steps[STEP_IVF], true);
215
+ }
216
+
94
217
  export function searchEmbeddings(
95
218
  database: Database<IvfEmbeddingRoot>,
96
219
  query: StoredEmbedding,
@@ -98,54 +221,86 @@ export function searchEmbeddings(
98
221
  ): SearchHit[] | undefined {
99
222
  const config = database.readData(root => root.config);
100
223
  if (!config) return undefined;
101
- const centroids = transactionRead<StoredEmbedding>(centroidStore(database));
102
- if (!centroids) return undefined;
103
- if (!centroids.size) return [];
224
+ const steps = database.readData(root => root.steps);
225
+ if (!steps) return undefined;
104
226
 
105
- const rankedCellIds = [...centroids.entries()]
106
- .map(([cellId, centroid]) => ({ cellId, closeness: getCloseness(query, centroid) }))
107
- .sort((left, right) => right.closeness - left.closeness)
108
- .map(ranked => ranked.cellId);
227
+ const hits: SearchHit[] = [];
228
+ if (!steps[STEP_IVF]) {
229
+ const flat = transactionRead(flatStore(database));
230
+ if (!flat) return undefined;
231
+ for (const ref of flat.keys()) {
232
+ hits.push({ ref, closeness: getCloseness(query, flat.get(ref)!) });
233
+ }
234
+ hits.sort((left, right) => right.closeness - left.closeness);
235
+ return hits.slice(0, options.resultCount);
236
+ }
109
237
 
110
- const probeCellCount = Math.max(1, Math.ceil(options.probeBudget / config.cellTarget));
111
- const probeCellIds = rankedCellIds.slice(0, probeCellCount);
238
+ const centroids = transactionRead(centroidStore(database));
239
+ if (!centroids) return undefined;
240
+ if (!centroids.size) return [];
241
+ const probeCellCount = Math.max(1, Math.ceil(options.probeBudget / config.cellTargetSize));
242
+ const probeCellIds = rankCellsByCloseness(query, centroids).slice(0, probeCellCount);
112
243
  const stores = database.readData(root => probeCellIds.map(cellId => root.cells[cellId]));
113
244
  if (!stores) return undefined;
114
-
115
- const hits: SearchHit[] = [];
116
245
  for (const store of stores) {
117
246
  const members = replayTransactionStore<StoredEmbedding>(store);
118
- for (const [ref, embedding] of members) {
119
- hits.push({ ref, closeness: getCloseness(query, embedding) });
247
+ for (const ref of members.keys()) {
248
+ hits.push({ ref, closeness: getCloseness(query, members.get(ref)!) });
120
249
  }
121
250
  }
122
251
  hits.sort((left, right) => right.closeness - left.closeness);
123
252
  return hits.slice(0, options.resultCount);
124
253
  }
125
254
 
126
- // Assign each embedding to its nearest cell and add it. An empty index bootstraps a first cell from the
127
- // inserted batch; a cell that crosses splitAt*cellTarget splits in place. undefined while not synced.
255
+ // Direct ref -> stored embedding lookup through the byRef index: one key-value read per ref, no cell scan. A
256
+ // search returns refs; this fetches the data behind them. Skips refs that aren't present; undefined if not synced.
257
+ export function lookupEmbeddings(
258
+ database: Database<IvfEmbeddingRoot>,
259
+ refs: string[],
260
+ ): Map<string, StoredEmbedding> | undefined {
261
+ const values = database.readData(root => refs.map(ref => root.byRef[ref]));
262
+ if (!values) return undefined;
263
+ const result = new Map<string, StoredEmbedding>();
264
+ for (let index = 0; index < refs.length; index++) {
265
+ const value = values[index];
266
+ if (value) {
267
+ result.set(refs[index], decodeRefValue(value));
268
+ }
269
+ }
270
+ return result;
271
+ }
272
+
128
273
  export function insertEmbeddings(
129
274
  database: Database<IvfEmbeddingRoot>,
130
275
  items: EmbeddingInput[],
131
- ): true | undefined {
132
- if (!items.length) return true;
276
+ ) {
277
+ if (!items.length) return;
133
278
  const config = database.readData(root => root.config);
134
- if (!config) return undefined;
135
- const centroids = transactionRead<StoredEmbedding>(centroidStore(database));
136
- if (!centroids) return undefined;
279
+ const steps = database.readData(root => root.steps);
280
+ let count = database.readData(root => root.count);
281
+ if (!config) return;
282
+ if (!steps) return;
283
+ if (count === undefined) return;
284
+ const newCount = count + items.length;
137
285
 
138
- const newCentroids: { key: string; value: StoredEmbedding | undefined }[] = [];
139
- if (!centroids.size) {
140
- const centroid = averageEmbeddings(items.map(item => item.embedding), config);
141
- const cellId = hashEmbedding(centroid);
142
- centroids.set(cellId, centroid);
143
- newCentroids.push({ key: cellId, value: centroid });
286
+ if (!steps[STEP_IVF]) {
287
+ const flatWrites = items.map(item => ({ key: item.ref, value: item.embedding }));
288
+ transactionMutate(flatStore(database), flatWrites);
289
+ for (const item of items) {
290
+ database.writeData(root => root.byRef[item.ref], encodeRefValue(item.embedding));
291
+ }
292
+ database.writeData(root => root.count, newCount);
293
+ if (newCount > FLAT_LIMIT) {
294
+ rebuildStructure(database);
295
+ }
296
+ return;
144
297
  }
145
298
 
299
+ const centroids = transactionRead(centroidStore(database));
300
+ if (!centroids) return undefined;
146
301
  const itemsByCell = new Map<string, EmbeddingInput[]>();
147
302
  for (const item of items) {
148
- const cellId = nearestCell(item.embedding, centroids);
303
+ const cellId = rankCellsByCloseness(item.embedding, centroids)[0];
149
304
  let group = itemsByCell.get(cellId);
150
305
  if (!group) {
151
306
  group = [];
@@ -153,75 +308,117 @@ export function insertEmbeddings(
153
308
  }
154
309
  group.push(item);
155
310
  }
156
-
157
- const targetCellIds = [...itemsByCell.keys()];
158
- const existingStores = database.readData(root => targetCellIds.map(cellId => root.cells[cellId]));
159
- if (!existingStores) return undefined;
160
-
161
- const splitThreshold = config.splitAt * config.cellTarget;
162
- const refWrites: { key: string; value: string | undefined }[] = [];
163
- for (let cellIndex = 0; cellIndex < targetCellIds.length; cellIndex++) {
164
- const cellId = targetCellIds[cellIndex];
311
+ for (const cellId of itemsByCell.keys()) {
165
312
  const group = itemsByCell.get(cellId)!;
166
- const combined = replayTransactionStore<StoredEmbedding>(existingStores[cellIndex]);
167
- for (const item of group) {
168
- combined.set(item.ref, item.embedding);
169
- }
313
+ const memberWrites = group.map(item => ({ key: item.ref, value: item.embedding }));
314
+ transactionMutate(cellStore(database, cellId), memberWrites);
315
+ }
316
+ for (const item of items) {
317
+ database.writeData(root => root.byRef[item.ref], encodeRefValue(item.embedding));
318
+ }
319
+ database.writeData(root => root.count, newCount);
170
320
 
171
- if (combined.size <= splitThreshold) {
172
- const memberWrites = group.map(item => ({ key: item.ref, value: item.embedding }));
173
- if (!transactionMutate(cellStore(database, cellId), memberWrites)) return undefined;
174
- for (const item of group) {
175
- refWrites.push({ key: item.ref, value: cellId });
176
- }
177
- continue;
178
- }
179
321
 
180
- const subCells = splitInTwo([...combined.entries()], config);
181
- for (const subCell of subCells) {
182
- const subCellId = hashEmbedding(subCell.centroid);
183
- newCentroids.push({ key: subCellId, value: subCell.centroid });
184
- const memberWrites = subCell.members.map(([ref, embedding]) => ({ key: ref, value: embedding }));
185
- if (!transactionMutate(cellStore(database, subCellId), memberWrites)) return undefined;
186
- for (const [ref] of subCell.members) {
187
- refWrites.push({ key: ref, value: subCellId });
322
+ const shouldRegenerate = (database: Database<IvfEmbeddingRoot>) => {
323
+ let steps = database.readData(root => root.steps);
324
+ if (!steps) return;
325
+ for (const threshold of REGENERATE_AT) {
326
+ const stepName = "regen" + threshold;
327
+ if (!steps[stepName] && newCount > threshold) {
328
+ database.writeData(root => root.steps[stepName], true);
329
+ return true;
188
330
  }
189
331
  }
190
- newCentroids.push({ key: cellId, value: undefined });
191
- database.deleteData(root => root.cells[cellId]);
332
+ const averageFill = newCount / Math.max(1, centroids.size) / config.cellTargetSize;
333
+ if (Math.random() < rebalanceProbability(averageFill)) {
334
+ rebuildStructure(database);
335
+ }
336
+ return true;
337
+ };
338
+ if (shouldRegenerate(database)) {
339
+ rebuildStructure(database);
192
340
  }
193
-
194
- if (newCentroids.length && !transactionMutate(centroidStore(database), newCentroids)) return undefined;
195
- if (refWrites.length && !transactionMutate(refIndexStore(database), refWrites)) return undefined;
196
- return true;
197
341
  }
198
342
 
199
- // Tombstone each ref in its cell (found via refIndex) and drop it from the index. undefined while not synced.
200
343
  export function removeEmbeddings(
201
344
  database: Database<IvfEmbeddingRoot>,
202
345
  refs: string[],
203
- ): true | undefined {
204
- if (!refs.length) return true;
205
- const refIndex = transactionRead<string>(refIndexStore(database));
206
- if (!refIndex) return undefined;
207
-
208
- const refsByCell = new Map<string, string[]>();
209
- for (const ref of refs) {
210
- const cellId = refIndex.get(ref);
211
- if (!cellId) continue;
212
- let group = refsByCell.get(cellId);
213
- if (!group) {
214
- group = [];
215
- refsByCell.set(cellId, group);
346
+ ) {
347
+ if (!refs.length) return;
348
+ const steps = database.readData(root => root.steps);
349
+ const count = database.readData(root => root.count);
350
+ if (!steps) return;
351
+ if (count === undefined) return;
352
+
353
+ if (!steps[STEP_IVF]) {
354
+ const flatDeletes = refs.map(ref => ({ key: ref, value: undefined }));
355
+ transactionMutate(flatStore(database), flatDeletes);
356
+ for (const ref of refs) {
357
+ database.deleteData(root => root.byRef[ref]);
358
+ }
359
+ database.writeData(root => root.count, Math.max(0, count - refs.length));
360
+ return;
361
+ }
362
+
363
+ const centroids = transactionRead(centroidStore(database));
364
+ if (!centroids) return;
365
+ if (!centroids.size) return;
366
+
367
+ // Look up each ref's embedding through byRef, then rank its cells the way an insert would, so we delete from
368
+ // the cell it actually landed in (plus a few neighbours, in case a rebuild nudged it across a boundary).
369
+ const refValues = database.readData(root => refs.map(ref => root.byRef[ref]));
370
+ if (!refValues) return;
371
+ const candidatesByItem: { ref: string; cellIds: string[] }[] = [];
372
+ const candidateSet = new Set<string>();
373
+ for (let index = 0; index < refs.length; index++) {
374
+ const value = refValues[index];
375
+ if (!value) {
376
+ continue;
377
+ }
378
+ const embedding = decodeRefValue(value);
379
+ const cellIds = rankCellsByCloseness(embedding, centroids).slice(0, 1 + DELETE_FALLBACK_CELLS);
380
+ candidatesByItem.push({ ref: refs[index], cellIds });
381
+ for (const cellId of cellIds) {
382
+ candidateSet.add(cellId);
216
383
  }
217
- group.push(ref);
218
384
  }
219
385
 
220
- for (const [cellId, group] of refsByCell) {
221
- const memberDeletes = group.map(ref => ({ key: ref, value: undefined }));
222
- if (!transactionMutate<StoredEmbedding>(cellStore(database, cellId), memberDeletes)) return undefined;
386
+ const candidateCellIds = Array.from(candidateSet);
387
+ const stores = database.readData(root => candidateCellIds.map(cellId => root.cells[cellId]));
388
+ if (!stores) return;
389
+ const membersByCell = new Map<string, Map<string, StoredEmbedding>>();
390
+ for (let index = 0; index < candidateCellIds.length; index++) {
391
+ membersByCell.set(candidateCellIds[index], replayTransactionStore(stores[index]));
392
+ }
393
+
394
+ const deletesByCell = new Map<string, string[]>();
395
+ const deletedRefs: string[] = [];
396
+ for (const candidate of candidatesByItem) {
397
+ for (const cellId of candidate.cellIds) {
398
+ const members = membersByCell.get(cellId);
399
+ if (!members || !members.has(candidate.ref)) {
400
+ continue;
401
+ }
402
+ let cellRefs = deletesByCell.get(cellId);
403
+ if (!cellRefs) {
404
+ cellRefs = [];
405
+ deletesByCell.set(cellId, cellRefs);
406
+ }
407
+ cellRefs.push(candidate.ref);
408
+ deletedRefs.push(candidate.ref);
409
+ break;
410
+ }
411
+ }
412
+
413
+ for (const cellId of deletesByCell.keys()) {
414
+ const cellRefs = deletesByCell.get(cellId)!;
415
+ const memberDeletes = cellRefs.map(ref => ({ key: ref, value: undefined }));
416
+ transactionMutate(cellStore(database, cellId), memberDeletes);
417
+ }
418
+ for (const ref of deletedRefs) {
419
+ database.deleteData(root => root.byRef[ref]);
420
+ }
421
+ if (deletedRefs.length) {
422
+ database.writeData(root => root.count, Math.max(0, count - deletedRefs.length));
223
423
  }
224
- const refDeletes = refs.map(ref => ({ key: ref, value: undefined }));
225
- if (!transactionMutate<string>(refIndexStore(database), refDeletes)) return undefined;
226
- return true;
227
424
  }
@@ -1,10 +1,14 @@
1
1
  import { Database } from "./Database";
2
- export type TransactionSetStore = {
2
+ declare const valueTag: unique symbol;
3
+ export type TransactionSetStore<Value> = {
3
4
  [fileNumber: string]: Uint8Array;
5
+ [valueTag]?: Value;
4
6
  };
5
- export declare function transactionRead<Value>(database: Database<TransactionSetStore>): Map<string, Value> | undefined;
6
- export declare function replayTransactionStore<Value>(store: TransactionSetStore | undefined): Map<string, Value>;
7
- export declare function transactionMutate<Value>(database: Database<TransactionSetStore>, transactions: {
7
+ export declare function transactionRead<Value>(database: Database<TransactionSetStore<Value>>): Map<string, Value> | undefined;
8
+ export declare function replayTransactionStore<Value>(store: TransactionSetStore<Value> | undefined): Map<string, Value>;
9
+ export declare function transactionMutate<Value>(database: Database<TransactionSetStore<Value>>, transactions: {
8
10
  key: string;
9
11
  value: Value | undefined;
10
- }[], compactAfterFiles?: number): true | undefined;
12
+ }[], compactAfterFiles?: number): void;
13
+ export declare function transactionDelete<Value>(database: Database<TransactionSetStore<Value>>): void;
14
+ export {};