@claude-flow/memory 3.0.0-alpha.9 → 3.0.1
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 +308 -37
- package/dist/agent-memory-scope.d.ts.map +1 -1
- package/dist/agent-memory-scope.js +10 -2
- package/dist/agent-memory-scope.js.map +1 -1
- package/dist/agentdb-adapter-key-isolation.test.d.ts +2 -0
- package/dist/agentdb-adapter-key-isolation.test.d.ts.map +1 -0
- package/dist/agentdb-adapter-key-isolation.test.js +175 -0
- package/dist/agentdb-adapter-key-isolation.test.js.map +1 -0
- package/dist/agentdb-adapter-upsert.test.d.ts +2 -0
- package/dist/agentdb-adapter-upsert.test.d.ts.map +1 -0
- package/dist/agentdb-adapter-upsert.test.js +216 -0
- package/dist/agentdb-adapter-upsert.test.js.map +1 -0
- package/dist/agentdb-adapter.d.ts +70 -1
- package/dist/agentdb-adapter.d.ts.map +1 -1
- package/dist/agentdb-adapter.js +365 -32
- package/dist/agentdb-adapter.js.map +1 -1
- package/dist/agentdb-backend.d.ts +25 -0
- package/dist/agentdb-backend.d.ts.map +1 -1
- package/dist/agentdb-backend.js +113 -28
- package/dist/agentdb-backend.js.map +1 -1
- package/dist/agentdb-retrieval-guard.d.ts +76 -0
- package/dist/agentdb-retrieval-guard.d.ts.map +1 -0
- package/dist/agentdb-retrieval-guard.js +77 -0
- package/dist/agentdb-retrieval-guard.js.map +1 -0
- package/dist/agentdb-retrieval-guard.test.d.ts +12 -0
- package/dist/agentdb-retrieval-guard.test.d.ts.map +1 -0
- package/dist/agentdb-retrieval-guard.test.js +129 -0
- package/dist/agentdb-retrieval-guard.test.js.map +1 -0
- package/dist/auto-memory-bridge.d.ts +12 -1
- package/dist/auto-memory-bridge.d.ts.map +1 -1
- package/dist/auto-memory-bridge.js +101 -7
- package/dist/auto-memory-bridge.js.map +1 -1
- package/dist/auto-memory-bridge.test.js +121 -15
- package/dist/auto-memory-bridge.test.js.map +1 -1
- package/dist/auto-memory-index-3224.test.d.ts +2 -0
- package/dist/auto-memory-index-3224.test.d.ts.map +1 -0
- package/dist/auto-memory-index-3224.test.js +77 -0
- package/dist/auto-memory-index-3224.test.js.map +1 -0
- package/dist/consolidator-embedding-benchmark.test.d.ts +39 -0
- package/dist/consolidator-embedding-benchmark.test.d.ts.map +1 -0
- package/dist/consolidator-embedding-benchmark.test.js +366 -0
- package/dist/consolidator-embedding-benchmark.test.js.map +1 -0
- package/dist/consolidator.d.ts +131 -0
- package/dist/consolidator.d.ts.map +1 -0
- package/dist/consolidator.js +309 -0
- package/dist/consolidator.js.map +1 -0
- package/dist/consolidator.test.d.ts +17 -0
- package/dist/consolidator.test.d.ts.map +1 -0
- package/dist/consolidator.test.js +376 -0
- package/dist/consolidator.test.js.map +1 -0
- package/dist/controller-registry.d.ts +290 -0
- package/dist/controller-registry.d.ts.map +1 -0
- package/dist/controller-registry.js +1095 -0
- package/dist/controller-registry.js.map +1 -0
- package/dist/controller-registry.test.d.ts +14 -0
- package/dist/controller-registry.test.d.ts.map +1 -0
- package/dist/controller-registry.test.js +818 -0
- package/dist/controller-registry.test.js.map +1 -0
- package/dist/database-provider.d.ts +14 -2
- package/dist/database-provider.d.ts.map +1 -1
- package/dist/database-provider.js +63 -3
- package/dist/database-provider.js.map +1 -1
- package/dist/database-provider.test.js +56 -3
- package/dist/database-provider.test.js.map +1 -1
- package/dist/entity-tagger.d.ts +31 -0
- package/dist/entity-tagger.d.ts.map +1 -0
- package/dist/entity-tagger.js +80 -0
- package/dist/entity-tagger.js.map +1 -0
- package/dist/entity-tagger.test.d.ts +12 -0
- package/dist/entity-tagger.test.d.ts.map +1 -0
- package/dist/entity-tagger.test.js +70 -0
- package/dist/entity-tagger.test.js.map +1 -0
- package/dist/fts5.test.d.ts +13 -0
- package/dist/fts5.test.d.ts.map +1 -0
- package/dist/fts5.test.js +144 -0
- package/dist/fts5.test.js.map +1 -0
- package/dist/graceful-retrieval.test.d.ts +14 -0
- package/dist/graceful-retrieval.test.d.ts.map +1 -0
- package/dist/graceful-retrieval.test.js +251 -0
- package/dist/graceful-retrieval.test.js.map +1 -0
- package/dist/hnsw-index.d.ts +62 -0
- package/dist/hnsw-index.d.ts.map +1 -1
- package/dist/hnsw-index.js +461 -16
- package/dist/hnsw-index.js.map +1 -1
- package/dist/hnsw-persistence.test.d.ts +13 -0
- package/dist/hnsw-persistence.test.d.ts.map +1 -0
- package/dist/hnsw-persistence.test.js +208 -0
- package/dist/hnsw-persistence.test.js.map +1 -0
- package/dist/hnsw-quantization.test.d.ts +17 -0
- package/dist/hnsw-quantization.test.d.ts.map +1 -0
- package/dist/hnsw-quantization.test.js +140 -0
- package/dist/hnsw-quantization.test.js.map +1 -0
- package/dist/hybrid-backend.d.ts +46 -2
- package/dist/hybrid-backend.d.ts.map +1 -1
- package/dist/hybrid-backend.js +120 -21
- package/dist/hybrid-backend.js.map +1 -1
- package/dist/hybrid-backend.test.js +139 -5
- package/dist/hybrid-backend.test.js.map +1 -1
- package/dist/index.d.ts +164 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +272 -30
- package/dist/index.js.map +1 -1
- package/dist/index.test.d.ts +10 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +71 -0
- package/dist/index.test.js.map +1 -0
- package/dist/json-security.d.ts +12 -0
- package/dist/json-security.d.ts.map +1 -0
- package/dist/json-security.js +20 -0
- package/dist/json-security.js.map +1 -0
- package/dist/learning-bridge.d.ts +29 -0
- package/dist/learning-bridge.d.ts.map +1 -1
- package/dist/learning-bridge.js +71 -6
- package/dist/learning-bridge.js.map +1 -1
- package/dist/learning-bridge.test.js +181 -1
- package/dist/learning-bridge.test.js.map +1 -1
- package/dist/memory-key.d.ts +3 -0
- package/dist/memory-key.d.ts.map +1 -0
- package/dist/memory-key.js +5 -0
- package/dist/memory-key.js.map +1 -0
- package/dist/migration.d.ts +19 -4
- package/dist/migration.d.ts.map +1 -1
- package/dist/migration.js +99 -24
- package/dist/migration.js.map +1 -1
- package/dist/migration.test.d.ts +11 -0
- package/dist/migration.test.d.ts.map +1 -0
- package/dist/migration.test.js +141 -0
- package/dist/migration.test.js.map +1 -0
- package/dist/mmr-benchmark.test.d.ts +2 -0
- package/dist/mmr-benchmark.test.d.ts.map +1 -0
- package/dist/mmr-benchmark.test.js +286 -0
- package/dist/mmr-benchmark.test.js.map +1 -0
- package/dist/mmr-rerank-cosine-cache.test.d.ts +2 -0
- package/dist/mmr-rerank-cosine-cache.test.d.ts.map +1 -0
- package/dist/mmr-rerank-cosine-cache.test.js +334 -0
- package/dist/mmr-rerank-cosine-cache.test.js.map +1 -0
- package/dist/mmr-rerank-perf.test.d.ts +2 -0
- package/dist/mmr-rerank-perf.test.d.ts.map +1 -0
- package/dist/mmr-rerank-perf.test.js +191 -0
- package/dist/mmr-rerank-perf.test.js.map +1 -0
- package/dist/persistent-sona.d.ts +144 -0
- package/dist/persistent-sona.d.ts.map +1 -0
- package/dist/persistent-sona.js +332 -0
- package/dist/persistent-sona.js.map +1 -0
- package/dist/rvf-backend.d.ts +53 -0
- package/dist/rvf-backend.d.ts.map +1 -0
- package/dist/rvf-backend.js +643 -0
- package/dist/rvf-backend.js.map +1 -0
- package/dist/rvf-learning-store.d.ts +139 -0
- package/dist/rvf-learning-store.d.ts.map +1 -0
- package/dist/rvf-learning-store.js +295 -0
- package/dist/rvf-learning-store.js.map +1 -0
- package/dist/rvf-migration.d.ts +45 -0
- package/dist/rvf-migration.d.ts.map +1 -0
- package/dist/rvf-migration.js +254 -0
- package/dist/rvf-migration.js.map +1 -0
- package/dist/smart-retrieval-scores.test.d.ts +2 -0
- package/dist/smart-retrieval-scores.test.d.ts.map +1 -0
- package/dist/smart-retrieval-scores.test.js +75 -0
- package/dist/smart-retrieval-scores.test.js.map +1 -0
- package/dist/smart-retrieval.d.ts +141 -0
- package/dist/smart-retrieval.d.ts.map +1 -0
- package/dist/smart-retrieval.js +399 -0
- package/dist/smart-retrieval.js.map +1 -0
- package/dist/smart-retrieval.test.d.ts +2 -0
- package/dist/smart-retrieval.test.d.ts.map +1 -0
- package/dist/smart-retrieval.test.js +418 -0
- package/dist/smart-retrieval.test.js.map +1 -0
- package/dist/sqlite-backend.d.ts +10 -0
- package/dist/sqlite-backend.d.ts.map +1 -1
- package/dist/sqlite-backend.js +101 -5
- package/dist/sqlite-backend.js.map +1 -1
- package/dist/sqljs-backend.d.ts +9 -0
- package/dist/sqljs-backend.d.ts.map +1 -1
- package/dist/sqljs-backend.js +91 -3
- package/dist/sqljs-backend.js.map +1 -1
- package/dist/tiered-memory.d.ts +171 -0
- package/dist/tiered-memory.d.ts.map +1 -0
- package/dist/tiered-memory.js +350 -0
- package/dist/tiered-memory.js.map +1 -0
- package/dist/tiered-memory.test.d.ts +9 -0
- package/dist/tiered-memory.test.d.ts.map +1 -0
- package/dist/tiered-memory.test.js +220 -0
- package/dist/tiered-memory.test.js.map +1 -0
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +10 -8
package/dist/hnsw-index.js
CHANGED
|
@@ -206,8 +206,11 @@ export class HNSWIndex extends EventEmitter {
|
|
|
206
206
|
const storedVector = this.quantizer
|
|
207
207
|
? this.quantizer.encode(vector)
|
|
208
208
|
: vector;
|
|
209
|
-
// Pre-normalize vector for O(1) cosine similarity
|
|
210
|
-
|
|
209
|
+
// Pre-normalize vector for O(1) cosine similarity.
|
|
210
|
+
// Skipped for product quantization: storedVector holds PQ centroid
|
|
211
|
+
// indices, not embedding coordinates, so normalizing it is meaningless
|
|
212
|
+
// (and productQuantizeDistance() below never consults normalizedVector).
|
|
213
|
+
const normalizedVector = this.config.metric === 'cosine' && !this.isProductQuantized()
|
|
211
214
|
? this.normalizeVector(storedVector)
|
|
212
215
|
: null;
|
|
213
216
|
// Generate random level for new node
|
|
@@ -254,8 +257,9 @@ export class HNSWIndex extends EventEmitter {
|
|
|
254
257
|
const queryVector = this.quantizer
|
|
255
258
|
? this.quantizer.encode(query)
|
|
256
259
|
: query;
|
|
257
|
-
// Pre-normalize query for O(1) cosine similarity
|
|
258
|
-
|
|
260
|
+
// Pre-normalize query for O(1) cosine similarity (see addPoint() for why
|
|
261
|
+
// this is skipped under product quantization).
|
|
262
|
+
const normalizedQuery = this.config.metric === 'cosine' && !this.isProductQuantized()
|
|
259
263
|
? this.normalizeVector(queryVector)
|
|
260
264
|
: null;
|
|
261
265
|
// Start from entry point and search down the layers
|
|
@@ -395,6 +399,192 @@ export class HNSWIndex extends EventEmitter {
|
|
|
395
399
|
get size() {
|
|
396
400
|
return this.nodes.size;
|
|
397
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* Read-only view of the resolved HNSW config (dimensions, M, etc.).
|
|
404
|
+
* Used by {@link MemoryConsolidator.compactHnsw} to rebuild the index with
|
|
405
|
+
* matching parameters.
|
|
406
|
+
*/
|
|
407
|
+
getConfig() {
|
|
408
|
+
return this.config;
|
|
409
|
+
}
|
|
410
|
+
// ===== Persistence (ADR-125 Phase 3) =====
|
|
411
|
+
/**
|
|
412
|
+
* Magic header for serialized HNSW snapshots.
|
|
413
|
+
* Format: "HNSW" + version byte (0x01).
|
|
414
|
+
*/
|
|
415
|
+
static SERIALIZATION_MAGIC = Buffer.from([0x48, 0x4e, 0x53, 0x57, 0x01]);
|
|
416
|
+
/**
|
|
417
|
+
* Serialize the index to a binary buffer.
|
|
418
|
+
*
|
|
419
|
+
* Layout (all integers big-endian):
|
|
420
|
+
* - 5 bytes: magic header "HNSW" + version (0x01)
|
|
421
|
+
* - 4 bytes: dimensions (uint32)
|
|
422
|
+
* - 4 bytes: M (uint32)
|
|
423
|
+
* - 4 bytes: efConstruction (uint32)
|
|
424
|
+
* - 4 bytes: metric length (uint32) + metric utf-8 bytes
|
|
425
|
+
* - 4 bytes: maxLevel (uint32)
|
|
426
|
+
* - 4 bytes: entryPoint length (uint32) + entryPoint utf-8 bytes (0 = null)
|
|
427
|
+
* - 4 bytes: node count (uint32)
|
|
428
|
+
* - per node:
|
|
429
|
+
* - 4 bytes id length + id utf-8 bytes
|
|
430
|
+
* - 4 bytes level
|
|
431
|
+
* - 4 bytes vector length (in floats) + vector bytes (Float32, little-endian native)
|
|
432
|
+
* - 1 byte hasNormalized (0/1)
|
|
433
|
+
* - if hasNormalized: 4 bytes normalized length + normalized bytes
|
|
434
|
+
* - 4 bytes connection-level count
|
|
435
|
+
* - per level:
|
|
436
|
+
* - 4 bytes layer index
|
|
437
|
+
* - 4 bytes neighbor count
|
|
438
|
+
* - per neighbor: 4 bytes id length + id utf-8 bytes
|
|
439
|
+
*/
|
|
440
|
+
serialize() {
|
|
441
|
+
const chunks = [];
|
|
442
|
+
chunks.push(HNSWIndex.SERIALIZATION_MAGIC);
|
|
443
|
+
// Config header
|
|
444
|
+
const header = Buffer.alloc(16);
|
|
445
|
+
header.writeUInt32BE(this.config.dimensions, 0);
|
|
446
|
+
header.writeUInt32BE(this.config.M, 4);
|
|
447
|
+
header.writeUInt32BE(this.config.efConstruction, 8);
|
|
448
|
+
header.writeUInt32BE(this.maxLevel, 12);
|
|
449
|
+
chunks.push(header);
|
|
450
|
+
chunks.push(this.encodeLengthPrefixedString(this.config.metric));
|
|
451
|
+
chunks.push(this.encodeLengthPrefixedString(this.entryPoint ?? ''));
|
|
452
|
+
const nodeCountBuf = Buffer.alloc(4);
|
|
453
|
+
nodeCountBuf.writeUInt32BE(this.nodes.size, 0);
|
|
454
|
+
chunks.push(nodeCountBuf);
|
|
455
|
+
for (const [id, node] of this.nodes) {
|
|
456
|
+
chunks.push(this.encodeLengthPrefixedString(id));
|
|
457
|
+
const meta = Buffer.alloc(4);
|
|
458
|
+
meta.writeUInt32BE(node.level, 0);
|
|
459
|
+
chunks.push(meta);
|
|
460
|
+
chunks.push(this.encodeFloat32Array(node.vector));
|
|
461
|
+
if (node.normalizedVector) {
|
|
462
|
+
chunks.push(Buffer.from([1]));
|
|
463
|
+
chunks.push(this.encodeFloat32Array(node.normalizedVector));
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
chunks.push(Buffer.from([0]));
|
|
467
|
+
}
|
|
468
|
+
const levels = [...node.connections.entries()];
|
|
469
|
+
const lvlCountBuf = Buffer.alloc(4);
|
|
470
|
+
lvlCountBuf.writeUInt32BE(levels.length, 0);
|
|
471
|
+
chunks.push(lvlCountBuf);
|
|
472
|
+
for (const [layer, neighbors] of levels) {
|
|
473
|
+
const layerBuf = Buffer.alloc(8);
|
|
474
|
+
layerBuf.writeUInt32BE(layer, 0);
|
|
475
|
+
layerBuf.writeUInt32BE(neighbors.size, 4);
|
|
476
|
+
chunks.push(layerBuf);
|
|
477
|
+
for (const neighborId of neighbors) {
|
|
478
|
+
chunks.push(this.encodeLengthPrefixedString(neighborId));
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return Buffer.concat(chunks);
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Deserialize an HNSW index from a buffer produced by {@link serialize}.
|
|
486
|
+
*
|
|
487
|
+
* Throws on magic-header mismatch, version mismatch, or truncated input.
|
|
488
|
+
*/
|
|
489
|
+
static deserialize(buf) {
|
|
490
|
+
if (buf.length < HNSWIndex.SERIALIZATION_MAGIC.length) {
|
|
491
|
+
throw new Error('HNSWIndex.deserialize: buffer too short for magic header');
|
|
492
|
+
}
|
|
493
|
+
for (let i = 0; i < HNSWIndex.SERIALIZATION_MAGIC.length; i++) {
|
|
494
|
+
if (buf[i] !== HNSWIndex.SERIALIZATION_MAGIC[i]) {
|
|
495
|
+
throw new Error(`HNSWIndex.deserialize: magic header mismatch at byte ${i} (got 0x${buf[i].toString(16)}, expected 0x${HNSWIndex.SERIALIZATION_MAGIC[i].toString(16)})`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
let offset = HNSWIndex.SERIALIZATION_MAGIC.length;
|
|
499
|
+
const dimensions = buf.readUInt32BE(offset);
|
|
500
|
+
offset += 4;
|
|
501
|
+
const M = buf.readUInt32BE(offset);
|
|
502
|
+
offset += 4;
|
|
503
|
+
const efConstruction = buf.readUInt32BE(offset);
|
|
504
|
+
offset += 4;
|
|
505
|
+
const maxLevel = buf.readUInt32BE(offset);
|
|
506
|
+
offset += 4;
|
|
507
|
+
const metricRead = readLengthPrefixedString(buf, offset);
|
|
508
|
+
offset = metricRead.offset;
|
|
509
|
+
const metric = metricRead.value;
|
|
510
|
+
const entryRead = readLengthPrefixedString(buf, offset);
|
|
511
|
+
offset = entryRead.offset;
|
|
512
|
+
const entryPoint = entryRead.value === '' ? null : entryRead.value;
|
|
513
|
+
const nodeCount = buf.readUInt32BE(offset);
|
|
514
|
+
offset += 4;
|
|
515
|
+
const index = new HNSWIndex({
|
|
516
|
+
dimensions,
|
|
517
|
+
M,
|
|
518
|
+
efConstruction,
|
|
519
|
+
metric,
|
|
520
|
+
});
|
|
521
|
+
index.maxLevel = maxLevel;
|
|
522
|
+
index.entryPoint = entryPoint;
|
|
523
|
+
for (let n = 0; n < nodeCount; n++) {
|
|
524
|
+
const idRead = readLengthPrefixedString(buf, offset);
|
|
525
|
+
offset = idRead.offset;
|
|
526
|
+
const id = idRead.value;
|
|
527
|
+
const level = buf.readUInt32BE(offset);
|
|
528
|
+
offset += 4;
|
|
529
|
+
const vecRead = readFloat32Array(buf, offset);
|
|
530
|
+
offset = vecRead.offset;
|
|
531
|
+
const vector = vecRead.value;
|
|
532
|
+
const hasNormalized = buf[offset];
|
|
533
|
+
offset += 1;
|
|
534
|
+
let normalizedVector = null;
|
|
535
|
+
if (hasNormalized) {
|
|
536
|
+
const normRead = readFloat32Array(buf, offset);
|
|
537
|
+
offset = normRead.offset;
|
|
538
|
+
normalizedVector = normRead.value;
|
|
539
|
+
}
|
|
540
|
+
const lvlCount = buf.readUInt32BE(offset);
|
|
541
|
+
offset += 4;
|
|
542
|
+
const connections = new Map();
|
|
543
|
+
for (let l = 0; l < lvlCount; l++) {
|
|
544
|
+
const layer = buf.readUInt32BE(offset);
|
|
545
|
+
offset += 4;
|
|
546
|
+
const neighborCount = buf.readUInt32BE(offset);
|
|
547
|
+
offset += 4;
|
|
548
|
+
const neighbors = new Set();
|
|
549
|
+
for (let k = 0; k < neighborCount; k++) {
|
|
550
|
+
const neighborRead = readLengthPrefixedString(buf, offset);
|
|
551
|
+
offset = neighborRead.offset;
|
|
552
|
+
neighbors.add(neighborRead.value);
|
|
553
|
+
}
|
|
554
|
+
connections.set(layer, neighbors);
|
|
555
|
+
}
|
|
556
|
+
index.nodes.set(id, {
|
|
557
|
+
id,
|
|
558
|
+
vector,
|
|
559
|
+
normalizedVector,
|
|
560
|
+
connections,
|
|
561
|
+
level,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
return index;
|
|
565
|
+
}
|
|
566
|
+
encodeLengthPrefixedString(s) {
|
|
567
|
+
const strBuf = Buffer.from(s, 'utf-8');
|
|
568
|
+
const out = Buffer.alloc(4 + strBuf.length);
|
|
569
|
+
out.writeUInt32BE(strBuf.length, 0);
|
|
570
|
+
strBuf.copy(out, 4);
|
|
571
|
+
return out;
|
|
572
|
+
}
|
|
573
|
+
encodeFloat32Array(arr) {
|
|
574
|
+
const lenBuf = Buffer.alloc(4);
|
|
575
|
+
lenBuf.writeUInt32BE(arr.length, 0);
|
|
576
|
+
const dataBuf = Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength);
|
|
577
|
+
return Buffer.concat([lenBuf, dataBuf]);
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Remove a point from the index — alias for {@link removePoint}.
|
|
581
|
+
*
|
|
582
|
+
* Added by ADR-125 Phase 4 so {@link MemoryConsolidator} has a stable
|
|
583
|
+
* synchronous-shaped API to call. Internally delegates to {@link removePoint}.
|
|
584
|
+
*/
|
|
585
|
+
async remove(id) {
|
|
586
|
+
return this.removePoint(id);
|
|
587
|
+
}
|
|
398
588
|
// ===== Private Methods =====
|
|
399
589
|
mergeConfig(config) {
|
|
400
590
|
return {
|
|
@@ -587,7 +777,43 @@ export class HNSWIndex extends EventEmitter {
|
|
|
587
777
|
}
|
|
588
778
|
}
|
|
589
779
|
}
|
|
780
|
+
/**
|
|
781
|
+
* True when this index uses product quantization with trained codebooks.
|
|
782
|
+
* Stored vectors are then PQ centroid indices, not embedding coordinates —
|
|
783
|
+
* generic metric distances (cosine/euclidean/dot/manhattan) computed
|
|
784
|
+
* directly on those indices are meaningless (index 5 vs index 200 has no
|
|
785
|
+
* relation to embedding-space proximity). Falls back to false during the
|
|
786
|
+
* pre-training bootstrap window (see Quantizer.productQuantize()), where
|
|
787
|
+
* stored vectors are still sub-vector-mean floats and generic distance,
|
|
788
|
+
* while imprecise, is not nonsensical — matching prior behavior exactly.
|
|
789
|
+
*/
|
|
790
|
+
isProductQuantized() {
|
|
791
|
+
return (this.quantizer !== null &&
|
|
792
|
+
this.config.quantization?.type === 'product' &&
|
|
793
|
+
this.quantizer.isPQTrained);
|
|
794
|
+
}
|
|
590
795
|
distance(a, b) {
|
|
796
|
+
// Pre-existing, disclosed limitation (not introduced by this fix): any
|
|
797
|
+
// vector added before PQ codebook training completes is stored under a
|
|
798
|
+
// sub-vector-mean fallback encoding (see Quantizer.productQuantize())
|
|
799
|
+
// and is never re-encoded once training finishes — the original raw
|
|
800
|
+
// vector isn't retained. isValidPQEncoding() is a *plausibility*
|
|
801
|
+
// heuristic, not a proof of provenance: it structurally guards the
|
|
802
|
+
// crash/out-of-bounds case (fallback values outside codebook range, or
|
|
803
|
+
// non-integer), but a fallback encoding whose sub-vector means
|
|
804
|
+
// coincidentally land on small non-negative integers (most concretely,
|
|
805
|
+
// an all-zero or otherwise degenerate vector added pre-training) can
|
|
806
|
+
// still pass the check and be silently mis-dispatched to
|
|
807
|
+
// productQuantizeDistance() as if it were a real centroid assignment.
|
|
808
|
+
// Closing this fully would need per-node encoding-provenance tracking
|
|
809
|
+
// (e.g. a sentinel/flag threaded through HNSWNode and its persistence
|
|
810
|
+
// format), out of scope for this patch — flagged as a known residual
|
|
811
|
+
// risk, not treated as fully solved.
|
|
812
|
+
if (this.isProductQuantized() &&
|
|
813
|
+
this.quantizer.isValidPQEncoding(a) &&
|
|
814
|
+
this.quantizer.isValidPQEncoding(b)) {
|
|
815
|
+
return this.quantizer.productQuantizeDistance(a, b);
|
|
816
|
+
}
|
|
591
817
|
switch (this.config.metric) {
|
|
592
818
|
case 'cosine':
|
|
593
819
|
return this.cosineDistance(a, b);
|
|
@@ -688,6 +914,14 @@ export class HNSWIndex extends EventEmitter {
|
|
|
688
914
|
class Quantizer {
|
|
689
915
|
config;
|
|
690
916
|
dimensions;
|
|
917
|
+
/** Trained PQ codebooks: codebooks[subquantizer][centroid][dimension] */
|
|
918
|
+
codebooks = null;
|
|
919
|
+
/** Accumulated vectors for lazy codebook training */
|
|
920
|
+
trainingVectors = [];
|
|
921
|
+
/** Minimum number of vectors needed before training codebooks */
|
|
922
|
+
pqTrainingThreshold = 256;
|
|
923
|
+
/** Whether PQ codebooks have been trained */
|
|
924
|
+
pqTrained = false;
|
|
691
925
|
constructor(config, dimensions) {
|
|
692
926
|
this.config = config;
|
|
693
927
|
this.dimensions = dimensions;
|
|
@@ -760,22 +994,233 @@ class Quantizer {
|
|
|
760
994
|
return quantized;
|
|
761
995
|
}
|
|
762
996
|
productQuantize(vector) {
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
997
|
+
const numSubquantizers = this.config.subquantizers || 8;
|
|
998
|
+
const numCentroids = this.config.codebookSize || 256;
|
|
999
|
+
// Accumulate training vectors until we have enough to train codebooks
|
|
1000
|
+
if (!this.pqTrained) {
|
|
1001
|
+
this.trainingVectors.push(Array.from(vector));
|
|
1002
|
+
if (this.trainingVectors.length >= this.pqTrainingThreshold) {
|
|
1003
|
+
this.codebooks = this.trainProductQuantizer(this.trainingVectors, numSubquantizers, numCentroids);
|
|
1004
|
+
this.pqTrained = true;
|
|
1005
|
+
this.trainingVectors = []; // Free training data
|
|
1006
|
+
}
|
|
1007
|
+
else {
|
|
1008
|
+
// Not enough data to train yet; fall back to sub-vector means
|
|
1009
|
+
const subvectorSize = Math.ceil(vector.length / numSubquantizers);
|
|
1010
|
+
const quantized = new Float32Array(numSubquantizers);
|
|
1011
|
+
for (let i = 0; i < numSubquantizers; i++) {
|
|
1012
|
+
let sum = 0;
|
|
1013
|
+
const start = i * subvectorSize;
|
|
1014
|
+
const end = Math.min(start + subvectorSize, vector.length);
|
|
1015
|
+
for (let j = start; j < end; j++) {
|
|
1016
|
+
sum += vector[j];
|
|
1017
|
+
}
|
|
1018
|
+
quantized[i] = sum / (end - start);
|
|
1019
|
+
}
|
|
1020
|
+
return quantized;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
// Encode: assign each sub-vector to its nearest centroid
|
|
1024
|
+
const subvectorSize = Math.ceil(vector.length / numSubquantizers);
|
|
1025
|
+
const encoded = new Float32Array(numSubquantizers);
|
|
1026
|
+
for (let m = 0; m < numSubquantizers; m++) {
|
|
1027
|
+
const start = m * subvectorSize;
|
|
771
1028
|
const end = Math.min(start + subvectorSize, vector.length);
|
|
772
|
-
|
|
773
|
-
|
|
1029
|
+
const codebook = this.codebooks[m];
|
|
1030
|
+
let bestIdx = 0;
|
|
1031
|
+
let bestDist = Infinity;
|
|
1032
|
+
for (let c = 0; c < codebook.length; c++) {
|
|
1033
|
+
let dist = 0;
|
|
1034
|
+
for (let d = 0; d < end - start; d++) {
|
|
1035
|
+
const diff = vector[start + d] - codebook[c][d];
|
|
1036
|
+
dist += diff * diff;
|
|
1037
|
+
}
|
|
1038
|
+
if (dist < bestDist) {
|
|
1039
|
+
bestDist = dist;
|
|
1040
|
+
bestIdx = c;
|
|
1041
|
+
}
|
|
774
1042
|
}
|
|
775
|
-
|
|
1043
|
+
// Store centroid index (fits in Uint8 when numCentroids <= 256)
|
|
1044
|
+
encoded[m] = bestIdx;
|
|
776
1045
|
}
|
|
777
|
-
return
|
|
1046
|
+
return encoded;
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Train Product Quantizer codebooks using k-means on sub-vector slices.
|
|
1050
|
+
*
|
|
1051
|
+
* Splits each training vector into `numSubquantizers` sub-vectors, then
|
|
1052
|
+
* learns `numCentroids` centroids per sub-quantizer position.
|
|
1053
|
+
*
|
|
1054
|
+
* @returns codebooks[m][c] = centroid vector for sub-quantizer m, centroid c
|
|
1055
|
+
*/
|
|
1056
|
+
trainProductQuantizer(trainingVectors, numSubquantizers, numCentroids) {
|
|
1057
|
+
const dim = trainingVectors[0].length;
|
|
1058
|
+
const subvectorSize = Math.ceil(dim / numSubquantizers);
|
|
1059
|
+
const codebooks = [];
|
|
1060
|
+
for (let m = 0; m < numSubquantizers; m++) {
|
|
1061
|
+
const start = m * subvectorSize;
|
|
1062
|
+
const end = Math.min(start + subvectorSize, dim);
|
|
1063
|
+
const subLen = end - start;
|
|
1064
|
+
// Extract sub-vectors for this position from all training vectors
|
|
1065
|
+
const subVectors = trainingVectors.map((vec) => {
|
|
1066
|
+
const sub = new Array(subLen);
|
|
1067
|
+
for (let d = 0; d < subLen; d++) {
|
|
1068
|
+
sub[d] = vec[start + d];
|
|
1069
|
+
}
|
|
1070
|
+
return sub;
|
|
1071
|
+
});
|
|
1072
|
+
// Clamp centroids to available data points
|
|
1073
|
+
const effectiveK = Math.min(numCentroids, subVectors.length);
|
|
1074
|
+
const centroids = this.kMeans(subVectors, effectiveK, 20);
|
|
1075
|
+
codebooks.push(centroids);
|
|
1076
|
+
}
|
|
1077
|
+
return codebooks;
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Compute approximate squared Euclidean distance between two PQ-encoded
|
|
1081
|
+
* vectors using their centroid indices and the shared codebooks.
|
|
1082
|
+
*
|
|
1083
|
+
* @param encoded1 - Centroid indices for vector 1 (length = numSubquantizers).
|
|
1084
|
+
* Typed Float32Array to match productQuantize()'s actual return type —
|
|
1085
|
+
* indices are small integers (0..codebookSize-1) exactly representable
|
|
1086
|
+
* in float32.
|
|
1087
|
+
* @param encoded2 - Centroid indices for vector 2 (length = numSubquantizers)
|
|
1088
|
+
* @param codebooks - Trained codebooks from trainProductQuantizer()
|
|
1089
|
+
* @returns Approximate squared Euclidean distance
|
|
1090
|
+
*/
|
|
1091
|
+
productQuantizeDistance(encoded1, encoded2, codebooks) {
|
|
1092
|
+
const cb = codebooks || this.codebooks;
|
|
1093
|
+
if (!cb) {
|
|
1094
|
+
throw new Error('Product quantizer codebooks not trained yet');
|
|
1095
|
+
}
|
|
1096
|
+
let totalDist = 0;
|
|
1097
|
+
for (let m = 0; m < encoded1.length; m++) {
|
|
1098
|
+
const c1 = cb[m][encoded1[m]];
|
|
1099
|
+
const c2 = cb[m][encoded2[m]];
|
|
1100
|
+
for (let d = 0; d < c1.length; d++) {
|
|
1101
|
+
const diff = c1[d] - c2[d];
|
|
1102
|
+
totalDist += diff * diff;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
return totalDist;
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* K-means clustering.
|
|
1109
|
+
*
|
|
1110
|
+
* Partitions `data` into `k` clusters and returns the centroid vectors.
|
|
1111
|
+
* Initialisation picks the first k data points (deterministic, avoids
|
|
1112
|
+
* the overhead of k-means++ for the small sub-vector slices used in PQ).
|
|
1113
|
+
*/
|
|
1114
|
+
kMeans(data, k, maxIter = 20) {
|
|
1115
|
+
const dim = data[0].length;
|
|
1116
|
+
// Initialise centroids from the first k data points
|
|
1117
|
+
const centroids = data.slice(0, k).map((v) => [...v]);
|
|
1118
|
+
for (let iter = 0; iter < maxIter; iter++) {
|
|
1119
|
+
// --- Assign each point to nearest centroid ---
|
|
1120
|
+
const assignments = new Int32Array(data.length);
|
|
1121
|
+
for (let i = 0; i < data.length; i++) {
|
|
1122
|
+
let bestDist = Infinity;
|
|
1123
|
+
let bestIdx = 0;
|
|
1124
|
+
for (let j = 0; j < k; j++) {
|
|
1125
|
+
let dist = 0;
|
|
1126
|
+
for (let d = 0; d < dim; d++) {
|
|
1127
|
+
const diff = data[i][d] - centroids[j][d];
|
|
1128
|
+
dist += diff * diff;
|
|
1129
|
+
}
|
|
1130
|
+
if (dist < bestDist) {
|
|
1131
|
+
bestDist = dist;
|
|
1132
|
+
bestIdx = j;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
assignments[i] = bestIdx;
|
|
1136
|
+
}
|
|
1137
|
+
// --- Update centroids ---
|
|
1138
|
+
const sums = Array.from({ length: k }, () => new Float64Array(dim));
|
|
1139
|
+
const counts = new Int32Array(k);
|
|
1140
|
+
for (let i = 0; i < data.length; i++) {
|
|
1141
|
+
const c = assignments[i];
|
|
1142
|
+
counts[c]++;
|
|
1143
|
+
for (let d = 0; d < dim; d++) {
|
|
1144
|
+
sums[c][d] += data[i][d];
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
let converged = true;
|
|
1148
|
+
for (let j = 0; j < k; j++) {
|
|
1149
|
+
if (counts[j] === 0)
|
|
1150
|
+
continue; // Dead centroid, leave unchanged
|
|
1151
|
+
for (let d = 0; d < dim; d++) {
|
|
1152
|
+
const newVal = sums[j][d] / counts[j];
|
|
1153
|
+
if (Math.abs(newVal - centroids[j][d]) > 1e-6)
|
|
1154
|
+
converged = false;
|
|
1155
|
+
centroids[j][d] = newVal;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
if (converged)
|
|
1159
|
+
break;
|
|
1160
|
+
}
|
|
1161
|
+
return centroids;
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Whether the product quantizer codebooks are trained and ready for encoding.
|
|
1165
|
+
*/
|
|
1166
|
+
get isPQTrained() {
|
|
1167
|
+
return this.pqTrained;
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Access trained codebooks (null if not yet trained).
|
|
1171
|
+
*/
|
|
1172
|
+
getCodebooks() {
|
|
1173
|
+
return this.codebooks;
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* True when `encoded` looks like a real trained-codebook PQ encoding
|
|
1177
|
+
* (length matches numSubquantizers, every value is a non-negative integer
|
|
1178
|
+
* in range for its sub-quantizer's codebook). False for the pre-training
|
|
1179
|
+
* sub-vector-mean fallback (see productQuantize()), which cannot be
|
|
1180
|
+
* compared via productQuantizeDistance() without corrupting results or
|
|
1181
|
+
* indexing out of bounds.
|
|
1182
|
+
*/
|
|
1183
|
+
isValidPQEncoding(encoded) {
|
|
1184
|
+
if (!this.codebooks || encoded.length !== this.codebooks.length)
|
|
1185
|
+
return false;
|
|
1186
|
+
for (let m = 0; m < encoded.length; m++) {
|
|
1187
|
+
const idx = encoded[m];
|
|
1188
|
+
if (!Number.isInteger(idx) || idx < 0 || idx >= this.codebooks[m].length)
|
|
1189
|
+
return false;
|
|
1190
|
+
}
|
|
1191
|
+
return true;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
// ===== Persistence Helpers (ADR-125 Phase 3) =====
|
|
1195
|
+
function readLengthPrefixedString(buf, offset) {
|
|
1196
|
+
if (offset + 4 > buf.length) {
|
|
1197
|
+
throw new Error(`HNSWIndex.deserialize: truncated string length at offset ${offset}`);
|
|
1198
|
+
}
|
|
1199
|
+
const len = buf.readUInt32BE(offset);
|
|
1200
|
+
const start = offset + 4;
|
|
1201
|
+
const end = start + len;
|
|
1202
|
+
if (end > buf.length) {
|
|
1203
|
+
throw new Error(`HNSWIndex.deserialize: truncated string payload at offset ${offset} (needed ${len} bytes, have ${buf.length - start})`);
|
|
1204
|
+
}
|
|
1205
|
+
const value = buf.toString('utf-8', start, end);
|
|
1206
|
+
return { value, offset: end };
|
|
1207
|
+
}
|
|
1208
|
+
function readFloat32Array(buf, offset) {
|
|
1209
|
+
if (offset + 4 > buf.length) {
|
|
1210
|
+
throw new Error(`HNSWIndex.deserialize: truncated array length at offset ${offset}`);
|
|
1211
|
+
}
|
|
1212
|
+
const floatCount = buf.readUInt32BE(offset);
|
|
1213
|
+
const start = offset + 4;
|
|
1214
|
+
const byteLen = floatCount * 4;
|
|
1215
|
+
const end = start + byteLen;
|
|
1216
|
+
if (end > buf.length) {
|
|
1217
|
+
throw new Error(`HNSWIndex.deserialize: truncated array payload at offset ${offset} (needed ${byteLen} bytes, have ${buf.length - start})`);
|
|
778
1218
|
}
|
|
1219
|
+
// Copy into a fresh ArrayBuffer so Float32Array isn't a view onto the original
|
|
1220
|
+
// (de-aligned) Node Buffer pool.
|
|
1221
|
+
const copy = new ArrayBuffer(byteLen);
|
|
1222
|
+
new Uint8Array(copy).set(new Uint8Array(buf.buffer, buf.byteOffset + start, byteLen));
|
|
1223
|
+
return { value: new Float32Array(copy), offset: end };
|
|
779
1224
|
}
|
|
780
1225
|
export default HNSWIndex;
|
|
781
1226
|
//# sourceMappingURL=hnsw-index.js.map
|