@delali/narsil 0.1.6 → 0.1.7
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 +130 -2
- package/dist/adapters/filesystem.d.ts +1 -1
- package/dist/adapters/filesystem.mjs +1 -1
- package/dist/adapters/indexeddb.d.ts +1 -1
- package/dist/adapters/indexeddb.mjs +1 -1
- package/dist/adapters/memory.d.ts +1 -1
- package/dist/{adapters-XODT2F3h.d.ts → adapters-BbRkoiu9.d.ts} +7 -1
- package/dist/{chunk-WYC635GI.mjs → chunk-I24SGLFS.mjs} +5 -6
- package/dist/{chunk-TFWBALU4.mjs → chunk-KHL4RPC3.mjs} +1957 -1053
- package/dist/{chunk-7GKPWGCN.mjs → chunk-R2C2EQGO.mjs} +12 -2
- package/dist/{chunk-GVN2W4VA.mjs → chunk-VVORJOXR.mjs} +273 -84
- package/dist/direct-executor-LBJLGMJ2.mjs +4 -0
- package/dist/embeddings/openai.d.ts +13 -0
- package/dist/embeddings/openai.mjs +248 -0
- package/dist/index.browser.mjs +7887 -5557
- package/dist/index.d.ts +83 -49
- package/dist/index.mjs +1886 -702
- package/dist/invalidation/broadcast-channel.d.ts +1 -1
- package/dist/invalidation/filesystem.d.ts +1 -1
- package/dist/invalidation/noop.d.ts +1 -1
- package/dist/languages/english.mjs +1 -1
- package/dist/vector/hnsw-build-worker.d.ts +9 -2
- package/dist/vector/hnsw-build-worker.mjs +54 -10
- package/dist/workers/entry.mjs +1 -1
- package/package.json +10 -3
- package/dist/direct-executor-6TSG7GWH.mjs +0 -4
|
@@ -21,13 +21,20 @@ var ErrorCodes = {
|
|
|
21
21
|
PERSISTENCE_CRC_MISMATCH: "PERSISTENCE_CRC_MISMATCH",
|
|
22
22
|
SEARCH_INVALID_FIELD: "SEARCH_INVALID_FIELD",
|
|
23
23
|
SEARCH_INVALID_VECTOR_SIZE: "SEARCH_INVALID_VECTOR_SIZE",
|
|
24
|
+
VECTOR_DIMENSION_MISMATCH: "VECTOR_DIMENSION_MISMATCH",
|
|
24
25
|
SEARCH_INVALID_FILTER: "SEARCH_INVALID_FILTER",
|
|
25
26
|
SEARCH_INVALID_MODE: "SEARCH_INVALID_MODE",
|
|
26
27
|
SEARCH_INVALID_CURSOR: "SEARCH_INVALID_CURSOR",
|
|
27
28
|
LANGUAGE_NOT_SUPPORTED: "LANGUAGE_NOT_SUPPORTED",
|
|
28
29
|
ENVELOPE_VERSION_MISMATCH: "ENVELOPE_VERSION_MISMATCH",
|
|
29
30
|
ENVELOPE_INVALID_MAGIC: "ENVELOPE_INVALID_MAGIC",
|
|
30
|
-
PARTITION_CAPACITY_EXCEEDED: "PARTITION_CAPACITY_EXCEEDED"
|
|
31
|
+
PARTITION_CAPACITY_EXCEEDED: "PARTITION_CAPACITY_EXCEEDED",
|
|
32
|
+
EMBEDDING_FAILED: "EMBEDDING_FAILED",
|
|
33
|
+
EMBEDDING_DIMENSION_MISMATCH: "EMBEDDING_DIMENSION_MISMATCH",
|
|
34
|
+
EMBEDDING_NO_SOURCE: "EMBEDDING_NO_SOURCE",
|
|
35
|
+
EMBEDDING_CONFIG_INVALID: "EMBEDDING_CONFIG_INVALID",
|
|
36
|
+
DOC_MISSING_REQUIRED_FIELD: "DOC_MISSING_REQUIRED_FIELD",
|
|
37
|
+
CONFIG_INVALID: "CONFIG_INVALID"
|
|
31
38
|
};
|
|
32
39
|
var NarsilError = class extends Error {
|
|
33
40
|
code;
|
|
@@ -39,5 +46,8 @@ var NarsilError = class extends Error {
|
|
|
39
46
|
this.details = details ?? {};
|
|
40
47
|
}
|
|
41
48
|
};
|
|
49
|
+
function createNarsilError(code, message, details) {
|
|
50
|
+
return new NarsilError(code, message, details);
|
|
51
|
+
}
|
|
42
52
|
|
|
43
|
-
export { ErrorCodes, NarsilError };
|
|
53
|
+
export { ErrorCodes, NarsilError, createNarsilError };
|
|
@@ -80,6 +80,32 @@ function isSimdAvailable() {
|
|
|
80
80
|
function createMinHeap(compare) {
|
|
81
81
|
return createHeap(compare);
|
|
82
82
|
}
|
|
83
|
+
function createMaxHeap(compare) {
|
|
84
|
+
return createHeap((a, b) => compare(b, a));
|
|
85
|
+
}
|
|
86
|
+
function createBoundedMaxHeap(compare, capacity) {
|
|
87
|
+
const heap = createMaxHeap(compare);
|
|
88
|
+
const innerPush = heap.push;
|
|
89
|
+
return {
|
|
90
|
+
push(item) {
|
|
91
|
+
if (heap.size < capacity) {
|
|
92
|
+
innerPush(item);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const top = heap.peek();
|
|
96
|
+
if (top !== void 0 && compare(item, top) < 0) {
|
|
97
|
+
heap.pop();
|
|
98
|
+
innerPush(item);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
pop: heap.pop,
|
|
102
|
+
peek: heap.peek,
|
|
103
|
+
get size() {
|
|
104
|
+
return heap.size;
|
|
105
|
+
},
|
|
106
|
+
toSortedArray: heap.toSortedArray
|
|
107
|
+
};
|
|
108
|
+
}
|
|
83
109
|
function createHeap(compare) {
|
|
84
110
|
const items = [];
|
|
85
111
|
function siftUp(idx) {
|
|
@@ -170,6 +196,8 @@ function euclideanDistance(a, b) {
|
|
|
170
196
|
|
|
171
197
|
// src/vector/hnsw.ts
|
|
172
198
|
var MAX_LAYER_CAP = 32;
|
|
199
|
+
var COMPACTION_TOMBSTONE_RATIO = 0.1;
|
|
200
|
+
var COMPACTION_ABSOLUTE_THRESHOLD = 1e3;
|
|
173
201
|
function toDistance(a, b, magA, magB, metric) {
|
|
174
202
|
switch (metric) {
|
|
175
203
|
case "cosine":
|
|
@@ -192,13 +220,15 @@ function toScore(distance, metric) {
|
|
|
192
220
|
}
|
|
193
221
|
var distanceAsc = (a, b) => a.distance - b.distance;
|
|
194
222
|
var distanceDesc = (a, b) => b.distance - a.distance;
|
|
195
|
-
|
|
223
|
+
var SQ8_OVERSELECTION_FACTOR = 2;
|
|
224
|
+
function createHNSWIndex(dimension, store, config, quantizer) {
|
|
196
225
|
const M = config?.m ?? 16;
|
|
197
226
|
const Mmax0 = M * 2;
|
|
198
227
|
const efCons = config?.efConstruction ?? 200;
|
|
199
228
|
const buildMetric = config?.metric ?? "cosine";
|
|
200
229
|
const mL = 1 / Math.log(M);
|
|
201
230
|
const nodes = /* @__PURE__ */ new Map();
|
|
231
|
+
const tombstones = /* @__PURE__ */ new Set();
|
|
202
232
|
let entryPointId = null;
|
|
203
233
|
let topLayer = -1;
|
|
204
234
|
function randomLevel() {
|
|
@@ -206,16 +236,25 @@ function createHNSWIndex(dimension, config) {
|
|
|
206
236
|
if (u === 0) u = Number.MIN_VALUE;
|
|
207
237
|
return Math.min(Math.floor(-Math.log(u) * mL), MAX_LAYER_CAP);
|
|
208
238
|
}
|
|
209
|
-
function
|
|
210
|
-
return
|
|
239
|
+
function getEntry(docId) {
|
|
240
|
+
return store.get(docId);
|
|
241
|
+
}
|
|
242
|
+
function nodeDistance(aId, bId, metric) {
|
|
243
|
+
const a = getEntry(aId);
|
|
244
|
+
const b = getEntry(bId);
|
|
245
|
+
if (!a || !b) return Number.POSITIVE_INFINITY;
|
|
246
|
+
return toDistance(a.vector, b.vector, a.magnitude, b.magnitude, metric);
|
|
211
247
|
}
|
|
212
|
-
function queryDistance(qVec, qMag,
|
|
213
|
-
|
|
248
|
+
function queryDistance(qVec, qMag, nodeId, metric) {
|
|
249
|
+
const entry = getEntry(nodeId);
|
|
250
|
+
if (!entry) return Number.POSITIVE_INFINITY;
|
|
251
|
+
return toDistance(qVec, entry.vector, qMag, entry.magnitude, metric);
|
|
214
252
|
}
|
|
215
253
|
function maxConns(layer) {
|
|
216
254
|
return layer === 0 ? Mmax0 : M;
|
|
217
255
|
}
|
|
218
|
-
function searchLayer(qVec, qMag, eps, ef, layer, metric) {
|
|
256
|
+
function searchLayer(qVec, qMag, eps, ef, layer, metric, skipTombstones, distFn) {
|
|
257
|
+
const getDistance = distFn ?? ((nodeId) => queryDistance(qVec, qMag, nodeId, metric));
|
|
219
258
|
const visited = /* @__PURE__ */ new Set();
|
|
220
259
|
const candidates = createMinHeap(distanceAsc);
|
|
221
260
|
const results = createMinHeap(distanceDesc);
|
|
@@ -225,7 +264,9 @@ function createHNSWIndex(dimension, config) {
|
|
|
225
264
|
visited.add(epId);
|
|
226
265
|
const node = nodes.get(epId);
|
|
227
266
|
if (!node) continue;
|
|
228
|
-
|
|
267
|
+
if (skipTombstones && tombstones.has(epId)) continue;
|
|
268
|
+
const dist = getDistance(epId);
|
|
269
|
+
if (dist === Number.POSITIVE_INFINITY) continue;
|
|
229
270
|
const pair = { docId: epId, distance: dist };
|
|
230
271
|
candidates.push(pair);
|
|
231
272
|
results.push(pair);
|
|
@@ -244,9 +285,11 @@ function createHNSWIndex(dimension, config) {
|
|
|
244
285
|
for (const neighborId of node.connections[layer]) {
|
|
245
286
|
if (visited.has(neighborId)) continue;
|
|
246
287
|
visited.add(neighborId);
|
|
288
|
+
if (skipTombstones && tombstones.has(neighborId)) continue;
|
|
247
289
|
const neighborNode = nodes.get(neighborId);
|
|
248
290
|
if (!neighborNode) continue;
|
|
249
|
-
const dist =
|
|
291
|
+
const dist = getDistance(neighborId);
|
|
292
|
+
if (dist === Number.POSITIVE_INFINITY) continue;
|
|
250
293
|
if (dist < furthestDist || results.size < ef) {
|
|
251
294
|
const pair = { docId: neighborId, distance: dist };
|
|
252
295
|
candidates.push(pair);
|
|
@@ -259,20 +302,25 @@ function createHNSWIndex(dimension, config) {
|
|
|
259
302
|
}
|
|
260
303
|
}
|
|
261
304
|
}
|
|
262
|
-
return results
|
|
305
|
+
return results;
|
|
306
|
+
}
|
|
307
|
+
function nearestFromHeap(heap) {
|
|
308
|
+
let nearest;
|
|
309
|
+
while (heap.size > 0) {
|
|
310
|
+
nearest = heap.pop();
|
|
311
|
+
}
|
|
312
|
+
return nearest;
|
|
263
313
|
}
|
|
264
|
-
function selectNeighborsHeuristic(
|
|
265
|
-
const working = [...candidates]
|
|
314
|
+
function selectNeighborsHeuristic(targetId, candidates, maxConnections, layer, metric, extendCandidates, keepPruned) {
|
|
315
|
+
const working = [...candidates];
|
|
316
|
+
working.sort((a, b) => a.distance - b.distance);
|
|
266
317
|
const selected = [];
|
|
267
318
|
const discarded = [];
|
|
268
319
|
for (const candidate of working) {
|
|
269
320
|
if (selected.length >= maxConnections) break;
|
|
270
321
|
let accepted = true;
|
|
271
322
|
for (const sel of selected) {
|
|
272
|
-
const
|
|
273
|
-
const candNode = nodes.get(candidate.docId);
|
|
274
|
-
if (!selNode || !candNode) continue;
|
|
275
|
-
const distBetween = nodeDistance(candNode, selNode, metric);
|
|
323
|
+
const distBetween = nodeDistance(candidate.docId, sel.docId, metric);
|
|
276
324
|
if (candidate.distance >= distBetween) {
|
|
277
325
|
accepted = false;
|
|
278
326
|
break;
|
|
@@ -292,34 +340,33 @@ function createHNSWIndex(dimension, config) {
|
|
|
292
340
|
}
|
|
293
341
|
return selected;
|
|
294
342
|
}
|
|
295
|
-
function pruneConnections(node, layer, metric) {
|
|
343
|
+
function pruneConnections(nodeId, node, layer, metric) {
|
|
296
344
|
const mc = maxConns(layer);
|
|
297
345
|
if (node.connections[layer].size <= mc) return;
|
|
298
346
|
const conns = [];
|
|
299
347
|
for (const connId of node.connections[layer]) {
|
|
300
|
-
const
|
|
301
|
-
if (
|
|
302
|
-
conns.push({
|
|
303
|
-
docId: connId,
|
|
304
|
-
distance: nodeDistance(node, connNode, metric)
|
|
305
|
-
});
|
|
348
|
+
const dist = nodeDistance(nodeId, connId, metric);
|
|
349
|
+
if (dist === Number.POSITIVE_INFINITY) continue;
|
|
350
|
+
conns.push({ docId: connId, distance: dist });
|
|
306
351
|
}
|
|
307
|
-
const pruned = selectNeighborsHeuristic(
|
|
352
|
+
const pruned = selectNeighborsHeuristic(nodeId, conns, mc, layer, metric);
|
|
308
353
|
node.connections[layer] = new Set(pruned.map((p) => p.docId));
|
|
309
354
|
}
|
|
310
|
-
function
|
|
311
|
-
|
|
312
|
-
|
|
355
|
+
function insertNode(docId) {
|
|
356
|
+
const vecEntry = store.get(docId);
|
|
357
|
+
if (!vecEntry) {
|
|
358
|
+
throw new Error(`Cannot insert HNSW node: vector for "${docId}" not found in VectorStore`);
|
|
359
|
+
}
|
|
360
|
+
if (vecEntry.vector.length !== dimension) {
|
|
361
|
+
throw new Error(`Vector dimension mismatch: expected ${dimension}, got ${vecEntry.vector.length}`);
|
|
313
362
|
}
|
|
314
363
|
if (nodes.has(docId)) {
|
|
315
|
-
|
|
364
|
+
removeNodeEager(docId);
|
|
316
365
|
}
|
|
317
|
-
|
|
366
|
+
tombstones.delete(docId);
|
|
318
367
|
const l = randomLevel();
|
|
319
368
|
const node = {
|
|
320
369
|
docId,
|
|
321
|
-
vector,
|
|
322
|
-
mag,
|
|
323
370
|
maxLayer: l,
|
|
324
371
|
connections: Array.from({ length: l + 1 }, () => /* @__PURE__ */ new Set())
|
|
325
372
|
};
|
|
@@ -332,25 +379,27 @@ function createHNSWIndex(dimension, config) {
|
|
|
332
379
|
const metric = buildMetric;
|
|
333
380
|
let currentEPs = [entryPointId];
|
|
334
381
|
for (let layer = topLayer; layer > l; layer--) {
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
382
|
+
const heap = searchLayer(vecEntry.vector, vecEntry.magnitude, currentEPs, 1, layer, metric, false);
|
|
383
|
+
const nearest = nearestFromHeap(heap);
|
|
384
|
+
if (nearest) {
|
|
385
|
+
currentEPs = [nearest.docId];
|
|
338
386
|
}
|
|
339
387
|
}
|
|
340
388
|
for (let layer = Math.min(l, topLayer); layer >= 0; layer--) {
|
|
389
|
+
const heap = searchLayer(vecEntry.vector, vecEntry.magnitude, currentEPs, efCons, layer, metric, false);
|
|
390
|
+
const candidates = heap.toSortedArray().reverse();
|
|
341
391
|
const mc = maxConns(layer);
|
|
342
|
-
const
|
|
343
|
-
const neighbors = selectNeighborsHeuristic(vector, mag, nearest, mc, layer, metric);
|
|
392
|
+
const neighbors = selectNeighborsHeuristic(docId, candidates, mc, layer, metric);
|
|
344
393
|
for (const neighbor of neighbors) {
|
|
345
394
|
node.connections[layer].add(neighbor.docId);
|
|
346
395
|
const neighborNode = nodes.get(neighbor.docId);
|
|
347
396
|
if (neighborNode && layer < neighborNode.connections.length) {
|
|
348
397
|
neighborNode.connections[layer].add(docId);
|
|
349
|
-
pruneConnections(neighborNode, layer, metric);
|
|
398
|
+
pruneConnections(neighbor.docId, neighborNode, layer, metric);
|
|
350
399
|
}
|
|
351
400
|
}
|
|
352
|
-
if (
|
|
353
|
-
currentEPs =
|
|
401
|
+
if (candidates.length > 0) {
|
|
402
|
+
currentEPs = candidates.map((n) => n.docId);
|
|
354
403
|
}
|
|
355
404
|
}
|
|
356
405
|
if (l > topLayer) {
|
|
@@ -358,7 +407,7 @@ function createHNSWIndex(dimension, config) {
|
|
|
358
407
|
topLayer = l;
|
|
359
408
|
}
|
|
360
409
|
}
|
|
361
|
-
function
|
|
410
|
+
function removeNodeEager(docId, excludeDocIds) {
|
|
362
411
|
const node = nodes.get(docId);
|
|
363
412
|
if (!node) return;
|
|
364
413
|
const metric = buildMetric;
|
|
@@ -379,32 +428,24 @@ function createHNSWIndex(dimension, config) {
|
|
|
379
428
|
const candidateIds = new Set(neighborNode.connections[layer]);
|
|
380
429
|
for (const otherId of formerNeighborIds) {
|
|
381
430
|
if (otherId !== neighborId && otherId !== docId) {
|
|
431
|
+
if (excludeDocIds?.has(otherId)) continue;
|
|
382
432
|
candidateIds.add(otherId);
|
|
383
433
|
}
|
|
384
434
|
}
|
|
385
435
|
const candidates = [];
|
|
386
436
|
for (const candId of candidateIds) {
|
|
387
|
-
const
|
|
388
|
-
if (
|
|
389
|
-
candidates.push({
|
|
390
|
-
docId: candId,
|
|
391
|
-
distance: nodeDistance(neighborNode, candNode, metric)
|
|
392
|
-
});
|
|
437
|
+
const dist = nodeDistance(neighborId, candId, metric);
|
|
438
|
+
if (dist === Number.POSITIVE_INFINITY) continue;
|
|
439
|
+
candidates.push({ docId: candId, distance: dist });
|
|
393
440
|
}
|
|
394
|
-
const selected = selectNeighborsHeuristic(
|
|
395
|
-
neighborNode.vector,
|
|
396
|
-
neighborNode.mag,
|
|
397
|
-
candidates,
|
|
398
|
-
mc,
|
|
399
|
-
layer,
|
|
400
|
-
metric);
|
|
441
|
+
const selected = selectNeighborsHeuristic(neighborId, candidates, mc, layer, metric);
|
|
401
442
|
const newConns = new Set(selected.map((s) => s.docId));
|
|
402
443
|
neighborNode.connections[layer] = newConns;
|
|
403
444
|
for (const newConnId of newConns) {
|
|
404
445
|
const newConnNode = nodes.get(newConnId);
|
|
405
446
|
if (newConnNode && layer < newConnNode.connections.length) {
|
|
406
447
|
newConnNode.connections[layer].add(neighborId);
|
|
407
|
-
pruneConnections(newConnNode, layer, metric);
|
|
448
|
+
pruneConnections(newConnId, newConnNode, layer, metric);
|
|
408
449
|
}
|
|
409
450
|
}
|
|
410
451
|
}
|
|
@@ -419,39 +460,89 @@ function createHNSWIndex(dimension, config) {
|
|
|
419
460
|
let newEntryId = null;
|
|
420
461
|
let newTopLayer = -1;
|
|
421
462
|
for (const [nId, n] of nodes) {
|
|
463
|
+
if (tombstones.has(nId)) continue;
|
|
422
464
|
if (n.maxLayer > newTopLayer) {
|
|
423
465
|
newTopLayer = n.maxLayer;
|
|
424
466
|
newEntryId = nId;
|
|
425
467
|
}
|
|
426
468
|
}
|
|
469
|
+
if (newEntryId === null) {
|
|
470
|
+
for (const [nId, n] of nodes) {
|
|
471
|
+
if (n.maxLayer > newTopLayer) {
|
|
472
|
+
newTopLayer = n.maxLayer;
|
|
473
|
+
newEntryId = nId;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
427
477
|
entryPointId = newEntryId;
|
|
428
478
|
topLayer = newTopLayer;
|
|
429
479
|
}
|
|
430
480
|
}
|
|
481
|
+
function markTombstone(docId) {
|
|
482
|
+
if (!nodes.has(docId)) return;
|
|
483
|
+
tombstones.add(docId);
|
|
484
|
+
if (entryPointId === docId) {
|
|
485
|
+
let newEntryId = null;
|
|
486
|
+
let newTopLayer = -1;
|
|
487
|
+
for (const [nId, n] of nodes) {
|
|
488
|
+
if (tombstones.has(nId)) continue;
|
|
489
|
+
if (n.maxLayer > newTopLayer) {
|
|
490
|
+
newTopLayer = n.maxLayer;
|
|
491
|
+
newEntryId = nId;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (newEntryId !== null) {
|
|
495
|
+
entryPointId = newEntryId;
|
|
496
|
+
topLayer = newTopLayer;
|
|
497
|
+
} else {
|
|
498
|
+
entryPointId = null;
|
|
499
|
+
topLayer = -1;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
431
503
|
function search(query, k, searchMetric, minSimilarity, filterDocIds, efSearch) {
|
|
432
504
|
if (query.length !== dimension) {
|
|
433
505
|
throw new Error(`Query dimension mismatch: expected ${dimension}, got ${query.length}`);
|
|
434
506
|
}
|
|
435
|
-
|
|
507
|
+
const liveSize = nodes.size - tombstones.size;
|
|
508
|
+
if (entryPointId === null || liveSize === 0) {
|
|
436
509
|
return [];
|
|
437
510
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
511
|
+
const useQuantized = quantizer?.isCalibrated() === true && quantizer.size > 0;
|
|
512
|
+
const defaultEf = 50;
|
|
513
|
+
let ef = Math.max(efSearch ?? defaultEf, k);
|
|
514
|
+
if (filterDocIds && filterDocIds.size < liveSize) {
|
|
515
|
+
const selectivity = filterDocIds.size / liveSize;
|
|
441
516
|
ef = Math.max(ef, Math.ceil(k / Math.max(selectivity, 0.01)));
|
|
442
|
-
ef = Math.min(ef,
|
|
517
|
+
ef = Math.min(ef, liveSize);
|
|
443
518
|
}
|
|
444
519
|
const qMag = magnitude(query);
|
|
520
|
+
let quantizedDistFn;
|
|
521
|
+
let prepared = null;
|
|
522
|
+
if (useQuantized && quantizer) {
|
|
523
|
+
prepared = quantizer.prepareQuery(query);
|
|
524
|
+
if (prepared) {
|
|
525
|
+
const p = prepared;
|
|
526
|
+
const metric = searchMetric;
|
|
527
|
+
const q = quantizer;
|
|
528
|
+
quantizedDistFn = (nodeId) => q.distanceFromPrepared(p, nodeId, metric);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
445
531
|
let currentEPs = [entryPointId];
|
|
446
532
|
for (let layer = topLayer; layer >= 1; layer--) {
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
533
|
+
const heap = searchLayer(query, qMag, currentEPs, 1, layer, searchMetric, true, quantizedDistFn);
|
|
534
|
+
const nearest = nearestFromHeap(heap);
|
|
535
|
+
if (nearest) {
|
|
536
|
+
currentEPs = [nearest.docId];
|
|
450
537
|
}
|
|
451
538
|
}
|
|
452
|
-
const
|
|
539
|
+
const candidateHeap = searchLayer(query, qMag, currentEPs, ef, 0, searchMetric, true, quantizedDistFn);
|
|
540
|
+
const candidateArray = candidateHeap.toSortedArray().reverse();
|
|
541
|
+
if (useQuantized) {
|
|
542
|
+
return rerankWithFullPrecision(candidateArray, query, qMag, k, searchMetric, minSimilarity, filterDocIds);
|
|
543
|
+
}
|
|
453
544
|
const results = [];
|
|
454
|
-
for (const cand of
|
|
545
|
+
for (const cand of candidateArray) {
|
|
455
546
|
if (filterDocIds && !filterDocIds.has(cand.docId)) continue;
|
|
456
547
|
const score = toScore(cand.distance, searchMetric);
|
|
457
548
|
if (score < minSimilarity) continue;
|
|
@@ -463,22 +554,46 @@ function createHNSWIndex(dimension, config) {
|
|
|
463
554
|
idf: {}
|
|
464
555
|
});
|
|
465
556
|
}
|
|
466
|
-
results.sort((a, b) => b.score - a.score);
|
|
557
|
+
results.sort((a, b) => b.score - a.score || a.docId.localeCompare(b.docId));
|
|
467
558
|
return results.slice(0, k);
|
|
468
559
|
}
|
|
560
|
+
function rerankWithFullPrecision(candidates, query, qMag, k, metric, minSimilarity, filterDocIds) {
|
|
561
|
+
const reranked = [];
|
|
562
|
+
const rerankLimit = Math.max(k * SQ8_OVERSELECTION_FACTOR, 10);
|
|
563
|
+
for (const cand of candidates) {
|
|
564
|
+
if (filterDocIds && !filterDocIds.has(cand.docId)) continue;
|
|
565
|
+
const entry = getEntry(cand.docId);
|
|
566
|
+
if (!entry) continue;
|
|
567
|
+
const fullDistance = toDistance(query, entry.vector, qMag, entry.magnitude, metric);
|
|
568
|
+
const score = toScore(fullDistance, metric);
|
|
569
|
+
if (score < minSimilarity) continue;
|
|
570
|
+
reranked.push({
|
|
571
|
+
docId: cand.docId,
|
|
572
|
+
score,
|
|
573
|
+
termFrequencies: {},
|
|
574
|
+
fieldLengths: {},
|
|
575
|
+
idf: {}
|
|
576
|
+
});
|
|
577
|
+
if (reranked.length >= rerankLimit) break;
|
|
578
|
+
}
|
|
579
|
+
reranked.sort((a, b) => b.score - a.score || a.docId.localeCompare(b.docId));
|
|
580
|
+
return reranked.slice(0, k);
|
|
581
|
+
}
|
|
469
582
|
function serialize() {
|
|
470
583
|
const nodeArray = [];
|
|
471
584
|
for (const [docId, node] of nodes) {
|
|
585
|
+
if (tombstones.has(docId)) continue;
|
|
472
586
|
const layerConns = [];
|
|
473
587
|
for (let layer = 0; layer < node.connections.length; layer++) {
|
|
474
|
-
|
|
475
|
-
|
|
588
|
+
const liveNeighbors = Array.from(node.connections[layer]).filter((id) => !tombstones.has(id));
|
|
589
|
+
if (liveNeighbors.length > 0) {
|
|
590
|
+
layerConns.push([layer, liveNeighbors]);
|
|
476
591
|
}
|
|
477
592
|
}
|
|
478
593
|
nodeArray.push([docId, node.maxLayer, layerConns]);
|
|
479
594
|
}
|
|
480
595
|
return {
|
|
481
|
-
entryPoint: entryPointId
|
|
596
|
+
entryPoint: entryPointId,
|
|
482
597
|
maxLayer: topLayer,
|
|
483
598
|
m: M,
|
|
484
599
|
efConstruction: efCons,
|
|
@@ -486,10 +601,11 @@ function createHNSWIndex(dimension, config) {
|
|
|
486
601
|
nodes: nodeArray
|
|
487
602
|
};
|
|
488
603
|
}
|
|
489
|
-
function deserialize(data
|
|
604
|
+
function deserialize(data) {
|
|
490
605
|
nodes.clear();
|
|
606
|
+
tombstones.clear();
|
|
491
607
|
for (const [docId, maxLayer, layerConns] of data.nodes) {
|
|
492
|
-
const vecData =
|
|
608
|
+
const vecData = store.get(docId);
|
|
493
609
|
if (!vecData) continue;
|
|
494
610
|
const connections = Array.from({ length: maxLayer + 1 }, () => /* @__PURE__ */ new Set());
|
|
495
611
|
for (const [layer, neighbors] of layerConns) {
|
|
@@ -497,15 +613,9 @@ function createHNSWIndex(dimension, config) {
|
|
|
497
613
|
connections[layer] = new Set(neighbors);
|
|
498
614
|
}
|
|
499
615
|
}
|
|
500
|
-
nodes.set(docId, {
|
|
501
|
-
docId,
|
|
502
|
-
vector: vecData.vector,
|
|
503
|
-
mag: vecData.mag,
|
|
504
|
-
maxLayer,
|
|
505
|
-
connections
|
|
506
|
-
});
|
|
616
|
+
nodes.set(docId, { docId, maxLayer, connections });
|
|
507
617
|
}
|
|
508
|
-
if (data.entryPoint && nodes.has(data.entryPoint)) {
|
|
618
|
+
if (data.entryPoint != null && data.entryPoint !== "" && nodes.has(data.entryPoint)) {
|
|
509
619
|
entryPointId = data.entryPoint;
|
|
510
620
|
topLayer = data.maxLayer;
|
|
511
621
|
} else if (nodes.size > 0) {
|
|
@@ -526,12 +636,43 @@ function createHNSWIndex(dimension, config) {
|
|
|
526
636
|
}
|
|
527
637
|
function clear() {
|
|
528
638
|
nodes.clear();
|
|
639
|
+
tombstones.clear();
|
|
529
640
|
entryPointId = null;
|
|
530
641
|
topLayer = -1;
|
|
531
642
|
}
|
|
532
643
|
function* entriesIterator() {
|
|
533
|
-
for (const [docId
|
|
534
|
-
|
|
644
|
+
for (const [docId] of nodes) {
|
|
645
|
+
if (tombstones.has(docId)) continue;
|
|
646
|
+
const entry = store.get(docId);
|
|
647
|
+
if (!entry) continue;
|
|
648
|
+
yield [docId, { docId, vector: entry.vector, magnitude: entry.magnitude }];
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function compactionNeeded() {
|
|
652
|
+
if (nodes.size === 0) return false;
|
|
653
|
+
return tombstones.size / nodes.size > COMPACTION_TOMBSTONE_RATIO || tombstones.size > COMPACTION_ABSOLUTE_THRESHOLD;
|
|
654
|
+
}
|
|
655
|
+
function compactTombstones() {
|
|
656
|
+
if (tombstones.size === 0) return;
|
|
657
|
+
const tombstonedDocIds = Array.from(tombstones);
|
|
658
|
+
for (const docId of tombstonedDocIds) {
|
|
659
|
+
removeNodeEager(docId, tombstones);
|
|
660
|
+
}
|
|
661
|
+
tombstones.clear();
|
|
662
|
+
}
|
|
663
|
+
function rebuild() {
|
|
664
|
+
if (tombstones.size === 0 && nodes.size === 0) return;
|
|
665
|
+
const liveDocIds = [];
|
|
666
|
+
for (const [docId] of nodes) {
|
|
667
|
+
if (!tombstones.has(docId)) {
|
|
668
|
+
liveDocIds.push(docId);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
clear();
|
|
672
|
+
for (const docId of liveDocIds) {
|
|
673
|
+
const entry = store.get(docId);
|
|
674
|
+
if (!entry) continue;
|
|
675
|
+
insertNode(docId);
|
|
535
676
|
}
|
|
536
677
|
}
|
|
537
678
|
return {
|
|
@@ -539,7 +680,10 @@ function createHNSWIndex(dimension, config) {
|
|
|
539
680
|
return dimension;
|
|
540
681
|
},
|
|
541
682
|
get size() {
|
|
542
|
-
return nodes.size;
|
|
683
|
+
return nodes.size - tombstones.size;
|
|
684
|
+
},
|
|
685
|
+
get tombstoneCount() {
|
|
686
|
+
return tombstones.size;
|
|
543
687
|
},
|
|
544
688
|
get entryPointId() {
|
|
545
689
|
return entryPointId;
|
|
@@ -556,15 +700,60 @@ function createHNSWIndex(dimension, config) {
|
|
|
556
700
|
get metric() {
|
|
557
701
|
return buildMetric;
|
|
558
702
|
},
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
has: (docId) => nodes.has(docId),
|
|
703
|
+
insertNode,
|
|
704
|
+
markTombstone,
|
|
705
|
+
has: (docId) => nodes.has(docId) && !tombstones.has(docId),
|
|
706
|
+
isTombstoned: (docId) => tombstones.has(docId),
|
|
562
707
|
search,
|
|
563
708
|
clear,
|
|
564
709
|
entries: entriesIterator,
|
|
710
|
+
compactionNeeded,
|
|
711
|
+
compactTombstones,
|
|
712
|
+
rebuild,
|
|
565
713
|
serialize,
|
|
566
714
|
deserialize
|
|
567
715
|
};
|
|
568
716
|
}
|
|
569
717
|
|
|
570
|
-
|
|
718
|
+
// src/vector/vector-store.ts
|
|
719
|
+
function createVectorStore() {
|
|
720
|
+
const vectors = /* @__PURE__ */ new Map();
|
|
721
|
+
return {
|
|
722
|
+
get size() {
|
|
723
|
+
return vectors.size;
|
|
724
|
+
},
|
|
725
|
+
insert(docId, vector) {
|
|
726
|
+
const mag = magnitude(vector);
|
|
727
|
+
vectors.set(docId, { vector, magnitude: mag });
|
|
728
|
+
},
|
|
729
|
+
remove(docId) {
|
|
730
|
+
vectors.delete(docId);
|
|
731
|
+
},
|
|
732
|
+
get(docId) {
|
|
733
|
+
return vectors.get(docId);
|
|
734
|
+
},
|
|
735
|
+
has(docId) {
|
|
736
|
+
return vectors.has(docId);
|
|
737
|
+
},
|
|
738
|
+
entries() {
|
|
739
|
+
return vectors.entries();
|
|
740
|
+
},
|
|
741
|
+
clear() {
|
|
742
|
+
vectors.clear();
|
|
743
|
+
},
|
|
744
|
+
estimateMemory(dimension) {
|
|
745
|
+
const count = vectors.size;
|
|
746
|
+
if (count === 0) return 0;
|
|
747
|
+
const MAP_OVERHEAD = 64;
|
|
748
|
+
const MAP_ENTRY = 72;
|
|
749
|
+
const ENTRY_OBJ = 32;
|
|
750
|
+
const AVG_DOCID_BYTES = 56;
|
|
751
|
+
const TYPED_ARRAY_HEADER = 64;
|
|
752
|
+
const MAGNITUDE_BYTES = 8;
|
|
753
|
+
const perEntry = MAP_ENTRY + ENTRY_OBJ + AVG_DOCID_BYTES + TYPED_ARRAY_HEADER + MAGNITUDE_BYTES;
|
|
754
|
+
return MAP_OVERHEAD + count * (perEntry + dimension * 4);
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
export { cosineSimilarityWithMagnitudes, createBoundedMaxHeap, createHNSWIndex, createVectorStore, dotProduct, euclideanDistance, isSimdAvailable, magnitude };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { E as EmbeddingAdapter } from '../adapters-BbRkoiu9.js';
|
|
2
|
+
|
|
3
|
+
interface OpenAIEmbeddingConfig {
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
apiKey: string | (() => string | Promise<string>);
|
|
6
|
+
model: string;
|
|
7
|
+
dimensions: number;
|
|
8
|
+
timeout?: number;
|
|
9
|
+
maxRetries?: number;
|
|
10
|
+
}
|
|
11
|
+
declare function createOpenAIEmbedding(config: OpenAIEmbeddingConfig): EmbeddingAdapter;
|
|
12
|
+
|
|
13
|
+
export { type OpenAIEmbeddingConfig, createOpenAIEmbedding };
|