@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.
@@ -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
- function createHNSWIndex(dimension, config) {
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 nodeDistance(a, b, metric) {
210
- return toDistance(a.vector, b.vector, a.mag, b.mag, metric);
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, node, metric) {
213
- return toDistance(qVec, node.vector, qMag, node.mag, metric);
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
- const dist = queryDistance(qVec, qMag, node, metric);
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 = queryDistance(qVec, qMag, neighborNode, metric);
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.toSortedArray();
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(targetVec, targetMag, candidates, maxConnections, layer, metric, extendCandidates, keepPruned) {
265
- const working = [...candidates].sort((a, b) => a.distance - b.distance);
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 selNode = nodes.get(sel.docId);
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 connNode = nodes.get(connId);
301
- if (!connNode) continue;
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(node.vector, node.mag, conns, mc, layer, metric);
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 insert(docId, vector) {
311
- if (vector.length !== dimension) {
312
- throw new Error(`Vector dimension mismatch: expected ${dimension}, got ${vector.length}`);
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
- remove(docId);
364
+ removeNodeEager(docId);
316
365
  }
317
- const mag = magnitude(vector);
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 nearest = searchLayer(vector, mag, currentEPs, 1, layer, metric);
336
- if (nearest.length > 0) {
337
- currentEPs = [nearest[0].docId];
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 nearest = searchLayer(vector, mag, currentEPs, efCons, layer, metric);
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 (nearest.length > 0) {
353
- currentEPs = nearest.map((n) => n.docId);
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 remove(docId) {
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 candNode = nodes.get(candId);
388
- if (!candNode) continue;
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
- if (entryPointId === null || nodes.size === 0) {
507
+ const liveSize = nodes.size - tombstones.size;
508
+ if (entryPointId === null || liveSize === 0) {
436
509
  return [];
437
510
  }
438
- let ef = Math.max(efSearch ?? 50, k);
439
- if (filterDocIds && filterDocIds.size < nodes.size) {
440
- const selectivity = filterDocIds.size / nodes.size;
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, nodes.size);
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 nearest = searchLayer(query, qMag, currentEPs, 1, layer, searchMetric);
448
- if (nearest.length > 0) {
449
- currentEPs = [nearest[0].docId];
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 candidates = searchLayer(query, qMag, currentEPs, ef, 0, searchMetric);
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 candidates) {
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
- if (node.connections[layer].size > 0) {
475
- layerConns.push([layer, Array.from(node.connections[layer])]);
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, vectors) {
604
+ function deserialize(data) {
490
605
  nodes.clear();
606
+ tombstones.clear();
491
607
  for (const [docId, maxLayer, layerConns] of data.nodes) {
492
- const vecData = vectors.get(docId);
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, node] of nodes) {
534
- yield [docId, { docId, vector: node.vector, magnitude: node.mag }];
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
- insert,
560
- remove,
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
- export { cosineSimilarityWithMagnitudes, createHNSWIndex, dotProduct, euclideanDistance, isSimdAvailable, magnitude };
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,4 @@
1
+ export { createDirectExecutor } from './chunk-KHL4RPC3.mjs';
2
+ import './chunk-VVORJOXR.mjs';
3
+ import './chunk-I24SGLFS.mjs';
4
+ import './chunk-R2C2EQGO.mjs';
@@ -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 };