@delali/narsil 0.1.1 → 0.1.3
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/README.md +39 -0
- package/dist/adapters/filesystem.mjs +1 -1
- package/dist/adapters/indexeddb.mjs +1 -1
- package/dist/{chunk-JKIBUPAY.mjs → chunk-7GKPWGCN.mjs} +2 -1
- package/dist/{chunk-5HXMVMSM.mjs → chunk-CHNP2QCL.mjs} +1177 -181
- package/dist/chunk-GVN2W4VA.mjs +570 -0
- package/dist/chunk-HRABJLPB.mjs +64 -0
- package/dist/direct-executor-FF5KPZM4.mjs +4 -0
- package/dist/index.browser.mjs +7114 -0
- package/dist/index.d.ts +46 -3
- package/dist/index.mjs +1371 -415
- package/dist/vector/hnsw-build-worker.d.ts +37 -0
- package/dist/vector/hnsw-build-worker.mjs +81 -0
- package/dist/workers/entry.mjs +3 -32
- package/package.json +31 -11
- package/dist/chunk-N37VMC62.mjs +0 -18
- package/dist/direct-executor-MQD7JRD4.mjs +0 -3
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
// src/vector/simd-wasm-binary.ts
|
|
2
|
+
var SIMD_DISTANCE_WASM_BASE64 = "AGFzbQEAAAABDgJgA39/fwF9YAJ/fwF9AwUEAAEAAAUDAQABB1YFBm1lbW9yeQIAC2RvdF9wcm9kdWN0AAAJbWFnbml0dWRlAAEac3F1YXJlZF9ldWNsaWRlYW5fZGlzdGFuY2UAAhJldWNsaWRlYW5fZGlzdGFuY2UAAwqqBASuAQQBfwF7AX0BfyACQXxxIQb9DAAAAAAAAAAAAAAAAAAAAAAhBEEAIQMCQANAIAMgBk8NASAEIAAgA0ECdGr9AAQAIAEgA0ECdGr9AAQA/eYB/eQBIQQgA0EEaiEDDAALCyAE/R8AIAT9HwGSIAT9HwIgBP0fA5KSIQUCQANAIAMgAk8NASAFIAAgA0ECdGoqAgAgASADQQJ0aioCAJSSIQUgA0EBaiEDDAALCyAFC6kBBAF/AnsBfQF/IAFBfHEhBv0MAAAAAAAAAAAAAAAAAAAAACEDQQAhAgJAA0AgAiAGTw0BIAAgAkECdGr9AAQAIQQgAyAEIAT95gH95AEhAyACQQRqIQIMAAsLIAP9HwAgA/0fAZIgA/0fAiAD/R8DkpIhBQJAA0AgAiABTw0BIAUgACACQQJ0aioCACAAIAJBAnRqKgIAlJIhBSACQQFqIQIMAAsLIAWRC8ABBQF/AnsBfQF/AX0gAkF8cSEH/QwAAAAAAAAAAAAAAAAAAAAAIQRBACEDAkADQCADIAdPDQEgACADQQJ0av0ABAAgASADQQJ0av0ABAD95QEhBSAEIAUgBf3mAf3kASEEIANBBGohAwwACwsgBP0fACAE/R8BkiAE/R8CIAT9HwOSkiEGAkADQCADIAJPDQEgACADQQJ0aioCACABIANBAnRqKgIAkyEIIAYgCCAIlJIhBiADQQFqIQMMAAsLIAYLCwAgACABIAIQApEL";
|
|
3
|
+
|
|
4
|
+
// src/vector/simd.ts
|
|
5
|
+
var wasmExports = null;
|
|
6
|
+
var f32View = null;
|
|
7
|
+
function decodeBase64(base64) {
|
|
8
|
+
if (typeof Buffer !== "undefined") {
|
|
9
|
+
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
10
|
+
}
|
|
11
|
+
const binary = atob(base64);
|
|
12
|
+
const bytes = new Uint8Array(binary.length);
|
|
13
|
+
for (let i = 0; i < binary.length; i++) {
|
|
14
|
+
bytes[i] = binary.charCodeAt(i);
|
|
15
|
+
}
|
|
16
|
+
return bytes;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const bytes = decodeBase64(SIMD_DISTANCE_WASM_BASE64);
|
|
20
|
+
const wasmBuffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
21
|
+
if (typeof WebAssembly !== "undefined" && typeof WebAssembly.validate === "function" && WebAssembly.validate(wasmBuffer)) {
|
|
22
|
+
const module = new WebAssembly.Module(wasmBuffer);
|
|
23
|
+
const instance = new WebAssembly.Instance(module);
|
|
24
|
+
wasmExports = instance.exports;
|
|
25
|
+
f32View = new Float32Array(wasmExports.memory.buffer);
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
wasmExports = null;
|
|
29
|
+
f32View = null;
|
|
30
|
+
}
|
|
31
|
+
var OFFSET_B_BYTES = 16384;
|
|
32
|
+
function ensureMemory(dim) {
|
|
33
|
+
if (!wasmExports || !f32View) return;
|
|
34
|
+
const needed = OFFSET_B_BYTES + dim * 4;
|
|
35
|
+
const currentSize = wasmExports.memory.buffer.byteLength;
|
|
36
|
+
if (needed > currentSize) {
|
|
37
|
+
const pagesNeeded = Math.ceil(needed / 65536);
|
|
38
|
+
const currentPages = currentSize / 65536;
|
|
39
|
+
wasmExports.memory.grow(pagesNeeded - currentPages);
|
|
40
|
+
f32View = new Float32Array(wasmExports.memory.buffer);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function getF32View() {
|
|
44
|
+
if (!f32View) {
|
|
45
|
+
throw new Error("SIMD memory is not initialized");
|
|
46
|
+
}
|
|
47
|
+
return f32View;
|
|
48
|
+
}
|
|
49
|
+
function copyOneVector(a) {
|
|
50
|
+
getF32View().set(a, 0);
|
|
51
|
+
}
|
|
52
|
+
function copyTwoVectors(a, b) {
|
|
53
|
+
const view = getF32View();
|
|
54
|
+
view.set(a, 0);
|
|
55
|
+
view.set(b, OFFSET_B_BYTES / 4);
|
|
56
|
+
}
|
|
57
|
+
function simdDotProduct(a, b) {
|
|
58
|
+
if (!wasmExports || !f32View) return null;
|
|
59
|
+
ensureMemory(a.length);
|
|
60
|
+
copyTwoVectors(a, b);
|
|
61
|
+
return wasmExports.dot_product(0, OFFSET_B_BYTES, a.length);
|
|
62
|
+
}
|
|
63
|
+
function simdMagnitude(a) {
|
|
64
|
+
if (!wasmExports || !f32View) return null;
|
|
65
|
+
ensureMemory(a.length);
|
|
66
|
+
copyOneVector(a);
|
|
67
|
+
return wasmExports.magnitude(0, a.length);
|
|
68
|
+
}
|
|
69
|
+
function simdEuclideanDistance(a, b) {
|
|
70
|
+
if (!wasmExports || !f32View) return null;
|
|
71
|
+
ensureMemory(a.length);
|
|
72
|
+
copyTwoVectors(a, b);
|
|
73
|
+
return wasmExports.euclidean_distance(0, OFFSET_B_BYTES, a.length);
|
|
74
|
+
}
|
|
75
|
+
function isSimdAvailable() {
|
|
76
|
+
return wasmExports !== null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/core/heap.ts
|
|
80
|
+
function createMinHeap(compare) {
|
|
81
|
+
return createHeap(compare);
|
|
82
|
+
}
|
|
83
|
+
function createHeap(compare) {
|
|
84
|
+
const items = [];
|
|
85
|
+
function siftUp(idx) {
|
|
86
|
+
while (idx > 0) {
|
|
87
|
+
const parent = idx - 1 >> 1;
|
|
88
|
+
if (compare(items[idx], items[parent]) >= 0) break;
|
|
89
|
+
const tmp = items[idx];
|
|
90
|
+
items[idx] = items[parent];
|
|
91
|
+
items[parent] = tmp;
|
|
92
|
+
idx = parent;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function siftDown(idx) {
|
|
96
|
+
const len = items.length;
|
|
97
|
+
while (true) {
|
|
98
|
+
let smallest = idx;
|
|
99
|
+
const left = 2 * idx + 1;
|
|
100
|
+
const right = 2 * idx + 2;
|
|
101
|
+
if (left < len && compare(items[left], items[smallest]) < 0) smallest = left;
|
|
102
|
+
if (right < len && compare(items[right], items[smallest]) < 0) smallest = right;
|
|
103
|
+
if (smallest === idx) break;
|
|
104
|
+
const tmp = items[idx];
|
|
105
|
+
items[idx] = items[smallest];
|
|
106
|
+
items[smallest] = tmp;
|
|
107
|
+
idx = smallest;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
push(item) {
|
|
112
|
+
items.push(item);
|
|
113
|
+
siftUp(items.length - 1);
|
|
114
|
+
},
|
|
115
|
+
pop() {
|
|
116
|
+
if (items.length === 0) return void 0;
|
|
117
|
+
const top = items[0];
|
|
118
|
+
const last = items.pop();
|
|
119
|
+
if (items.length > 0) {
|
|
120
|
+
items[0] = last;
|
|
121
|
+
siftDown(0);
|
|
122
|
+
}
|
|
123
|
+
return top;
|
|
124
|
+
},
|
|
125
|
+
peek() {
|
|
126
|
+
return items[0];
|
|
127
|
+
},
|
|
128
|
+
get size() {
|
|
129
|
+
return items.length;
|
|
130
|
+
},
|
|
131
|
+
toSortedArray() {
|
|
132
|
+
return [...items].sort(compare);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/vector/similarity.ts
|
|
138
|
+
function magnitude(v) {
|
|
139
|
+
const result = simdMagnitude(v);
|
|
140
|
+
if (result !== null) return result;
|
|
141
|
+
let sum = 0;
|
|
142
|
+
for (let i = 0; i < v.length; i++) {
|
|
143
|
+
sum += v[i] * v[i];
|
|
144
|
+
}
|
|
145
|
+
return Math.sqrt(sum);
|
|
146
|
+
}
|
|
147
|
+
function cosineSimilarityWithMagnitudes(a, b, magA, magB) {
|
|
148
|
+
if (magA === 0 || magB === 0) return 0;
|
|
149
|
+
return dotProduct(a, b) / (magA * magB);
|
|
150
|
+
}
|
|
151
|
+
function dotProduct(a, b) {
|
|
152
|
+
const result = simdDotProduct(a, b);
|
|
153
|
+
if (result !== null) return result;
|
|
154
|
+
let sum = 0;
|
|
155
|
+
for (let i = 0; i < a.length; i++) {
|
|
156
|
+
sum += a[i] * b[i];
|
|
157
|
+
}
|
|
158
|
+
return sum;
|
|
159
|
+
}
|
|
160
|
+
function euclideanDistance(a, b) {
|
|
161
|
+
const result = simdEuclideanDistance(a, b);
|
|
162
|
+
if (result !== null) return result;
|
|
163
|
+
let sum = 0;
|
|
164
|
+
for (let i = 0; i < a.length; i++) {
|
|
165
|
+
const diff = a[i] - b[i];
|
|
166
|
+
sum += diff * diff;
|
|
167
|
+
}
|
|
168
|
+
return Math.sqrt(sum);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/vector/hnsw.ts
|
|
172
|
+
var MAX_LAYER_CAP = 32;
|
|
173
|
+
function toDistance(a, b, magA, magB, metric) {
|
|
174
|
+
switch (metric) {
|
|
175
|
+
case "cosine":
|
|
176
|
+
return 1 - cosineSimilarityWithMagnitudes(a, b, magA, magB);
|
|
177
|
+
case "dotProduct":
|
|
178
|
+
return -dotProduct(a, b);
|
|
179
|
+
case "euclidean":
|
|
180
|
+
return euclideanDistance(a, b);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function toScore(distance, metric) {
|
|
184
|
+
switch (metric) {
|
|
185
|
+
case "cosine":
|
|
186
|
+
return 1 - distance;
|
|
187
|
+
case "dotProduct":
|
|
188
|
+
return -distance;
|
|
189
|
+
case "euclidean":
|
|
190
|
+
return 1 / (1 + distance);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
var distanceAsc = (a, b) => a.distance - b.distance;
|
|
194
|
+
var distanceDesc = (a, b) => b.distance - a.distance;
|
|
195
|
+
function createHNSWIndex(dimension, config) {
|
|
196
|
+
const M = config?.m ?? 16;
|
|
197
|
+
const Mmax0 = M * 2;
|
|
198
|
+
const efCons = config?.efConstruction ?? 200;
|
|
199
|
+
const buildMetric = config?.metric ?? "cosine";
|
|
200
|
+
const mL = 1 / Math.log(M);
|
|
201
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
202
|
+
let entryPointId = null;
|
|
203
|
+
let topLayer = -1;
|
|
204
|
+
function randomLevel() {
|
|
205
|
+
let u = Math.random();
|
|
206
|
+
if (u === 0) u = Number.MIN_VALUE;
|
|
207
|
+
return Math.min(Math.floor(-Math.log(u) * mL), MAX_LAYER_CAP);
|
|
208
|
+
}
|
|
209
|
+
function nodeDistance(a, b, metric) {
|
|
210
|
+
return toDistance(a.vector, b.vector, a.mag, b.mag, metric);
|
|
211
|
+
}
|
|
212
|
+
function queryDistance(qVec, qMag, node, metric) {
|
|
213
|
+
return toDistance(qVec, node.vector, qMag, node.mag, metric);
|
|
214
|
+
}
|
|
215
|
+
function maxConns(layer) {
|
|
216
|
+
return layer === 0 ? Mmax0 : M;
|
|
217
|
+
}
|
|
218
|
+
function searchLayer(qVec, qMag, eps, ef, layer, metric) {
|
|
219
|
+
const visited = /* @__PURE__ */ new Set();
|
|
220
|
+
const candidates = createMinHeap(distanceAsc);
|
|
221
|
+
const results = createMinHeap(distanceDesc);
|
|
222
|
+
let furthestDist = Number.POSITIVE_INFINITY;
|
|
223
|
+
for (const epId of eps) {
|
|
224
|
+
if (visited.has(epId)) continue;
|
|
225
|
+
visited.add(epId);
|
|
226
|
+
const node = nodes.get(epId);
|
|
227
|
+
if (!node) continue;
|
|
228
|
+
const dist = queryDistance(qVec, qMag, node, metric);
|
|
229
|
+
const pair = { docId: epId, distance: dist };
|
|
230
|
+
candidates.push(pair);
|
|
231
|
+
results.push(pair);
|
|
232
|
+
if (results.size > ef) {
|
|
233
|
+
results.pop();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const topResult = results.peek();
|
|
237
|
+
if (topResult) furthestDist = topResult.distance;
|
|
238
|
+
while (candidates.size > 0) {
|
|
239
|
+
const nearest = candidates.pop();
|
|
240
|
+
if (!nearest) break;
|
|
241
|
+
if (nearest.distance > furthestDist) break;
|
|
242
|
+
const node = nodes.get(nearest.docId);
|
|
243
|
+
if (!node || layer >= node.connections.length) continue;
|
|
244
|
+
for (const neighborId of node.connections[layer]) {
|
|
245
|
+
if (visited.has(neighborId)) continue;
|
|
246
|
+
visited.add(neighborId);
|
|
247
|
+
const neighborNode = nodes.get(neighborId);
|
|
248
|
+
if (!neighborNode) continue;
|
|
249
|
+
const dist = queryDistance(qVec, qMag, neighborNode, metric);
|
|
250
|
+
if (dist < furthestDist || results.size < ef) {
|
|
251
|
+
const pair = { docId: neighborId, distance: dist };
|
|
252
|
+
candidates.push(pair);
|
|
253
|
+
results.push(pair);
|
|
254
|
+
if (results.size > ef) {
|
|
255
|
+
results.pop();
|
|
256
|
+
}
|
|
257
|
+
const newTop = results.peek();
|
|
258
|
+
if (newTop) furthestDist = newTop.distance;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return results.toSortedArray();
|
|
263
|
+
}
|
|
264
|
+
function selectNeighborsHeuristic(targetVec, targetMag, candidates, maxConnections, layer, metric, extendCandidates, keepPruned) {
|
|
265
|
+
const working = [...candidates].sort((a, b) => a.distance - b.distance);
|
|
266
|
+
const selected = [];
|
|
267
|
+
const discarded = [];
|
|
268
|
+
for (const candidate of working) {
|
|
269
|
+
if (selected.length >= maxConnections) break;
|
|
270
|
+
let accepted = true;
|
|
271
|
+
for (const sel of selected) {
|
|
272
|
+
const selNode = nodes.get(sel.docId);
|
|
273
|
+
const candNode = nodes.get(candidate.docId);
|
|
274
|
+
if (!selNode || !candNode) continue;
|
|
275
|
+
const distBetween = nodeDistance(candNode, selNode, metric);
|
|
276
|
+
if (candidate.distance >= distBetween) {
|
|
277
|
+
accepted = false;
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (accepted) {
|
|
282
|
+
selected.push(candidate);
|
|
283
|
+
} else {
|
|
284
|
+
discarded.push(candidate);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
{
|
|
288
|
+
for (const disc of discarded) {
|
|
289
|
+
if (selected.length >= maxConnections) break;
|
|
290
|
+
selected.push(disc);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return selected;
|
|
294
|
+
}
|
|
295
|
+
function pruneConnections(node, layer, metric) {
|
|
296
|
+
const mc = maxConns(layer);
|
|
297
|
+
if (node.connections[layer].size <= mc) return;
|
|
298
|
+
const conns = [];
|
|
299
|
+
for (const connId of node.connections[layer]) {
|
|
300
|
+
const connNode = nodes.get(connId);
|
|
301
|
+
if (!connNode) continue;
|
|
302
|
+
conns.push({
|
|
303
|
+
docId: connId,
|
|
304
|
+
distance: nodeDistance(node, connNode, metric)
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
const pruned = selectNeighborsHeuristic(node.vector, node.mag, conns, mc, layer, metric);
|
|
308
|
+
node.connections[layer] = new Set(pruned.map((p) => p.docId));
|
|
309
|
+
}
|
|
310
|
+
function insert(docId, vector) {
|
|
311
|
+
if (vector.length !== dimension) {
|
|
312
|
+
throw new Error(`Vector dimension mismatch: expected ${dimension}, got ${vector.length}`);
|
|
313
|
+
}
|
|
314
|
+
if (nodes.has(docId)) {
|
|
315
|
+
remove(docId);
|
|
316
|
+
}
|
|
317
|
+
const mag = magnitude(vector);
|
|
318
|
+
const l = randomLevel();
|
|
319
|
+
const node = {
|
|
320
|
+
docId,
|
|
321
|
+
vector,
|
|
322
|
+
mag,
|
|
323
|
+
maxLayer: l,
|
|
324
|
+
connections: Array.from({ length: l + 1 }, () => /* @__PURE__ */ new Set())
|
|
325
|
+
};
|
|
326
|
+
nodes.set(docId, node);
|
|
327
|
+
if (entryPointId === null) {
|
|
328
|
+
entryPointId = docId;
|
|
329
|
+
topLayer = l;
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const metric = buildMetric;
|
|
333
|
+
let currentEPs = [entryPointId];
|
|
334
|
+
for (let layer = topLayer; layer > l; layer--) {
|
|
335
|
+
const nearest = searchLayer(vector, mag, currentEPs, 1, layer, metric);
|
|
336
|
+
if (nearest.length > 0) {
|
|
337
|
+
currentEPs = [nearest[0].docId];
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
for (let layer = Math.min(l, topLayer); layer >= 0; layer--) {
|
|
341
|
+
const mc = maxConns(layer);
|
|
342
|
+
const nearest = searchLayer(vector, mag, currentEPs, efCons, layer, metric);
|
|
343
|
+
const neighbors = selectNeighborsHeuristic(vector, mag, nearest, mc, layer, metric);
|
|
344
|
+
for (const neighbor of neighbors) {
|
|
345
|
+
node.connections[layer].add(neighbor.docId);
|
|
346
|
+
const neighborNode = nodes.get(neighbor.docId);
|
|
347
|
+
if (neighborNode && layer < neighborNode.connections.length) {
|
|
348
|
+
neighborNode.connections[layer].add(docId);
|
|
349
|
+
pruneConnections(neighborNode, layer, metric);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (nearest.length > 0) {
|
|
353
|
+
currentEPs = nearest.map((n) => n.docId);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (l > topLayer) {
|
|
357
|
+
entryPointId = docId;
|
|
358
|
+
topLayer = l;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function remove(docId) {
|
|
362
|
+
const node = nodes.get(docId);
|
|
363
|
+
if (!node) return;
|
|
364
|
+
const metric = buildMetric;
|
|
365
|
+
for (let layer = 0; layer <= node.maxLayer; layer++) {
|
|
366
|
+
if (layer >= node.connections.length) continue;
|
|
367
|
+
const formerNeighborIds = new Set(node.connections[layer]);
|
|
368
|
+
for (const neighborId of formerNeighborIds) {
|
|
369
|
+
const neighborNode = nodes.get(neighborId);
|
|
370
|
+
if (neighborNode && layer < neighborNode.connections.length) {
|
|
371
|
+
neighborNode.connections[layer].delete(docId);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
for (const neighborId of formerNeighborIds) {
|
|
375
|
+
const neighborNode = nodes.get(neighborId);
|
|
376
|
+
if (!neighborNode || layer >= neighborNode.connections.length) continue;
|
|
377
|
+
const mc = maxConns(layer);
|
|
378
|
+
if (neighborNode.connections[layer].size >= mc) continue;
|
|
379
|
+
const candidateIds = new Set(neighborNode.connections[layer]);
|
|
380
|
+
for (const otherId of formerNeighborIds) {
|
|
381
|
+
if (otherId !== neighborId && otherId !== docId) {
|
|
382
|
+
candidateIds.add(otherId);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const candidates = [];
|
|
386
|
+
for (const candId of candidateIds) {
|
|
387
|
+
const candNode = nodes.get(candId);
|
|
388
|
+
if (!candNode) continue;
|
|
389
|
+
candidates.push({
|
|
390
|
+
docId: candId,
|
|
391
|
+
distance: nodeDistance(neighborNode, candNode, metric)
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
const selected = selectNeighborsHeuristic(
|
|
395
|
+
neighborNode.vector,
|
|
396
|
+
neighborNode.mag,
|
|
397
|
+
candidates,
|
|
398
|
+
mc,
|
|
399
|
+
layer,
|
|
400
|
+
metric);
|
|
401
|
+
const newConns = new Set(selected.map((s) => s.docId));
|
|
402
|
+
neighborNode.connections[layer] = newConns;
|
|
403
|
+
for (const newConnId of newConns) {
|
|
404
|
+
const newConnNode = nodes.get(newConnId);
|
|
405
|
+
if (newConnNode && layer < newConnNode.connections.length) {
|
|
406
|
+
newConnNode.connections[layer].add(neighborId);
|
|
407
|
+
pruneConnections(newConnNode, layer, metric);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
nodes.delete(docId);
|
|
413
|
+
if (entryPointId === docId) {
|
|
414
|
+
if (nodes.size === 0) {
|
|
415
|
+
entryPointId = null;
|
|
416
|
+
topLayer = -1;
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
let newEntryId = null;
|
|
420
|
+
let newTopLayer = -1;
|
|
421
|
+
for (const [nId, n] of nodes) {
|
|
422
|
+
if (n.maxLayer > newTopLayer) {
|
|
423
|
+
newTopLayer = n.maxLayer;
|
|
424
|
+
newEntryId = nId;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
entryPointId = newEntryId;
|
|
428
|
+
topLayer = newTopLayer;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function search(query, k, searchMetric, minSimilarity, filterDocIds, efSearch) {
|
|
432
|
+
if (query.length !== dimension) {
|
|
433
|
+
throw new Error(`Query dimension mismatch: expected ${dimension}, got ${query.length}`);
|
|
434
|
+
}
|
|
435
|
+
if (entryPointId === null || nodes.size === 0) {
|
|
436
|
+
return [];
|
|
437
|
+
}
|
|
438
|
+
let ef = Math.max(efSearch ?? 50, k);
|
|
439
|
+
if (filterDocIds && filterDocIds.size < nodes.size) {
|
|
440
|
+
const selectivity = filterDocIds.size / nodes.size;
|
|
441
|
+
ef = Math.max(ef, Math.ceil(k / Math.max(selectivity, 0.01)));
|
|
442
|
+
ef = Math.min(ef, nodes.size);
|
|
443
|
+
}
|
|
444
|
+
const qMag = magnitude(query);
|
|
445
|
+
let currentEPs = [entryPointId];
|
|
446
|
+
for (let layer = topLayer; layer >= 1; layer--) {
|
|
447
|
+
const nearest = searchLayer(query, qMag, currentEPs, 1, layer, searchMetric);
|
|
448
|
+
if (nearest.length > 0) {
|
|
449
|
+
currentEPs = [nearest[0].docId];
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
const candidates = searchLayer(query, qMag, currentEPs, ef, 0, searchMetric);
|
|
453
|
+
const results = [];
|
|
454
|
+
for (const cand of candidates) {
|
|
455
|
+
if (filterDocIds && !filterDocIds.has(cand.docId)) continue;
|
|
456
|
+
const score = toScore(cand.distance, searchMetric);
|
|
457
|
+
if (score < minSimilarity) continue;
|
|
458
|
+
results.push({
|
|
459
|
+
docId: cand.docId,
|
|
460
|
+
score,
|
|
461
|
+
termFrequencies: {},
|
|
462
|
+
fieldLengths: {},
|
|
463
|
+
idf: {}
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
results.sort((a, b) => b.score - a.score);
|
|
467
|
+
return results.slice(0, k);
|
|
468
|
+
}
|
|
469
|
+
function serialize() {
|
|
470
|
+
const nodeArray = [];
|
|
471
|
+
for (const [docId, node] of nodes) {
|
|
472
|
+
const layerConns = [];
|
|
473
|
+
for (let layer = 0; layer < node.connections.length; layer++) {
|
|
474
|
+
if (node.connections[layer].size > 0) {
|
|
475
|
+
layerConns.push([layer, Array.from(node.connections[layer])]);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
nodeArray.push([docId, node.maxLayer, layerConns]);
|
|
479
|
+
}
|
|
480
|
+
return {
|
|
481
|
+
entryPoint: entryPointId ?? "",
|
|
482
|
+
maxLayer: topLayer,
|
|
483
|
+
m: M,
|
|
484
|
+
efConstruction: efCons,
|
|
485
|
+
metric: buildMetric,
|
|
486
|
+
nodes: nodeArray
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
function deserialize(data, vectors) {
|
|
490
|
+
nodes.clear();
|
|
491
|
+
for (const [docId, maxLayer, layerConns] of data.nodes) {
|
|
492
|
+
const vecData = vectors.get(docId);
|
|
493
|
+
if (!vecData) continue;
|
|
494
|
+
const connections = Array.from({ length: maxLayer + 1 }, () => /* @__PURE__ */ new Set());
|
|
495
|
+
for (const [layer, neighbors] of layerConns) {
|
|
496
|
+
if (layer < connections.length) {
|
|
497
|
+
connections[layer] = new Set(neighbors);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
nodes.set(docId, {
|
|
501
|
+
docId,
|
|
502
|
+
vector: vecData.vector,
|
|
503
|
+
mag: vecData.mag,
|
|
504
|
+
maxLayer,
|
|
505
|
+
connections
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
if (data.entryPoint && nodes.has(data.entryPoint)) {
|
|
509
|
+
entryPointId = data.entryPoint;
|
|
510
|
+
topLayer = data.maxLayer;
|
|
511
|
+
} else if (nodes.size > 0) {
|
|
512
|
+
let bestId = null;
|
|
513
|
+
let bestLayer = -1;
|
|
514
|
+
for (const [nId, n] of nodes) {
|
|
515
|
+
if (n.maxLayer > bestLayer) {
|
|
516
|
+
bestLayer = n.maxLayer;
|
|
517
|
+
bestId = nId;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
entryPointId = bestId;
|
|
521
|
+
topLayer = bestLayer;
|
|
522
|
+
} else {
|
|
523
|
+
entryPointId = null;
|
|
524
|
+
topLayer = -1;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
function clear() {
|
|
528
|
+
nodes.clear();
|
|
529
|
+
entryPointId = null;
|
|
530
|
+
topLayer = -1;
|
|
531
|
+
}
|
|
532
|
+
function* entriesIterator() {
|
|
533
|
+
for (const [docId, node] of nodes) {
|
|
534
|
+
yield [docId, { docId, vector: node.vector, magnitude: node.mag }];
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return {
|
|
538
|
+
get dimension() {
|
|
539
|
+
return dimension;
|
|
540
|
+
},
|
|
541
|
+
get size() {
|
|
542
|
+
return nodes.size;
|
|
543
|
+
},
|
|
544
|
+
get entryPointId() {
|
|
545
|
+
return entryPointId;
|
|
546
|
+
},
|
|
547
|
+
get topLayer() {
|
|
548
|
+
return topLayer;
|
|
549
|
+
},
|
|
550
|
+
get m() {
|
|
551
|
+
return M;
|
|
552
|
+
},
|
|
553
|
+
get efConstruction() {
|
|
554
|
+
return efCons;
|
|
555
|
+
},
|
|
556
|
+
get metric() {
|
|
557
|
+
return buildMetric;
|
|
558
|
+
},
|
|
559
|
+
insert,
|
|
560
|
+
remove,
|
|
561
|
+
has: (docId) => nodes.has(docId),
|
|
562
|
+
search,
|
|
563
|
+
clear,
|
|
564
|
+
entries: entriesIterator,
|
|
565
|
+
serialize,
|
|
566
|
+
deserialize
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export { cosineSimilarityWithMagnitudes, createHNSWIndex, dotProduct, euclideanDistance, isSimdAvailable, magnitude };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// src/core/id-generator.ts
|
|
2
|
+
function generateId() {
|
|
3
|
+
const bytes = new Uint8Array(16);
|
|
4
|
+
crypto.getRandomValues(bytes);
|
|
5
|
+
const now = Date.now();
|
|
6
|
+
bytes[0] = Math.floor(now / 2 ** 40);
|
|
7
|
+
bytes[1] = Math.floor(now / 2 ** 32) % 256;
|
|
8
|
+
bytes[2] = Math.floor(now / 2 ** 24) % 256;
|
|
9
|
+
bytes[3] = Math.floor(now / 2 ** 16) % 256;
|
|
10
|
+
bytes[4] = Math.floor(now / 2 ** 8) % 256;
|
|
11
|
+
bytes[5] = now % 256;
|
|
12
|
+
bytes[6] = bytes[6] & 15 | 112;
|
|
13
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
14
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
15
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/workers/protocol.ts
|
|
19
|
+
var KNOWN_ACTION_TYPES = [
|
|
20
|
+
"insert",
|
|
21
|
+
"remove",
|
|
22
|
+
"update",
|
|
23
|
+
"query",
|
|
24
|
+
"preflight",
|
|
25
|
+
"get",
|
|
26
|
+
"has",
|
|
27
|
+
"count",
|
|
28
|
+
"createIndex",
|
|
29
|
+
"dropIndex",
|
|
30
|
+
"getStats",
|
|
31
|
+
"clear",
|
|
32
|
+
"serialize",
|
|
33
|
+
"deserialize",
|
|
34
|
+
"memoryReport",
|
|
35
|
+
"shutdown"
|
|
36
|
+
];
|
|
37
|
+
function createRequestId() {
|
|
38
|
+
return generateId();
|
|
39
|
+
}
|
|
40
|
+
function isValidWorkerAction(msg) {
|
|
41
|
+
if (msg === null || msg === void 0 || typeof msg !== "object") {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const candidate = msg;
|
|
45
|
+
if (typeof candidate.type !== "string" || typeof candidate.requestId !== "string") {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return KNOWN_ACTION_TYPES.includes(candidate.type);
|
|
49
|
+
}
|
|
50
|
+
function isValidWorkerResponse(msg) {
|
|
51
|
+
if (msg === null || msg === void 0 || typeof msg !== "object") {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
const candidate = msg;
|
|
55
|
+
if (typeof candidate.requestId !== "string") {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (candidate.type !== "success" && candidate.type !== "error") {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { createRequestId, generateId, isValidWorkerAction, isValidWorkerResponse };
|