sliftutils 1.4.87 → 1.5.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/index.d.ts CHANGED
@@ -72,7 +72,7 @@ declare module "sliftutils/misc/https/certs" {
72
72
  import * as forge from "node-forge";
73
73
  import { MaybePromise } from "socket-function/src/types";
74
74
  export declare const CA_NOT_FOUND_ERROR = "18aa7318-f88f-4d2d-b41f-3daf4a433827";
75
- export declare const identityStorageKey = "machineCA_10";
75
+ export declare const identityStorageKey = "machineCA_11";
76
76
  export type IdentityStorageType = {
77
77
  domain: string;
78
78
  certB64: string;
@@ -99,7 +99,7 @@ declare module "sliftutils/misc/https/certs" {
99
99
  export declare const sign: (keyPair: {
100
100
  key: string | Buffer;
101
101
  }, data: unknown) => string;
102
- export declare function verify(cert: string, signature: string, data: unknown): boolean;
102
+ export declare function verify(cert: string, signature: string, data: unknown): void;
103
103
  export declare function validateCACert(domain: string, cert: string | Buffer): void;
104
104
  export declare function validateCertificate(domain: string, cert: Buffer | string, issuerCert: Buffer | string): void;
105
105
  export declare function generateKeyPair(): forge.pki.rsa.KeyPair;
@@ -108,21 +108,22 @@ declare module "sliftutils/misc/https/certs" {
108
108
  export declare function createCertFromCA(config: {
109
109
  CAKeyPair: X509KeyPair;
110
110
  }): X509KeyPair;
111
- export declare function getMachineId(domainName: string): string;
111
+ export declare function getMachineId(domainNameOrNodeId: string, domain: string): string;
112
112
  export type NodeIdParts = {
113
113
  threadId: string;
114
114
  machineId: string;
115
115
  domain: string;
116
116
  port: number;
117
117
  };
118
- export declare function decodeNodeId(nodeId: string): NodeIdParts | undefined;
119
- export declare function decodeNodeIdAssert(nodeId: string): NodeIdParts;
118
+ export declare function decodeNodeId(nodeId: string, domain: string, allowMissingThreadId?: "allowMissingThreadId"): NodeIdParts | undefined;
119
+ export declare function decodeNodeIdAssert(nodeId: string, domain: string, allowMissingThreadId?: "allowMissingThreadId"): NodeIdParts;
120
120
  export declare function encodeNodeId(parts: NodeIdParts): string;
121
121
  export declare function setIdentityCARaw(domain: string, json: string): Promise<void>;
122
122
  export declare function loadIdentityCA(domain: string): Promise<void>;
123
123
  export declare function getIdentityCA(domain: string): X509KeyPair;
124
124
  export declare function getIdentityCAPromise(domain: string): MaybePromise<X509KeyPair>;
125
125
  export declare function getOwnMachineId(domain: string): string;
126
+ export declare function getOwnThreadId(domain: string): string;
126
127
  /** Part of the machineId comes from the publicKey, so we can use it to verify */
127
128
  export declare function verifyMachineIdForPublicKey(config: {
128
129
  machineId: string;
@@ -291,6 +292,11 @@ declare module "sliftutils/misc/random" {
291
292
 
292
293
  }
293
294
 
295
+ declare module "sliftutils/misc/strings" {
296
+ export declare function ellipsize(text: string, maxLength: number): string;
297
+
298
+ }
299
+
294
300
  declare module "sliftutils/misc/types" {
295
301
  export declare function isDefined<T>(value: T | undefined | null): value is T;
296
302
  export declare function freezeObject(value: unknown): unknown;
@@ -1153,6 +1159,7 @@ declare module "sliftutils/storage/BulkDatabase2/BulkDatabaseFormat" {
1153
1159
  export declare function buildFileBuffer(rows: Record<string, unknown>[], times: number[], targetBytes?: number): BuiltFile[];
1154
1160
  export declare function buildFileBufferRaw(rows: RawRow[], targetBytes?: number): BuiltFile[];
1155
1161
  export type BaseBulkDatabaseReader = {
1162
+ name?: string;
1156
1163
  rowCount: number;
1157
1164
  totalBytes: number;
1158
1165
  minTime: number;
@@ -1194,6 +1201,7 @@ declare module "sliftutils/storage/BulkDatabase2/BulkDatabaseFormat" {
1194
1201
  export declare function loadBulkDatabase(config: {
1195
1202
  totalBytes: number;
1196
1203
  getRange: (start: number, end: number) => Promise<Buffer>;
1204
+ name?: string;
1197
1205
  }): Promise<BaseBulkDatabaseReader>;
1198
1206
 
1199
1207
  }
@@ -2393,7 +2401,7 @@ declare module "sliftutils/storage/proxydatabase/ivfEmbeddingDatabase" {
2393
2401
  };
2394
2402
  export declare function rebuildStructure(database: Database<IvfEmbeddingRoot>): void;
2395
2403
  export declare function searchEmbeddings(database: Database<IvfEmbeddingRoot>, query: StoredEmbedding, options: {
2396
- probeCellCount: number;
2404
+ probeBudget: number;
2397
2405
  resultCount: number;
2398
2406
  }): SearchHit[] | undefined;
2399
2407
  export declare function lookupEmbeddings(database: Database<IvfEmbeddingRoot>, refs: string[]): Map<string, StoredEmbedding> | undefined;
@@ -5,7 +5,7 @@
5
5
  import * as forge from "node-forge";
6
6
  import { MaybePromise } from "socket-function/src/types";
7
7
  export declare const CA_NOT_FOUND_ERROR = "18aa7318-f88f-4d2d-b41f-3daf4a433827";
8
- export declare const identityStorageKey = "machineCA_10";
8
+ export declare const identityStorageKey = "machineCA_11";
9
9
  export type IdentityStorageType = {
10
10
  domain: string;
11
11
  certB64: string;
@@ -32,7 +32,7 @@ export declare function getPublicIdentifier(PEMorDER: string | Buffer): Buffer;
32
32
  export declare const sign: (keyPair: {
33
33
  key: string | Buffer;
34
34
  }, data: unknown) => string;
35
- export declare function verify(cert: string, signature: string, data: unknown): boolean;
35
+ export declare function verify(cert: string, signature: string, data: unknown): void;
36
36
  export declare function validateCACert(domain: string, cert: string | Buffer): void;
37
37
  export declare function validateCertificate(domain: string, cert: Buffer | string, issuerCert: Buffer | string): void;
38
38
  export declare function generateKeyPair(): forge.pki.rsa.KeyPair;
@@ -41,21 +41,22 @@ export declare function generateTestCA(domain: string): X509KeyPair;
41
41
  export declare function createCertFromCA(config: {
42
42
  CAKeyPair: X509KeyPair;
43
43
  }): X509KeyPair;
44
- export declare function getMachineId(domainName: string): string;
44
+ export declare function getMachineId(domainNameOrNodeId: string, domain: string): string;
45
45
  export type NodeIdParts = {
46
46
  threadId: string;
47
47
  machineId: string;
48
48
  domain: string;
49
49
  port: number;
50
50
  };
51
- export declare function decodeNodeId(nodeId: string): NodeIdParts | undefined;
52
- export declare function decodeNodeIdAssert(nodeId: string): NodeIdParts;
51
+ export declare function decodeNodeId(nodeId: string, domain: string, allowMissingThreadId?: "allowMissingThreadId"): NodeIdParts | undefined;
52
+ export declare function decodeNodeIdAssert(nodeId: string, domain: string, allowMissingThreadId?: "allowMissingThreadId"): NodeIdParts;
53
53
  export declare function encodeNodeId(parts: NodeIdParts): string;
54
54
  export declare function setIdentityCARaw(domain: string, json: string): Promise<void>;
55
55
  export declare function loadIdentityCA(domain: string): Promise<void>;
56
56
  export declare function getIdentityCA(domain: string): X509KeyPair;
57
57
  export declare function getIdentityCAPromise(domain: string): MaybePromise<X509KeyPair>;
58
58
  export declare function getOwnMachineId(domain: string): string;
59
+ export declare function getOwnThreadId(domain: string): string;
59
60
  /** Part of the machineId comes from the publicKey, so we can use it to verify */
60
61
  export declare function verifyMachineIdForPublicKey(config: {
61
62
  machineId: string;
@@ -19,6 +19,7 @@ import { MaybePromise } from "socket-function/src/types";
19
19
  import { SocketFunction } from "socket-function/SocketFunction";
20
20
  import { resetAllNodeCallFactories } from "socket-function/src/nodeCache";
21
21
  import { getKeyStore } from "./persistentLocalStorage";
22
+ import { ellipsize } from "../strings";
22
23
 
23
24
  setFlag(require, "node-forge", "allowclient", true);
24
25
  setFlag(require, "js-sha256", "allowclient", true);
@@ -27,7 +28,7 @@ const timeInDay = 1000 * 60 * 60 * 24;
27
28
 
28
29
  export const CA_NOT_FOUND_ERROR = "18aa7318-f88f-4d2d-b41f-3daf4a433827";
29
30
 
30
- export const identityStorageKey = "machineCA_10";
31
+ export const identityStorageKey = "machineCA_11";
31
32
  export type IdentityStorageType = { domain: string; certB64: string; keyB64: string };
32
33
 
33
34
  function getIdentityStore(domain: string) {
@@ -99,21 +100,17 @@ export function createX509(
99
100
  //{ type: 7, ip: "127.0.0.1" }
100
101
  ]
101
102
  },
102
- // NOTE: nameConstraints are supported with our branch. But... chrome doesn't support them, so there's no point in using them.
103
+ // NOTE: nameConstraints require our forked node-forge:
103
104
  // "node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
104
- // {
105
- // name: "nameConstraints",
106
- // permittedSubtrees: [
107
- // // Chrome doesn't respect nameConstraints per https://bugs.chromium.org/p/chromium/issues/detail?id=1072083,
108
- // // as the spec decided that "free to process or ignore such information" (when present in self
109
- // // signed certificates), and therefore the chrome implementation decided "The first order is to behave predictably",
110
- // // so... they're not going to support it, because why have a feature in one place, if it isn't
111
- // // on android as well... ugh...
112
- // // Works fine on Edge though
113
- // { type: 2, value: forge.util.encodeUtf8(domain) },
114
- // { type: 2, value: forge.util.encodeUtf8(localHostDomain) },
115
- // ]
116
- // },
105
+ // Chrome ignores them (https://bugs.chromium.org/p/chromium/issues/detail?id=1072083),
106
+ // but our own validation (validateCACert/validateCertificate) enforces them.
107
+ {
108
+ name: "nameConstraints",
109
+ permittedSubtrees: [
110
+ { type: 2, value: forge.util.encodeUtf8(domain) },
111
+ { type: 2, value: forge.util.encodeUtf8(localHostDomain) },
112
+ ]
113
+ },
117
114
  ]);
118
115
  certObj.setExtensions(extensions);
119
116
 
@@ -191,7 +188,9 @@ export const sign = measureWrap(function sign(keyPair: { key: string | Buffer },
191
188
 
192
189
  export function verify(cert: string, signature: string, data: unknown) {
193
190
  let certObj = parseCert(cert);
194
- return (certObj.publicKey as forge.pki.rsa.PublicKey).verify(JSON.stringify(data), signature);
191
+ if (!(certObj.publicKey as forge.pki.rsa.PublicKey).verify(JSON.stringify(data), signature)) {
192
+ throw new Error(`Signature verification failed. Signature: ${JSON.stringify(signature)} | Data: ${ellipsize(JSON.stringify(data), 1024)}`);
193
+ }
195
194
  }
196
195
 
197
196
  function normalizeCertToPEM(PEMorDER: string | Buffer): string {
@@ -211,7 +210,7 @@ function getDomainPartFromPublicKey(publicKey: { publicKeyBytes: Buffer } | forg
211
210
  } else {
212
211
  bytes = Buffer.from(new Uint32Array((publicKey as any).n.data).buffer);
213
212
  }
214
- return "b" + sha265.sha256(Buffer.from(bytes)).slice(0, 16).replaceAll("+", "-").replaceAll("/", "_");
213
+ return "b" + Buffer.from(sha265.sha256.array(Buffer.from(bytes))).toString("base64").slice(0, 16).replaceAll("+", "-").replaceAll("/", "_");
215
214
  }
216
215
 
217
216
  export function validateCACert(domain: string, cert: string | Buffer) {
@@ -440,8 +439,8 @@ export function createCertFromCA(config: {
440
439
  });
441
440
  }
442
441
 
443
- export function getMachineId(domainName: string) {
444
- return domainName.split(".").slice(-3).join(".");
442
+ export function getMachineId(domainNameOrNodeId: string, domain: string) {
443
+ return decodeNodeIdAssert(domainNameOrNodeId, domain, "allowMissingThreadId").machineId;
445
444
  }
446
445
 
447
446
  export type NodeIdParts = {
@@ -450,13 +449,18 @@ export type NodeIdParts = {
450
449
  domain: string;
451
450
  port: number;
452
451
  };
453
- export function decodeNodeId(nodeId: string): NodeIdParts | undefined {
452
+ export function decodeNodeId(nodeId: string, domain: string, allowMissingThreadId?: "allowMissingThreadId"): NodeIdParts | undefined {
454
453
  let locationObj = getNodeIdLocation(nodeId);
455
454
  if (!locationObj) {
456
455
  return undefined;
457
456
  }
458
457
  let parts = locationObj.address.split(".");
459
- if (nodeId.startsWith("127-0-0-1.") && parts.length === 3) {
458
+ // NOTE: We have to only allow localhost domains on our own domain, as the underlying domain
459
+ // gets stripped when we're looking at the machineId. So if we allowed localhost domains on
460
+ // other domains, a server could trick us into connecting to it, and then once the connection
461
+ // is established, it could talk back and we would think it has a localhost machineId, which
462
+ // is implicitly trusted, which would then give it access to everything.
463
+ if (locationObj.address === `127-0-0-1.${domain}` && nodeId.includes(":")) {
460
464
  return {
461
465
  threadId: "",
462
466
  machineId: parts.at(-3) || "",
@@ -464,7 +468,8 @@ export function decodeNodeId(nodeId: string): NodeIdParts | undefined {
464
468
  port: locationObj.port,
465
469
  };
466
470
  }
467
- if (parts.length < 4) {
471
+ let isValid = parts.length >= 4 || allowMissingThreadId && parts.length === 3;
472
+ if (!isValid) {
468
473
  return undefined;
469
474
  }
470
475
  return {
@@ -474,10 +479,10 @@ export function decodeNodeId(nodeId: string): NodeIdParts | undefined {
474
479
  port: locationObj.port,
475
480
  };
476
481
  }
477
- export function decodeNodeIdAssert(nodeId: string): NodeIdParts {
478
- let result = decodeNodeId(nodeId);
482
+ export function decodeNodeIdAssert(nodeId: string, domain: string, allowMissingThreadId?: "allowMissingThreadId"): NodeIdParts {
483
+ let result = decodeNodeId(nodeId, domain, allowMissingThreadId);
479
484
  if (!result) {
480
- throw new Error(`Invalid nodeId: ${nodeId}`);
485
+ throw new Error(`Invalid nodeId: ${JSON.stringify(nodeId)}`);
481
486
  }
482
487
  return result;
483
488
  }
@@ -523,7 +528,10 @@ export function getIdentityCAPromise(domain: string): MaybePromise<X509KeyPair>
523
528
 
524
529
 
525
530
  export function getOwnMachineId(domain: string) {
526
- return getMachineId(getIdentityCA(domain).domain);
531
+ return getMachineId(getIdentityCA(domain).domain, domain);
532
+ }
533
+ export function getOwnThreadId(domain: string) {
534
+ return decodeNodeIdAssert(getThreadKeyCert(domain).domain, domain).threadId;
527
535
  }
528
536
 
529
537
  /** Part of the machineId comes from the publicKey, so we can use it to verify */
@@ -533,7 +541,8 @@ export function verifyMachineIdForPublicKey(config: {
533
541
  }): boolean {
534
542
  let { machineId, publicKey } = config;
535
543
  let domainPart = getDomainPartFromPublicKey(publicKey);
536
- return machineId.split(".").at(-3) === domainPart;
544
+ // machineId is the bare key-hash label, but also accept legacy "hash.domain.tld" forms
545
+ return machineId.split(".")[0] === domainPart;
537
546
  }
538
547
 
539
548
  // NOTE: We don't have a cache per CA, as... the CA should be set first
@@ -0,0 +1 @@
1
+ export declare function ellipsize(text: string, maxLength: number): string;
@@ -0,0 +1,8 @@
1
+ module.allowclient = true;
2
+
3
+ export function ellipsize(text: string, maxLength: number): string {
4
+ if (text.length <= maxLength) {
5
+ return text;
6
+ }
7
+ return text.slice(0, maxLength - 3) + "...";
8
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sliftutils",
3
- "version": "1.4.87",
3
+ "version": "1.5.0",
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);