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 +3 -1
- package/package.json +1 -1
- package/storage/BulkDatabase2/BulkDatabaseFormat.d.ts +2 -0
- package/storage/BulkDatabase2/BulkDatabaseFormat.ts +5 -0
- package/storage/BulkDatabase2/LoadedIndex.ts +28 -4
- package/storage/BulkDatabase2/streamLog.ts +1 -0
- package/storage/proxydatabase/ivfDbCheck.ts +1 -1
- package/storage/proxydatabase/ivfEmbeddingDatabase.d.ts +1 -1
- package/storage/proxydatabase/ivfEmbeddingDatabase.ts +7 -31
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
|
-
|
|
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
|
@@ -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
|
-
|
|
371
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
|
272
|
-
hits.push({ ref, closeness: getCloseness(query,
|
|
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);
|