sliftutils 1.4.80 → 1.4.82
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 +98 -6
- package/package.json +1 -1
- package/spec.txt +18 -0
- package/storage/BulkDatabase2/BulkDatabaseBase.d.ts +4 -1
- package/storage/BulkDatabase2/BulkDatabaseBase.ts +23 -7
- package/storage/dist/embeddingFormats.ts.cache +340 -0
- package/storage/dist/faceFramesData.ts.cache +55 -0
- package/storage/embeddingBench.d.ts +1 -0
- package/storage/embeddingBench.ts +152 -0
- package/storage/embeddingFormats.d.ts +9 -1
- package/storage/embeddingFormats.ts +190 -95
- package/storage/faceFramesData.d.ts +1 -0
- package/storage/faceFramesData.ts +49 -0
- package/storage/faceFramesServer.d.ts +1 -0
- package/storage/faceFramesServer.ts +56 -0
- package/storage/proxydatabase/dist/Database.ts.cache +13 -0
- package/storage/proxydatabase/dist/inMemoryDatabase.ts.cache +105 -0
- package/storage/proxydatabase/dist/ivfDbCheck.ts.cache +81 -0
- package/storage/proxydatabase/dist/ivfEmbeddingDatabase.ts.cache +334 -0
- package/storage/proxydatabase/dist/transactionSet.ts.cache +108 -0
- package/storage/proxydatabase/inMemoryDatabase.d.ts +13 -0
- package/storage/proxydatabase/inMemoryDatabase.ts +108 -0
- package/storage/proxydatabase/ivfDbCheck.d.ts +1 -0
- package/storage/proxydatabase/ivfDbCheck.ts +85 -0
- package/storage/proxydatabase/ivfEmbeddingDatabase.d.ts +35 -0
- package/storage/proxydatabase/ivfEmbeddingDatabase.ts +372 -0
- package/storage/proxydatabase/transactionSet.d.ts +9 -4
- package/storage/proxydatabase/transactionSet.ts +55 -24
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Database } from "./Database";
|
|
2
|
+
|
|
3
|
+
// Rough byte size of a value, for the read/written-bytes metrics.
|
|
4
|
+
function sizeOf(value: unknown): number {
|
|
5
|
+
if (value === undefined || value === null) {
|
|
6
|
+
return 0;
|
|
7
|
+
}
|
|
8
|
+
if (value instanceof Uint8Array) {
|
|
9
|
+
return value.byteLength;
|
|
10
|
+
}
|
|
11
|
+
if (typeof value === "string") {
|
|
12
|
+
return value.length;
|
|
13
|
+
}
|
|
14
|
+
if (typeof value === "number") {
|
|
15
|
+
return 8;
|
|
16
|
+
}
|
|
17
|
+
if (typeof value === "boolean") {
|
|
18
|
+
return 1;
|
|
19
|
+
}
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
let total = 0;
|
|
22
|
+
for (const entry of value) {
|
|
23
|
+
total += sizeOf(entry);
|
|
24
|
+
}
|
|
25
|
+
return total;
|
|
26
|
+
}
|
|
27
|
+
if (typeof value === "object") {
|
|
28
|
+
let total = 0;
|
|
29
|
+
const record = value as Record<string, unknown>;
|
|
30
|
+
for (const key of Object.keys(record)) {
|
|
31
|
+
total += sizeOf(record[key]);
|
|
32
|
+
}
|
|
33
|
+
return total;
|
|
34
|
+
}
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Record the property path a dereference lambda walks, so a write/delete can be applied at it.
|
|
39
|
+
function capturePath(deref: (root: unknown) => unknown): string[] {
|
|
40
|
+
const path: string[] = [];
|
|
41
|
+
const handler: ProxyHandler<object> = {
|
|
42
|
+
get(_target, key) {
|
|
43
|
+
if (typeof key === "string") {
|
|
44
|
+
path.push(key);
|
|
45
|
+
}
|
|
46
|
+
return new Proxy({}, handler);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
deref(new Proxy({}, handler));
|
|
50
|
+
return path;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function setPath(root: Record<string, unknown>, path: string[], value: unknown): void {
|
|
54
|
+
let node = root;
|
|
55
|
+
for (let index = 0; index < path.length - 1; index++) {
|
|
56
|
+
const key = path[index];
|
|
57
|
+
const next = node[key];
|
|
58
|
+
if (!next || typeof next !== "object") {
|
|
59
|
+
node[key] = {};
|
|
60
|
+
}
|
|
61
|
+
node = node[key] as Record<string, unknown>;
|
|
62
|
+
}
|
|
63
|
+
node[path[path.length - 1]] = value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function deletePath(root: Record<string, unknown>, path: string[]): void {
|
|
67
|
+
let node = root;
|
|
68
|
+
for (let index = 0; index < path.length - 1; index++) {
|
|
69
|
+
const next = node[path[index]];
|
|
70
|
+
if (!next || typeof next !== "object") {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
node = next as Record<string, unknown>;
|
|
74
|
+
}
|
|
75
|
+
delete node[path[path.length - 1]];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// In-memory Database for tests/dev: holds the whole root as a plain object and counts every call plus the
|
|
79
|
+
// bytes flowing through. Synchronous and always "synced", so reads only return undefined for a genuinely
|
|
80
|
+
// missing scalar (a missing transaction set reads as empty inside the set helpers).
|
|
81
|
+
export class InMemoryDatabase<Root> implements Database<Root> {
|
|
82
|
+
public readCalls = 0;
|
|
83
|
+
public writeCalls = 0;
|
|
84
|
+
public deleteCalls = 0;
|
|
85
|
+
public bytesRead = 0;
|
|
86
|
+
public bytesWritten = 0;
|
|
87
|
+
private root: Record<string, unknown>;
|
|
88
|
+
|
|
89
|
+
constructor(initial: Root) {
|
|
90
|
+
this.root = initial as Record<string, unknown>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
readData<Value>(deref: (root: Root) => Value): Value | undefined {
|
|
94
|
+
this.readCalls++;
|
|
95
|
+
const result = deref(this.root as Root);
|
|
96
|
+
this.bytesRead += sizeOf(result);
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
writeData<Value>(deref: (root: Root) => Value, newValue: Value): void {
|
|
100
|
+
this.writeCalls++;
|
|
101
|
+
this.bytesWritten += sizeOf(newValue);
|
|
102
|
+
setPath(this.root, capturePath(deref as (root: unknown) => unknown), newValue);
|
|
103
|
+
}
|
|
104
|
+
deleteData(deref: (root: Root) => unknown): void {
|
|
105
|
+
this.deleteCalls++;
|
|
106
|
+
deletePath(this.root, capturePath(deref as (root: unknown) => unknown));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { InMemoryDatabase } from "./inMemoryDatabase";
|
|
3
|
+
import { IvfEmbeddingRoot, IvfConfig, EmbeddingInput, insertEmbeddings, searchEmbeddings } from "./ivfEmbeddingDatabase";
|
|
4
|
+
import { encodeEmbedding, EmbeddingFormat } from "../embeddingFormats";
|
|
5
|
+
import { ensureFaceData } from "../faceFramesData";
|
|
6
|
+
|
|
7
|
+
// Exercises the tiered IVF embedding database through the in-memory Database wrapper, which counts every
|
|
8
|
+
// read/write/delete call and the bytes through each, across dataset sizes and load patterns, with timing.
|
|
9
|
+
|
|
10
|
+
const DIM = 512;
|
|
11
|
+
const FILE_NAME = "faceFrames2_full.f32";
|
|
12
|
+
const MODEL = "buffalo_l";
|
|
13
|
+
const FORMAT: EmbeddingFormat = "q8g8_2048";
|
|
14
|
+
|
|
15
|
+
async function loadFloats(count: number): Promise<Float32Array> {
|
|
16
|
+
const byteLength = count * DIM * 4;
|
|
17
|
+
const filePath = await ensureFaceData(FILE_NAME, byteLength);
|
|
18
|
+
const buffer = Buffer.alloc(byteLength);
|
|
19
|
+
const handle = fs.openSync(filePath, "r");
|
|
20
|
+
fs.readSync(handle, buffer, 0, byteLength, 0);
|
|
21
|
+
fs.closeSync(handle);
|
|
22
|
+
return new Float32Array(buffer.buffer, buffer.byteOffset, count * DIM);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function buildInputs(count: number): Promise<EmbeddingInput[]> {
|
|
26
|
+
const floats = await loadFloats(count);
|
|
27
|
+
const inputs: EmbeddingInput[] = [];
|
|
28
|
+
for (let index = 0; index < count; index++) {
|
|
29
|
+
const slice = floats.subarray(index * DIM, (index + 1) * DIM);
|
|
30
|
+
inputs.push({ ref: "e" + index, embedding: encodeEmbedding({ input: slice, format: FORMAT, model: MODEL }) });
|
|
31
|
+
}
|
|
32
|
+
return inputs;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function freshDatabase(config: IvfConfig): InMemoryDatabase<IvfEmbeddingRoot> {
|
|
36
|
+
const root: IvfEmbeddingRoot = { config, count: 0, flat: {}, steps: {}, centroids: {}, cells: {} };
|
|
37
|
+
return new InMemoryDatabase<IvfEmbeddingRoot>(root);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function pad(value: string | number, width: number): string {
|
|
41
|
+
return String(value).padStart(width);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function main() {
|
|
45
|
+
const config: IvfConfig = { model: MODEL, format: FORMAT, cellTargetSize: 128 };
|
|
46
|
+
const runs: { size: number; batch: number }[] = [
|
|
47
|
+
{ size: 10000, batch: 1 },
|
|
48
|
+
{ size: 10000, batch: 100 },
|
|
49
|
+
{ size: 100000, batch: 1000 },
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
let maxSize = 0;
|
|
53
|
+
for (const run of runs) {
|
|
54
|
+
if (run.size > maxSize) {
|
|
55
|
+
maxSize = run.size;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const loadStart = Date.now();
|
|
59
|
+
const allInputs = await buildInputs(maxSize);
|
|
60
|
+
console.log(`loaded + encoded ${maxSize} embeddings in ${((Date.now() - loadStart) / 1000).toFixed(1)}s\n`);
|
|
61
|
+
|
|
62
|
+
console.log(`${pad("added", 7)} ${pad("batch", 5)} | ${pad("secs", 7)} ${pad("reads", 8)} ${pad("writes", 8)} ${pad("deletes", 8)} ${pad("readMB", 9)} ${pad("writeMB", 8)} | recall`);
|
|
63
|
+
for (const run of runs) {
|
|
64
|
+
const database = freshDatabase(config);
|
|
65
|
+
const start = Date.now();
|
|
66
|
+
for (let offset = 0; offset < run.size; offset += run.batch) {
|
|
67
|
+
insertEmbeddings(database, allInputs.slice(offset, offset + run.batch));
|
|
68
|
+
}
|
|
69
|
+
const seconds = (Date.now() - start) / 1000;
|
|
70
|
+
|
|
71
|
+
let found = 0;
|
|
72
|
+
const sampleCount = 100;
|
|
73
|
+
for (let index = 0; index < sampleCount; index++) {
|
|
74
|
+
const query = allInputs[Math.floor(index * run.size / sampleCount)];
|
|
75
|
+
const hits = searchEmbeddings(database, query.embedding, { probeBudget: 512, resultCount: 1 });
|
|
76
|
+
if (hits && hits.length && hits[0].ref === query.ref) {
|
|
77
|
+
found++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const recall = (found / sampleCount).toFixed(2);
|
|
81
|
+
console.log(`${pad(run.size, 7)} ${pad(run.batch, 5)} | ${pad(seconds.toFixed(1), 7)} ${pad(database.readCalls, 8)} ${pad(database.writeCalls, 8)} ${pad(database.deleteCalls, 8)} ${pad((database.bytesRead / 1e6).toFixed(0), 9)} ${pad((database.bytesWritten / 1e6).toFixed(0), 8)} | ${recall}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
void main();
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Database } from "./Database";
|
|
2
|
+
import { TransactionSetStore } from "./transactionSet";
|
|
3
|
+
import { StoredEmbedding, EmbeddingFormat } from "../embeddingFormats";
|
|
4
|
+
export type IvfConfig = {
|
|
5
|
+
model: string;
|
|
6
|
+
format: EmbeddingFormat;
|
|
7
|
+
cellTargetSize: number;
|
|
8
|
+
};
|
|
9
|
+
export type IvfEmbeddingRoot = {
|
|
10
|
+
config: IvfConfig;
|
|
11
|
+
count: number;
|
|
12
|
+
flat: TransactionSetStore<StoredEmbedding>;
|
|
13
|
+
steps: {
|
|
14
|
+
[step: string]: boolean;
|
|
15
|
+
};
|
|
16
|
+
centroids: TransactionSetStore<StoredEmbedding>;
|
|
17
|
+
cells: {
|
|
18
|
+
[cellId: string]: TransactionSetStore<StoredEmbedding>;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export type EmbeddingInput = {
|
|
22
|
+
ref: string;
|
|
23
|
+
embedding: StoredEmbedding;
|
|
24
|
+
};
|
|
25
|
+
export type SearchHit = {
|
|
26
|
+
ref: string;
|
|
27
|
+
closeness: number;
|
|
28
|
+
};
|
|
29
|
+
export declare function rebuildStructure(database: Database<IvfEmbeddingRoot>): void;
|
|
30
|
+
export declare function searchEmbeddings(database: Database<IvfEmbeddingRoot>, query: StoredEmbedding, options: {
|
|
31
|
+
probeBudget: number;
|
|
32
|
+
resultCount: number;
|
|
33
|
+
}): SearchHit[] | undefined;
|
|
34
|
+
export declare function insertEmbeddings(database: Database<IvfEmbeddingRoot>, items: EmbeddingInput[]): undefined;
|
|
35
|
+
export declare function removeEmbeddings(database: Database<IvfEmbeddingRoot>, items: EmbeddingInput[]): void;
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { Database, namespaceDatabase } from "./Database";
|
|
2
|
+
import { TransactionSetStore, transactionRead, transactionMutate, transactionDelete, replayTransactionStore } from "./transactionSet";
|
|
3
|
+
import { StoredEmbedding, EmbeddingFormat, getCloseness, embeddingToFloat32, releaseFloat32, encodeEmbedding, hashEmbedding } from "../embeddingFormats";
|
|
4
|
+
|
|
5
|
+
export type IvfConfig = {
|
|
6
|
+
model: string;
|
|
7
|
+
format: EmbeddingFormat;
|
|
8
|
+
// Target number of embeddings per cell.
|
|
9
|
+
cellTargetSize: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Tiered storage. Below FLAT_LIMIT embeddings the whole set lives in one flat transaction set (no IVF);
|
|
13
|
+
// once it grows past that we build the IVF (centroids + per-cell sets) and stay there forever. `count` is
|
|
14
|
+
// the live embedding count (re-derived exactly on a rebuild). `steps` records one-time upgrades that must
|
|
15
|
+
// never re-run even if the set later shrinks and regrows.
|
|
16
|
+
export type IvfEmbeddingRoot = {
|
|
17
|
+
config: IvfConfig;
|
|
18
|
+
count: number;
|
|
19
|
+
flat: TransactionSetStore<StoredEmbedding>;
|
|
20
|
+
steps: { [step: string]: boolean };
|
|
21
|
+
centroids: TransactionSetStore<StoredEmbedding>;
|
|
22
|
+
cells: { [cellId: string]: TransactionSetStore<StoredEmbedding> };
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type EmbeddingInput = { ref: string; embedding: StoredEmbedding };
|
|
26
|
+
export type SearchHit = { ref: string; closeness: number };
|
|
27
|
+
type CellEntry = { ref: string; embedding: StoredEmbedding };
|
|
28
|
+
|
|
29
|
+
// Below this many embeddings we skip the IVF entirely (flat store). 1024 = a clean power of two.
|
|
30
|
+
const FLAT_LIMIT = 1024;
|
|
31
|
+
// 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.
|
|
32
|
+
const REGENERATE_AT = [4096, 16384];
|
|
33
|
+
const STEP_IVF = "ivf";
|
|
34
|
+
// On delete, the member's exact cell plus this many nearby cells are checked, in case a rebuild left it non-optimal.
|
|
35
|
+
const DELETE_FALLBACK_CELLS = 10;
|
|
36
|
+
const REBALANCE_ITERATIONS = 4;
|
|
37
|
+
|
|
38
|
+
function flatStore(database: Database<IvfEmbeddingRoot>): Database<TransactionSetStore<StoredEmbedding>> {
|
|
39
|
+
return namespaceDatabase(database, root => root.flat);
|
|
40
|
+
}
|
|
41
|
+
function centroidStore(database: Database<IvfEmbeddingRoot>): Database<TransactionSetStore<StoredEmbedding>> {
|
|
42
|
+
return namespaceDatabase(database, root => root.centroids);
|
|
43
|
+
}
|
|
44
|
+
function cellStore(database: Database<IvfEmbeddingRoot>, cellId: string): Database<TransactionSetStore<StoredEmbedding>> {
|
|
45
|
+
return namespaceDatabase(database, root => root.cells[cellId]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function rankCellsByCloseness(embedding: StoredEmbedding, centroids: Map<string, StoredEmbedding>): string[] {
|
|
49
|
+
const ranked: { cellId: string; closeness: number }[] = [];
|
|
50
|
+
for (const cellId of centroids.keys()) {
|
|
51
|
+
ranked.push({ cellId, closeness: getCloseness(embedding, centroids.get(cellId)!) });
|
|
52
|
+
}
|
|
53
|
+
ranked.sort((left, right) => right.closeness - left.closeness);
|
|
54
|
+
return ranked.map(entry => entry.cellId);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Per-write chance of a full rebuild. Zero at/under the target size, then rises (cubically) past it so cells
|
|
58
|
+
// stay roughly between target and ~2x target.
|
|
59
|
+
function rebalanceProbability(fillRatio: number): number {
|
|
60
|
+
if (fillRatio <= 1) {
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
const over = fillRatio - 1;
|
|
64
|
+
return Math.min(1, over * over * over * 0.25);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// k-means. Decodes every member to a pooled float32 buffer ONCE, then assigns with a plain internal float
|
|
68
|
+
// dot (no getCloseness call — comparing two float vectors is trivial) and keeps centroids as float means,
|
|
69
|
+
// encoding them to StoredEmbedding only at the end. Releases the borrowed buffers when done.
|
|
70
|
+
function clusterMembers(members: CellEntry[], clusterCount: number, config: IvfConfig): { centroid: StoredEmbedding; members: CellEntry[] }[] {
|
|
71
|
+
const memberFloats: Float32Array[] = [];
|
|
72
|
+
for (const member of members) {
|
|
73
|
+
memberFloats.push(embeddingToFloat32(member.embedding, true));
|
|
74
|
+
}
|
|
75
|
+
const length = memberFloats.length ? memberFloats[0].length : 0;
|
|
76
|
+
let centroids: Float32Array[] = [];
|
|
77
|
+
const seedStep = members.length / clusterCount;
|
|
78
|
+
for (let clusterIndex = 0; clusterIndex < clusterCount; clusterIndex++) {
|
|
79
|
+
centroids.push(new Float32Array(memberFloats[Math.floor(clusterIndex * seedStep)]));
|
|
80
|
+
}
|
|
81
|
+
let groups: number[][] = [];
|
|
82
|
+
for (let iteration = 0; iteration < REBALANCE_ITERATIONS; iteration++) {
|
|
83
|
+
groups = [];
|
|
84
|
+
for (let clusterIndex = 0; clusterIndex < centroids.length; clusterIndex++) {
|
|
85
|
+
groups.push([]);
|
|
86
|
+
}
|
|
87
|
+
for (let memberIndex = 0; memberIndex < memberFloats.length; memberIndex++) {
|
|
88
|
+
const memberFloat = memberFloats[memberIndex];
|
|
89
|
+
let bestIndex = 0;
|
|
90
|
+
let bestDot = -Infinity;
|
|
91
|
+
for (let centroidIndex = 0; centroidIndex < centroids.length; centroidIndex++) {
|
|
92
|
+
const centroidFloat = centroids[centroidIndex];
|
|
93
|
+
let dot = 0;
|
|
94
|
+
for (let dim = 0; dim < length; dim++) {
|
|
95
|
+
dot += memberFloat[dim] * centroidFloat[dim];
|
|
96
|
+
}
|
|
97
|
+
if (dot > bestDot) {
|
|
98
|
+
bestDot = dot;
|
|
99
|
+
bestIndex = centroidIndex;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
groups[bestIndex].push(memberIndex);
|
|
103
|
+
}
|
|
104
|
+
const nextCentroids: Float32Array[] = [];
|
|
105
|
+
const nextGroups: number[][] = [];
|
|
106
|
+
for (const group of groups) {
|
|
107
|
+
if (!group.length) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const mean = new Float32Array(length);
|
|
111
|
+
for (const memberIndex of group) {
|
|
112
|
+
const memberFloat = memberFloats[memberIndex];
|
|
113
|
+
for (let dim = 0; dim < length; dim++) {
|
|
114
|
+
mean[dim] += memberFloat[dim];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
let norm = 0;
|
|
118
|
+
for (let dim = 0; dim < length; dim++) {
|
|
119
|
+
norm += mean[dim] * mean[dim];
|
|
120
|
+
}
|
|
121
|
+
const magnitude = Math.sqrt(norm) || 1;
|
|
122
|
+
for (let dim = 0; dim < length; dim++) {
|
|
123
|
+
mean[dim] /= magnitude;
|
|
124
|
+
}
|
|
125
|
+
nextCentroids.push(mean);
|
|
126
|
+
nextGroups.push(group);
|
|
127
|
+
}
|
|
128
|
+
centroids = nextCentroids;
|
|
129
|
+
groups = nextGroups;
|
|
130
|
+
}
|
|
131
|
+
const result: { centroid: StoredEmbedding; members: CellEntry[] }[] = [];
|
|
132
|
+
for (let clusterIndex = 0; clusterIndex < centroids.length; clusterIndex++) {
|
|
133
|
+
const centroid = encodeEmbedding({ input: centroids[clusterIndex], format: config.format, model: config.model });
|
|
134
|
+
const cellMembers: CellEntry[] = [];
|
|
135
|
+
for (const memberIndex of groups[clusterIndex]) {
|
|
136
|
+
cellMembers.push(members[memberIndex]);
|
|
137
|
+
}
|
|
138
|
+
result.push({ centroid, members: cellMembers });
|
|
139
|
+
}
|
|
140
|
+
for (const memberFloat of memberFloats) {
|
|
141
|
+
releaseFloat32(memberFloat);
|
|
142
|
+
}
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Re-cluster EVERY embedding (flat tier + all cells) into a fresh IVF, clear the flat tier, and mark IVF
|
|
147
|
+
// mode. This is the rearranger: run probabilistically as the set grows, once at each regenerate threshold,
|
|
148
|
+
// and once to upgrade from flat. Reads everything it uses, so it bails (does nothing) if anything isn't
|
|
149
|
+
// synced and the caller's retry re-runs it; no caller cares about its result.
|
|
150
|
+
export function rebuildStructure(database: Database<IvfEmbeddingRoot>): void {
|
|
151
|
+
const config = database.readData(root => root.config);
|
|
152
|
+
if (!config) return;
|
|
153
|
+
const steps = database.readData(root => root.steps);
|
|
154
|
+
if (!steps) return;
|
|
155
|
+
|
|
156
|
+
const allMembers: CellEntry[] = [];
|
|
157
|
+
// Only the one-time flat->IVF upgrade pulls from the flat tier. Once STEP_IVF is set the flat data has
|
|
158
|
+
// already been folded into cells, so later rebuilds ignore it. It's left in place, never deleted: deleteData
|
|
159
|
+
// only removes primitive leaves, not a whole store, and the flat tier is small enough not to matter.
|
|
160
|
+
if (!steps[STEP_IVF]) {
|
|
161
|
+
const flat = transactionRead(flatStore(database));
|
|
162
|
+
if (!flat) return;
|
|
163
|
+
for (const ref of flat.keys()) {
|
|
164
|
+
allMembers.push({ ref, embedding: flat.get(ref)! });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const centroids = transactionRead(centroidStore(database));
|
|
168
|
+
if (!centroids) return;
|
|
169
|
+
const oldCellIds = Array.from(centroids.keys());
|
|
170
|
+
const cellStores = database.readData(root => oldCellIds.map(cellId => root.cells[cellId]));
|
|
171
|
+
if (!cellStores) return;
|
|
172
|
+
for (const store of cellStores) {
|
|
173
|
+
const members = replayTransactionStore<StoredEmbedding>(store);
|
|
174
|
+
for (const ref of members.keys()) {
|
|
175
|
+
allMembers.push({ ref, embedding: members.get(ref)! });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (!allMembers.length) return;
|
|
179
|
+
|
|
180
|
+
const clusterCount = Math.max(1, Math.round(allMembers.length / config.cellTargetSize));
|
|
181
|
+
const clusters = clusterMembers(allMembers, clusterCount, config);
|
|
182
|
+
|
|
183
|
+
const newCellIds = new Set<string>();
|
|
184
|
+
const centroidWrites: { key: string; value: StoredEmbedding | undefined }[] = [];
|
|
185
|
+
for (const cluster of clusters) {
|
|
186
|
+
const cellId = hashEmbedding(cluster.centroid);
|
|
187
|
+
newCellIds.add(cellId);
|
|
188
|
+
centroidWrites.push({ key: cellId, value: cluster.centroid });
|
|
189
|
+
const memberWrites = cluster.members.map(member => ({ key: member.ref, value: member.embedding }));
|
|
190
|
+
transactionMutate(cellStore(database, cellId), memberWrites);
|
|
191
|
+
}
|
|
192
|
+
for (const oldCellId of oldCellIds) {
|
|
193
|
+
if (newCellIds.has(oldCellId)) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
centroidWrites.push({ key: oldCellId, value: undefined });
|
|
197
|
+
transactionDelete(cellStore(database, oldCellId));
|
|
198
|
+
}
|
|
199
|
+
transactionMutate(centroidStore(database), centroidWrites);
|
|
200
|
+
database.writeData(root => root.count, allMembers.length);
|
|
201
|
+
database.writeData(root => root.steps[STEP_IVF], true);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function searchEmbeddings(
|
|
205
|
+
database: Database<IvfEmbeddingRoot>,
|
|
206
|
+
query: StoredEmbedding,
|
|
207
|
+
options: { probeBudget: number; resultCount: number },
|
|
208
|
+
): SearchHit[] | undefined {
|
|
209
|
+
const config = database.readData(root => root.config);
|
|
210
|
+
if (!config) return undefined;
|
|
211
|
+
const steps = database.readData(root => root.steps);
|
|
212
|
+
if (!steps) return undefined;
|
|
213
|
+
|
|
214
|
+
const hits: SearchHit[] = [];
|
|
215
|
+
if (!steps[STEP_IVF]) {
|
|
216
|
+
const flat = transactionRead(flatStore(database));
|
|
217
|
+
if (!flat) return undefined;
|
|
218
|
+
for (const ref of flat.keys()) {
|
|
219
|
+
hits.push({ ref, closeness: getCloseness(query, flat.get(ref)!) });
|
|
220
|
+
}
|
|
221
|
+
hits.sort((left, right) => right.closeness - left.closeness);
|
|
222
|
+
return hits.slice(0, options.resultCount);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const centroids = transactionRead(centroidStore(database));
|
|
226
|
+
if (!centroids) return undefined;
|
|
227
|
+
if (!centroids.size) return [];
|
|
228
|
+
const probeCellCount = Math.max(1, Math.ceil(options.probeBudget / config.cellTargetSize));
|
|
229
|
+
const probeCellIds = rankCellsByCloseness(query, centroids).slice(0, probeCellCount);
|
|
230
|
+
const stores = database.readData(root => probeCellIds.map(cellId => root.cells[cellId]));
|
|
231
|
+
if (!stores) return undefined;
|
|
232
|
+
for (const store of stores) {
|
|
233
|
+
const members = replayTransactionStore<StoredEmbedding>(store);
|
|
234
|
+
for (const ref of members.keys()) {
|
|
235
|
+
hits.push({ ref, closeness: getCloseness(query, members.get(ref)!) });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
hits.sort((left, right) => right.closeness - left.closeness);
|
|
239
|
+
return hits.slice(0, options.resultCount);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function insertEmbeddings(
|
|
243
|
+
database: Database<IvfEmbeddingRoot>,
|
|
244
|
+
items: EmbeddingInput[],
|
|
245
|
+
) {
|
|
246
|
+
if (!items.length) return;
|
|
247
|
+
const config = database.readData(root => root.config);
|
|
248
|
+
const steps = database.readData(root => root.steps);
|
|
249
|
+
let count = database.readData(root => root.count);
|
|
250
|
+
if (!config) return;
|
|
251
|
+
if (!steps) return;
|
|
252
|
+
if (count === undefined) return;
|
|
253
|
+
const newCount = count + items.length;
|
|
254
|
+
|
|
255
|
+
if (!steps[STEP_IVF]) {
|
|
256
|
+
const flatWrites = items.map(item => ({ key: item.ref, value: item.embedding }));
|
|
257
|
+
transactionMutate(flatStore(database), flatWrites);
|
|
258
|
+
database.writeData(root => root.count, newCount);
|
|
259
|
+
if (newCount > FLAT_LIMIT) {
|
|
260
|
+
rebuildStructure(database);
|
|
261
|
+
}
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const centroids = transactionRead(centroidStore(database));
|
|
266
|
+
if (!centroids) return undefined;
|
|
267
|
+
const itemsByCell = new Map<string, EmbeddingInput[]>();
|
|
268
|
+
for (const item of items) {
|
|
269
|
+
const cellId = rankCellsByCloseness(item.embedding, centroids)[0];
|
|
270
|
+
let group = itemsByCell.get(cellId);
|
|
271
|
+
if (!group) {
|
|
272
|
+
group = [];
|
|
273
|
+
itemsByCell.set(cellId, group);
|
|
274
|
+
}
|
|
275
|
+
group.push(item);
|
|
276
|
+
}
|
|
277
|
+
for (const cellId of itemsByCell.keys()) {
|
|
278
|
+
const group = itemsByCell.get(cellId)!;
|
|
279
|
+
const memberWrites = group.map(item => ({ key: item.ref, value: item.embedding }));
|
|
280
|
+
transactionMutate(cellStore(database, cellId), memberWrites);
|
|
281
|
+
}
|
|
282
|
+
database.writeData(root => root.count, newCount);
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
const shouldRegenerate = (database: Database<IvfEmbeddingRoot>) => {
|
|
286
|
+
let steps = database.readData(root => root.steps);
|
|
287
|
+
if (!steps) return;
|
|
288
|
+
for (const threshold of REGENERATE_AT) {
|
|
289
|
+
const stepName = "regen" + threshold;
|
|
290
|
+
if (!steps[stepName] && newCount > threshold) {
|
|
291
|
+
database.writeData(root => root.steps[stepName], true);
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const averageFill = newCount / Math.max(1, centroids.size) / config.cellTargetSize;
|
|
296
|
+
if (Math.random() < rebalanceProbability(averageFill)) {
|
|
297
|
+
rebuildStructure(database);
|
|
298
|
+
}
|
|
299
|
+
return true;
|
|
300
|
+
};
|
|
301
|
+
if (shouldRegenerate(database)) {
|
|
302
|
+
rebuildStructure(database);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function removeEmbeddings(
|
|
307
|
+
database: Database<IvfEmbeddingRoot>,
|
|
308
|
+
items: EmbeddingInput[],
|
|
309
|
+
) {
|
|
310
|
+
if (!items.length) return;
|
|
311
|
+
const steps = database.readData(root => root.steps);
|
|
312
|
+
const count = database.readData(root => root.count);
|
|
313
|
+
if (!steps) return;
|
|
314
|
+
if (count === undefined) return;
|
|
315
|
+
|
|
316
|
+
if (!steps[STEP_IVF]) {
|
|
317
|
+
const flatDeletes = items.map(item => ({ key: item.ref, value: undefined }));
|
|
318
|
+
transactionMutate(flatStore(database), flatDeletes);
|
|
319
|
+
database.writeData(root => root.count, Math.max(0, count - items.length));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const centroids = transactionRead(centroidStore(database));
|
|
324
|
+
if (!centroids) return;
|
|
325
|
+
if (!centroids.size) return;
|
|
326
|
+
|
|
327
|
+
const candidatesByItem: { ref: string; cellIds: string[] }[] = [];
|
|
328
|
+
const candidateSet = new Set<string>();
|
|
329
|
+
for (const item of items) {
|
|
330
|
+
const cellIds = rankCellsByCloseness(item.embedding, centroids).slice(0, 1 + DELETE_FALLBACK_CELLS);
|
|
331
|
+
candidatesByItem.push({ ref: item.ref, cellIds });
|
|
332
|
+
for (const cellId of cellIds) {
|
|
333
|
+
candidateSet.add(cellId);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const candidateCellIds = Array.from(candidateSet);
|
|
338
|
+
const stores = database.readData(root => candidateCellIds.map(cellId => root.cells[cellId]));
|
|
339
|
+
if (!stores) return;
|
|
340
|
+
const membersByCell = new Map<string, Map<string, StoredEmbedding>>();
|
|
341
|
+
for (let index = 0; index < candidateCellIds.length; index++) {
|
|
342
|
+
membersByCell.set(candidateCellIds[index], replayTransactionStore(stores[index]));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const deletesByCell = new Map<string, string[]>();
|
|
346
|
+
for (const candidate of candidatesByItem) {
|
|
347
|
+
for (const cellId of candidate.cellIds) {
|
|
348
|
+
const members = membersByCell.get(cellId);
|
|
349
|
+
if (!members || !members.has(candidate.ref)) {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
let refs = deletesByCell.get(cellId);
|
|
353
|
+
if (!refs) {
|
|
354
|
+
refs = [];
|
|
355
|
+
deletesByCell.set(cellId, refs);
|
|
356
|
+
}
|
|
357
|
+
refs.push(candidate.ref);
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
let deletedCount = 0;
|
|
363
|
+
for (const cellId of deletesByCell.keys()) {
|
|
364
|
+
const refs = deletesByCell.get(cellId)!;
|
|
365
|
+
deletedCount += refs.length;
|
|
366
|
+
const memberDeletes = refs.map(ref => ({ key: ref, value: undefined }));
|
|
367
|
+
transactionMutate(cellStore(database, cellId), memberDeletes);
|
|
368
|
+
}
|
|
369
|
+
if (deletedCount) {
|
|
370
|
+
database.writeData(root => root.count, Math.max(0, count - deletedCount));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { Database } from "./Database";
|
|
2
|
-
|
|
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
|
|
6
|
-
export declare function
|
|
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: {
|
|
7
10
|
key: string;
|
|
8
11
|
value: Value | undefined;
|
|
9
|
-
}[], compactAfterFiles?: number):
|
|
12
|
+
}[], compactAfterFiles?: number): void;
|
|
13
|
+
export declare function transactionDelete<Value>(database: Database<TransactionSetStore<Value>>): void;
|
|
14
|
+
export {};
|