sliftutils 1.4.87 → 1.4.88

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/index.d.ts CHANGED
@@ -1153,6 +1153,7 @@ declare module "sliftutils/storage/BulkDatabase2/BulkDatabaseFormat" {
1153
1153
  export declare function buildFileBuffer(rows: Record<string, unknown>[], times: number[], targetBytes?: number): BuiltFile[];
1154
1154
  export declare function buildFileBufferRaw(rows: RawRow[], targetBytes?: number): BuiltFile[];
1155
1155
  export type BaseBulkDatabaseReader = {
1156
+ name?: string;
1156
1157
  rowCount: number;
1157
1158
  totalBytes: number;
1158
1159
  minTime: number;
@@ -1194,6 +1195,7 @@ declare module "sliftutils/storage/BulkDatabase2/BulkDatabaseFormat" {
1194
1195
  export declare function loadBulkDatabase(config: {
1195
1196
  totalBytes: number;
1196
1197
  getRange: (start: number, end: number) => Promise<Buffer>;
1198
+ name?: string;
1197
1199
  }): Promise<BaseBulkDatabaseReader>;
1198
1200
 
1199
1201
  }
@@ -2393,7 +2395,7 @@ declare module "sliftutils/storage/proxydatabase/ivfEmbeddingDatabase" {
2393
2395
  };
2394
2396
  export declare function rebuildStructure(database: Database<IvfEmbeddingRoot>): void;
2395
2397
  export declare function searchEmbeddings(database: Database<IvfEmbeddingRoot>, query: StoredEmbedding, options: {
2396
- probeCellCount: number;
2398
+ probeBudget: number;
2397
2399
  resultCount: number;
2398
2400
  }): SearchHit[] | undefined;
2399
2401
  export declare function lookupEmbeddings(database: Database<IvfEmbeddingRoot>, refs: string[]): Map<string, StoredEmbedding> | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sliftutils",
3
- "version": "1.4.87",
3
+ "version": "1.4.88",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -41,6 +41,7 @@ export interface BuiltFile {
41
41
  export declare function buildFileBuffer(rows: Record<string, unknown>[], times: number[], targetBytes?: number): BuiltFile[];
42
42
  export declare function buildFileBufferRaw(rows: RawRow[], targetBytes?: number): BuiltFile[];
43
43
  export type BaseBulkDatabaseReader = {
44
+ name?: string;
44
45
  rowCount: number;
45
46
  totalBytes: number;
46
47
  minTime: number;
@@ -82,4 +83,5 @@ export declare function loadBulkHeader(getRange: (start: number, end: number) =>
82
83
  export declare function loadBulkDatabase(config: {
83
84
  totalBytes: number;
84
85
  getRange: (start: number, end: number) => Promise<Buffer>;
86
+ name?: string;
85
87
  }): Promise<BaseBulkDatabaseReader>;
@@ -410,6 +410,9 @@ export function buildFileBufferRaw(rows: RawRow[], targetBytes = TARGET_FILE_BYT
410
410
  }
411
411
 
412
412
  export type BaseBulkDatabaseReader = {
413
+ // Identifies the source this reader came from (the bulk file name, or "(streams)") so the join can
414
+ // name the offending file when one of its reads fails. Undefined for readers built without one.
415
+ name?: string;
413
416
  rowCount: number;
414
417
  totalBytes: number;
415
418
  // Write-time bounds of this reader's data (0 if unknown — old bulk files).
@@ -468,6 +471,7 @@ export async function loadBulkHeader(getRange: (start: number, end: number) => P
468
471
  export async function loadBulkDatabase(config: {
469
472
  totalBytes: number;
470
473
  getRange: (start: number, end: number) => Promise<Buffer>;
474
+ name?: string;
471
475
  }): Promise<BaseBulkDatabaseReader> {
472
476
  const headerLength = (await config.getRange(0, 4)).readUInt32LE(0);
473
477
  if (headerLength <= 0 || headerLength > config.totalBytes) {
@@ -502,6 +506,7 @@ export async function loadBulkDatabase(config: {
502
506
  : keys.map(() => header.maxTime || 0);
503
507
 
504
508
  return {
509
+ name: config.name,
505
510
  rowCount,
506
511
  totalBytes: config.totalBytes,
507
512
  minTime: header.minTime || 0,
@@ -257,7 +257,7 @@ export async function loadFileReader(name: string, storage: FileStorage, f: Bulk
257
257
  const raw = await makeRawGetRange(storage, f.fileName);
258
258
  const fileId = nullJoin(name, f.fileName);
259
259
  const opened = await blockCache.open(fileId, raw.size, raw.rawGetRange);
260
- const reader = await loadBulkDatabase({ totalBytes: opened.uncompressedSize, getRange: opened.getRange });
260
+ const reader = await loadBulkDatabase({ totalBytes: opened.uncompressedSize, getRange: opened.getRange, name: f.fileName });
261
261
  cache.set(f.fileName, reader);
262
262
  return reader;
263
263
  }
@@ -327,6 +327,19 @@ function pruneSubCaches(bulkFiles: BulkFileInfo[], streamFiles: StreamFileInfo[]
327
327
  for (const n of subCaches.stream.keys()) if (!liveStream.has(n)) subCaches.stream.delete(n);
328
328
  }
329
329
 
330
+ // A corrupt/unreadable column in ONE underlying file must not break the whole joined read. When a
331
+ // source's column read throws, we drop that source for that column (falling through to older readers,
332
+ // exactly as if the file never set the column) and warn once per (file, column) so the corruption is
333
+ // visible without flooding the log on every rebuild.
334
+ const warnedCorruptReads = new Set<string>();
335
+ function warnCorruptRead(db: BaseBulkDatabaseReader, column: string, e: unknown): void {
336
+ const fileName = db.name || "(unknown source)";
337
+ const dedupeKey = nullJoin(fileName, column);
338
+ if (warnedCorruptReads.has(dedupeKey)) return;
339
+ warnedCorruptReads.add(dedupeKey);
340
+ console.warn(`${red("corrupt read")}: file ${fileName} column ${JSON.stringify(column)} could not be read - skipping it (its data is dropped from results): ${(e as Error).message}`);
341
+ }
342
+
330
343
  function joinBulkDatabases(databases: BaseBulkDatabaseReader[]): ResolvedReader {
331
344
  const deleteTime = new Map<string, number>();
332
345
  for (const db of databases) {
@@ -367,8 +380,13 @@ function joinBulkDatabases(databases: BaseBulkDatabaseReader[]): ResolvedReader
367
380
  async getColumn(column) {
368
381
  const perReader = await Promise.all(databases.map(async db => {
369
382
  if (!db.columns.some(c => c.column === column)) return undefined;
370
- const entries = await db.getColumn(column);
371
- return new Map(entries.map(e => [e.key, { value: e.value, time: e.time }]));
383
+ try {
384
+ const entries = await db.getColumn(column);
385
+ return new Map(entries.map(e => [e.key, { value: e.value, time: e.time }]));
386
+ } catch (e) {
387
+ warnCorruptRead(db, column, e);
388
+ return undefined;
389
+ }
372
390
  }));
373
391
  return keys.map(key => {
374
392
  let bestTime = -Infinity;
@@ -391,7 +409,13 @@ function joinBulkDatabases(databases: BaseBulkDatabaseReader[]): ResolvedReader
391
409
  let found = false;
392
410
  for (const db of databases) {
393
411
  if (!db.columns.some(c => c.column === column)) continue;
394
- const r = await db.getSingleField(key, column);
412
+ let r;
413
+ try {
414
+ r = await db.getSingleField(key, column);
415
+ } catch (e) {
416
+ warnCorruptRead(db, column, e);
417
+ continue;
418
+ }
395
419
  if (r === ABSENT) continue;
396
420
  if (r.time > bestTime) { bestTime = r.time; bestVal = r.value; found = true; }
397
421
  }
@@ -106,6 +106,7 @@ export function streamReaderFromEntries(entries: StreamEntry[], totalBytes: numb
106
106
  }
107
107
  const keyIndexMap = buildKeyIndex(keys);
108
108
  let reader: BaseBulkDatabaseReader = {
109
+ name: "(streams)",
109
110
  totalBytes,
110
111
  rowCount: keys.length,
111
112
  minTime: times.size ? minTime : 0,
@@ -72,7 +72,7 @@ async function main() {
72
72
  const sampleCount = 100;
73
73
  for (let index = 0; index < sampleCount; index++) {
74
74
  const query = allInputs[Math.floor(index * run.size / sampleCount)];
75
- const hits = searchEmbeddings(database, query.embedding, { probeCellCount: 64, resultCount: 1 });
75
+ const hits = searchEmbeddings(database, query.embedding, { probeBudget: 512, resultCount: 1 });
76
76
  if (hits && hits.length && hits[0].ref === query.ref) {
77
77
  found++;
78
78
  }
@@ -31,7 +31,7 @@ export type SearchHit = {
31
31
  };
32
32
  export declare function rebuildStructure(database: Database<IvfEmbeddingRoot>): void;
33
33
  export declare function searchEmbeddings(database: Database<IvfEmbeddingRoot>, query: StoredEmbedding, options: {
34
- probeCellCount: number;
34
+ probeBudget: number;
35
35
  resultCount: number;
36
36
  }): SearchHit[] | undefined;
37
37
  export declare function lookupEmbeddings(database: Database<IvfEmbeddingRoot>, refs: string[]): Map<string, StoredEmbedding> | undefined;
@@ -2,7 +2,6 @@ import cborx from "cbor-x";
2
2
  import { Database, namespaceDatabase } from "./Database";
3
3
  import { TransactionSetStore, transactionRead, transactionMutate, transactionDelete, replayTransactionStore } from "./transactionSet";
4
4
  import { StoredEmbedding, EmbeddingFormat, getCloseness, embeddingToFloat32, releaseFloat32, encodeEmbedding, hashEmbedding } from "../embeddingFormats";
5
- import { isDefined } from "../../misc/types";
6
5
 
7
6
  export type IvfConfig = {
8
7
  model: string;
@@ -218,8 +217,7 @@ export function rebuildStructure(database: Database<IvfEmbeddingRoot>): void {
218
217
  export function searchEmbeddings(
219
218
  database: Database<IvfEmbeddingRoot>,
220
219
  query: StoredEmbedding,
221
- // NOTE: We do a progressive search, so probe cell count can be very high.
222
- options: { probeCellCount: number; resultCount: number },
220
+ options: { probeBudget: number; resultCount: number },
223
221
  ): SearchHit[] | undefined {
224
222
  const config = database.readData(root => root.config);
225
223
  if (!config) return undefined;
@@ -240,36 +238,14 @@ export function searchEmbeddings(
240
238
  const centroids = transactionRead(centroidStore(database));
241
239
  if (!centroids) return undefined;
242
240
  if (!centroids.size) return [];
243
- const probeCellCount = Math.min(centroids.size, options.probeCellCount);
244
-
245
- let rankedCellIds = rankCellsByCloseness(query, centroids);
246
- let stores: TransactionSetStore<StoredEmbedding>[] = [];
247
- // A somewhat strange search, trying to load more and more data. The reason we can't load everything at once is because the server has no way of knowing what we want first, and so it'll just wait until it can give us everything at once. By waiting until all of the earlier values load before even requesting the later values, it means that we will receive the earlier values sooner. Which will result in the search immediately returning results with very few accesses, And then the results improving in quality as time goes on, which is very desirable.
248
- {
249
- let stopAtNextKeyPoint = false;
250
- let keyPoints: number[] = [];
251
- let currentProbeAdd = 4;
252
- let currentTarget = currentProbeAdd;
253
- while (currentTarget < probeCellCount) {
254
- keyPoints.push(currentTarget);
255
- currentTarget += currentProbeAdd;
256
- currentProbeAdd *= 2;
257
- }
258
- for (let i = 0; i < probeCellCount; i++) {
259
- if (keyPoints.includes(i) && stopAtNextKeyPoint) break;
260
- let store = database.readData(root => root.cells[rankedCellIds[i]]);
261
- if (!store) {
262
- stopAtNextKeyPoint = true;
263
- continue;
264
- }
265
- stores.push(store);
266
- }
267
- }
268
-
241
+ const probeCellCount = Math.max(1, Math.ceil(options.probeBudget / config.cellTargetSize));
242
+ const probeCellIds = rankCellsByCloseness(query, centroids).slice(0, probeCellCount);
243
+ const stores = database.readData(root => probeCellIds.map(cellId => root.cells[cellId]));
244
+ if (!stores) return undefined;
269
245
  for (const store of stores) {
270
246
  const members = replayTransactionStore<StoredEmbedding>(store);
271
- for (const [ref, value] of members) {
272
- hits.push({ ref, closeness: getCloseness(query, value) });
247
+ for (const ref of members.keys()) {
248
+ hits.push({ ref, closeness: getCloseness(query, members.get(ref)!) });
273
249
  }
274
250
  }
275
251
  hits.sort((left, right) => right.closeness - left.closeness);